| 1 | /* | 
|---|
| 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 "src/gpu/ops/GrDrawAtlasOp.h" | 
|---|
| 9 |  | 
|---|
| 10 | #include "include/core/SkRSXform.h" | 
|---|
| 11 | #include "include/private/GrRecordingContext.h" | 
|---|
| 12 | #include "include/utils/SkRandom.h" | 
|---|
| 13 | #include "src/core/SkRectPriv.h" | 
|---|
| 14 | #include "src/gpu/GrCaps.h" | 
|---|
| 15 | #include "src/gpu/GrDefaultGeoProcFactory.h" | 
|---|
| 16 | #include "src/gpu/GrDrawOpTest.h" | 
|---|
| 17 | #include "src/gpu/GrOpFlushState.h" | 
|---|
| 18 | #include "src/gpu/GrProgramInfo.h" | 
|---|
| 19 | #include "src/gpu/GrRecordingContextPriv.h" | 
|---|
| 20 | #include "src/gpu/SkGr.h" | 
|---|
| 21 | #include "src/gpu/ops/GrSimpleMeshDrawOpHelper.h" | 
|---|
| 22 |  | 
|---|
| 23 | namespace { | 
|---|
| 24 |  | 
|---|
| 25 | class DrawAtlasOp final : public GrMeshDrawOp { | 
|---|
| 26 | private: | 
|---|
| 27 | using Helper = GrSimpleMeshDrawOpHelper; | 
|---|
| 28 |  | 
|---|
| 29 | public: | 
|---|
| 30 | DEFINE_OP_CLASS_ID | 
|---|
| 31 |  | 
|---|
| 32 | DrawAtlasOp(const Helper::MakeArgs&, const SkPMColor4f& color, | 
|---|
| 33 | const SkMatrix& viewMatrix, GrAAType, int spriteCount, const SkRSXform* xforms, | 
|---|
| 34 | const SkRect* rects, const SkColor* colors); | 
|---|
| 35 |  | 
|---|
| 36 | const char* name() const override { return "DrawAtlasOp"; } | 
|---|
| 37 |  | 
|---|
| 38 | void visitProxies(const VisitProxyFunc& func) const override { | 
|---|
| 39 | if (fProgramInfo) { | 
|---|
| 40 | fProgramInfo->visitFPProxies(func); | 
|---|
| 41 | } else { | 
|---|
| 42 | fHelper.visitProxies(func); | 
|---|
| 43 | } | 
|---|
| 44 | } | 
|---|
| 45 |  | 
|---|
| 46 | #ifdef SK_DEBUG | 
|---|
| 47 | SkString dumpInfo() const override; | 
|---|
| 48 | #endif | 
|---|
| 49 |  | 
|---|
| 50 | FixedFunctionFlags fixedFunctionFlags() const override; | 
|---|
| 51 |  | 
|---|
| 52 | GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*, | 
|---|
| 53 | bool hasMixedSampledCoverage, GrClampType) override; | 
|---|
| 54 |  | 
|---|
| 55 | private: | 
|---|
| 56 | GrProgramInfo* programInfo() override { return fProgramInfo; } | 
|---|
| 57 |  | 
|---|
| 58 | void onCreateProgramInfo(const GrCaps*, | 
|---|
| 59 | SkArenaAlloc*, | 
|---|
| 60 | const GrSurfaceProxyView* writeView, | 
|---|
| 61 | GrAppliedClip&&, | 
|---|
| 62 | const GrXferProcessor::DstProxyView&) override; | 
|---|
| 63 |  | 
|---|
| 64 | void onPrepareDraws(Target*) override; | 
|---|
| 65 | void onExecute(GrOpFlushState*, const SkRect& chainBounds) override; | 
|---|
| 66 |  | 
|---|
| 67 | const SkPMColor4f& color() const { return fColor; } | 
|---|
| 68 | const SkMatrix& viewMatrix() const { return fViewMatrix; } | 
|---|
| 69 | bool hasColors() const { return fHasColors; } | 
|---|
| 70 | int quadCount() const { return fQuadCount; } | 
|---|
| 71 |  | 
|---|
| 72 | CombineResult onCombineIfPossible(GrOp* t, GrRecordingContext::Arenas*, const GrCaps&) override; | 
|---|
| 73 |  | 
|---|
| 74 | struct Geometry { | 
|---|
| 75 | SkPMColor4f fColor; | 
|---|
| 76 | SkTArray<uint8_t, true> fVerts; | 
|---|
| 77 | }; | 
|---|
| 78 |  | 
|---|
| 79 | SkSTArray<1, Geometry, true> fGeoData; | 
|---|
| 80 | Helper fHelper; | 
|---|
| 81 | SkMatrix fViewMatrix; | 
|---|
| 82 | SkPMColor4f fColor; | 
|---|
| 83 | int fQuadCount; | 
|---|
| 84 | bool fHasColors; | 
|---|
| 85 |  | 
|---|
| 86 | GrSimpleMesh* fMesh = nullptr; | 
|---|
| 87 | GrProgramInfo* fProgramInfo = nullptr; | 
|---|
| 88 |  | 
|---|
| 89 | typedef GrMeshDrawOp INHERITED; | 
|---|
| 90 | }; | 
|---|
| 91 |  | 
|---|
| 92 | static GrGeometryProcessor* make_gp(SkArenaAlloc* arena, | 
|---|
| 93 | bool hasColors, | 
|---|
| 94 | const SkPMColor4f& color, | 
|---|
| 95 | const SkMatrix& viewMatrix) { | 
|---|
| 96 | using namespace GrDefaultGeoProcFactory; | 
|---|
| 97 | Color gpColor(color); | 
|---|
| 98 | if (hasColors) { | 
|---|
| 99 | gpColor.fType = Color::kPremulGrColorAttribute_Type; | 
|---|
| 100 | } | 
|---|
| 101 |  | 
|---|
| 102 | return GrDefaultGeoProcFactory::Make(arena, gpColor, Coverage::kSolid_Type, | 
|---|
| 103 | LocalCoords::kHasExplicit_Type, viewMatrix); | 
|---|
| 104 | } | 
|---|
| 105 |  | 
|---|
| 106 | DrawAtlasOp::DrawAtlasOp(const Helper::MakeArgs& helperArgs, const SkPMColor4f& color, | 
|---|
| 107 | const SkMatrix& viewMatrix, GrAAType aaType, int spriteCount, | 
|---|
| 108 | const SkRSXform* xforms, const SkRect* rects, const SkColor* colors) | 
|---|
| 109 | : INHERITED(ClassID()), fHelper(helperArgs, aaType), fColor(color) { | 
|---|
| 110 | SkASSERT(xforms); | 
|---|
| 111 | SkASSERT(rects); | 
|---|
| 112 |  | 
|---|
| 113 | fViewMatrix = viewMatrix; | 
|---|
| 114 | Geometry& installedGeo = fGeoData.push_back(); | 
|---|
| 115 | installedGeo.fColor = color; | 
|---|
| 116 |  | 
|---|
| 117 | // Figure out stride and offsets | 
|---|
| 118 | // Order within the vertex is: position [color] texCoord | 
|---|
| 119 | size_t texOffset = sizeof(SkPoint); | 
|---|
| 120 | size_t vertexStride = 2 * sizeof(SkPoint); | 
|---|
| 121 | fHasColors = SkToBool(colors); | 
|---|
| 122 | if (colors) { | 
|---|
| 123 | texOffset += sizeof(GrColor); | 
|---|
| 124 | vertexStride += sizeof(GrColor); | 
|---|
| 125 | } | 
|---|
| 126 |  | 
|---|
| 127 | // Compute buffer size and alloc buffer | 
|---|
| 128 | fQuadCount = spriteCount; | 
|---|
| 129 | int allocSize = static_cast<int>(4 * vertexStride * spriteCount); | 
|---|
| 130 | installedGeo.fVerts.reset(allocSize); | 
|---|
| 131 | uint8_t* currVertex = installedGeo.fVerts.begin(); | 
|---|
| 132 |  | 
|---|
| 133 | SkRect bounds = SkRectPriv::MakeLargestInverted(); | 
|---|
| 134 | // TODO4F: Preserve float colors | 
|---|
| 135 | int paintAlpha = GrColorUnpackA(installedGeo.fColor.toBytes_RGBA()); | 
|---|
| 136 | for (int spriteIndex = 0; spriteIndex < spriteCount; ++spriteIndex) { | 
|---|
| 137 | // Transform rect | 
|---|
| 138 | SkPoint strip[4]; | 
|---|
| 139 | const SkRect& currRect = rects[spriteIndex]; | 
|---|
| 140 | xforms[spriteIndex].toTriStrip(currRect.width(), currRect.height(), strip); | 
|---|
| 141 |  | 
|---|
| 142 | // Copy colors if necessary | 
|---|
| 143 | if (colors) { | 
|---|
| 144 | // convert to GrColor | 
|---|
| 145 | SkColor color = colors[spriteIndex]; | 
|---|
| 146 | if (paintAlpha != 255) { | 
|---|
| 147 | color = SkColorSetA(color, SkMulDiv255Round(SkColorGetA(color), paintAlpha)); | 
|---|
| 148 | } | 
|---|
| 149 | GrColor grColor = SkColorToPremulGrColor(color); | 
|---|
| 150 |  | 
|---|
| 151 | *(reinterpret_cast<GrColor*>(currVertex + sizeof(SkPoint))) = grColor; | 
|---|
| 152 | *(reinterpret_cast<GrColor*>(currVertex + vertexStride + sizeof(SkPoint))) = grColor; | 
|---|
| 153 | *(reinterpret_cast<GrColor*>(currVertex + 2 * vertexStride + sizeof(SkPoint))) = | 
|---|
| 154 | grColor; | 
|---|
| 155 | *(reinterpret_cast<GrColor*>(currVertex + 3 * vertexStride + sizeof(SkPoint))) = | 
|---|
| 156 | grColor; | 
|---|
| 157 | } | 
|---|
| 158 |  | 
|---|
| 159 | // Copy position and uv to verts | 
|---|
| 160 | *(reinterpret_cast<SkPoint*>(currVertex)) = strip[0]; | 
|---|
| 161 | *(reinterpret_cast<SkPoint*>(currVertex + texOffset)) = | 
|---|
| 162 | SkPoint::Make(currRect.fLeft, currRect.fTop); | 
|---|
| 163 | SkRectPriv::GrowToInclude(&bounds, strip[0]); | 
|---|
| 164 | currVertex += vertexStride; | 
|---|
| 165 |  | 
|---|
| 166 | *(reinterpret_cast<SkPoint*>(currVertex)) = strip[1]; | 
|---|
| 167 | *(reinterpret_cast<SkPoint*>(currVertex + texOffset)) = | 
|---|
| 168 | SkPoint::Make(currRect.fLeft, currRect.fBottom); | 
|---|
| 169 | SkRectPriv::GrowToInclude(&bounds, strip[1]); | 
|---|
| 170 | currVertex += vertexStride; | 
|---|
| 171 |  | 
|---|
| 172 | *(reinterpret_cast<SkPoint*>(currVertex)) = strip[2]; | 
|---|
| 173 | *(reinterpret_cast<SkPoint*>(currVertex + texOffset)) = | 
|---|
| 174 | SkPoint::Make(currRect.fRight, currRect.fTop); | 
|---|
| 175 | SkRectPriv::GrowToInclude(&bounds, strip[2]); | 
|---|
| 176 | currVertex += vertexStride; | 
|---|
| 177 |  | 
|---|
| 178 | *(reinterpret_cast<SkPoint*>(currVertex)) = strip[3]; | 
|---|
| 179 | *(reinterpret_cast<SkPoint*>(currVertex + texOffset)) = | 
|---|
| 180 | SkPoint::Make(currRect.fRight, currRect.fBottom); | 
|---|
| 181 | SkRectPriv::GrowToInclude(&bounds, strip[3]); | 
|---|
| 182 | currVertex += vertexStride; | 
|---|
| 183 | } | 
|---|
| 184 |  | 
|---|
| 185 | this->setTransformedBounds(bounds, viewMatrix, HasAABloat::kNo, IsHairline::kNo); | 
|---|
| 186 | } | 
|---|
| 187 |  | 
|---|
| 188 | #ifdef SK_DEBUG | 
|---|
| 189 | SkString DrawAtlasOp::dumpInfo() const { | 
|---|
| 190 | SkString string; | 
|---|
| 191 | for (const auto& geo : fGeoData) { | 
|---|
| 192 | string.appendf( "Color: 0x%08x, Quads: %d\n", geo.fColor.toBytes_RGBA(), | 
|---|
| 193 | geo.fVerts.count() / 4); | 
|---|
| 194 | } | 
|---|
| 195 | string += fHelper.dumpInfo(); | 
|---|
| 196 | string += INHERITED::dumpInfo(); | 
|---|
| 197 | return string; | 
|---|
| 198 | } | 
|---|
| 199 | #endif | 
|---|
| 200 |  | 
|---|
| 201 | void DrawAtlasOp::onCreateProgramInfo(const GrCaps* caps, | 
|---|
| 202 | SkArenaAlloc* arena, | 
|---|
| 203 | const GrSurfaceProxyView* writeView, | 
|---|
| 204 | GrAppliedClip&& appliedClip, | 
|---|
| 205 | const GrXferProcessor::DstProxyView& dstProxyView) { | 
|---|
| 206 | // Setup geometry processor | 
|---|
| 207 | GrGeometryProcessor* gp = make_gp(arena, | 
|---|
| 208 | this->hasColors(), | 
|---|
| 209 | this->color(), | 
|---|
| 210 | this->viewMatrix()); | 
|---|
| 211 |  | 
|---|
| 212 | fProgramInfo = fHelper.createProgramInfo(caps, arena, writeView, std::move(appliedClip), | 
|---|
| 213 | dstProxyView, gp, GrPrimitiveType::kTriangles); | 
|---|
| 214 | } | 
|---|
| 215 |  | 
|---|
| 216 | void DrawAtlasOp::onPrepareDraws(Target* target) { | 
|---|
| 217 | if (!fProgramInfo) { | 
|---|
| 218 | this->createProgramInfo(target); | 
|---|
| 219 | } | 
|---|
| 220 |  | 
|---|
| 221 | int instanceCount = fGeoData.count(); | 
|---|
| 222 | size_t vertexStride = fProgramInfo->primProc().vertexStride(); | 
|---|
| 223 |  | 
|---|
| 224 | int numQuads = this->quadCount(); | 
|---|
| 225 | QuadHelper helper(target, vertexStride, numQuads); | 
|---|
| 226 | void* verts = helper.vertices(); | 
|---|
| 227 | if (!verts) { | 
|---|
| 228 | SkDebugf( "Could not allocate vertices\n"); | 
|---|
| 229 | return; | 
|---|
| 230 | } | 
|---|
| 231 |  | 
|---|
| 232 | uint8_t* vertPtr = reinterpret_cast<uint8_t*>(verts); | 
|---|
| 233 | for (int i = 0; i < instanceCount; i++) { | 
|---|
| 234 | const Geometry& args = fGeoData[i]; | 
|---|
| 235 |  | 
|---|
| 236 | size_t allocSize = args.fVerts.count(); | 
|---|
| 237 | memcpy(vertPtr, args.fVerts.begin(), allocSize); | 
|---|
| 238 | vertPtr += allocSize; | 
|---|
| 239 | } | 
|---|
| 240 |  | 
|---|
| 241 | fMesh = helper.mesh(); | 
|---|
| 242 | } | 
|---|
| 243 |  | 
|---|
| 244 | void DrawAtlasOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) { | 
|---|
| 245 | if (!fProgramInfo || !fMesh) { | 
|---|
| 246 | return; | 
|---|
| 247 | } | 
|---|
| 248 |  | 
|---|
| 249 | flushState->bindPipelineAndScissorClip(*fProgramInfo, chainBounds); | 
|---|
| 250 | flushState->bindTextures(fProgramInfo->primProc(), nullptr, fProgramInfo->pipeline()); | 
|---|
| 251 | flushState->drawMesh(*fMesh); | 
|---|
| 252 | } | 
|---|
| 253 |  | 
|---|
| 254 | GrOp::CombineResult DrawAtlasOp::onCombineIfPossible(GrOp* t, GrRecordingContext::Arenas*, | 
|---|
| 255 | const GrCaps& caps) { | 
|---|
| 256 | DrawAtlasOp* that = t->cast<DrawAtlasOp>(); | 
|---|
| 257 |  | 
|---|
| 258 | if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) { | 
|---|
| 259 | return CombineResult::kCannotCombine; | 
|---|
| 260 | } | 
|---|
| 261 |  | 
|---|
| 262 | // We currently use a uniform viewmatrix for this op. | 
|---|
| 263 | if (!SkMatrixPriv::CheapEqual(this->viewMatrix(), that->viewMatrix())) { | 
|---|
| 264 | return CombineResult::kCannotCombine; | 
|---|
| 265 | } | 
|---|
| 266 |  | 
|---|
| 267 | if (this->hasColors() != that->hasColors()) { | 
|---|
| 268 | return CombineResult::kCannotCombine; | 
|---|
| 269 | } | 
|---|
| 270 |  | 
|---|
| 271 | if (!this->hasColors() && this->color() != that->color()) { | 
|---|
| 272 | return CombineResult::kCannotCombine; | 
|---|
| 273 | } | 
|---|
| 274 |  | 
|---|
| 275 | fGeoData.push_back_n(that->fGeoData.count(), that->fGeoData.begin()); | 
|---|
| 276 | fQuadCount += that->quadCount(); | 
|---|
| 277 |  | 
|---|
| 278 | return CombineResult::kMerged; | 
|---|
| 279 | } | 
|---|
| 280 |  | 
|---|
| 281 | GrDrawOp::FixedFunctionFlags DrawAtlasOp::fixedFunctionFlags() const { | 
|---|
| 282 | return fHelper.fixedFunctionFlags(); | 
|---|
| 283 | } | 
|---|
| 284 |  | 
|---|
| 285 | GrProcessorSet::Analysis DrawAtlasOp::finalize( | 
|---|
| 286 | const GrCaps& caps, const GrAppliedClip* clip, bool hasMixedSampledCoverage, | 
|---|
| 287 | GrClampType clampType) { | 
|---|
| 288 | GrProcessorAnalysisColor gpColor; | 
|---|
| 289 | if (this->hasColors()) { | 
|---|
| 290 | gpColor.setToUnknown(); | 
|---|
| 291 | } else { | 
|---|
| 292 | gpColor.setToConstant(fColor); | 
|---|
| 293 | } | 
|---|
| 294 | auto result = fHelper.finalizeProcessors(caps, clip, hasMixedSampledCoverage, clampType, | 
|---|
| 295 | GrProcessorAnalysisCoverage::kNone, &gpColor); | 
|---|
| 296 | if (gpColor.isConstant(&fColor)) { | 
|---|
| 297 | fHasColors = false; | 
|---|
| 298 | } | 
|---|
| 299 | return result; | 
|---|
| 300 | } | 
|---|
| 301 |  | 
|---|
| 302 | } // anonymous namespace | 
|---|
| 303 |  | 
|---|
| 304 | std::unique_ptr<GrDrawOp> GrDrawAtlasOp::Make(GrRecordingContext* context, | 
|---|
| 305 | GrPaint&& paint, | 
|---|
| 306 | const SkMatrix& viewMatrix, | 
|---|
| 307 | GrAAType aaType, | 
|---|
| 308 | int spriteCount, | 
|---|
| 309 | const SkRSXform* xforms, | 
|---|
| 310 | const SkRect* rects, | 
|---|
| 311 | const SkColor* colors) { | 
|---|
| 312 | return GrSimpleMeshDrawOpHelper::FactoryHelper<DrawAtlasOp>(context, std::move(paint), | 
|---|
| 313 | viewMatrix, aaType, | 
|---|
| 314 | spriteCount, xforms, | 
|---|
| 315 | rects, colors); | 
|---|
| 316 | } | 
|---|
| 317 |  | 
|---|
| 318 | #if GR_TEST_UTILS | 
|---|
| 319 |  | 
|---|
| 320 | static SkRSXform random_xform(SkRandom* random) { | 
|---|
| 321 | static const SkScalar kMinExtent = -100.f; | 
|---|
| 322 | static const SkScalar kMaxExtent = 100.f; | 
|---|
| 323 | static const SkScalar kMinScale = 0.1f; | 
|---|
| 324 | static const SkScalar kMaxScale = 100.f; | 
|---|
| 325 | static const SkScalar kMinRotate = -SK_ScalarPI; | 
|---|
| 326 | static const SkScalar kMaxRotate = SK_ScalarPI; | 
|---|
| 327 |  | 
|---|
| 328 | SkRSXform xform = SkRSXform::MakeFromRadians(random->nextRangeScalar(kMinScale, kMaxScale), | 
|---|
| 329 | random->nextRangeScalar(kMinRotate, kMaxRotate), | 
|---|
| 330 | random->nextRangeScalar(kMinExtent, kMaxExtent), | 
|---|
| 331 | random->nextRangeScalar(kMinExtent, kMaxExtent), | 
|---|
| 332 | random->nextRangeScalar(kMinExtent, kMaxExtent), | 
|---|
| 333 | random->nextRangeScalar(kMinExtent, kMaxExtent)); | 
|---|
| 334 | return xform; | 
|---|
| 335 | } | 
|---|
| 336 |  | 
|---|
| 337 | static SkRect random_texRect(SkRandom* random) { | 
|---|
| 338 | static const SkScalar kMinCoord = 0.0f; | 
|---|
| 339 | static const SkScalar kMaxCoord = 1024.f; | 
|---|
| 340 |  | 
|---|
| 341 | SkRect texRect = SkRect::MakeLTRB(random->nextRangeScalar(kMinCoord, kMaxCoord), | 
|---|
| 342 | random->nextRangeScalar(kMinCoord, kMaxCoord), | 
|---|
| 343 | random->nextRangeScalar(kMinCoord, kMaxCoord), | 
|---|
| 344 | random->nextRangeScalar(kMinCoord, kMaxCoord)); | 
|---|
| 345 | texRect.sort(); | 
|---|
| 346 | return texRect; | 
|---|
| 347 | } | 
|---|
| 348 |  | 
|---|
| 349 | static void randomize_params(uint32_t count, SkRandom* random, SkTArray<SkRSXform>* xforms, | 
|---|
| 350 | SkTArray<SkRect>* texRects, SkTArray<GrColor>* colors, | 
|---|
| 351 | bool hasColors) { | 
|---|
| 352 | for (uint32_t v = 0; v < count; v++) { | 
|---|
| 353 | xforms->push_back(random_xform(random)); | 
|---|
| 354 | texRects->push_back(random_texRect(random)); | 
|---|
| 355 | if (hasColors) { | 
|---|
| 356 | colors->push_back(GrRandomColor(random)); | 
|---|
| 357 | } | 
|---|
| 358 | } | 
|---|
| 359 | } | 
|---|
| 360 |  | 
|---|
| 361 | GR_DRAW_OP_TEST_DEFINE(DrawAtlasOp) { | 
|---|
| 362 | uint32_t spriteCount = random->nextRangeU(1, 100); | 
|---|
| 363 |  | 
|---|
| 364 | SkTArray<SkRSXform> xforms(spriteCount); | 
|---|
| 365 | SkTArray<SkRect> texRects(spriteCount); | 
|---|
| 366 | SkTArray<GrColor> colors; | 
|---|
| 367 |  | 
|---|
| 368 | bool hasColors = random->nextBool(); | 
|---|
| 369 |  | 
|---|
| 370 | randomize_params(spriteCount, random, &xforms, &texRects, &colors, hasColors); | 
|---|
| 371 |  | 
|---|
| 372 | SkMatrix viewMatrix = GrTest::TestMatrix(random); | 
|---|
| 373 | GrAAType aaType = GrAAType::kNone; | 
|---|
| 374 | if (numSamples > 1 && random->nextBool()) { | 
|---|
| 375 | aaType = GrAAType::kMSAA; | 
|---|
| 376 | } | 
|---|
| 377 |  | 
|---|
| 378 | return GrDrawAtlasOp::Make(context, std::move(paint), viewMatrix, aaType, spriteCount, | 
|---|
| 379 | xforms.begin(), texRects.begin(), | 
|---|
| 380 | hasColors ? colors.begin() : nullptr); | 
|---|
| 381 | } | 
|---|
| 382 |  | 
|---|
| 383 | #endif | 
|---|
| 384 |  | 
|---|