1 /*
2 * Copyright (c) 2011 VMware, Inc.
3 * Copyright © 2015 Intel Corporation
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a
6 * copy of this software and associated documentation files (the "Software"),
7 * to deal in the Software without restriction, including without limitation
8 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 * and/or sell copies of the Software, and to permit persons to whom the
10 * Software is furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice (including the next
13 * paragraph) shall be included in all copies or substantial portions of the
14 * Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22 * IN THE SOFTWARE.
23 */
24
25 /**
26 * Test rendering to a depth texture, sampling from it, and comparing the
27 * texture depth values against the fragment Z values when drawing the
28 * same object a second time.
29 *
30 * The left side of the window should be mostly black. Red pixels indicate
31 * errors.
32 * The center and right parts of the window should show gray-scale spheres
33 * on a white background (they're just Z buffer images as gray-scale).
34 *
35 * Brian Paul
36 * 17 Feb 2011
37 */
38
39
40 #include <assert.h>
41 #include "piglit-util-gl.h"
42
43 /** Set DEBUG to 1 to enable extra output when trying to debug failures */
44 #define DEBUG 0
45
46 #define SIZE 256
47
48 PIGLIT_GL_TEST_CONFIG_BEGIN
49
50 config.supports_gl_compat_version = 10;
51
52 config.window_width = 3*SIZE;
53 config.window_height = SIZE;
54 config.window_visual = PIGLIT_GL_VISUAL_DOUBLE | PIGLIT_GL_VISUAL_DEPTH;
55 config.khr_no_error_support = PIGLIT_NO_ERRORS;
56
57 PIGLIT_GL_TEST_CONFIG_END
58
59 static GLfloat ErrorScale = 0.0;
60 static GLuint ColorTex, DepthTex, FBO;
61 static GLuint ShaderProg;
62 static GLint Zbits;
63 static GLenum TexTarget = GL_TEXTURE_2D;
64
65
66 static void
create_fbo(void)67 create_fbo(void)
68 {
69 GLenum depthIntFormat = GL_DEPTH_COMPONENT24;
70 GLenum status;
71
72 /* depth texture */
73 glGenTextures(1, &DepthTex);
74 glBindTexture(TexTarget, DepthTex);
75 glTexParameteri(TexTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
76 glTexParameteri(TexTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
77 glTexImage2D(TexTarget, 0, depthIntFormat,
78 SIZE, SIZE, 0,
79 GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL);
80 if (!piglit_check_gl_error(GL_NO_ERROR))
81 piglit_report_result(PIGLIT_FAIL);
82 glGetTexLevelParameteriv(TexTarget, 0, GL_TEXTURE_DEPTH_SIZE, &Zbits);
83
84 /* color texture */
85 glGenTextures(1, &ColorTex);
86 glBindTexture(TexTarget, ColorTex);
87 glTexParameteri(TexTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
88 glTexParameteri(TexTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
89 glTexImage2D(TexTarget, 0, GL_RGBA,
90 SIZE, SIZE, 0,
91 GL_RGBA, GL_UNSIGNED_BYTE, NULL);
92 if (!piglit_check_gl_error(GL_NO_ERROR))
93 piglit_report_result(PIGLIT_FAIL);
94
95 /* Create FBO */
96 glGenFramebuffersEXT(1, &FBO);
97 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, FBO);
98
99 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
100 GL_COLOR_ATTACHMENT0_EXT,
101 TexTarget,
102 ColorTex,
103 0);
104
105 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
106 GL_DEPTH_ATTACHMENT_EXT,
107 TexTarget,
108 DepthTex,
109 0);
110
111 if (!piglit_check_gl_error(GL_NO_ERROR))
112 piglit_report_result(PIGLIT_FAIL);
113
114 status = glCheckFramebufferStatusEXT (GL_FRAMEBUFFER_EXT);
115 if (status != GL_FRAMEBUFFER_COMPLETE_EXT) {
116 piglit_report_result(PIGLIT_SKIP);
117 }
118 }
119
120
121 static void
create_frag_shader(void)122 create_frag_shader(void)
123 {
124 /* This shader samples the currently bound depth texture, then compares
125 * that value to the current fragment Z value to produce a shade of red
126 * indicating error/difference.
127 *
128 * E.g: gl_FragColor = scale * abs(texture.Z - fragment.Z);
129 *
130 * Note that we have to be pretty careful with converting gl_FragCoord
131 * into a 2D texture coordinate. There's a -0.5 bias and scale factor.
132 */
133 static const char *text_2d =
134 "uniform sampler2D zTex; \n"
135 "uniform float sizeScale; \n"
136 "uniform float errorScale; \n"
137 "void main() \n"
138 "{ \n"
139 " vec2 coord = (gl_FragCoord.xy - vec2(0.5)) / sizeScale; \n"
140 " vec4 z = texture2D(zTex, coord); \n"
141 " float diff = errorScale * abs(z.r - gl_FragCoord.z); \n"
142 " //gl_FragColor = vec4(gl_FragCoord.z, 0, 0, 0); \n"
143 " //gl_FragColor = z; \n"
144 " gl_FragColor = vec4(diff, 0, 0, 0); \n"
145 " gl_FragDepth = gl_FragCoord.z; \n"
146 "} \n";
147 static const char *text_rect =
148 "#extension GL_ARB_texture_rectangle: require \n"
149 "uniform sampler2DRect zTex; \n"
150 "uniform float sizeScale; \n"
151 "uniform float errorScale; \n"
152 "void main() \n"
153 "{ \n"
154 " vec2 coord = gl_FragCoord.xy; \n"
155 " vec4 z = texture2DRect(zTex, coord); \n"
156 " float diff = errorScale * abs(z.r - gl_FragCoord.z); \n"
157 " //gl_FragColor = vec4(gl_FragCoord.z, 0, 0, 0); \n"
158 " //gl_FragColor = z; \n"
159 " gl_FragColor = vec4(diff, 0, 0, 0); \n"
160 " gl_FragDepth = gl_FragCoord.z; \n"
161 "} \n";
162 GLint zTex, errorScale, sizeScale;
163 const char *const fs_source = (TexTarget == GL_TEXTURE_2D)
164 ? text_2d : text_rect;
165
166 ShaderProg = piglit_build_simple_program(NULL, fs_source);
167 assert(ShaderProg);
168
169 glUseProgram(ShaderProg);
170
171 zTex = glGetUniformLocation(ShaderProg, "zTex");
172 glUniform1i(zTex, 0); /* unit 0 */
173
174 errorScale = glGetUniformLocation(ShaderProg, "errorScale");
175 glUniform1f(errorScale, ErrorScale);
176
177 sizeScale = glGetUniformLocation(ShaderProg, "sizeScale");
178 glUniform1f(sizeScale, (float) (SIZE - 1));
179
180 glUseProgram(0);
181 }
182
183
184 #if DEBUG
185 static void
find_float_min_max_center(const GLfloat * buf,GLuint n,GLfloat * min,GLfloat * max,GLfloat * center)186 find_float_min_max_center(const GLfloat *buf, GLuint n,
187 GLfloat *min, GLfloat *max, GLfloat *center)
188 {
189 GLint cx = SIZE/4, cy = SIZE/4;
190 GLuint i;
191
192 *min = 1.0e20;
193 *max = -1.0e20;
194
195 for (i = 0; i < n; i++) {
196 if (buf[i] != 1.0) {
197 if (buf[i] < *min)
198 *min = buf[i];
199 if (buf[i] > *max)
200 *max = buf[i];
201 }
202 }
203
204 *center = buf[cy * SIZE + cx];
205 }
206
207 static void
find_uint_min_max_center(const GLuint * buf,GLuint n,GLuint * min,GLuint * max,GLuint * center)208 find_uint_min_max_center(const GLuint *buf, GLuint n,
209 GLuint *min, GLuint *max, GLuint *center)
210 {
211 GLint cx = SIZE/4, cy = SIZE/4;
212 GLuint i;
213
214 *min = ~0U;
215 *max = 0;
216
217 for (i = 0; i < n; i++) {
218 if (buf[i] != ~0U) {
219 if (buf[i] < *min)
220 *min = buf[i];
221 if (buf[i] > *max)
222 *max = buf[i];
223 }
224 }
225
226 *center = buf[cy * SIZE + cx];
227 }
228 #endif /* DEBUG */
229
230
231 static void
draw_sphere(void)232 draw_sphere(void)
233 {
234 /* Without this enum hack, GCC complains about variable length arrays
235 * below... even if you make the variables const.
236 */
237 enum {
238 slices = 40,
239 stacks = 20,
240
241 /* There are (stacks - 1) interior stacks (see the comment before y
242 * below). Each interior stack is (slices + 1) vertices. There is on
243 * additional vertex at the top, and there is one at the bottom.
244 */
245 num_vertices = (stacks - 1) * (slices + 1) + 2,
246
247 /* Each slice is a single triangle strip. There is a triangle at the
248 * top (3 elements), and there is one at the bottom (1 element).
249 * Between is (stacks - 2) quadrilaterals (2 elements each).
250 */
251 elements_per_slice = 3 + ((stacks - 2) * 2) + 1,
252 };
253
254 const GLdouble radius = 0.95;
255 unsigned i;
256
257 static float vertex_data[num_vertices * 4];
258 static unsigned element_data[elements_per_slice * slices];
259 static bool generated = false;
260
261 if (!generated) {
262 float *v = vertex_data;
263 unsigned *e = element_data;
264 unsigned j;
265
266 assert(num_vertices < 65535);
267
268 for (i = 1; i < stacks; i++) {
269 /* The y values of the sphere interpolate from -radius to radius.
270 * The two extrema have a single point (in terms of the "circular
271 * slice" mentioned below, r_c = 0). Those points are generated at
272 * the very end. If there are N slices of the sphere, there are N+1
273 * layers of data. This loop generates data for layers 1 through
274 * N-1, inclusive. Layers 0 and N are the extrema previously
275 * mentioned.
276 *
277 * NOTE: Then angle range from the north pole to the south pole is
278 * PI. When going around the equator (inner loop below), the angle
279 * range is 0 to 2PI.
280 */
281 const double y = -cos(i * M_PI / stacks) * radius;
282
283 /* The radius of the sphere is, r_s, sqrt(x**2 + y**2 + z**2). The
284 * radius of the circular slice of the sphere parallel to the X/Z
285 * plane, r_c, is sqrt(x**2 + z**2). r_s and y are known. Solve for
286 * r_c.
287 *
288 * r_s**2 = x**2 + y**2 + z**2
289 * r_s**2 = r_c**2 + y**2
290 * r_c**2 = r_s**2 - y**2
291 * r_c = sqrt(r_s**2 - y**2)
292 */
293 const double r_c = sqrt((radius * radius) - (y * y));
294
295 for (j = 0; j <= slices; j++) {
296 const double angle = j * 2.0 * M_PI / slices;
297
298 v[0] = r_c * sin(angle);
299 v[1] = y;
300 v[2] = r_c * cos(angle);
301 v[3] = 1.0;
302
303 assert(fabs(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] -
304 radius * radius) < 1e-6);
305 v += 4;
306 assert(v < &vertex_data[ARRAY_SIZE(vertex_data)]);
307 }
308 }
309
310 v[0] = 0.0;
311 v[1] = -radius;
312 v[2] = 0.0;
313 v[3] = 1.0;
314
315 v[4] = 0.0;
316 v[5] = radius;
317 v[6] = 0.0;
318 v[7] = 1.0;
319 assert(&v[8] == &vertex_data[ARRAY_SIZE(vertex_data)]);
320
321 for (i = 0; i < slices; i++) {
322 /* The outer loop walks around the first circluar slice of vertex
323 * data. This occupies vertices [0, slices]. Looking at the sphere,
324 * there is a vertex on the left side of the polygon being emitted,
325 * and the next vertex in the sequence is on the right.
326 */
327 unsigned left = i;
328
329 /* Emit the "base" triangle. */
330 e[0] = num_vertices - 2;
331 e++;
332 assert(e < &element_data[ARRAY_SIZE(element_data)]);
333
334 for (j = 0; j < (stacks - 1); j++) {
335 const unsigned right = left + 1;
336
337 e[0] = left;
338 e[1] = right;
339 e += 2;
340 assert(e < &element_data[ARRAY_SIZE(element_data)]);
341
342 left += (slices + 1);
343 }
344
345 /* Emit the bottom vertex for the final triangle. */
346 e[0] = num_vertices - 1;
347 e++;
348 assert(e <= &element_data[ARRAY_SIZE(element_data)]);
349 }
350
351 assert(e == &element_data[ARRAY_SIZE(element_data)]);
352 generated = true;
353 }
354
355 glVertexPointer(4, GL_FLOAT, 4 * sizeof(float), vertex_data);
356 glEnableClientState(GL_VERTEX_ARRAY);
357 for (i = 0; i < slices; i++) {
358 glDrawElements(GL_TRIANGLE_STRIP,
359 elements_per_slice,
360 GL_UNSIGNED_INT,
361 &element_data[i * elements_per_slice]);
362 }
363 }
364
365
366 static void
render_to_fbo(void)367 render_to_fbo(void)
368 {
369 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, FBO);
370
371 glViewport(0, 0, SIZE, SIZE);
372
373 glEnable(GL_DEPTH_TEST);
374
375 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
376
377 glMatrixMode(GL_MODELVIEW);
378 glLoadIdentity();
379 glMatrixMode(GL_PROJECTION);
380 glLoadIdentity();
381 glOrtho(-1.0, 1.0, -1.0, 1.0, -1, 1.0);
382
383 glColor4f(1.0, 0.0, 0.0, 0.0);
384 draw_sphere();
385
386 glDisable(GL_DEPTH_TEST);
387
388 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, piglit_winsys_fbo);
389 }
390
391
392 static GLfloat *
read_float_z_image(GLint x,GLint y)393 read_float_z_image(GLint x, GLint y)
394 {
395 GLfloat *z = (GLfloat *) malloc(SIZE * SIZE * sizeof(GLfloat));
396
397 glReadPixels(x, y, SIZE, SIZE, GL_DEPTH_COMPONENT, GL_FLOAT, z);
398
399 return z;
400 }
401
402
403 #if DEBUG
404 static GLuint *
read_uint_z_image(GLint x,GLint y)405 read_uint_z_image(GLint x, GLint y)
406 {
407 GLuint *z = (GLuint *) malloc(SIZE * SIZE * sizeof(GLuint));
408
409 glReadPixels(x, y, SIZE, SIZE, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, z);
410
411 return z;
412 }
413 #endif /* DEBUG */
414
415
416 /** Show contents of depth buffer in middle of window */
417 static void
show_depth_fbo(void)418 show_depth_fbo(void)
419 {
420 GLfloat *zf;
421
422 glViewport(1 * SIZE, 0, SIZE, SIZE); /* not really needed */
423
424 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, FBO);
425 zf = read_float_z_image(0, 0);
426
427 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, piglit_winsys_fbo);
428
429 glWindowPos2i(SIZE, 0);
430 glDrawPixels(SIZE, SIZE, GL_LUMINANCE, GL_FLOAT, zf);
431 if (!piglit_check_gl_error(GL_NO_ERROR))
432 piglit_report_result(PIGLIT_FAIL);
433
434 #if DEBUG
435 {
436 GLfloat min, max, center;
437 find_float_min_max_center(zf, SIZE * SIZE, &min, &max, ¢er);
438 printf("depth fbo min %f max %f center %f\n", min, max, center);
439 }
440
441 {
442 GLuint min, max, center;
443 GLuint *zi;
444
445 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, FBO);
446 zi = read_uint_z_image(0, 0);
447 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, piglit_winsys_fbo);
448
449 find_uint_min_max_center(zi, SIZE * SIZE, &min, &max, ¢er);
450 printf("depth fbo min 0x%x max 0x%x center 0x%x\n", min, max, center);
451 free(zi);
452 }
453 #endif /* DEBUG */
454
455 free(zf);
456 }
457
458
459 /** Draw quad textured with depth image on right side of window */
460 static void
draw_quad_with_depth_texture(void)461 draw_quad_with_depth_texture(void)
462 {
463 GLfloat s1, t1;
464
465 if (TexTarget == GL_TEXTURE_2D) {
466 s1 = t1 = 1.0;
467 }
468 else {
469 s1 = SIZE;
470 t1 = SIZE;
471 }
472
473 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, piglit_winsys_fbo);
474
475 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
476
477 glViewport(2 * SIZE, 0, SIZE, SIZE);
478
479 glMatrixMode(GL_MODELVIEW);
480 glLoadIdentity();
481 glMatrixMode(GL_PROJECTION);
482 glLoadIdentity();
483 glOrtho(-1.0, 1.0, -1.0, 1.0, -1, 1.0);
484
485 glBindTexture(TexTarget, DepthTex);
486 glEnable(TexTarget);
487
488 piglit_draw_rect_tex(-1, -1, 2, 2, 0, 0, s1, t1);
489
490 glDisable(TexTarget);
491 }
492
493
494 /**
495 * Draw quad with fragment shader that compares fragment.z against the
496 * depth texture value (draw on left side of window).
497 * We draw on the left side of the window to easily convert gl_FragCoord
498 * into a texture coordinate.
499 */
500 static void
draw_sphere_with_fragment_shader_compare(void)501 draw_sphere_with_fragment_shader_compare(void)
502 {
503 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, piglit_winsys_fbo);
504
505 glViewport(0 * SIZE, 0, SIZE, SIZE);
506
507 glMatrixMode(GL_MODELVIEW);
508 glLoadIdentity();
509 glMatrixMode(GL_PROJECTION);
510 glLoadIdentity();
511 glOrtho(-1.0, 1.0, -1.0, 1.0, -1, 1.0);
512
513 glBindTexture(TexTarget, DepthTex);
514
515 glUseProgram(ShaderProg);
516
517 glEnable(GL_DEPTH_TEST);
518
519 if (1) {
520 draw_sphere();
521 }
522 else {
523 /* To test using gl_TexCoord[0].xy instead of gl_FragCoord.xy in the shader
524 */
525 static const GLfloat sPlane[4] = {0.5, 0, 0, 0.5};
526 static const GLfloat tPlane[4] = {0, 0.5, 0, 0.5};
527
528 glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
529 glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
530 glTexGenfv(GL_S, GL_EYE_PLANE, sPlane);
531 glTexGenfv(GL_T, GL_EYE_PLANE, tPlane);
532 glEnable(GL_TEXTURE_GEN_S);
533 glEnable(GL_TEXTURE_GEN_T);
534
535 draw_sphere();
536
537 glDisable(GL_TEXTURE_GEN_S);
538 glDisable(GL_TEXTURE_GEN_T);
539 }
540
541 glDisable(GL_DEPTH_TEST);
542
543 glUseProgram(0);
544
545 #if DEBUG
546 {
547 GLfloat *z = read_float_z_image(0, 0);
548 GLfloat min, max, center;
549
550 find_float_min_max_center(z, SIZE * SIZE, &min, &max, ¢er);
551 printf("rendered min %f max %f center %f\n", min, max, center);
552
553 free(z);
554 }
555 {
556 GLuint *z = read_uint_z_image(0, 0);
557 GLuint min, max, center;
558
559 find_uint_min_max_center(z, SIZE * SIZE, &min, &max, ¢er);
560 printf("rendered min 0x%x max 0x%x center 0x%x\n", min, max, center);
561
562 free(z);
563 }
564 #endif /* DEBUG */
565 }
566
567
568 static enum piglit_result
count_and_report_bad_pixels(void)569 count_and_report_bad_pixels(void)
570 {
571 GLubyte *z;
572 GLuint error = 0;
573 int i;
574
575 z = (GLubyte *) malloc(SIZE * SIZE * 4 * sizeof(GLubyte));
576 glReadPixels(0, 0, SIZE, SIZE, GL_RGBA, GL_UNSIGNED_BYTE, z);
577
578 for (i = 0; i < SIZE * SIZE * 4; i += 4) {
579 error += z[i];
580 }
581 free(z);
582
583 if (!piglit_automatic)
584 printf("total error = %u\n", error);
585
586 /* XXX this error test is a total hack for now */
587 if (error > ErrorScale)
588 return PIGLIT_FAIL;
589 else
590 return PIGLIT_PASS;
591 }
592
593
594 enum piglit_result
piglit_display(void)595 piglit_display(void)
596 {
597 enum piglit_result result;
598
599 render_to_fbo();
600
601 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
602
603 show_depth_fbo();
604
605 draw_quad_with_depth_texture();
606
607 draw_sphere_with_fragment_shader_compare();
608
609 result = count_and_report_bad_pixels();
610
611 piglit_present_results();
612
613 return result;
614 }
615
616
617 void
piglit_init(int argc,char ** argv)618 piglit_init(int argc, char **argv)
619 {
620 int i = 1;
621
622 if (i < argc && strcmp(argv[i], "rect") == 0) {
623 TexTarget = GL_TEXTURE_RECTANGLE;
624 i++;
625 }
626 if (i < argc) {
627 ErrorScale = atof(argv[i]);
628 }
629
630 piglit_require_extension("GL_EXT_framebuffer_object");
631 piglit_require_fragment_shader();
632 if (TexTarget == GL_TEXTURE_RECTANGLE) {
633 piglit_require_extension("GL_ARB_texture_rectangle");
634 }
635
636 create_fbo();
637
638 if (ErrorScale == 0.0) {
639 /* A 1-bit error/difference in Z values results in a delta of 64 in
640 * pixel intensity (where pixels are in [0,255]).
641 */
642 ErrorScale = ((double) (1ull << Zbits)) * 64.0 / 255.0;
643 }
644
645 create_frag_shader();
646
647 if (!piglit_automatic) {
648 printf("GL_RENDERER = %s\n", (char *) glGetString(GL_RENDERER));
649 printf("Left: Shader showing difference pixels (black=good, red=error)\n");
650 printf("Middle: Depth buffer of FBO\n");
651 printf("Right: Quad textured with depth values\n");
652 printf("Z bits = %d\n", Zbits);
653 printf("ErrorScale = %f\n", ErrorScale);
654 printf("Texture target: %s\n",
655 TexTarget == GL_TEXTURE_RECTANGLE ? "RECTANGLE" : "2D" );
656 }
657 }
658