2009-10-21 15:20:49 -05:00
|
|
|
/*
|
|
|
|
* Test clipping for Present and Surface-to-Screen blits.
|
|
|
|
*
|
|
|
|
* This example requires SVGA Screen Object and SVGA3D support.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "svga.h"
|
|
|
|
#include "svga3d.h"
|
|
|
|
#include "svga3dutil.h"
|
|
|
|
#include "matrix.h"
|
|
|
|
#include "math.h"
|
|
|
|
#include "gmr.h"
|
|
|
|
#include "screen.h"
|
|
|
|
#include "intr.h"
|
|
|
|
#include "screendraw.h"
|
|
|
|
#include "console_vga.h"
|
|
|
|
|
|
|
|
/*
|
|
|
|
* 3D Rendering Definitions
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
float position[3];
|
|
|
|
uint32 color;
|
|
|
|
} MyVertex;
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
int numRects;
|
|
|
|
SVGASignedRect rects[2048];
|
|
|
|
} ClipBuffer;
|
|
|
|
|
|
|
|
uint32 vertexSid, indexSid;
|
|
|
|
ClipBuffer circles[2];
|
|
|
|
|
|
|
|
const int surfWidth = 224;
|
|
|
|
const int surfHeight = 168;
|
|
|
|
|
|
|
|
SVGA3dSurfaceImageId colorImage;
|
|
|
|
SVGA3dSurfaceImageId depthImage;
|
|
|
|
|
|
|
|
static const MyVertex vertexData[] = {
|
|
|
|
{ {-1, -1, -1}, 0xffffff },
|
|
|
|
{ {-1, -1, 1}, 0xffff00 },
|
|
|
|
{ {-1, 1, -1}, 0xff00ff },
|
|
|
|
{ {-1, 1, 1}, 0xff0000 },
|
|
|
|
{ { 1, -1, -1}, 0x00ffff },
|
|
|
|
{ { 1, -1, 1}, 0x00ff00 },
|
|
|
|
{ { 1, 1, -1}, 0x0000ff },
|
|
|
|
{ { 1, 1, 1}, 0x000000 },
|
|
|
|
};
|
|
|
|
|
|
|
|
static const uint16 indexData[] = {
|
|
|
|
0, 1, 1, 3, 3, 2, 2, 0, // -X
|
|
|
|
4, 5, 5, 7, 7, 6, 6, 4, // +X
|
|
|
|
0, 4,
|
|
|
|
1, 5,
|
|
|
|
2, 6,
|
|
|
|
3, 7,
|
|
|
|
};
|
|
|
|
|
|
|
|
const uint32 numLines = arraysize(indexData) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* initScreens --
|
|
|
|
*
|
|
|
|
* Set up our Screen Objects, and label them.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void
|
|
|
|
initScreens(void)
|
|
|
|
{
|
2011-09-19 08:21:29 -05:00
|
|
|
static SVGAScreenObject screen = {
|
2009-10-21 15:20:49 -05:00
|
|
|
.structSize = sizeof(SVGAScreenObject),
|
|
|
|
.id = 0,
|
|
|
|
.flags = SVGA_SCREEN_HAS_ROOT | SVGA_SCREEN_IS_PRIMARY,
|
|
|
|
.size = { 1024, 768 },
|
|
|
|
.root = { 1000, 2000 },
|
|
|
|
};
|
|
|
|
|
2011-09-19 08:21:29 -05:00
|
|
|
Screen_Create(&screen);
|
2009-10-21 15:20:49 -05:00
|
|
|
|
|
|
|
ScreenDraw_SetScreen(screen.id, screen.size.width, screen.size.height);
|
|
|
|
Console_Clear();
|
|
|
|
|
|
|
|
Console_Format("Surface-to-Screen Blit Clipping Test\n");
|
|
|
|
ScreenDraw_Border(0, 0, screen.size.width, screen.size.height, 0xFF0000, 1);
|
|
|
|
|
|
|
|
Console_MoveTo(20, 45);
|
|
|
|
Console_Format("Stair-step clipping (small tiles)");
|
|
|
|
|
|
|
|
Console_MoveTo(20, 245);
|
|
|
|
Console_Format("Top/bottom halves swapped");
|
|
|
|
|
|
|
|
Console_MoveTo(20, 445);
|
|
|
|
Console_Format("Scaled bottom half, with hole");
|
|
|
|
|
|
|
|
Console_MoveTo(350, 65);
|
|
|
|
Console_Format("Zoomed to 1.5x full screen, two circular clip regions");
|
|
|
|
|
|
|
|
Console_MoveTo(5, 660);
|
|
|
|
Console_Format("Stair-step, clipped against screen edges");
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* presentWithClipBuf --
|
|
|
|
*
|
|
|
|
* Present our surface to the screen, with clipping data from a ClipBuffer.
|
|
|
|
*
|
|
|
|
* The supplied ClipBuffer is always in screen coordinates. We
|
|
|
|
* convert them into dest-relative coordinates for the
|
|
|
|
* surface-to-screen blit.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void
|
|
|
|
presentWithClipBuf(ClipBuffer *buf, int dstL, int dstT, int dstR, int dstB)
|
|
|
|
{
|
|
|
|
SVGASignedRect srcRect = { 0, 0, surfWidth, surfHeight };
|
|
|
|
SVGASignedRect dstRect = { dstL, dstT, dstR, dstB };
|
|
|
|
SVGASignedRect *clip;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
SVGA3D_BeginBlitSurfaceToScreen(&colorImage, &srcRect, 0,
|
|
|
|
&dstRect, &clip, buf->numRects);
|
|
|
|
|
|
|
|
for (i = 0; i < buf->numRects; i++) {
|
|
|
|
clip->left = buf->rects[i].left - dstL;
|
|
|
|
clip->top = buf->rects[i].top - dstT;
|
|
|
|
clip->right = buf->rects[i].right - dstL;
|
|
|
|
clip->bottom = buf->rects[i].bottom - dstT;
|
|
|
|
clip++;
|
|
|
|
}
|
|
|
|
|
|
|
|
SVGA_FIFOCommitAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* prepareCircle --
|
|
|
|
*
|
|
|
|
* Prepare a ClipBuffer with a circular clip region, and draw an
|
|
|
|
* outline around the region.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void
|
|
|
|
prepareCircle(ClipBuffer *buf, int centerX, int centerY, int radius)
|
|
|
|
{
|
|
|
|
int r, i;
|
|
|
|
|
|
|
|
i = 0;
|
|
|
|
for (r = -radius; r <= radius; r++) {
|
|
|
|
int chordRadius = __builtin_sqrtf(radius * radius - r * r) + 0.5f;
|
|
|
|
SVGASignedRect *rect = &buf->rects[i++];
|
|
|
|
|
|
|
|
rect->left = centerX - chordRadius;
|
|
|
|
rect->top = centerY - r;
|
|
|
|
rect->right = centerX + chordRadius;
|
|
|
|
rect->bottom = centerY - r + 1;
|
|
|
|
|
|
|
|
ScreenDraw_Rectangle(rect->left - 1, rect->top - 1,
|
|
|
|
rect->right + 1, rect->bottom + 1,
|
|
|
|
0xffffff);
|
|
|
|
}
|
|
|
|
|
|
|
|
buf->numRects = i;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* presentStairStep --
|
|
|
|
*
|
|
|
|
* Use a non-scaled Present to draw many small square tiles, and
|
|
|
|
* clip the edge to a stair-step pattern. This tests performance
|
|
|
|
* for large numbers of clip rectangles, and it will make any edge
|
|
|
|
* artifacts very noticeable.
|
|
|
|
*
|
|
|
|
* This is a set of copyrects where all of the sources line up,
|
|
|
|
* so it is also expressable as a clip rectangle.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void
|
|
|
|
presentStairStep(int xOffset, int yOffset)
|
|
|
|
{
|
|
|
|
const int gridSize = 16;
|
|
|
|
const int numRects = (gridSize + 1) * gridSize / 2;
|
|
|
|
const int squareWidth = surfWidth / gridSize;
|
|
|
|
const int squareHeight = surfHeight / gridSize;
|
|
|
|
int x, y, i;
|
|
|
|
SVGA3dCopyRect *cr;
|
|
|
|
|
|
|
|
SVGA3D_BeginPresent(colorImage.sid, &cr, numRects);
|
|
|
|
i = 0;
|
|
|
|
for (x = 0; x < gridSize; x++) {
|
|
|
|
for (y = 0; y < gridSize; y++) {
|
|
|
|
if (x + y < gridSize) {
|
|
|
|
|
|
|
|
cr[i].srcx = x * squareWidth;
|
|
|
|
cr[i].srcy = y * squareHeight;
|
|
|
|
cr[i].x = cr[i].srcx + xOffset;
|
|
|
|
cr[i].y = cr[i].srcy + yOffset;
|
|
|
|
cr[i].w = squareWidth;
|
|
|
|
cr[i].h = squareHeight;
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (i != numRects) {
|
|
|
|
SVGA_Panic("Incorrect numRects in present()");
|
|
|
|
}
|
|
|
|
SVGA_FIFOCommitAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* present --
|
|
|
|
*
|
|
|
|
* Copy our rendered cube to the screen. This is where we test clipping.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void
|
|
|
|
present(void)
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* Main stair-step unscaled present test.
|
|
|
|
*/
|
|
|
|
presentStairStep(1020, 2065);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Another non-scaled present, this time using the copyrects in a
|
|
|
|
* way which is not also expressable as a clip rectangle. In this
|
|
|
|
* case, we're using one Present to split the image in half (top
|
|
|
|
* and bottom) and reverse the two halves.
|
|
|
|
*/
|
|
|
|
{
|
|
|
|
SVGA3dCopyRect *cr;
|
|
|
|
|
|
|
|
SVGA3D_BeginPresent(colorImage.sid, &cr, 2);
|
|
|
|
|
|
|
|
cr[0].srcx = 0;
|
|
|
|
cr[0].srcy = surfHeight / 2;
|
|
|
|
cr[0].x = 1020;
|
|
|
|
cr[0].y = 2265;
|
|
|
|
cr[0].w = surfWidth;
|
|
|
|
cr[0].h = surfHeight / 2;
|
|
|
|
|
|
|
|
cr[1].srcx = 0;
|
|
|
|
cr[1].srcy = 0;
|
|
|
|
cr[1].x = 1020;
|
|
|
|
cr[1].y = 2265 + surfHeight / 2;
|
|
|
|
cr[1].w = surfWidth;
|
|
|
|
cr[1].h = surfHeight / 2;
|
|
|
|
|
|
|
|
SVGA_FIFOCommitAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* A fairly normal scaled blit. This one is only slightly scaled, unlike the
|
|
|
|
* large one below- so it may be easier to see a different class of bugs.
|
|
|
|
* For clipping, we remove a hole from the center of the image.
|
|
|
|
*
|
|
|
|
* We also test source clipping by displaying the bottom half.
|
|
|
|
*/
|
|
|
|
{
|
|
|
|
SVGASignedRect *clip;
|
|
|
|
|
|
|
|
SVGASignedRect srcRect = { 0, surfHeight/2, surfWidth, surfHeight };
|
|
|
|
SVGASignedRect dstRect = { 20, 465, 325, 655 };
|
|
|
|
|
|
|
|
SVGA3D_BeginBlitSurfaceToScreen(&colorImage, &srcRect, 0,
|
|
|
|
&dstRect, &clip, 4);
|
|
|
|
|
|
|
|
// Top
|
|
|
|
clip[0].left = 0;
|
|
|
|
clip[0].top = 0;
|
|
|
|
clip[0].right = 445;
|
|
|
|
clip[0].bottom = 75;
|
|
|
|
|
|
|
|
// Bottom
|
|
|
|
clip[1].left = 0;
|
|
|
|
clip[1].top = 115;
|
|
|
|
clip[1].right = 445;
|
|
|
|
clip[1].bottom = 330;
|
|
|
|
|
|
|
|
// Left
|
|
|
|
clip[2].left = 0;
|
|
|
|
clip[2].top = 75;
|
|
|
|
clip[2].right = 63;
|
|
|
|
clip[2].bottom = 115;
|
|
|
|
|
|
|
|
// Right
|
|
|
|
clip[3].left = 242;
|
|
|
|
clip[3].top = 75;
|
|
|
|
clip[3].right = 305;
|
|
|
|
clip[3].bottom = 115;
|
|
|
|
|
|
|
|
SVGA_FIFOCommitAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Stair-step, clipped against the bottom and left sides of the screen.
|
|
|
|
*/
|
|
|
|
presentStairStep(1000 - surfHeight/2, 2000 + 768 - surfHeight/2);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Scaled circles. We scale these asymmetrically, to about 1.5x the
|
|
|
|
* size of the screen.
|
|
|
|
*/
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
for (i = 0; i < arraysize(circles); i++) {
|
|
|
|
presentWithClipBuf(&circles[i], -500, -300, 1300, 1000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* setup3D --
|
|
|
|
*
|
|
|
|
* Allocate 3D resources.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void
|
|
|
|
setup3D(void)
|
|
|
|
{
|
|
|
|
colorImage.sid = SVGA3DUtil_DefineSurface2D(surfWidth, surfHeight, SVGA3D_X8R8G8B8);
|
|
|
|
depthImage.sid = SVGA3DUtil_DefineSurface2D(surfWidth, surfHeight, SVGA3D_Z_D16);
|
|
|
|
|
|
|
|
SVGA3D_DefineContext(CID);
|
|
|
|
|
|
|
|
vertexSid = SVGA3DUtil_DefineStaticBuffer(vertexData, sizeof vertexData);
|
|
|
|
indexSid = SVGA3DUtil_DefineStaticBuffer(indexData, sizeof indexData);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* drawCube --
|
|
|
|
*
|
|
|
|
* Draw a spinning wireframe cube.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void
|
|
|
|
drawCube(void)
|
|
|
|
{
|
|
|
|
static float angle = 0.5f;
|
|
|
|
SVGA3dRect *rect;
|
|
|
|
Matrix perspectiveMat;
|
|
|
|
SVGA3dTextureState *ts;
|
|
|
|
SVGA3dRenderState *rs;
|
|
|
|
SVGA3dRect viewport = { 0, 0, surfWidth, surfHeight };
|
|
|
|
|
|
|
|
SVGA3D_SetRenderTarget(CID, SVGA3D_RT_COLOR0, &colorImage);
|
|
|
|
SVGA3D_SetRenderTarget(CID, SVGA3D_RT_DEPTH, &depthImage);
|
|
|
|
|
|
|
|
SVGA3D_SetViewport(CID, &viewport);
|
|
|
|
SVGA3D_SetZRange(CID, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
SVGA3D_BeginSetRenderState(CID, &rs, 5);
|
|
|
|
{
|
|
|
|
rs[0].state = SVGA3D_RS_BLENDENABLE;
|
|
|
|
rs[0].uintValue = FALSE;
|
|
|
|
|
|
|
|
rs[1].state = SVGA3D_RS_ZENABLE;
|
|
|
|
rs[1].uintValue = TRUE;
|
|
|
|
|
|
|
|
rs[2].state = SVGA3D_RS_ZWRITEENABLE;
|
|
|
|
rs[2].uintValue = TRUE;
|
|
|
|
|
|
|
|
rs[3].state = SVGA3D_RS_ZFUNC;
|
|
|
|
rs[3].uintValue = SVGA3D_CMP_LESS;
|
|
|
|
|
|
|
|
rs[4].state = SVGA3D_RS_LIGHTINGENABLE;
|
|
|
|
rs[4].uintValue = FALSE;
|
|
|
|
}
|
|
|
|
SVGA_FIFOCommitAll();
|
|
|
|
|
|
|
|
SVGA3D_BeginSetTextureState(CID, &ts, 4);
|
|
|
|
{
|
|
|
|
ts[0].stage = 0;
|
|
|
|
ts[0].name = SVGA3D_TS_BIND_TEXTURE;
|
|
|
|
ts[0].value = SVGA3D_INVALID_ID;
|
|
|
|
|
|
|
|
ts[1].stage = 0;
|
|
|
|
ts[1].name = SVGA3D_TS_COLOROP;
|
|
|
|
ts[1].value = SVGA3D_TC_SELECTARG1;
|
|
|
|
|
|
|
|
ts[2].stage = 0;
|
|
|
|
ts[2].name = SVGA3D_TS_COLORARG1;
|
|
|
|
ts[2].value = SVGA3D_TA_DIFFUSE;
|
|
|
|
|
|
|
|
ts[3].stage = 0;
|
|
|
|
ts[3].name = SVGA3D_TS_ALPHAARG1;
|
|
|
|
ts[3].value = SVGA3D_TA_DIFFUSE;
|
|
|
|
}
|
|
|
|
SVGA_FIFOCommitAll();
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Draw a red border around the render target, to test edge
|
|
|
|
* accuracy in Present.
|
|
|
|
*/
|
|
|
|
SVGA3D_BeginClear(CID, SVGA3D_CLEAR_COLOR | SVGA3D_CLEAR_DEPTH,
|
|
|
|
0xFF0000, 1.0f, 0, &rect, 1);
|
|
|
|
*rect = viewport;
|
|
|
|
SVGA_FIFOCommitAll();
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Draw the background color
|
|
|
|
*/
|
|
|
|
SVGA3D_BeginClear(CID, SVGA3D_CLEAR_COLOR | SVGA3D_CLEAR_DEPTH,
|
|
|
|
0x336699, 1.0f, 0, &rect, 1);
|
|
|
|
rect->x = viewport.x + 1;
|
|
|
|
rect->y = viewport.y + 1;
|
|
|
|
rect->w = viewport.w - 2;
|
|
|
|
rect->h = viewport.h - 2;
|
|
|
|
SVGA_FIFOCommitAll();
|
|
|
|
|
|
|
|
SVGA3dVertexDecl *decls;
|
|
|
|
SVGA3dPrimitiveRange *ranges;
|
|
|
|
Matrix view;
|
|
|
|
|
|
|
|
Matrix_Copy(view, gIdentityMatrix);
|
|
|
|
Matrix_Scale(view, 0.5, 0.5, 0.5, 1.0);
|
|
|
|
Matrix_RotateX(view, 30.0 * M_PI / 180.0);
|
|
|
|
Matrix_RotateY(view, angle);
|
|
|
|
Matrix_Translate(view, 0, 0, 2.2);
|
|
|
|
|
|
|
|
angle += 0.02;
|
|
|
|
|
|
|
|
Matrix_Perspective(perspectiveMat, 45.0f, 4.0f / 3.0f, 0.1f, 100.0f);
|
|
|
|
SVGA3D_SetTransform(CID, SVGA3D_TRANSFORM_WORLD, gIdentityMatrix);
|
|
|
|
SVGA3D_SetTransform(CID, SVGA3D_TRANSFORM_PROJECTION, perspectiveMat);
|
|
|
|
SVGA3D_SetTransform(CID, SVGA3D_TRANSFORM_VIEW, view);
|
|
|
|
|
|
|
|
SVGA3D_BeginDrawPrimitives(CID, &decls, 2, &ranges, 1);
|
|
|
|
{
|
|
|
|
decls[0].identity.type = SVGA3D_DECLTYPE_FLOAT3;
|
|
|
|
decls[0].identity.usage = SVGA3D_DECLUSAGE_POSITION;
|
|
|
|
decls[0].array.surfaceId = vertexSid;
|
|
|
|
decls[0].array.stride = sizeof(MyVertex);
|
|
|
|
decls[0].array.offset = offsetof(MyVertex, position);
|
|
|
|
|
|
|
|
decls[1].identity.type = SVGA3D_DECLTYPE_D3DCOLOR;
|
|
|
|
decls[1].identity.usage = SVGA3D_DECLUSAGE_COLOR;
|
|
|
|
decls[1].array.surfaceId = vertexSid;
|
|
|
|
decls[1].array.stride = sizeof(MyVertex);
|
|
|
|
decls[1].array.offset = offsetof(MyVertex, color);
|
|
|
|
|
|
|
|
ranges[0].primType = SVGA3D_PRIMITIVE_LINELIST;
|
|
|
|
ranges[0].primitiveCount = numLines;
|
|
|
|
ranges[0].indexArray.surfaceId = indexSid;
|
|
|
|
ranges[0].indexArray.stride = sizeof(uint16);
|
|
|
|
ranges[0].indexWidth = sizeof(uint16);
|
|
|
|
}
|
|
|
|
SVGA_FIFOCommitAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* main --
|
|
|
|
*
|
|
|
|
* Initialization, main loop.
|
|
|
|
*/
|
|
|
|
|
|
|
|
int
|
|
|
|
main(void)
|
|
|
|
{
|
|
|
|
static FPSCounterState fps;
|
|
|
|
uint32 frameFence = 0;
|
|
|
|
uint32 nextFence;
|
|
|
|
|
|
|
|
Intr_Init();
|
|
|
|
Intr_SetFaultHandlers(SVGA_DefaultFaultHandler);
|
|
|
|
SVGA_Init();
|
|
|
|
GMR_Init();
|
|
|
|
Heap_Reset();
|
|
|
|
SVGA_SetMode(0, 0, 32);
|
|
|
|
SVGA3D_Init();
|
|
|
|
Screen_Init();
|
|
|
|
ScreenDraw_Init(0);
|
|
|
|
|
|
|
|
initScreens();
|
|
|
|
setup3D();
|
|
|
|
|
|
|
|
/*
|
|
|
|
* One big circle, and a smaller one that overlaps the top-right
|
|
|
|
* corner. (This tests positive and negative clipping extremes.)
|
|
|
|
*/
|
|
|
|
prepareCircle(&circles[0], 650, 400, 300);
|
|
|
|
prepareCircle(&circles[1], 1000, 50, 250);
|
|
|
|
|
|
|
|
while (1) {
|
|
|
|
if (SVGA3DUtil_UpdateFPSCounter(&fps)) {
|
|
|
|
Console_MoveTo(900, 730);
|
|
|
|
Console_Format("%s ", fps.text);
|
|
|
|
}
|
|
|
|
|
|
|
|
drawCube();
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Flow control- one frame in the FIFO at a time.
|
|
|
|
*/
|
|
|
|
nextFence = SVGA_InsertFence();
|
|
|
|
SVGA_SyncToFence(frameFence);
|
|
|
|
frameFence = nextFence;
|
|
|
|
|
|
|
|
present();
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|