• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /****************************************************************************
2  * Copyright (C) 2014-2018 Intel Corporation.   All Rights Reserved.
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a
5  * copy of this software and associated documentation files (the "Software"),
6  * to deal in the Software without restriction, including without limitation
7  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8  * and/or sell copies of the Software, and to permit persons to whom the
9  * Software is furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice (including the next
12  * paragraph) shall be included in all copies or substantial portions of the
13  * Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
18  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21  * IN THE SOFTWARE.
22  *
23  * @file rasterizer.cpp
24  *
25  * @brief Implementation for the rasterizer.
26  *
27  ******************************************************************************/
28 
29 #include <vector>
30 #include <algorithm>
31 
32 #include "rasterizer.h"
33 #include "backends/gen_rasterizer.hpp"
34 #include "rdtsc_core.h"
35 #include "backend.h"
36 #include "utils.h"
37 #include "frontend.h"
38 #include "tilemgr.h"
39 #include "memory/tilingtraits.h"
40 #include "rasterizer_impl.h"
41 
42 PFN_WORK_FUNC gRasterizerFuncs[SWR_MULTISAMPLE_TYPE_COUNT][2][2][SWR_INPUT_COVERAGE_COUNT]
43                               [STATE_VALID_TRI_EDGE_COUNT][2];
44 
RasterizeLine(DRAW_CONTEXT * pDC,uint32_t workerId,uint32_t macroTile,void * pData)45 void RasterizeLine(DRAW_CONTEXT* pDC, uint32_t workerId, uint32_t macroTile, void* pData)
46 {
47     const TRIANGLE_WORK_DESC& workDesc = *((TRIANGLE_WORK_DESC*)pData);
48 #if KNOB_ENABLE_TOSS_POINTS
49     if (KNOB_TOSS_BIN_TRIS)
50     {
51         return;
52     }
53 #endif
54 
55     // bloat line to two tris and call the triangle rasterizer twice
56     RDTSC_BEGIN(pDC->pContext->pBucketMgr, BERasterizeLine, pDC->drawId);
57 
58     const API_STATE&     state     = GetApiState(pDC);
59     const SWR_RASTSTATE& rastState = state.rastState;
60 
61     // macrotile dimensioning
62     uint32_t macroX, macroY;
63     MacroTileMgr::getTileIndices(macroTile, macroX, macroY);
64     int32_t macroBoxLeft   = macroX * KNOB_MACROTILE_X_DIM_FIXED;
65     int32_t macroBoxRight  = macroBoxLeft + KNOB_MACROTILE_X_DIM_FIXED - 1;
66     int32_t macroBoxTop    = macroY * KNOB_MACROTILE_Y_DIM_FIXED;
67     int32_t macroBoxBottom = macroBoxTop + KNOB_MACROTILE_Y_DIM_FIXED - 1;
68 
69     const SWR_RECT& scissorInFixedPoint =
70         state.scissorsInFixedPoint[workDesc.triFlags.viewportIndex];
71 
72     // create a copy of the triangle buffer to write our adjusted vertices to
73     OSALIGNSIMD(float) newTriBuffer[4 * 4];
74     TRIANGLE_WORK_DESC newWorkDesc = workDesc;
75     newWorkDesc.pTriBuffer         = &newTriBuffer[0];
76 
77     // create a copy of the attrib buffer to write our adjusted attribs to
78     OSALIGNSIMD(float) newAttribBuffer[4 * 3 * SWR_VTX_NUM_SLOTS];
79     newWorkDesc.pAttribs = &newAttribBuffer[0];
80 
81     const __m128 vBloat0 = _mm_set_ps(0.5f, -0.5f, -0.5f, 0.5f);
82     const __m128 vBloat1 = _mm_set_ps(0.5f, 0.5f, 0.5f, -0.5f);
83 
84     __m128 vX, vY, vZ, vRecipW;
85 
86     vX      = _mm_load_ps(workDesc.pTriBuffer);
87     vY      = _mm_load_ps(workDesc.pTriBuffer + 4);
88     vZ      = _mm_load_ps(workDesc.pTriBuffer + 8);
89     vRecipW = _mm_load_ps(workDesc.pTriBuffer + 12);
90 
91     // triangle 0
92     // v0,v1 -> v0,v0,v1
93     __m128 vXa      = _mm_shuffle_ps(vX, vX, _MM_SHUFFLE(1, 1, 0, 0));
94     __m128 vYa      = _mm_shuffle_ps(vY, vY, _MM_SHUFFLE(1, 1, 0, 0));
95     __m128 vZa      = _mm_shuffle_ps(vZ, vZ, _MM_SHUFFLE(1, 1, 0, 0));
96     __m128 vRecipWa = _mm_shuffle_ps(vRecipW, vRecipW, _MM_SHUFFLE(1, 1, 0, 0));
97 
98     __m128 vLineWidth = _mm_set1_ps(pDC->pState->state.rastState.lineWidth);
99     __m128 vAdjust    = _mm_mul_ps(vLineWidth, vBloat0);
100     if (workDesc.triFlags.yMajor)
101     {
102         vXa = _mm_add_ps(vAdjust, vXa);
103     }
104     else
105     {
106         vYa = _mm_add_ps(vAdjust, vYa);
107     }
108 
109     // Store triangle description for rasterizer
110     _mm_store_ps((float*)&newTriBuffer[0], vXa);
111     _mm_store_ps((float*)&newTriBuffer[4], vYa);
112     _mm_store_ps((float*)&newTriBuffer[8], vZa);
113     _mm_store_ps((float*)&newTriBuffer[12], vRecipWa);
114 
115     // binner bins 3 edges for lines as v0, v1, v1
116     // tri0 needs v0, v0, v1
117     for (uint32_t a = 0; a < workDesc.numAttribs; ++a)
118     {
119         __m128 vAttrib0 = _mm_load_ps(&workDesc.pAttribs[a * 12 + 0]);
120         __m128 vAttrib1 = _mm_load_ps(&workDesc.pAttribs[a * 12 + 4]);
121 
122         _mm_store_ps((float*)&newAttribBuffer[a * 12 + 0], vAttrib0);
123         _mm_store_ps((float*)&newAttribBuffer[a * 12 + 4], vAttrib0);
124         _mm_store_ps((float*)&newAttribBuffer[a * 12 + 8], vAttrib1);
125     }
126 
127     // Store user clip distances for triangle 0
128     float    newClipBuffer[3 * 8];
129     uint32_t numClipDist = _mm_popcnt_u32(state.backendState.clipDistanceMask);
130     if (numClipDist)
131     {
132         newWorkDesc.pUserClipBuffer = newClipBuffer;
133 
134         float* pOldBuffer = workDesc.pUserClipBuffer;
135         float* pNewBuffer = newClipBuffer;
136         for (uint32_t i = 0; i < numClipDist; ++i)
137         {
138             // read barycentric coeffs from binner
139             float a = *(pOldBuffer++);
140             float b = *(pOldBuffer++);
141 
142             // reconstruct original clip distance at vertices
143             float c0 = a + b;
144             float c1 = b;
145 
146             // construct triangle barycentrics
147             *(pNewBuffer++) = c0 - c1;
148             *(pNewBuffer++) = c0 - c1;
149             *(pNewBuffer++) = c1;
150         }
151     }
152 
153     // setup triangle rasterizer function
154     PFN_WORK_FUNC pfnTriRast;
155     // conservative rast not supported for points/lines
156     pfnTriRast = GetRasterizerFunc(rastState.sampleCount,
157                                    rastState.bIsCenterPattern,
158                                    false,
159                                    SWR_INPUT_COVERAGE_NONE,
160                                    EdgeValToEdgeState(ALL_EDGES_VALID),
161                                    (pDC->pState->state.scissorsTileAligned == false));
162 
163     // make sure this macrotile intersects the triangle
164     __m128i vXai = fpToFixedPoint(vXa);
165     __m128i vYai = fpToFixedPoint(vYa);
166     OSALIGNSIMD(SWR_RECT) bboxA;
167     calcBoundingBoxInt(vXai, vYai, bboxA);
168 
169     if (!(bboxA.xmin > macroBoxRight || bboxA.xmin > scissorInFixedPoint.xmax ||
170           bboxA.xmax - 1 < macroBoxLeft || bboxA.xmax - 1 < scissorInFixedPoint.xmin ||
171           bboxA.ymin > macroBoxBottom || bboxA.ymin > scissorInFixedPoint.ymax ||
172           bboxA.ymax - 1 < macroBoxTop || bboxA.ymax - 1 < scissorInFixedPoint.ymin))
173     {
174         // rasterize triangle
175         pfnTriRast(pDC, workerId, macroTile, (void*)&newWorkDesc);
176     }
177 
178     // triangle 1
179     // v0,v1 -> v1,v1,v0
180     vXa      = _mm_shuffle_ps(vX, vX, _MM_SHUFFLE(1, 0, 1, 1));
181     vYa      = _mm_shuffle_ps(vY, vY, _MM_SHUFFLE(1, 0, 1, 1));
182     vZa      = _mm_shuffle_ps(vZ, vZ, _MM_SHUFFLE(1, 0, 1, 1));
183     vRecipWa = _mm_shuffle_ps(vRecipW, vRecipW, _MM_SHUFFLE(1, 0, 1, 1));
184 
185     vAdjust = _mm_mul_ps(vLineWidth, vBloat1);
186     if (workDesc.triFlags.yMajor)
187     {
188         vXa = _mm_add_ps(vAdjust, vXa);
189     }
190     else
191     {
192         vYa = _mm_add_ps(vAdjust, vYa);
193     }
194 
195     // Store triangle description for rasterizer
196     _mm_store_ps((float*)&newTriBuffer[0], vXa);
197     _mm_store_ps((float*)&newTriBuffer[4], vYa);
198     _mm_store_ps((float*)&newTriBuffer[8], vZa);
199     _mm_store_ps((float*)&newTriBuffer[12], vRecipWa);
200 
201     // binner bins 3 edges for lines as v0, v1, v1
202     // tri1 needs v1, v1, v0
203     for (uint32_t a = 0; a < workDesc.numAttribs; ++a)
204     {
205         __m128 vAttrib0 = _mm_load_ps(&workDesc.pAttribs[a * 12 + 0]);
206         __m128 vAttrib1 = _mm_load_ps(&workDesc.pAttribs[a * 12 + 4]);
207 
208         _mm_store_ps((float*)&newAttribBuffer[a * 12 + 0], vAttrib1);
209         _mm_store_ps((float*)&newAttribBuffer[a * 12 + 4], vAttrib1);
210         _mm_store_ps((float*)&newAttribBuffer[a * 12 + 8], vAttrib0);
211     }
212 
213     // store user clip distance for triangle 1
214     if (numClipDist)
215     {
216         float* pOldBuffer = workDesc.pUserClipBuffer;
217         float* pNewBuffer = newClipBuffer;
218         for (uint32_t i = 0; i < numClipDist; ++i)
219         {
220             // read barycentric coeffs from binner
221             float a = *(pOldBuffer++);
222             float b = *(pOldBuffer++);
223 
224             // reconstruct original clip distance at vertices
225             float c0 = a + b;
226             float c1 = b;
227 
228             // construct triangle barycentrics
229             *(pNewBuffer++) = c1 - c0;
230             *(pNewBuffer++) = c1 - c0;
231             *(pNewBuffer++) = c0;
232         }
233     }
234 
235     vXai = fpToFixedPoint(vXa);
236     vYai = fpToFixedPoint(vYa);
237     calcBoundingBoxInt(vXai, vYai, bboxA);
238 
239     if (!(bboxA.xmin > macroBoxRight || bboxA.xmin > scissorInFixedPoint.xmax ||
240           bboxA.xmax - 1 < macroBoxLeft || bboxA.xmax - 1 < scissorInFixedPoint.xmin ||
241           bboxA.ymin > macroBoxBottom || bboxA.ymin > scissorInFixedPoint.ymax ||
242           bboxA.ymax - 1 < macroBoxTop || bboxA.ymax - 1 < scissorInFixedPoint.ymin))
243     {
244         // rasterize triangle
245         pfnTriRast(pDC, workerId, macroTile, (void*)&newWorkDesc);
246     }
247 
248     RDTSC_BEGIN(pDC->pContext->pBucketMgr, BERasterizeLine, 1);
249 }
250 
RasterizeSimplePoint(DRAW_CONTEXT * pDC,uint32_t workerId,uint32_t macroTile,void * pData)251 void RasterizeSimplePoint(DRAW_CONTEXT* pDC, uint32_t workerId, uint32_t macroTile, void* pData)
252 {
253 #if KNOB_ENABLE_TOSS_POINTS
254     if (KNOB_TOSS_BIN_TRIS)
255     {
256         return;
257     }
258 #endif
259 
260     const TRIANGLE_WORK_DESC& workDesc     = *(const TRIANGLE_WORK_DESC*)pData;
261     const BACKEND_FUNCS&      backendFuncs = pDC->pState->backendFuncs;
262 
263     // map x,y relative offsets from start of raster tile to bit position in
264     // coverage mask for the point
265     static const uint32_t coverageMap[8][8] = {{0, 1, 4, 5, 8, 9, 12, 13},
266                                                {2, 3, 6, 7, 10, 11, 14, 15},
267                                                {16, 17, 20, 21, 24, 25, 28, 29},
268                                                {18, 19, 22, 23, 26, 27, 30, 31},
269                                                {32, 33, 36, 37, 40, 41, 44, 45},
270                                                {34, 35, 38, 39, 42, 43, 46, 47},
271                                                {48, 49, 52, 53, 56, 57, 60, 61},
272                                                {50, 51, 54, 55, 58, 59, 62, 63}};
273 
274     OSALIGNSIMD(SWR_TRIANGLE_DESC) triDesc = {};
275 
276     // pull point information from triangle buffer
277     // @todo use structs for readability
278     uint32_t tileAlignedX = *(uint32_t*)workDesc.pTriBuffer;
279     uint32_t tileAlignedY = *(uint32_t*)(workDesc.pTriBuffer + 1);
280     float    z            = *(workDesc.pTriBuffer + 2);
281 
282     // construct triangle descriptor for point
283     // no interpolation, set up i,j for constant interpolation of z and attribs
284     // @todo implement an optimized backend that doesn't require triangle information
285 
286     // compute coverage mask from x,y packed into the coverageMask flag
287     // mask indices by the maximum valid index for x/y of coveragemap.
288     uint32_t tX = workDesc.triFlags.coverageMask & 0x7;
289     uint32_t tY = (workDesc.triFlags.coverageMask >> 4) & 0x7;
290     for (uint32_t i = 0; i < _countof(triDesc.coverageMask); ++i)
291     {
292         triDesc.coverageMask[i] = 1ULL << coverageMap[tY][tX];
293     }
294     triDesc.anyCoveredSamples = triDesc.coverageMask[0];
295     triDesc.innerCoverageMask = triDesc.coverageMask[0];
296 
297     // no persp divide needed for points
298     triDesc.pAttribs = triDesc.pPerspAttribs = workDesc.pAttribs;
299     triDesc.triFlags                         = workDesc.triFlags;
300     triDesc.recipDet                         = 1.0f;
301     triDesc.OneOverW[0] = triDesc.OneOverW[1] = triDesc.OneOverW[2] = 1.0f;
302     triDesc.I[0] = triDesc.I[1] = triDesc.I[2] = 0.0f;
303     triDesc.J[0] = triDesc.J[1] = triDesc.J[2] = 0.0f;
304     triDesc.Z[0] = triDesc.Z[1] = triDesc.Z[2] = z;
305 
306     RenderOutputBuffers renderBuffers;
307     GetRenderHotTiles(pDC,
308                       workerId,
309                       macroTile,
310                       tileAlignedX >> KNOB_TILE_X_DIM_SHIFT,
311                       tileAlignedY >> KNOB_TILE_Y_DIM_SHIFT,
312                       renderBuffers,
313                       triDesc.triFlags.renderTargetArrayIndex);
314 
315     RDTSC_BEGIN(pDC->pContext->pBucketMgr, BEPixelBackend, pDC->drawId);
316     backendFuncs.pfnBackend(pDC, workerId, tileAlignedX, tileAlignedY, triDesc, renderBuffers);
317     RDTSC_END(pDC->pContext->pBucketMgr, BEPixelBackend, 0);
318 }
319 
RasterizeTriPoint(DRAW_CONTEXT * pDC,uint32_t workerId,uint32_t macroTile,void * pData)320 void RasterizeTriPoint(DRAW_CONTEXT* pDC, uint32_t workerId, uint32_t macroTile, void* pData)
321 {
322     const TRIANGLE_WORK_DESC& workDesc     = *(const TRIANGLE_WORK_DESC*)pData;
323     const SWR_RASTSTATE&      rastState    = pDC->pState->state.rastState;
324     const SWR_BACKEND_STATE&  backendState = pDC->pState->state.backendState;
325 
326     bool isPointSpriteTexCoordEnabled = backendState.pointSpriteTexCoordMask != 0;
327 
328     // load point vertex
329     float x = *workDesc.pTriBuffer;
330     float y = *(workDesc.pTriBuffer + 1);
331     float z = *(workDesc.pTriBuffer + 2);
332 
333     // create a copy of the triangle buffer to write our adjusted vertices to
334     OSALIGNSIMD(float) newTriBuffer[4 * 4];
335     TRIANGLE_WORK_DESC newWorkDesc = workDesc;
336     newWorkDesc.pTriBuffer         = &newTriBuffer[0];
337 
338     // create a copy of the attrib buffer to write our adjusted attribs to
339     OSALIGNSIMD(float) newAttribBuffer[4 * 3 * SWR_VTX_NUM_SLOTS];
340     newWorkDesc.pAttribs = &newAttribBuffer[0];
341 
342     newWorkDesc.pUserClipBuffer = workDesc.pUserClipBuffer;
343     newWorkDesc.numAttribs      = workDesc.numAttribs;
344     newWorkDesc.triFlags        = workDesc.triFlags;
345 
346     // construct two tris by bloating point by point size
347     float halfPointSize = workDesc.triFlags.pointSize * 0.5f;
348     float lowerX        = x - halfPointSize;
349     float upperX        = x + halfPointSize;
350     float lowerY        = y - halfPointSize;
351     float upperY        = y + halfPointSize;
352 
353     // tri 0
354     float* pBuf = &newTriBuffer[0];
355     *pBuf++     = lowerX;
356     *pBuf++     = lowerX;
357     *pBuf++     = upperX;
358     pBuf++;
359     *pBuf++ = lowerY;
360     *pBuf++ = upperY;
361     *pBuf++ = upperY;
362     pBuf++;
363     _mm_store_ps(pBuf, _mm_set1_ps(z));
364     _mm_store_ps(pBuf += 4, _mm_set1_ps(1.0f));
365 
366     // setup triangle rasterizer function
367     PFN_WORK_FUNC pfnTriRast;
368     // conservative rast not supported for points/lines
369     pfnTriRast = GetRasterizerFunc(rastState.sampleCount,
370                                    rastState.bIsCenterPattern,
371                                    false,
372                                    SWR_INPUT_COVERAGE_NONE,
373                                    EdgeValToEdgeState(ALL_EDGES_VALID),
374                                    (pDC->pState->state.scissorsTileAligned == false));
375 
376     // overwrite texcoords for point sprites
377     if (isPointSpriteTexCoordEnabled)
378     {
379         // copy original attribs
380         memcpy(&newAttribBuffer[0], workDesc.pAttribs, 4 * 3 * workDesc.numAttribs * sizeof(float));
381         newWorkDesc.pAttribs = &newAttribBuffer[0];
382 
383         // overwrite texcoord for point sprites
384         uint32_t texCoordMask   = backendState.pointSpriteTexCoordMask;
385         unsigned long texCoordAttrib = 0;
386 
387         while (_BitScanForward(&texCoordAttrib, texCoordMask))
388         {
389             texCoordMask &= ~(1 << texCoordAttrib);
390             __m128* pTexAttrib = (__m128*)&newAttribBuffer[0] + 3 * texCoordAttrib;
391             if (rastState.pointSpriteTopOrigin)
392             {
393                 pTexAttrib[0] = _mm_set_ps(1, 0, 0, 0);
394                 pTexAttrib[1] = _mm_set_ps(1, 0, 1, 0);
395                 pTexAttrib[2] = _mm_set_ps(1, 0, 1, 1);
396             }
397             else
398             {
399                 pTexAttrib[0] = _mm_set_ps(1, 0, 1, 0);
400                 pTexAttrib[1] = _mm_set_ps(1, 0, 0, 0);
401                 pTexAttrib[2] = _mm_set_ps(1, 0, 0, 1);
402             }
403         }
404     }
405     else
406     {
407         // no texcoord overwrite, can reuse the attrib buffer from frontend
408         newWorkDesc.pAttribs = workDesc.pAttribs;
409     }
410 
411     pfnTriRast(pDC, workerId, macroTile, (void*)&newWorkDesc);
412 
413     // tri 1
414     pBuf    = &newTriBuffer[0];
415     *pBuf++ = lowerX;
416     *pBuf++ = upperX;
417     *pBuf++ = upperX;
418     pBuf++;
419     *pBuf++ = lowerY;
420     *pBuf++ = upperY;
421     *pBuf++ = lowerY;
422     // z, w unchanged
423 
424     if (isPointSpriteTexCoordEnabled)
425     {
426         uint32_t texCoordMask   = backendState.pointSpriteTexCoordMask;
427         unsigned long texCoordAttrib = 0;
428 
429         while (_BitScanForward(&texCoordAttrib, texCoordMask))
430         {
431             texCoordMask &= ~(1 << texCoordAttrib);
432             __m128* pTexAttrib = (__m128*)&newAttribBuffer[0] + 3 * texCoordAttrib;
433             if (rastState.pointSpriteTopOrigin)
434             {
435                 pTexAttrib[0] = _mm_set_ps(1, 0, 0, 0);
436                 pTexAttrib[1] = _mm_set_ps(1, 0, 1, 1);
437                 pTexAttrib[2] = _mm_set_ps(1, 0, 0, 1);
438             }
439             else
440             {
441                 pTexAttrib[0] = _mm_set_ps(1, 0, 1, 0);
442                 pTexAttrib[1] = _mm_set_ps(1, 0, 0, 1);
443                 pTexAttrib[2] = _mm_set_ps(1, 0, 1, 1);
444             }
445         }
446     }
447 
448     pfnTriRast(pDC, workerId, macroTile, (void*)&newWorkDesc);
449 }
450 
InitRasterizerFunctions()451 void InitRasterizerFunctions()
452 {
453     InitRasterizerFuncs();
454 }
455 
456 // Selector for correct templated RasterizeTriangle function
GetRasterizerFunc(SWR_MULTISAMPLE_COUNT numSamples,bool IsCenter,bool IsConservative,SWR_INPUT_COVERAGE InputCoverage,uint32_t EdgeEnable,bool RasterizeScissorEdges)457 PFN_WORK_FUNC GetRasterizerFunc(SWR_MULTISAMPLE_COUNT numSamples,
458                                 bool                  IsCenter,
459                                 bool                  IsConservative,
460                                 SWR_INPUT_COVERAGE    InputCoverage,
461                                 uint32_t              EdgeEnable,
462                                 bool                  RasterizeScissorEdges)
463 {
464     SWR_ASSERT(numSamples >= 0 && numSamples < SWR_MULTISAMPLE_TYPE_COUNT);
465     SWR_ASSERT(InputCoverage >= 0 && InputCoverage < SWR_INPUT_COVERAGE_COUNT);
466     SWR_ASSERT(EdgeEnable < STATE_VALID_TRI_EDGE_COUNT);
467 
468     PFN_WORK_FUNC func = gRasterizerFuncs[numSamples][IsCenter][IsConservative][InputCoverage]
469                                          [EdgeEnable][RasterizeScissorEdges];
470     SWR_ASSERT(func);
471 
472     return func;
473 }
474