blob: fd1e31532facacbefcfb0ded0c37e17af2729b5c [file] [log] [blame]
Jim Van Verthc5903412016-11-17 15:27:09 -05001/*
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
Mike Kleinc0bd9f92019-04-23 12:05:21 -05008#include "src/gpu/ops/GrShadowRRectOp.h"
Robert Phillips7c525e62018-06-12 10:11:12 -04009
Robert Phillipsb7bfbc22020-07-01 12:55:01 -040010#include "include/gpu/GrRecordingContext.h"
Mike Kleinc0bd9f92019-04-23 12:05:21 -050011#include "src/core/SkRRectPriv.h"
Greg Daniel6f5441a2020-01-28 17:02:49 -050012#include "src/gpu/GrBitmapTextureMaker.h"
Mike Kleinc0bd9f92019-04-23 12:05:21 -050013#include "src/gpu/GrDrawOpTest.h"
14#include "src/gpu/GrMemoryPool.h"
15#include "src/gpu/GrOpFlushState.h"
Robert Phillips6941f4a2020-03-12 09:41:54 -040016#include "src/gpu/GrProgramInfo.h"
Jim Van Verth7da048b2019-10-29 13:28:14 -040017#include "src/gpu/GrProxyProvider.h"
Mike Kleinc0bd9f92019-04-23 12:05:21 -050018#include "src/gpu/GrRecordingContextPriv.h"
19#include "src/gpu/effects/GrShadowGeoProc.h"
Robert Phillips3968fcb2019-12-05 16:40:31 -050020#include "src/gpu/ops/GrSimpleMeshDrawOpHelper.h"
Jim Van Verthc5903412016-11-17 15:27:09 -050021
22///////////////////////////////////////////////////////////////////////////////
Jim Van Verth57061ee2017-04-28 17:30:30 -040023// Circle Data
24//
Jim Van Verthc5903412016-11-17 15:27:09 -050025// We have two possible cases for geometry for a circle:
26
27// In the case of a normal fill, we draw geometry for the circle as an octagon.
28static const uint16_t gFillCircleIndices[] = {
Brian Salomonfc527d22016-12-14 21:07:01 -050029 // enter the octagon
30 // clang-format off
31 0, 1, 8, 1, 2, 8,
32 2, 3, 8, 3, 4, 8,
33 4, 5, 8, 5, 6, 8,
34 6, 7, 8, 7, 0, 8,
35 // clang-format on
Jim Van Verthc5903412016-11-17 15:27:09 -050036};
37
38// For stroked circles, we use two nested octagons.
39static const uint16_t gStrokeCircleIndices[] = {
Brian Salomonfc527d22016-12-14 21:07:01 -050040 // enter the octagon
41 // clang-format off
42 0, 1, 9, 0, 9, 8,
43 1, 2, 10, 1, 10, 9,
44 2, 3, 11, 2, 11, 10,
45 3, 4, 12, 3, 12, 11,
46 4, 5, 13, 4, 13, 12,
47 5, 6, 14, 5, 14, 13,
48 6, 7, 15, 6, 15, 14,
49 7, 0, 8, 7, 8, 15,
50 // clang-format on
Jim Van Verthc5903412016-11-17 15:27:09 -050051};
52
53static const int kIndicesPerFillCircle = SK_ARRAY_COUNT(gFillCircleIndices);
54static const int kIndicesPerStrokeCircle = SK_ARRAY_COUNT(gStrokeCircleIndices);
55static const int kVertsPerStrokeCircle = 16;
56static const int kVertsPerFillCircle = 9;
57
58static int circle_type_to_vert_count(bool stroked) {
59 return stroked ? kVertsPerStrokeCircle : kVertsPerFillCircle;
60}
61
62static int circle_type_to_index_count(bool stroked) {
63 return stroked ? kIndicesPerStrokeCircle : kIndicesPerFillCircle;
64}
65
66static const uint16_t* circle_type_to_indices(bool stroked) {
67 return stroked ? gStrokeCircleIndices : gFillCircleIndices;
68}
69
70///////////////////////////////////////////////////////////////////////////////
Jim Van Verth57061ee2017-04-28 17:30:30 -040071// RoundRect Data
72//
Jim Van Verthb6069df2017-04-28 11:00:35 -040073// The geometry for a shadow roundrect is similar to a 9-patch:
Jim Van Verthc5903412016-11-17 15:27:09 -050074// ____________
75// |_|________|_|
76// | | | |
77// | | | |
78// | | | |
79// |_|________|_|
80// |_|________|_|
81//
Jim Van Verthb6069df2017-04-28 11:00:35 -040082// However, each corner is rendered as a fan rather than a simple quad, as below. (The diagram
83// shows the upper part of the upper left corner. The bottom triangle would similarly be split
84// into two triangles.)
85// ________
86// |\ \ |
87// | \ \ |
88// | \\ |
89// | \|
90// --------
91//
92// The center of the fan handles the curve of the corner. For roundrects where the stroke width
93// is greater than the corner radius, the outer triangles blend from the curve to the straight
94// sides. Otherwise these triangles will be degenerate.
95//
96// In the case where the stroke width is greater than the corner radius and the
97// blur radius (overstroke), we add additional geometry to mark out the rectangle in the center.
98// This rectangle extends the coverage values of the center edges of the 9-patch.
Jim Van Verthc5903412016-11-17 15:27:09 -050099// ____________
100// |_|________|_|
101// | |\ ____ /| |
102// | | | | | |
103// | | |____| | |
104// |_|/______\|_|
105// |_|________|_|
106//
Jim Van Verthb6069df2017-04-28 11:00:35 -0400107// For filled rrects we reuse the stroke geometry but add an additional quad to the center.
Jim Van Verthc5903412016-11-17 15:27:09 -0500108
Jim Van Verthb6069df2017-04-28 11:00:35 -0400109static const uint16_t gRRectIndices[] = {
110 // clang-format off
111 // overstroke quads
112 // we place this at the beginning so that we can skip these indices when rendering as filled
113 0, 6, 25, 0, 25, 24,
114 6, 18, 27, 6, 27, 25,
115 18, 12, 26, 18, 26, 27,
116 12, 0, 24, 12, 24, 26,
Jim Van Verthc5903412016-11-17 15:27:09 -0500117
Jim Van Verthb6069df2017-04-28 11:00:35 -0400118 // corners
119 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5,
120 6, 11, 10, 6, 10, 9, 6, 9, 8, 6, 8, 7,
121 12, 17, 16, 12, 16, 15, 12, 15, 14, 12, 14, 13,
122 18, 19, 20, 18, 20, 21, 18, 21, 22, 18, 22, 23,
Jim Van Verthc5903412016-11-17 15:27:09 -0500123
Jim Van Verthb6069df2017-04-28 11:00:35 -0400124 // edges
125 0, 5, 11, 0, 11, 6,
126 6, 7, 19, 6, 19, 18,
127 18, 23, 17, 18, 17, 12,
128 12, 13, 1, 12, 1, 0,
129
130 // fill quad
131 // we place this at the end so that we can skip these indices when rendering as stroked
132 0, 6, 18, 0, 18, 12,
133 // clang-format on
Jim Van Verthc5903412016-11-17 15:27:09 -0500134};
Jim Van Verthc5903412016-11-17 15:27:09 -0500135
136// overstroke count
Jim Van Verthb6069df2017-04-28 11:00:35 -0400137static const int kIndicesPerOverstrokeRRect = SK_ARRAY_COUNT(gRRectIndices) - 6;
Jim Van Verthc5903412016-11-17 15:27:09 -0500138// simple stroke count skips overstroke indices
Jim Van Verthb6069df2017-04-28 11:00:35 -0400139static const int kIndicesPerStrokeRRect = kIndicesPerOverstrokeRRect - 6*4;
140// fill count adds final quad to stroke count
141static const int kIndicesPerFillRRect = kIndicesPerStrokeRRect + 6;
142static const int kVertsPerStrokeRRect = 24;
143static const int kVertsPerOverstrokeRRect = 28;
144static const int kVertsPerFillRRect = 24;
Jim Van Verthc5903412016-11-17 15:27:09 -0500145
146enum RRectType {
147 kFill_RRectType,
148 kStroke_RRectType,
149 kOverstroke_RRectType,
150};
151
152static int rrect_type_to_vert_count(RRectType type) {
Brian Salomonfc527d22016-12-14 21:07:01 -0500153 switch (type) {
154 case kFill_RRectType:
Jim Van Verthb6069df2017-04-28 11:00:35 -0400155 return kVertsPerFillRRect;
Brian Salomonfc527d22016-12-14 21:07:01 -0500156 case kStroke_RRectType:
157 return kVertsPerStrokeRRect;
158 case kOverstroke_RRectType:
159 return kVertsPerOverstrokeRRect;
160 }
Ben Wagnerb4aab9a2017-08-16 10:53:04 -0400161 SK_ABORT("Invalid type");
Jim Van Verthc5903412016-11-17 15:27:09 -0500162}
163
164static int rrect_type_to_index_count(RRectType type) {
Brian Salomonfc527d22016-12-14 21:07:01 -0500165 switch (type) {
166 case kFill_RRectType:
Jim Van Verthb6069df2017-04-28 11:00:35 -0400167 return kIndicesPerFillRRect;
Brian Salomonfc527d22016-12-14 21:07:01 -0500168 case kStroke_RRectType:
169 return kIndicesPerStrokeRRect;
170 case kOverstroke_RRectType:
171 return kIndicesPerOverstrokeRRect;
172 }
Ben Wagnerb4aab9a2017-08-16 10:53:04 -0400173 SK_ABORT("Invalid type");
Jim Van Verthc5903412016-11-17 15:27:09 -0500174}
175
176static const uint16_t* rrect_type_to_indices(RRectType type) {
Brian Salomonfc527d22016-12-14 21:07:01 -0500177 switch (type) {
178 case kFill_RRectType:
Brian Salomonfc527d22016-12-14 21:07:01 -0500179 case kStroke_RRectType:
Jim Van Verthb6069df2017-04-28 11:00:35 -0400180 return gRRectIndices + 6*4;
Brian Salomonfc527d22016-12-14 21:07:01 -0500181 case kOverstroke_RRectType:
Jim Van Verthb6069df2017-04-28 11:00:35 -0400182 return gRRectIndices;
Brian Salomonfc527d22016-12-14 21:07:01 -0500183 }
Ben Wagnerb4aab9a2017-08-16 10:53:04 -0400184 SK_ABORT("Invalid type");
Jim Van Verthc5903412016-11-17 15:27:09 -0500185}
186
Jim Van Verth57061ee2017-04-28 17:30:30 -0400187///////////////////////////////////////////////////////////////////////////////
Brian Salomon05969092017-07-13 11:20:51 -0400188namespace {
Jim Van Verth57061ee2017-04-28 17:30:30 -0400189
Brian Salomon05969092017-07-13 11:20:51 -0400190class ShadowCircularRRectOp final : public GrMeshDrawOp {
Jim Van Verthc5903412016-11-17 15:27:09 -0500191public:
Brian Salomon25a88092016-12-01 09:36:50 -0500192 DEFINE_OP_CLASS_ID
Jim Van Verthc5903412016-11-17 15:27:09 -0500193
Jim Van Verth8d1e0ac2017-05-05 15:53:23 -0400194 // An insetWidth > 1/2 rect width or height indicates a simple fill.
Brian Salomon05969092017-07-13 11:20:51 -0400195 ShadowCircularRRectOp(GrColor color, const SkRect& devRect,
Jim Van Verth7da048b2019-10-29 13:28:14 -0400196 float devRadius, bool isCircle, float blurRadius, float insetWidth,
Greg Danielad994cd2019-12-10 09:35:16 -0500197 GrSurfaceProxyView falloffView)
Jim Van Verth7da048b2019-10-29 13:28:14 -0400198 : INHERITED(ClassID())
Greg Danielad994cd2019-12-10 09:35:16 -0500199 , fFalloffView(std::move(falloffView)) {
Jim Van Verthc5903412016-11-17 15:27:09 -0500200 SkRect bounds = devRect;
Jim Van Verth8d1e0ac2017-05-05 15:53:23 -0400201 SkASSERT(insetWidth > 0);
Jim Van Verth57061ee2017-04-28 17:30:30 -0400202 SkScalar innerRadius = 0.0f;
Jim Van Verthc5903412016-11-17 15:27:09 -0500203 SkScalar outerRadius = devRadius;
Jim Van Verth57061ee2017-04-28 17:30:30 -0400204 SkScalar umbraInset;
Jim Van Verth8d1e0ac2017-05-05 15:53:23 -0400205
206 RRectType type = kFill_RRectType;
Jim Van Verth57061ee2017-04-28 17:30:30 -0400207 if (isCircle) {
208 umbraInset = 0;
209 } else {
Brian Osman788b9162020-02-07 10:36:46 -0500210 umbraInset = std::max(outerRadius, blurRadius);
Jim Van Verth57061ee2017-04-28 17:30:30 -0400211 }
212
Jim Van Verth8d1e0ac2017-05-05 15:53:23 -0400213 // If stroke is greater than width or height, this is still a fill,
214 // otherwise we compute stroke params.
215 if (isCircle) {
216 innerRadius = devRadius - insetWidth;
217 type = innerRadius > 0 ? kStroke_RRectType : kFill_RRectType;
218 } else {
Brian Osman788b9162020-02-07 10:36:46 -0500219 if (insetWidth <= 0.5f*std::min(devRect.width(), devRect.height())) {
Jim Van Verth8d1e0ac2017-05-05 15:53:23 -0400220 // We don't worry about a real inner radius, we just need to know if we
221 // need to create overstroke vertices.
Brian Osman788b9162020-02-07 10:36:46 -0500222 innerRadius = std::max(insetWidth - umbraInset, 0.0f);
Jim Van Verth8d1e0ac2017-05-05 15:53:23 -0400223 type = innerRadius > 0 ? kOverstroke_RRectType : kStroke_RRectType;
Jim Van Verthb6069df2017-04-28 11:00:35 -0400224 }
Jim Van Verthc5903412016-11-17 15:27:09 -0500225 }
226
Greg Daniel5faf4742019-10-01 15:14:44 -0400227 this->setBounds(bounds, HasAABloat::kNo, IsHairline::kNo);
Jim Van Verthc5903412016-11-17 15:27:09 -0500228
Jim Van Verth57061ee2017-04-28 17:30:30 -0400229 fGeoData.emplace_back(Geometry{color, outerRadius, umbraInset, innerRadius,
Jim Van Verthfb186392018-09-11 11:37:46 -0400230 blurRadius, bounds, type, isCircle});
Jim Van Verth57061ee2017-04-28 17:30:30 -0400231 if (isCircle) {
232 fVertCount = circle_type_to_vert_count(kStroke_RRectType == type);
233 fIndexCount = circle_type_to_index_count(kStroke_RRectType == type);
234 } else {
235 fVertCount = rrect_type_to_vert_count(type);
236 fIndexCount = rrect_type_to_index_count(type);
237 }
Jim Van Verthc5903412016-11-17 15:27:09 -0500238 }
239
Brian Salomonfc527d22016-12-14 21:07:01 -0500240 const char* name() const override { return "ShadowCircularRRectOp"; }
Jim Van Verthc5903412016-11-17 15:27:09 -0500241
John Stiles8d9bf642020-08-12 15:07:45 -0400242#if GR_TEST_UTILS
John Stiles8dd1e222020-08-12 19:06:24 -0400243 SkString onDumpInfo() const override {
Jim Van Verthc5903412016-11-17 15:27:09 -0500244 SkString string;
245 for (int i = 0; i < fGeoData.count(); ++i) {
Brian Salomonfc527d22016-12-14 21:07:01 -0500246 string.appendf(
247 "Color: 0x%08x Rect [L: %.2f, T: %.2f, R: %.2f, B: %.2f],"
Jim Van Verth57061ee2017-04-28 17:30:30 -0400248 "OuterRad: %.2f, Umbra: %.2f, InnerRad: %.2f, BlurRad: %.2f\n",
Brian Salomonfc527d22016-12-14 21:07:01 -0500249 fGeoData[i].fColor, fGeoData[i].fDevBounds.fLeft, fGeoData[i].fDevBounds.fTop,
250 fGeoData[i].fDevBounds.fRight, fGeoData[i].fDevBounds.fBottom,
Jim Van Verthb6069df2017-04-28 11:00:35 -0400251 fGeoData[i].fOuterRadius, fGeoData[i].fUmbraInset,
Jim Van Verth57061ee2017-04-28 17:30:30 -0400252 fGeoData[i].fInnerRadius, fGeoData[i].fBlurRadius);
Jim Van Verthc5903412016-11-17 15:27:09 -0500253 }
Jim Van Verthc5903412016-11-17 15:27:09 -0500254 return string;
255 }
Brian Osman9a390ac2018-11-12 09:47:48 -0500256#endif
Jim Van Verthc5903412016-11-17 15:27:09 -0500257
Brian Salomon05969092017-07-13 11:20:51 -0400258 FixedFunctionFlags fixedFunctionFlags() const override { return FixedFunctionFlags::kNone; }
259
Chris Dalton6ce447a2019-06-23 18:07:38 -0600260 GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*,
261 bool hasMixedSampledCoverage, GrClampType) override {
Chris Dalton4b62aed2019-01-15 11:53:00 -0700262 return GrProcessorSet::EmptySetAnalysis();
Brian Salomon05969092017-07-13 11:20:51 -0400263 }
264
Brian Salomon92aee3d2016-12-21 09:20:25 -0500265private:
Jim Van Verth57061ee2017-04-28 17:30:30 -0400266 struct Geometry {
267 GrColor fColor;
268 SkScalar fOuterRadius;
269 SkScalar fUmbraInset;
270 SkScalar fInnerRadius;
271 SkScalar fBlurRadius;
272 SkRect fDevBounds;
273 RRectType fType;
274 bool fIsCircle;
275 };
276
Jim Van Verthc5903412016-11-17 15:27:09 -0500277 struct CircleVertex {
Brian Salomonfc527d22016-12-14 21:07:01 -0500278 SkPoint fPos;
279 GrColor fColor;
280 SkPoint fOffset;
Jim Van Verthb6069df2017-04-28 11:00:35 -0400281 SkScalar fDistanceCorrection;
Jim Van Verthc5903412016-11-17 15:27:09 -0500282 };
283
Jim Van Verth57061ee2017-04-28 17:30:30 -0400284 void fillInCircleVerts(const Geometry& args, bool isStroked, CircleVertex** verts) const {
285
286 GrColor color = args.fColor;
287 SkScalar outerRadius = args.fOuterRadius;
288 SkScalar innerRadius = args.fInnerRadius;
289 SkScalar blurRadius = args.fBlurRadius;
290 SkScalar distanceCorrection = outerRadius / blurRadius;
291
292 const SkRect& bounds = args.fDevBounds;
293
294 // The inner radius in the vertex data must be specified in normalized space.
295 innerRadius = innerRadius / outerRadius;
296
297 SkPoint center = SkPoint::Make(bounds.centerX(), bounds.centerY());
298 SkScalar halfWidth = 0.5f * bounds.width();
299 SkScalar octOffset = 0.41421356237f; // sqrt(2) - 1
300
301 (*verts)->fPos = center + SkPoint::Make(-octOffset * halfWidth, -halfWidth);
Jim Van Verthc5903412016-11-17 15:27:09 -0500302 (*verts)->fColor = color;
Jim Van Verth57061ee2017-04-28 17:30:30 -0400303 (*verts)->fOffset = SkPoint::Make(-octOffset, -1);
Jim Van Verthb6069df2017-04-28 11:00:35 -0400304 (*verts)->fDistanceCorrection = distanceCorrection;
Jim Van Verthc5903412016-11-17 15:27:09 -0500305 (*verts)++;
306
Jim Van Verth57061ee2017-04-28 17:30:30 -0400307 (*verts)->fPos = center + SkPoint::Make(octOffset * halfWidth, -halfWidth);
Jim Van Verthc5903412016-11-17 15:27:09 -0500308 (*verts)->fColor = color;
Jim Van Verth57061ee2017-04-28 17:30:30 -0400309 (*verts)->fOffset = SkPoint::Make(octOffset, -1);
Jim Van Verthb6069df2017-04-28 11:00:35 -0400310 (*verts)->fDistanceCorrection = distanceCorrection;
Jim Van Verthc5903412016-11-17 15:27:09 -0500311 (*verts)++;
312
Jim Van Verth57061ee2017-04-28 17:30:30 -0400313 (*verts)->fPos = center + SkPoint::Make(halfWidth, -octOffset * halfWidth);
Jim Van Verthc5903412016-11-17 15:27:09 -0500314 (*verts)->fColor = color;
Jim Van Verth57061ee2017-04-28 17:30:30 -0400315 (*verts)->fOffset = SkPoint::Make(1, -octOffset);
Jim Van Verthb6069df2017-04-28 11:00:35 -0400316 (*verts)->fDistanceCorrection = distanceCorrection;
Jim Van Verthc5903412016-11-17 15:27:09 -0500317 (*verts)++;
318
Jim Van Verth57061ee2017-04-28 17:30:30 -0400319 (*verts)->fPos = center + SkPoint::Make(halfWidth, octOffset * halfWidth);
Jim Van Verthc5903412016-11-17 15:27:09 -0500320 (*verts)->fColor = color;
Jim Van Verth57061ee2017-04-28 17:30:30 -0400321 (*verts)->fOffset = SkPoint::Make(1, octOffset);
Jim Van Verthb6069df2017-04-28 11:00:35 -0400322 (*verts)->fDistanceCorrection = distanceCorrection;
Jim Van Verthc5903412016-11-17 15:27:09 -0500323 (*verts)++;
Jim Van Verth57061ee2017-04-28 17:30:30 -0400324
325 (*verts)->fPos = center + SkPoint::Make(octOffset * halfWidth, halfWidth);
326 (*verts)->fColor = color;
327 (*verts)->fOffset = SkPoint::Make(octOffset, 1);
328 (*verts)->fDistanceCorrection = distanceCorrection;
329 (*verts)++;
330
331 (*verts)->fPos = center + SkPoint::Make(-octOffset * halfWidth, halfWidth);
332 (*verts)->fColor = color;
333 (*verts)->fOffset = SkPoint::Make(-octOffset, 1);
334 (*verts)->fDistanceCorrection = distanceCorrection;
335 (*verts)++;
336
337 (*verts)->fPos = center + SkPoint::Make(-halfWidth, octOffset * halfWidth);
338 (*verts)->fColor = color;
339 (*verts)->fOffset = SkPoint::Make(-1, octOffset);
340 (*verts)->fDistanceCorrection = distanceCorrection;
341 (*verts)++;
342
343 (*verts)->fPos = center + SkPoint::Make(-halfWidth, -octOffset * halfWidth);
344 (*verts)->fColor = color;
345 (*verts)->fOffset = SkPoint::Make(-1, -octOffset);
346 (*verts)->fDistanceCorrection = distanceCorrection;
347 (*verts)++;
348
349 if (isStroked) {
350 // compute the inner ring
351
352 // cosine and sine of pi/8
353 SkScalar c = 0.923579533f;
354 SkScalar s = 0.382683432f;
355 SkScalar r = args.fInnerRadius;
356
357 (*verts)->fPos = center + SkPoint::Make(-s * r, -c * r);
358 (*verts)->fColor = color;
359 (*verts)->fOffset = SkPoint::Make(-s * innerRadius, -c * innerRadius);
360 (*verts)->fDistanceCorrection = distanceCorrection;
361 (*verts)++;
362
363 (*verts)->fPos = center + SkPoint::Make(s * r, -c * r);
364 (*verts)->fColor = color;
365 (*verts)->fOffset = SkPoint::Make(s * innerRadius, -c * innerRadius);
366 (*verts)->fDistanceCorrection = distanceCorrection;
367 (*verts)++;
368
369 (*verts)->fPos = center + SkPoint::Make(c * r, -s * r);
370 (*verts)->fColor = color;
371 (*verts)->fOffset = SkPoint::Make(c * innerRadius, -s * innerRadius);
372 (*verts)->fDistanceCorrection = distanceCorrection;
373 (*verts)++;
374
375 (*verts)->fPos = center + SkPoint::Make(c * r, s * r);
376 (*verts)->fColor = color;
377 (*verts)->fOffset = SkPoint::Make(c * innerRadius, s * innerRadius);
378 (*verts)->fDistanceCorrection = distanceCorrection;
379 (*verts)++;
380
381 (*verts)->fPos = center + SkPoint::Make(s * r, c * r);
382 (*verts)->fColor = color;
383 (*verts)->fOffset = SkPoint::Make(s * innerRadius, c * innerRadius);
384 (*verts)->fDistanceCorrection = distanceCorrection;
385 (*verts)++;
386
387 (*verts)->fPos = center + SkPoint::Make(-s * r, c * r);
388 (*verts)->fColor = color;
389 (*verts)->fOffset = SkPoint::Make(-s * innerRadius, c * innerRadius);
390 (*verts)->fDistanceCorrection = distanceCorrection;
391 (*verts)++;
392
393 (*verts)->fPos = center + SkPoint::Make(-c * r, s * r);
394 (*verts)->fColor = color;
395 (*verts)->fOffset = SkPoint::Make(-c * innerRadius, s * innerRadius);
396 (*verts)->fDistanceCorrection = distanceCorrection;
397 (*verts)++;
398
399 (*verts)->fPos = center + SkPoint::Make(-c * r, -s * r);
400 (*verts)->fColor = color;
401 (*verts)->fOffset = SkPoint::Make(-c * innerRadius, -s * innerRadius);
402 (*verts)->fDistanceCorrection = distanceCorrection;
403 (*verts)++;
404 } else {
405 // filled
406 (*verts)->fPos = center;
407 (*verts)->fColor = color;
408 (*verts)->fOffset = SkPoint::Make(0, 0);
409 (*verts)->fDistanceCorrection = distanceCorrection;
410 (*verts)++;
411 }
412 }
413
414 void fillInRRectVerts(const Geometry& args, CircleVertex** verts) const {
415 GrColor color = args.fColor;
416 SkScalar outerRadius = args.fOuterRadius;
417
418 const SkRect& bounds = args.fDevBounds;
419
420 SkScalar umbraInset = args.fUmbraInset;
Brian Osman788b9162020-02-07 10:36:46 -0500421 SkScalar minDim = 0.5f*std::min(bounds.width(), bounds.height());
Jim Van Verth57061ee2017-04-28 17:30:30 -0400422 if (umbraInset > minDim) {
423 umbraInset = minDim;
424 }
425
426 SkScalar xInner[4] = { bounds.fLeft + umbraInset, bounds.fRight - umbraInset,
427 bounds.fLeft + umbraInset, bounds.fRight - umbraInset };
428 SkScalar xMid[4] = { bounds.fLeft + outerRadius, bounds.fRight - outerRadius,
429 bounds.fLeft + outerRadius, bounds.fRight - outerRadius };
430 SkScalar xOuter[4] = { bounds.fLeft, bounds.fRight,
431 bounds.fLeft, bounds.fRight };
432 SkScalar yInner[4] = { bounds.fTop + umbraInset, bounds.fTop + umbraInset,
433 bounds.fBottom - umbraInset, bounds.fBottom - umbraInset };
434 SkScalar yMid[4] = { bounds.fTop + outerRadius, bounds.fTop + outerRadius,
435 bounds.fBottom - outerRadius, bounds.fBottom - outerRadius };
436 SkScalar yOuter[4] = { bounds.fTop, bounds.fTop,
437 bounds.fBottom, bounds.fBottom };
438
439 SkScalar blurRadius = args.fBlurRadius;
440
441 // In the case where we have to inset more for the umbra, our two triangles in the
442 // corner get skewed to a diamond rather than a square. To correct for that,
443 // we also skew the vectors we send to the shader that help define the circle.
444 // By doing so, we end up with a quarter circle in the corner rather than the
445 // elliptical curve.
Jim Van Verth4c8c1e82018-04-23 17:14:48 -0400446
447 // This is a bit magical, but it gives us the correct results at extrema:
448 // a) umbraInset == outerRadius produces an orthogonal vector
449 // b) outerRadius == 0 produces a diagonal vector
450 // And visually the corner looks correct.
451 SkVector outerVec = SkVector::Make(outerRadius - umbraInset, -outerRadius - umbraInset);
Jim Van Verth57061ee2017-04-28 17:30:30 -0400452 outerVec.normalize();
Jim Van Verth4c8c1e82018-04-23 17:14:48 -0400453 // We want the circle edge to fall fractionally along the diagonal at
454 // (sqrt(2)*(umbraInset - outerRadius) + outerRadius)/sqrt(2)*umbraInset
455 //
456 // Setting the components of the diagonal offset to the following value will give us that.
457 SkScalar diagVal = umbraInset / (SK_ScalarSqrt2*(outerRadius - umbraInset) - outerRadius);
458 SkVector diagVec = SkVector::Make(diagVal, diagVal);
Jim Van Verth57061ee2017-04-28 17:30:30 -0400459 SkScalar distanceCorrection = umbraInset / blurRadius;
460
461 // build corner by corner
462 for (int i = 0; i < 4; ++i) {
463 // inner point
464 (*verts)->fPos = SkPoint::Make(xInner[i], yInner[i]);
465 (*verts)->fColor = color;
466 (*verts)->fOffset = SkVector::Make(0, 0);
467 (*verts)->fDistanceCorrection = distanceCorrection;
468 (*verts)++;
469
470 // outer points
471 (*verts)->fPos = SkPoint::Make(xOuter[i], yInner[i]);
472 (*verts)->fColor = color;
473 (*verts)->fOffset = SkVector::Make(0, -1);
474 (*verts)->fDistanceCorrection = distanceCorrection;
475 (*verts)++;
476
477 (*verts)->fPos = SkPoint::Make(xOuter[i], yMid[i]);
478 (*verts)->fColor = color;
479 (*verts)->fOffset = outerVec;
480 (*verts)->fDistanceCorrection = distanceCorrection;
481 (*verts)++;
482
483 (*verts)->fPos = SkPoint::Make(xOuter[i], yOuter[i]);
484 (*verts)->fColor = color;
485 (*verts)->fOffset = diagVec;
486 (*verts)->fDistanceCorrection = distanceCorrection;
487 (*verts)++;
488
489 (*verts)->fPos = SkPoint::Make(xMid[i], yOuter[i]);
490 (*verts)->fColor = color;
491 (*verts)->fOffset = outerVec;
492 (*verts)->fDistanceCorrection = distanceCorrection;
493 (*verts)++;
494
495 (*verts)->fPos = SkPoint::Make(xInner[i], yOuter[i]);
496 (*verts)->fColor = color;
497 (*verts)->fOffset = SkVector::Make(0, -1);
498 (*verts)->fDistanceCorrection = distanceCorrection;
499 (*verts)++;
500 }
501
502 // Add the additional vertices for overstroked rrects.
503 // Effectively this is an additional stroked rrect, with its
504 // parameters equal to those in the center of the 9-patch. This will
505 // give constant values across this inner ring.
506 if (kOverstroke_RRectType == args.fType) {
507 SkASSERT(args.fInnerRadius > 0.0f);
508
509 SkScalar inset = umbraInset + args.fInnerRadius;
510
511 // TL
512 (*verts)->fPos = SkPoint::Make(bounds.fLeft + inset, bounds.fTop + inset);
513 (*verts)->fColor = color;
514 (*verts)->fOffset = SkPoint::Make(0, 0);
515 (*verts)->fDistanceCorrection = distanceCorrection;
516 (*verts)++;
517
518 // TR
519 (*verts)->fPos = SkPoint::Make(bounds.fRight - inset, bounds.fTop + inset);
520 (*verts)->fColor = color;
521 (*verts)->fOffset = SkPoint::Make(0, 0);
522 (*verts)->fDistanceCorrection = distanceCorrection;
523 (*verts)++;
524
525 // BL
526 (*verts)->fPos = SkPoint::Make(bounds.fLeft + inset, bounds.fBottom - inset);
527 (*verts)->fColor = color;
528 (*verts)->fOffset = SkPoint::Make(0, 0);
529 (*verts)->fDistanceCorrection = distanceCorrection;
530 (*verts)++;
531
532 // BR
533 (*verts)->fPos = SkPoint::Make(bounds.fRight - inset, bounds.fBottom - inset);
534 (*verts)->fColor = color;
535 (*verts)->fOffset = SkPoint::Make(0, 0);
536 (*verts)->fDistanceCorrection = distanceCorrection;
537 (*verts)++;
538 }
539
Jim Van Verthc5903412016-11-17 15:27:09 -0500540 }
541
Robert Phillips2669a7b2020-03-12 12:07:19 -0400542 GrProgramInfo* programInfo() override { return fProgramInfo; }
543
Robert Phillips6941f4a2020-03-12 09:41:54 -0400544 void onCreateProgramInfo(const GrCaps* caps,
545 SkArenaAlloc* arena,
Brian Salomon8afde5f2020-04-01 16:22:00 -0400546 const GrSurfaceProxyView* writeView,
Robert Phillips6941f4a2020-03-12 09:41:54 -0400547 GrAppliedClip&& appliedClip,
548 const GrXferProcessor::DstProxyView& dstProxyView) override {
549 GrGeometryProcessor* gp = GrRRectShadowGeoProc::Make(arena, fFalloffView);
550 SkASSERT(sizeof(CircleVertex) == gp->vertexStride());
551
Brian Salomon8afde5f2020-04-01 16:22:00 -0400552 fProgramInfo = GrSimpleMeshDrawOpHelper::CreateProgramInfo(caps, arena, writeView,
Robert Phillips6941f4a2020-03-12 09:41:54 -0400553 std::move(appliedClip),
554 dstProxyView, gp,
555 GrProcessorSet::MakeEmptySet(),
556 GrPrimitiveType::kTriangles,
557 GrPipeline::InputFlags::kNone,
Chris Dalton765ed362020-03-16 17:34:44 -0600558 &GrUserStencilSettings::kUnused);
Robert Phillips6941f4a2020-03-12 09:41:54 -0400559 }
560
Brian Salomon91326c32017-08-09 16:02:19 -0400561 void onPrepareDraws(Target* target) override {
Jim Van Verthc5903412016-11-17 15:27:09 -0500562 int instanceCount = fGeoData.count();
Jim Van Verthc5903412016-11-17 15:27:09 -0500563
Brian Salomon12d22642019-01-29 14:38:50 -0500564 sk_sp<const GrBuffer> vertexBuffer;
Jim Van Verthc5903412016-11-17 15:27:09 -0500565 int firstVertex;
Brian Salomon92be2f72018-06-19 14:33:47 -0400566 CircleVertex* verts = (CircleVertex*)target->makeVertexSpace(
567 sizeof(CircleVertex), fVertCount, &vertexBuffer, &firstVertex);
Jim Van Verthc5903412016-11-17 15:27:09 -0500568 if (!verts) {
569 SkDebugf("Could not allocate vertices\n");
570 return;
571 }
572
Brian Salomon12d22642019-01-29 14:38:50 -0500573 sk_sp<const GrBuffer> indexBuffer;
Jim Van Verthc5903412016-11-17 15:27:09 -0500574 int firstIndex = 0;
575 uint16_t* indices = target->makeIndexSpace(fIndexCount, &indexBuffer, &firstIndex);
576 if (!indices) {
577 SkDebugf("Could not allocate indices\n");
578 return;
579 }
580
581 int currStartVertex = 0;
582 for (int i = 0; i < instanceCount; i++) {
583 const Geometry& args = fGeoData[i];
584
Jim Van Verth57061ee2017-04-28 17:30:30 -0400585 if (args.fIsCircle) {
586 bool isStroked = SkToBool(kStroke_RRectType == args.fType);
587 this->fillInCircleVerts(args, isStroked, &verts);
Jim Van Verthc5903412016-11-17 15:27:09 -0500588
Jim Van Verth57061ee2017-04-28 17:30:30 -0400589 const uint16_t* primIndices = circle_type_to_indices(isStroked);
590 const int primIndexCount = circle_type_to_index_count(isStroked);
591 for (int i = 0; i < primIndexCount; ++i) {
592 *indices++ = primIndices[i] + currStartVertex;
593 }
Jim Van Verthc5903412016-11-17 15:27:09 -0500594
Jim Van Verth57061ee2017-04-28 17:30:30 -0400595 currStartVertex += circle_type_to_vert_count(isStroked);
Jim Van Verthc5903412016-11-17 15:27:09 -0500596
Jim Van Verth57061ee2017-04-28 17:30:30 -0400597 } else {
598 this->fillInRRectVerts(args, &verts);
599
600 const uint16_t* primIndices = rrect_type_to_indices(args.fType);
601 const int primIndexCount = rrect_type_to_index_count(args.fType);
602 for (int i = 0; i < primIndexCount; ++i) {
603 *indices++ = primIndices[i] + currStartVertex;
604 }
605
606 currStartVertex += rrect_type_to_vert_count(args.fType);
Jim Van Verthb6069df2017-04-28 11:00:35 -0400607 }
Jim Van Verthc5903412016-11-17 15:27:09 -0500608 }
609
Robert Phillips6941f4a2020-03-12 09:41:54 -0400610 fMesh = target->allocMesh();
611 fMesh->setIndexed(std::move(indexBuffer), fIndexCount, firstIndex, 0, fVertCount - 1,
Chris Dalton37c7bdd2020-03-13 09:21:12 -0600612 GrPrimitiveRestart::kNo, std::move(vertexBuffer), firstVertex);
Chris Dalton07cdcfc92019-02-26 11:13:22 -0700613 }
614
615 void onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) override {
Robert Phillips6941f4a2020-03-12 09:41:54 -0400616 if (!fProgramInfo) {
617 this->createProgramInfo(flushState);
618 }
Robert Phillips3968fcb2019-12-05 16:40:31 -0500619
Robert Phillips6941f4a2020-03-12 09:41:54 -0400620 if (!fProgramInfo || !fMesh) {
621 return;
622 }
623
Chris Dalton765ed362020-03-16 17:34:44 -0600624 flushState->bindPipelineAndScissorClip(*fProgramInfo, chainBounds);
625 flushState->bindTextures(fProgramInfo->primProc(), *fFalloffView.proxy(),
626 fProgramInfo->pipeline());
627 flushState->drawMesh(*fMesh);
Jim Van Verthc5903412016-11-17 15:27:09 -0500628 }
629
Michael Ludwig28b0c5d2019-12-19 14:51:00 -0500630 CombineResult onCombineIfPossible(GrOp* t, GrRecordingContext::Arenas*,
631 const GrCaps& caps) override {
Brian Salomonfc527d22016-12-14 21:07:01 -0500632 ShadowCircularRRectOp* that = t->cast<ShadowCircularRRectOp>();
Jim Van Verthc5903412016-11-17 15:27:09 -0500633 fGeoData.push_back_n(that->fGeoData.count(), that->fGeoData.begin());
Jim Van Verthc5903412016-11-17 15:27:09 -0500634 fVertCount += that->fVertCount;
635 fIndexCount += that->fIndexCount;
Brian Salomon7eae3e02018-08-07 14:02:38 +0000636 return CombineResult::kMerged;
Jim Van Verthc5903412016-11-17 15:27:09 -0500637 }
638
Jim Van Verth7da048b2019-10-29 13:28:14 -0400639 void visitProxies(const VisitProxyFunc& func) const override {
Brian Salomon7e67dca2020-07-21 09:27:25 -0400640 func(fFalloffView.proxy(), GrMipmapped(false));
Robert Phillips6941f4a2020-03-12 09:41:54 -0400641 if (fProgramInfo) {
Chris Daltonbe457422020-03-16 18:05:03 -0600642 fProgramInfo->visitFPProxies(func);
Robert Phillips6941f4a2020-03-12 09:41:54 -0400643 }
Jim Van Verth7da048b2019-10-29 13:28:14 -0400644 }
645
Jim Van Verthc5903412016-11-17 15:27:09 -0500646 SkSTArray<1, Geometry, true> fGeoData;
Brian Salomonfc527d22016-12-14 21:07:01 -0500647 int fVertCount;
648 int fIndexCount;
Greg Danielad994cd2019-12-10 09:35:16 -0500649 GrSurfaceProxyView fFalloffView;
Jim Van Verthc5903412016-11-17 15:27:09 -0500650
Chris Daltoneb694b72020-03-16 09:25:50 -0600651 GrSimpleMesh* fMesh = nullptr;
Robert Phillips6941f4a2020-03-12 09:41:54 -0400652 GrProgramInfo* fProgramInfo = nullptr;
653
Brian Salomon05969092017-07-13 11:20:51 -0400654 typedef GrMeshDrawOp INHERITED;
Jim Van Verthc5903412016-11-17 15:27:09 -0500655};
656
Brian Salomon05969092017-07-13 11:20:51 -0400657} // anonymous namespace
658
Jim Van Verthc5903412016-11-17 15:27:09 -0500659///////////////////////////////////////////////////////////////////////////////
660
Jim Van Verth57061ee2017-04-28 17:30:30 -0400661namespace GrShadowRRectOp {
Jim Van Verth7da048b2019-10-29 13:28:14 -0400662
Greg Daniel9f0dfbd2020-02-10 11:47:11 -0500663static GrSurfaceProxyView create_falloff_texture(GrRecordingContext* context) {
Jim Van Verth7da048b2019-10-29 13:28:14 -0400664 static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
665 GrUniqueKey key;
666 GrUniqueKey::Builder builder(&key, kDomain, 0, "Shadow Gaussian Falloff");
667 builder.finish();
668
Greg Daniel6f5441a2020-01-28 17:02:49 -0500669 GrProxyProvider* proxyProvider = context->priv().proxyProvider();
670
Brian Salomondf1bd6d2020-03-26 20:37:01 -0400671 if (sk_sp<GrTextureProxy> falloffTexture = proxyProvider->findOrCreateProxyByUniqueKey(key)) {
Greg Daniel9f0dfbd2020-02-10 11:47:11 -0500672 GrSwizzle swizzle = context->priv().caps()->getReadSwizzle(falloffTexture->backendFormat(),
673 GrColorType::kAlpha_8);
674 return {std::move(falloffTexture), kTopLeft_GrSurfaceOrigin, swizzle};
Jim Van Verth7da048b2019-10-29 13:28:14 -0400675 }
676
Greg Daniel9f0dfbd2020-02-10 11:47:11 -0500677 static const int kWidth = 128;
678 static const size_t kRowBytes = kWidth * GrColorTypeBytesPerPixel(GrColorType::kAlpha_8);
679 SkImageInfo ii = SkImageInfo::MakeA8(kWidth, 1);
680
681 SkBitmap bitmap;
682 bitmap.allocPixels(ii, kRowBytes);
683
684 unsigned char* values = (unsigned char*)bitmap.getPixels();
685 for (int i = 0; i < 128; ++i) {
686 SkScalar d = SK_Scalar1 - i / SkIntToScalar(127);
687 values[i] = SkScalarRoundToInt((SkScalarExp(-4 * d * d) - 0.018f) * 255);
688 }
689 bitmap.setImmutable();
690
Brian Salomonbc074a62020-03-18 10:06:13 -0400691 GrBitmapTextureMaker maker(context, bitmap, GrImageTexGenPolicy::kNew_Uncached_Budgeted);
Brian Salomon7e67dca2020-07-21 09:27:25 -0400692 auto view = maker.view(GrMipmapped::kNo);
Greg Daniel9f0dfbd2020-02-10 11:47:11 -0500693 SkASSERT(view.origin() == kTopLeft_GrSurfaceOrigin);
694
695 if (view) {
696 proxyProvider->assignUniqueKeyToProxy(key, view.asTextureProxy());
697 }
698 return view;
Jim Van Verth7da048b2019-10-29 13:28:14 -0400699}
700
701
Robert Phillipsb97da532019-02-12 15:24:12 -0500702std::unique_ptr<GrDrawOp> Make(GrRecordingContext* context,
Robert Phillips7c525e62018-06-12 10:11:12 -0400703 GrColor color,
Brian Salomon05969092017-07-13 11:20:51 -0400704 const SkMatrix& viewMatrix,
705 const SkRRect& rrect,
706 SkScalar blurWidth,
Jim Van Verthfb186392018-09-11 11:37:46 -0400707 SkScalar insetWidth) {
Brian Salomon53e4c3c2016-12-21 11:38:53 -0500708 // Shadow rrect ops only handle simple circular rrects.
Mike Reed242135a2018-02-22 13:41:39 -0500709 SkASSERT(viewMatrix.isSimilarity() && SkRRectPriv::EqualRadii(rrect));
Jim Van Verth57061ee2017-04-28 17:30:30 -0400710
Greg Daniel9f0dfbd2020-02-10 11:47:11 -0500711 GrSurfaceProxyView falloffView = create_falloff_texture(context);
712 if (!falloffView) {
Jim Van Verth7da048b2019-10-29 13:28:14 -0400713 return nullptr;
714 }
715
Brian Salomon53e4c3c2016-12-21 11:38:53 -0500716 // Do any matrix crunching before we reset the draw state for device coords.
Jim Van Verthc5903412016-11-17 15:27:09 -0500717 const SkRect& rrectBounds = rrect.getBounds();
718 SkRect bounds;
719 viewMatrix.mapRect(&bounds, rrectBounds);
720
Jim Van Verth8d1e0ac2017-05-05 15:53:23 -0400721 // Map radius and inset. As the matrix is a similarity matrix, this should be isotropic.
Mike Reed242135a2018-02-22 13:41:39 -0500722 SkScalar radius = SkRRectPriv::GetSimpleRadii(rrect).fX;
Jim Van Verth8d1e0ac2017-05-05 15:53:23 -0400723 SkScalar matrixFactor = viewMatrix[SkMatrix::kMScaleX] + viewMatrix[SkMatrix::kMSkewX];
724 SkScalar scaledRadius = SkScalarAbs(radius*matrixFactor);
725 SkScalar scaledInsetWidth = SkScalarAbs(insetWidth*matrixFactor);
Jim Van Verthc5903412016-11-17 15:27:09 -0500726
Robert Phillipse5763782019-04-17 14:38:24 -0400727 if (scaledInsetWidth <= 0) {
728 return nullptr;
729 }
730
Robert Phillips9da87e02019-02-04 13:26:26 -0500731 GrOpMemoryPool* pool = context->priv().opMemoryPool();
Robert Phillipsc994a932018-06-19 13:09:54 -0400732
733 return pool->allocate<ShadowCircularRRectOp>(color, bounds,
734 scaledRadius,
735 rrect.isOval(),
736 blurWidth,
Jim Van Verth7da048b2019-10-29 13:28:14 -0400737 scaledInsetWidth,
Greg Danielad994cd2019-12-10 09:35:16 -0500738 std::move(falloffView));
Jim Van Verthc5903412016-11-17 15:27:09 -0500739}
John Stilesa6841be2020-08-06 14:11:56 -0400740} // namespace GrShadowRRectOp
Jim Van Verth57061ee2017-04-28 17:30:30 -0400741
Jim Van Verthc5903412016-11-17 15:27:09 -0500742///////////////////////////////////////////////////////////////////////////////
743
Hal Canary6f6961e2017-01-31 13:50:44 -0500744#if GR_TEST_UTILS
Jim Van Verthc5903412016-11-17 15:27:09 -0500745
Brian Salomon05969092017-07-13 11:20:51 -0400746GR_DRAW_OP_TEST_DEFINE(ShadowRRectOp) {
Brian Salomonfc118442019-11-22 19:09:27 -0500747 // We may choose matrix and inset values that cause the factory to fail. We loop until we find
748 // an acceptable combination.
Brian Osman4462c042018-06-08 16:35:44 -0400749 do {
Brian Salomonfc118442019-11-22 19:09:27 -0500750 // create a similarity matrix
751 SkScalar rotate = random->nextSScalar1() * 360.f;
752 SkScalar translateX = random->nextSScalar1() * 1000.f;
753 SkScalar translateY = random->nextSScalar1() * 1000.f;
754 SkScalar scale = random->nextSScalar1() * 100.f;
755 SkMatrix viewMatrix;
756 viewMatrix.setRotate(rotate);
757 viewMatrix.postTranslate(translateX, translateY);
758 viewMatrix.postScale(scale, scale);
759 SkScalar insetWidth = random->nextSScalar1() * 72.f;
760 SkScalar blurWidth = random->nextSScalar1() * 72.f;
761 bool isCircle = random->nextBool();
762 // This op doesn't use a full GrPaint, just a color.
763 GrColor color = paint.getColor4f().toBytes_RGBA();
764 if (isCircle) {
765 SkRect circle = GrTest::TestSquare(random);
766 SkRRect rrect = SkRRect::MakeOval(circle);
767 if (auto op = GrShadowRRectOp::Make(
768 context, color, viewMatrix, rrect, blurWidth, insetWidth)) {
769 return op;
770 }
771 } else {
772 SkRRect rrect;
773 do {
774 // This may return a rrect with elliptical corners, which will cause an assert.
775 rrect = GrTest::TestRRectSimple(random);
776 } while (!SkRRectPriv::IsSimpleCircular(rrect));
777 if (auto op = GrShadowRRectOp::Make(
778 context, color, viewMatrix, rrect, blurWidth, insetWidth)) {
779 return op;
780 }
781 }
782 } while (true);
Jim Van Verthc5903412016-11-17 15:27:09 -0500783}
784
785#endif