blob: 52603688902762895888d5e655afb5403aab57ee [file] [log] [blame]
senorblancod6ed19c2015-02-26 06:58:17 -08001/*
2 * Copyright 2015 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8#include "GrTessellatingPathRenderer.h"
9
robertphillips976f5f02016-06-03 10:59:20 -070010#include "GrAuditTrail.h"
bsalomon75398562015-08-17 12:55:38 -070011#include "GrBatchFlushState.h"
joshualitt2fbd4062015-05-07 13:06:41 -070012#include "GrBatchTest.h"
robertphillips976f5f02016-06-03 10:59:20 -070013#include "GrClip.h"
senorblancod6ed19c2015-02-26 06:58:17 -080014#include "GrDefaultGeoProcFactory.h"
egdaniel0e1853c2016-03-17 11:35:45 -070015#include "GrMesh.h"
senorblancod6ed19c2015-02-26 06:58:17 -080016#include "GrPathUtils.h"
bsalomonbb243832016-07-22 07:10:19 -070017#include "GrPipelineBuilder.h"
senorblanco84cd6212015-08-04 10:01:58 -070018#include "GrResourceCache.h"
19#include "GrResourceProvider.h"
ethannicholase9709e82016-01-07 13:34:16 -080020#include "GrTessellator.h"
senorblancod6ed19c2015-02-26 06:58:17 -080021#include "SkGeometry.h"
22
bsalomon16b99132015-08-13 14:55:50 -070023#include "batches/GrVertexBatch.h"
joshualitt74417822015-08-07 11:42:16 -070024
senorblancod6ed19c2015-02-26 06:58:17 -080025#include <stdio.h>
26
27/*
senorblancof57372d2016-08-31 10:36:19 -070028 * This path renderer tessellates the path into triangles using GrTessellator, uploads the
29 * triangles to a vertex buffer, and renders them with a single draw call. It can do screenspace
30 * antialiasing with a one-pixel coverage ramp.
senorblancod6ed19c2015-02-26 06:58:17 -080031 */
senorblancod6ed19c2015-02-26 06:58:17 -080032namespace {
33
senorblanco84cd6212015-08-04 10:01:58 -070034struct TessInfo {
35 SkScalar fTolerance;
senorblanco06f989a2015-09-02 09:05:17 -070036 int fCount;
senorblanco84cd6212015-08-04 10:01:58 -070037};
38
ethannicholase9709e82016-01-07 13:34:16 -080039// When the SkPathRef genID changes, invalidate a corresponding GrResource described by key.
40class PathInvalidator : public SkPathRef::GenIDChangeListener {
41public:
42 explicit PathInvalidator(const GrUniqueKey& key) : fMsg(key) {}
43private:
44 GrUniqueKeyInvalidatedMessage fMsg;
45
46 void onChange() override {
47 SkMessageBus<GrUniqueKeyInvalidatedMessage>::Post(fMsg);
48 }
49};
50
cdalton397536c2016-03-25 12:15:03 -070051bool cache_match(GrBuffer* vertexBuffer, SkScalar tol, int* actualCount) {
senorblanco84cd6212015-08-04 10:01:58 -070052 if (!vertexBuffer) {
53 return false;
54 }
55 const SkData* data = vertexBuffer->getUniqueKey().getCustomData();
56 SkASSERT(data);
57 const TessInfo* info = static_cast<const TessInfo*>(data->data());
58 if (info->fTolerance == 0 || info->fTolerance < 3.0f * tol) {
senorblanco06f989a2015-09-02 09:05:17 -070059 *actualCount = info->fCount;
senorblanco84cd6212015-08-04 10:01:58 -070060 return true;
61 }
62 return false;
63}
64
senorblanco6599eff2016-03-10 08:38:45 -080065class StaticVertexAllocator : public GrTessellator::VertexAllocator {
66public:
senorblancof57372d2016-08-31 10:36:19 -070067 StaticVertexAllocator(size_t stride, GrResourceProvider* resourceProvider, bool canMapVB)
68 : VertexAllocator(stride)
69 , fResourceProvider(resourceProvider)
senorblanco6599eff2016-03-10 08:38:45 -080070 , fCanMapVB(canMapVB)
71 , fVertices(nullptr) {
72 }
senorblancof57372d2016-08-31 10:36:19 -070073 void* lock(int vertexCount) override {
74 size_t size = vertexCount * stride();
cdalton397536c2016-03-25 12:15:03 -070075 fVertexBuffer.reset(fResourceProvider->createBuffer(
cdaltone2e71c22016-04-07 18:13:29 -070076 size, kVertex_GrBufferType, kStatic_GrAccessPattern, 0));
senorblanco6599eff2016-03-10 08:38:45 -080077 if (!fVertexBuffer.get()) {
78 return nullptr;
79 }
80 if (fCanMapVB) {
senorblancof57372d2016-08-31 10:36:19 -070081 fVertices = fVertexBuffer->map();
senorblanco6599eff2016-03-10 08:38:45 -080082 } else {
senorblancof57372d2016-08-31 10:36:19 -070083 fVertices = sk_malloc_throw(vertexCount * stride());
senorblanco6599eff2016-03-10 08:38:45 -080084 }
85 return fVertices;
86 }
87 void unlock(int actualCount) override {
88 if (fCanMapVB) {
89 fVertexBuffer->unmap();
90 } else {
senorblancof57372d2016-08-31 10:36:19 -070091 fVertexBuffer->updateData(fVertices, actualCount * stride());
92 sk_free(fVertices);
senorblanco6599eff2016-03-10 08:38:45 -080093 }
94 fVertices = nullptr;
95 }
cdalton397536c2016-03-25 12:15:03 -070096 GrBuffer* vertexBuffer() { return fVertexBuffer.get(); }
senorblanco6599eff2016-03-10 08:38:45 -080097private:
Hal Canary144caf52016-11-07 17:57:18 -050098 sk_sp<GrBuffer> fVertexBuffer;
senorblanco6599eff2016-03-10 08:38:45 -080099 GrResourceProvider* fResourceProvider;
100 bool fCanMapVB;
senorblancof57372d2016-08-31 10:36:19 -0700101 void* fVertices;
102};
103
104class DynamicVertexAllocator : public GrTessellator::VertexAllocator {
105public:
106 DynamicVertexAllocator(size_t stride, GrVertexBatch::Target* target)
107 : VertexAllocator(stride)
108 , fTarget(target)
109 , fVertexBuffer(nullptr)
110 , fVertices(nullptr) {
111 }
112 void* lock(int vertexCount) override {
113 fVertexCount = vertexCount;
114 fVertices = fTarget->makeVertexSpace(stride(), vertexCount, &fVertexBuffer, &fFirstVertex);
115 return fVertices;
116 }
117 void unlock(int actualCount) override {
118 fTarget->putBackVertices(fVertexCount - actualCount, stride());
119 fVertices = nullptr;
120 }
121 const GrBuffer* vertexBuffer() const { return fVertexBuffer; }
122 int firstVertex() const { return fFirstVertex; }
123private:
124 GrVertexBatch::Target* fTarget;
125 const GrBuffer* fVertexBuffer;
126 int fVertexCount;
127 int fFirstVertex;
128 void* fVertices;
senorblanco6599eff2016-03-10 08:38:45 -0800129};
130
ethannicholase9709e82016-01-07 13:34:16 -0800131} // namespace
senorblancod6ed19c2015-02-26 06:58:17 -0800132
133GrTessellatingPathRenderer::GrTessellatingPathRenderer() {
134}
135
bsalomon0aff2fa2015-07-31 06:48:27 -0700136bool GrTessellatingPathRenderer::onCanDrawPath(const CanDrawPathArgs& args) const {
senorblancof57372d2016-08-31 10:36:19 -0700137 // This path renderer can draw fill styles, and can do screenspace antialiasing via a
138 // one-pixel coverage ramp. It can do convex and concave paths, but we'll leave the convex
139 // ones to simpler algorithms. We pass on paths that have styles, though they may come back
140 // around after applying the styling information to the geometry to create a filled path. In
141 // the non-AA case, We skip paths thta don't have a key since the real advantage of this path
142 // renderer comes from caching the tessellated geometry. In the AA case, we do not cache, so we
143 // accept paths without keys.
144 if (!args.fShape->style().isSimpleFill() || args.fShape->knownToBeConvex()) {
145 return false;
146 }
147 if (args.fAntiAlias) {
148#ifdef SK_DISABLE_SCREENSPACE_TESS_AA_PATH_RENDERER
149 return false;
150#else
151 SkPath path;
152 args.fShape->asPath(&path);
153 if (path.countVerbs() > 10) {
154 return false;
155 }
156#endif
157 } else if (!args.fShape->hasUnstyledKey()) {
158 return false;
159 }
160 return true;
senorblancod6ed19c2015-02-26 06:58:17 -0800161}
162
bsalomonabd30f52015-08-13 13:34:48 -0700163class TessellatingPathBatch : public GrVertexBatch {
senorblanco9ba39722015-03-05 07:13:42 -0800164public:
reed1b55a962015-09-17 20:16:13 -0700165 DEFINE_BATCH_CLASS_ID
senorblanco9ba39722015-03-05 07:13:42 -0800166
bsalomonabd30f52015-08-13 13:34:48 -0700167 static GrDrawBatch* Create(const GrColor& color,
bsalomonee432412016-06-27 07:18:18 -0700168 const GrShape& shape,
bsalomonabd30f52015-08-13 13:34:48 -0700169 const SkMatrix& viewMatrix,
senorblancof57372d2016-08-31 10:36:19 -0700170 SkIRect devClipBounds,
171 bool antiAlias) {
172 return new TessellatingPathBatch(color, shape, viewMatrix, devClipBounds, antiAlias);
senorblanco9ba39722015-03-05 07:13:42 -0800173 }
174
mtklein36352bf2015-03-25 18:17:31 -0700175 const char* name() const override { return "TessellatingPathBatch"; }
senorblanco9ba39722015-03-05 07:13:42 -0800176
halcanary9d524f22016-03-29 09:03:52 -0700177 void computePipelineOptimizations(GrInitInvariantOutput* color,
ethannicholasff210322015-11-24 12:10:10 -0800178 GrInitInvariantOutput* coverage,
179 GrBatchToXPOverrides* overrides) const override {
180 color->setKnownFourComponents(fColor);
181 coverage->setUnknownSingleComponent();
senorblanco9ba39722015-03-05 07:13:42 -0800182 }
183
bsalomone46f9fe2015-08-18 06:05:14 -0700184private:
ethannicholasff210322015-11-24 12:10:10 -0800185 void initBatchTracker(const GrXPOverridesForBatch& overrides) override {
senorblanco9ba39722015-03-05 07:13:42 -0800186 // Handle any color overrides
ethannicholasff210322015-11-24 12:10:10 -0800187 if (!overrides.readsColor()) {
senorblanco9ba39722015-03-05 07:13:42 -0800188 fColor = GrColor_ILLEGAL;
senorblanco9ba39722015-03-05 07:13:42 -0800189 }
ethannicholasff210322015-11-24 12:10:10 -0800190 overrides.getOverrideColorIfSet(&fColor);
191 fPipelineInfo = overrides;
senorblanco9ba39722015-03-05 07:13:42 -0800192 }
193
senorblancof57372d2016-08-31 10:36:19 -0700194 SkPath getPath() const {
195 SkASSERT(!fShape.style().applies());
bsalomonee432412016-06-27 07:18:18 -0700196 SkPath path;
197 fShape.asPath(&path);
senorblancof57372d2016-08-31 10:36:19 -0700198 return path;
199 }
200
201 void draw(Target* target, const GrGeometryProcessor* gp) const {
202 SkASSERT(!fAntiAlias);
203 GrResourceProvider* rp = target->resourceProvider();
204 bool inverseFill = fShape.inverseFilled();
senorblanco84cd6212015-08-04 10:01:58 -0700205 // construct a cache key from the path's genID and the view matrix
206 static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
207 GrUniqueKey key;
senorblancof57372d2016-08-31 10:36:19 -0700208 static constexpr int kClipBoundsCnt = sizeof(fDevClipBounds) / sizeof(uint32_t);
bsalomonee432412016-06-27 07:18:18 -0700209 int shapeKeyDataCnt = fShape.unstyledKeySize();
210 SkASSERT(shapeKeyDataCnt >= 0);
211 GrUniqueKey::Builder builder(&key, kDomain, shapeKeyDataCnt + kClipBoundsCnt);
212 fShape.writeUnstyledKey(&builder[0]);
213 // For inverse fills, the tessellation is dependent on clip bounds.
214 if (inverseFill) {
senorblancof57372d2016-08-31 10:36:19 -0700215 memcpy(&builder[shapeKeyDataCnt], &fDevClipBounds, sizeof(fDevClipBounds));
bsalomonee432412016-06-27 07:18:18 -0700216 } else {
senorblancof57372d2016-08-31 10:36:19 -0700217 memset(&builder[shapeKeyDataCnt], 0, sizeof(fDevClipBounds));
bsalomonee432412016-06-27 07:18:18 -0700218 }
219 builder.finish();
Hal Canary144caf52016-11-07 17:57:18 -0500220 sk_sp<GrBuffer> cachedVertexBuffer(rp->findAndRefTByUniqueKey<GrBuffer>(key));
bsalomonee432412016-06-27 07:18:18 -0700221 int actualCount;
senorblancof57372d2016-08-31 10:36:19 -0700222 SkScalar tol = GrPathUtils::kDefaultTolerance;
223 tol = GrPathUtils::scaleToleranceToSrc(tol, fViewMatrix, fShape.bounds());
bsalomonee432412016-06-27 07:18:18 -0700224 if (cache_match(cachedVertexBuffer.get(), tol, &actualCount)) {
225 this->drawVertices(target, gp, cachedVertexBuffer.get(), 0, actualCount);
226 return;
senorblanco84cd6212015-08-04 10:01:58 -0700227 }
228
senorblancof57372d2016-08-31 10:36:19 -0700229 SkRect clipBounds = SkRect::Make(fDevClipBounds);
230
231 SkMatrix vmi;
232 if (!fViewMatrix.invert(&vmi)) {
233 return;
234 }
235 vmi.mapRect(&clipBounds);
senorblanco6599eff2016-03-10 08:38:45 -0800236 bool isLinear;
237 bool canMapVB = GrCaps::kNone_MapFlags != target->caps().mapBufferFlags();
senorblancof57372d2016-08-31 10:36:19 -0700238 StaticVertexAllocator allocator(gp->getVertexStride(), rp, canMapVB);
239 int count = GrTessellator::PathToTriangles(getPath(), tol, clipBounds, &allocator,
240 false, GrColor(), false, &isLinear);
senorblanco6599eff2016-03-10 08:38:45 -0800241 if (count == 0) {
242 return;
243 }
senorblancoaf1e21e2016-03-16 10:25:58 -0700244 this->drawVertices(target, gp, allocator.vertexBuffer(), 0, count);
bsalomonee432412016-06-27 07:18:18 -0700245 TessInfo info;
246 info.fTolerance = isLinear ? 0 : tol;
247 info.fCount = count;
bungeman38d909e2016-08-02 14:40:46 -0700248 key.setCustomData(SkData::MakeWithCopy(&info, sizeof(info)));
bsalomonee432412016-06-27 07:18:18 -0700249 rp->assignUniqueKeyToResource(key, allocator.vertexBuffer());
senorblanco6599eff2016-03-10 08:38:45 -0800250 }
251
senorblancof57372d2016-08-31 10:36:19 -0700252 void drawAA(Target* target, const GrGeometryProcessor* gp) const {
253 SkASSERT(fAntiAlias);
254 SkPath path = getPath();
255 if (path.isEmpty()) {
256 return;
257 }
258 SkRect clipBounds = SkRect::Make(fDevClipBounds);
259 path.transform(fViewMatrix);
260 SkScalar tol = GrPathUtils::kDefaultTolerance;
261 bool isLinear;
262 DynamicVertexAllocator allocator(gp->getVertexStride(), target);
263 bool canTweakAlphaForCoverage = fPipelineInfo.canTweakAlphaForCoverage();
264 int count = GrTessellator::PathToTriangles(path, tol, clipBounds, &allocator,
265 true, fColor, canTweakAlphaForCoverage,
266 &isLinear);
267 if (count == 0) {
268 return;
269 }
270 drawVertices(target, gp, allocator.vertexBuffer(), allocator.firstVertex(), count);
271 }
272
senorblanco6599eff2016-03-10 08:38:45 -0800273 void onPrepareDraws(Target* target) const override {
bungeman06ca8ec2016-06-09 08:01:03 -0700274 sk_sp<GrGeometryProcessor> gp;
joshualittdf0c5572015-08-03 11:35:28 -0700275 {
276 using namespace GrDefaultGeoProcFactory;
277
278 Color color(fColor);
279 LocalCoords localCoords(fPipelineInfo.readsLocalCoords() ?
280 LocalCoords::kUsePosition_Type :
281 LocalCoords::kUnused_Type);
282 Coverage::Type coverageType;
senorblancof57372d2016-08-31 10:36:19 -0700283 if (fAntiAlias) {
284 color = Color(Color::kAttribute_Type);
285 if (fPipelineInfo.canTweakAlphaForCoverage()) {
286 coverageType = Coverage::kSolid_Type;
287 } else {
288 coverageType = Coverage::kAttribute_Type;
289 }
290 } else if (fPipelineInfo.readsCoverage()) {
joshualittdf0c5572015-08-03 11:35:28 -0700291 coverageType = Coverage::kSolid_Type;
292 } else {
293 coverageType = Coverage::kNone_Type;
294 }
295 Coverage coverage(coverageType);
senorblancof57372d2016-08-31 10:36:19 -0700296 if (fAntiAlias) {
297 gp = GrDefaultGeoProcFactory::MakeForDeviceSpace(color, coverage, localCoords,
298 fViewMatrix);
299 } else {
300 gp = GrDefaultGeoProcFactory::Make(color, coverage, localCoords, fViewMatrix);
301 }
joshualittdf0c5572015-08-03 11:35:28 -0700302 }
senorblancof57372d2016-08-31 10:36:19 -0700303 if (fAntiAlias) {
304 this->drawAA(target, gp.get());
305 } else {
306 this->draw(target, gp.get());
307 }
senorblanco6599eff2016-03-10 08:38:45 -0800308 }
senorblanco84cd6212015-08-04 10:01:58 -0700309
cdalton397536c2016-03-25 12:15:03 -0700310 void drawVertices(Target* target, const GrGeometryProcessor* gp, const GrBuffer* vb,
senorblanco6599eff2016-03-10 08:38:45 -0800311 int firstVertex, int count) const {
ethannicholase9709e82016-01-07 13:34:16 -0800312 GrPrimitiveType primitiveType = TESSELLATOR_WIREFRAME ? kLines_GrPrimitiveType
313 : kTriangles_GrPrimitiveType;
egdaniel0e1853c2016-03-17 11:35:45 -0700314 GrMesh mesh;
315 mesh.init(primitiveType, vb, firstVertex, count);
bsalomon342bfc22016-04-01 06:06:20 -0700316 target->draw(gp, mesh);
senorblanco9ba39722015-03-05 07:13:42 -0800317 }
318
bsalomoncb02b382015-08-12 11:14:50 -0700319 bool onCombineIfPossible(GrBatch*, const GrCaps&) override { return false; }
senorblanco9ba39722015-03-05 07:13:42 -0800320
senorblanco9ba39722015-03-05 07:13:42 -0800321 TessellatingPathBatch(const GrColor& color,
bsalomonee432412016-06-27 07:18:18 -0700322 const GrShape& shape,
senorblanco9ba39722015-03-05 07:13:42 -0800323 const SkMatrix& viewMatrix,
senorblancof57372d2016-08-31 10:36:19 -0700324 const SkIRect& devClipBounds,
325 bool antiAlias)
reed1b55a962015-09-17 20:16:13 -0700326 : INHERITED(ClassID())
327 , fColor(color)
bsalomonee432412016-06-27 07:18:18 -0700328 , fShape(shape)
senorblancof57372d2016-08-31 10:36:19 -0700329 , fViewMatrix(viewMatrix)
330 , fDevClipBounds(devClipBounds)
331 , fAntiAlias(antiAlias) {
332 SkRect devBounds;
333 viewMatrix.mapRect(&devBounds, shape.bounds());
334 if (shape.inverseFilled()) {
335 // Because the clip bounds are used to add a contour for inverse fills, they must also
336 // include the path bounds.
337 devBounds.join(SkRect::Make(fDevClipBounds));
338 }
339 this->setBounds(devBounds, HasAABloat::kNo, IsZeroArea::kNo);
senorblanco9ba39722015-03-05 07:13:42 -0800340 }
341
bsalomon91d844d2015-08-10 10:47:29 -0700342 GrColor fColor;
bsalomonee432412016-06-27 07:18:18 -0700343 GrShape fShape;
bsalomon91d844d2015-08-10 10:47:29 -0700344 SkMatrix fViewMatrix;
senorblancof57372d2016-08-31 10:36:19 -0700345 SkIRect fDevClipBounds;
346 bool fAntiAlias;
ethannicholasff210322015-11-24 12:10:10 -0800347 GrXPOverridesForBatch fPipelineInfo;
reed1b55a962015-09-17 20:16:13 -0700348
349 typedef GrVertexBatch INHERITED;
senorblanco9ba39722015-03-05 07:13:42 -0800350};
351
bsalomon0aff2fa2015-07-31 06:48:27 -0700352bool GrTessellatingPathRenderer::onDrawPath(const DrawPathArgs& args) {
Brian Osman11052242016-10-27 14:47:55 -0400353 GR_AUDIT_TRAIL_AUTO_FRAME(args.fRenderTargetContext->auditTrail(),
joshualittde83b412016-01-14 09:58:36 -0800354 "GrTessellatingPathRenderer::onDrawPath");
senorblancod6ed19c2015-02-26 06:58:17 -0800355 SkIRect clipBoundsI;
Brian Osman11052242016-10-27 14:47:55 -0400356 args.fClip->getConservativeBounds(args.fRenderTargetContext->width(),
357 args.fRenderTargetContext->height(),
robertphillips976f5f02016-06-03 10:59:20 -0700358 &clipBoundsI);
Hal Canary144caf52016-11-07 17:57:18 -0500359 sk_sp<GrDrawBatch> batch(TessellatingPathBatch::Create(args.fPaint->getColor(),
360 *args.fShape,
361 *args.fViewMatrix,
362 clipBoundsI,
363 args.fAntiAlias));
robertphillips976f5f02016-06-03 10:59:20 -0700364
Brian Osman11052242016-10-27 14:47:55 -0400365 GrPipelineBuilder pipelineBuilder(*args.fPaint,
366 args.fRenderTargetContext->mustUseHWAA(*args.fPaint));
bsalomonbb243832016-07-22 07:10:19 -0700367 pipelineBuilder.setUserStencil(args.fUserStencilSettings);
368
Hal Canary144caf52016-11-07 17:57:18 -0500369 args.fRenderTargetContext->drawBatch(pipelineBuilder, *args.fClip, batch.get());
senorblancod6ed19c2015-02-26 06:58:17 -0800370
371 return true;
372}
joshualitt2fbd4062015-05-07 13:06:41 -0700373
374///////////////////////////////////////////////////////////////////////////////////////////////////
375
376#ifdef GR_TEST_UTILS
377
bsalomonabd30f52015-08-13 13:34:48 -0700378DRAW_BATCH_TEST_DEFINE(TesselatingPathBatch) {
joshualitt2fbd4062015-05-07 13:06:41 -0700379 GrColor color = GrRandomColor(random);
380 SkMatrix viewMatrix = GrTest::TestMatrixInvertible(random);
381 SkPath path = GrTest::TestPath(random);
bsalomond3030ac2016-09-01 07:20:29 -0700382 SkIRect devClipBounds = SkIRect::MakeLTRB(
senorblancof57372d2016-08-31 10:36:19 -0700383 random->nextU(), random->nextU(), random->nextU(), random->nextU());
bsalomond3030ac2016-09-01 07:20:29 -0700384 devClipBounds.sort();
senorblancof57372d2016-08-31 10:36:19 -0700385 bool antiAlias = random->nextBool();
bsalomon6663acf2016-05-10 09:14:17 -0700386 GrStyle style;
387 do {
388 GrTest::TestStyle(random, &style);
senorblancof57372d2016-08-31 10:36:19 -0700389 } while (!style.isSimpleFill());
bsalomonee432412016-06-27 07:18:18 -0700390 GrShape shape(path, style);
senorblancof57372d2016-08-31 10:36:19 -0700391 return TessellatingPathBatch::Create(color, shape, viewMatrix, devClipBounds, antiAlias);
joshualitt2fbd4062015-05-07 13:06:41 -0700392}
393
394#endif