1 /**************************************************************************
2 *
3 * Copyright 2010 Thomas Balling Sørensen & Orasanu Lucian.
4 * Copyright 2014 Advanced Micro Devices, Inc.
5 * All Rights Reserved.
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a
8 * copy of this software and associated documentation files (the
9 * "Software"), to deal in the Software without restriction, including
10 * without limitation the rights to use, copy, modify, merge, publish,
11 * distribute, sub license, and/or sell copies of the Software, and to
12 * permit persons to whom the Software is furnished to do so, subject to
13 * the following conditions:
14 *
15 * The above copyright notice and this permission notice (including the
16 * next paragraph) shall be included in all copies or substantial portions
17 * of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
22 * IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR
23 * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
24 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
25 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 *
27 **************************************************************************/
28
29 #include "pipe/p_screen.h"
30 #include "frontend/drm_driver.h"
31 #include "util/u_memory.h"
32 #include "util/u_handle_table.h"
33 #include "util/u_transfer.h"
34 #include "util/set.h"
35 #include "vl/vl_winsys.h"
36
37 #include "va_private.h"
38
39 #ifdef _WIN32
40 #include <va/va_win32.h>
41 #endif
42
43 #ifndef VA_MAPBUFFER_FLAG_DEFAULT
44 #define VA_MAPBUFFER_FLAG_DEFAULT 0
45 #define VA_MAPBUFFER_FLAG_READ 1
46 #define VA_MAPBUFFER_FLAG_WRITE 2
47 #endif
48
49 VAStatus
vlVaCreateBuffer(VADriverContextP ctx,VAContextID context,VABufferType type,unsigned int size,unsigned int num_elements,void * data,VABufferID * buf_id)50 vlVaCreateBuffer(VADriverContextP ctx, VAContextID context, VABufferType type,
51 unsigned int size, unsigned int num_elements, void *data,
52 VABufferID *buf_id)
53 {
54 vlVaDriver *drv;
55 vlVaBuffer *buf;
56
57 if (!ctx)
58 return VA_STATUS_ERROR_INVALID_CONTEXT;
59
60 buf = CALLOC(1, sizeof(vlVaBuffer));
61 if (!buf)
62 return VA_STATUS_ERROR_ALLOCATION_FAILED;
63
64 buf->type = type;
65 buf->size = size;
66 buf->num_elements = num_elements;
67
68 if (buf->type == VAEncCodedBufferType)
69 buf->data = CALLOC(1, sizeof(VACodedBufferSegment));
70 else
71 buf->data = MALLOC(size * num_elements);
72
73 if (!buf->data) {
74 FREE(buf);
75 return VA_STATUS_ERROR_ALLOCATION_FAILED;
76 }
77
78 if (data)
79 memcpy(buf->data, data, size * num_elements);
80
81 drv = VL_VA_DRIVER(ctx);
82 mtx_lock(&drv->mutex);
83 *buf_id = handle_table_add(drv->htab, buf);
84 mtx_unlock(&drv->mutex);
85
86 return VA_STATUS_SUCCESS;
87 }
88
89 VAStatus
vlVaBufferSetNumElements(VADriverContextP ctx,VABufferID buf_id,unsigned int num_elements)90 vlVaBufferSetNumElements(VADriverContextP ctx, VABufferID buf_id,
91 unsigned int num_elements)
92 {
93 vlVaDriver *drv;
94 vlVaBuffer *buf;
95
96 if (!ctx)
97 return VA_STATUS_ERROR_INVALID_CONTEXT;
98
99 drv = VL_VA_DRIVER(ctx);
100 mtx_lock(&drv->mutex);
101 buf = handle_table_get(drv->htab, buf_id);
102 mtx_unlock(&drv->mutex);
103 if (!buf)
104 return VA_STATUS_ERROR_INVALID_BUFFER;
105
106 if (buf->derived_surface.resource)
107 return VA_STATUS_ERROR_INVALID_BUFFER;
108
109 buf->data = REALLOC(buf->data, buf->size * buf->num_elements,
110 buf->size * num_elements);
111 buf->num_elements = num_elements;
112
113 if (!buf->data)
114 return VA_STATUS_ERROR_ALLOCATION_FAILED;
115
116 return VA_STATUS_SUCCESS;
117 }
118
119 VAStatus
vlVaMapBuffer(VADriverContextP ctx,VABufferID buf_id,void ** pbuff)120 vlVaMapBuffer(VADriverContextP ctx, VABufferID buf_id, void **pbuff)
121 {
122 return vlVaMapBuffer2(ctx, buf_id, pbuff, VA_MAPBUFFER_FLAG_DEFAULT);
123 }
124
vlVaMapBuffer2(VADriverContextP ctx,VABufferID buf_id,void ** pbuff,uint32_t flags)125 VAStatus vlVaMapBuffer2(VADriverContextP ctx, VABufferID buf_id,
126 void **pbuff, uint32_t flags)
127 {
128 vlVaDriver *drv;
129 vlVaBuffer *buf;
130
131 if (!ctx)
132 return VA_STATUS_ERROR_INVALID_CONTEXT;
133
134 drv = VL_VA_DRIVER(ctx);
135 if (!drv)
136 return VA_STATUS_ERROR_INVALID_CONTEXT;
137
138 if (!pbuff)
139 return VA_STATUS_ERROR_INVALID_PARAMETER;
140
141 mtx_lock(&drv->mutex);
142 buf = handle_table_get(drv->htab, buf_id);
143 if (!buf || buf->export_refcount > 0) {
144 mtx_unlock(&drv->mutex);
145 return VA_STATUS_ERROR_INVALID_BUFFER;
146 }
147
148 if (buf->type == VAEncCodedBufferType)
149 vlVaGetBufferFeedback(buf);
150
151 if (buf->derived_surface.resource) {
152 struct pipe_resource *resource;
153 struct pipe_box box;
154 unsigned usage = 0;
155 void *(*map_func)(struct pipe_context *,
156 struct pipe_resource *resource,
157 unsigned level,
158 unsigned usage, /* a combination of PIPE_MAP_x */
159 const struct pipe_box *,
160 struct pipe_transfer **out_transfer);
161
162 memset(&box, 0, sizeof(box));
163 resource = buf->derived_surface.resource;
164 box.width = resource->width0;
165 box.height = resource->height0;
166 box.depth = resource->depth0;
167
168 if (resource->target == PIPE_BUFFER)
169 map_func = drv->pipe->buffer_map;
170 else
171 map_func = drv->pipe->texture_map;
172
173 if (flags == VA_MAPBUFFER_FLAG_DEFAULT) {
174 /* For VAImageBufferType, use PIPE_MAP_WRITE for now,
175 * PIPE_MAP_READ_WRITE degradate perf with two copies when map/unmap. */
176 if (buf->type == VAEncCodedBufferType)
177 usage = PIPE_MAP_READ;
178 else
179 usage = PIPE_MAP_WRITE;
180
181 /* Map decoder and postproc surfaces also for reading. */
182 if (buf->derived_surface.entrypoint == PIPE_VIDEO_ENTRYPOINT_BITSTREAM ||
183 buf->derived_surface.entrypoint == PIPE_VIDEO_ENTRYPOINT_PROCESSING)
184 usage |= PIPE_MAP_READ;
185 }
186
187 if (flags & VA_MAPBUFFER_FLAG_READ)
188 usage |= PIPE_MAP_READ;
189 if (flags & VA_MAPBUFFER_FLAG_WRITE)
190 usage |= PIPE_MAP_WRITE;
191
192 assert(usage);
193
194 *pbuff = map_func(drv->pipe, resource, 0, usage,
195 &box, &buf->derived_surface.transfer);
196 mtx_unlock(&drv->mutex);
197
198 if (!buf->derived_surface.transfer || !*pbuff)
199 return VA_STATUS_ERROR_INVALID_BUFFER;
200
201 if (buf->type == VAEncCodedBufferType) {
202 VACodedBufferSegment* curr_buf_ptr = (VACodedBufferSegment*) buf->data;
203
204 if ((buf->extended_metadata.present_metadata & PIPE_VIDEO_FEEDBACK_METADATA_TYPE_ENCODE_RESULT) &&
205 (buf->extended_metadata.encode_result & PIPE_VIDEO_FEEDBACK_METADATA_ENCODE_FLAG_FAILED)) {
206 curr_buf_ptr->status = VA_CODED_BUF_STATUS_BAD_BITSTREAM;
207 return VA_STATUS_ERROR_OPERATION_FAILED;
208 }
209
210 curr_buf_ptr->status = (buf->extended_metadata.average_frame_qp & VA_CODED_BUF_STATUS_PICTURE_AVE_QP_MASK);
211 if (buf->extended_metadata.encode_result & PIPE_VIDEO_FEEDBACK_METADATA_ENCODE_FLAG_MAX_FRAME_SIZE_OVERFLOW)
212 curr_buf_ptr->status |= VA_CODED_BUF_STATUS_FRAME_SIZE_OVERFLOW;
213
214 if ((buf->extended_metadata.present_metadata & PIPE_VIDEO_FEEDBACK_METADATA_TYPE_CODEC_UNIT_LOCATION) == 0) {
215 curr_buf_ptr->buf = *pbuff;
216 curr_buf_ptr->size = buf->coded_size;
217 *pbuff = buf->data;
218 } else {
219 uint8_t* compressed_bitstream_data = *pbuff;
220 *pbuff = buf->data;
221
222 for (size_t i = 0; i < buf->extended_metadata.codec_unit_metadata_count - 1; i++) {
223 if (!curr_buf_ptr->next)
224 curr_buf_ptr->next = CALLOC(1, sizeof(VACodedBufferSegment));
225 if (!curr_buf_ptr->next)
226 return VA_STATUS_ERROR_ALLOCATION_FAILED;
227 curr_buf_ptr = curr_buf_ptr->next;
228 }
229 if (curr_buf_ptr->next) {
230 VACodedBufferSegment *node = curr_buf_ptr->next;
231 while (node) {
232 VACodedBufferSegment *next = node->next;
233 FREE(node);
234 node = next;
235 }
236 }
237 curr_buf_ptr->next = NULL;
238
239 curr_buf_ptr = buf->data;
240 for (size_t i = 0; i < buf->extended_metadata.codec_unit_metadata_count; i++) {
241 curr_buf_ptr->size = buf->extended_metadata.codec_unit_metadata[i].size;
242 curr_buf_ptr->buf = compressed_bitstream_data + buf->extended_metadata.codec_unit_metadata[i].offset;
243 if (buf->extended_metadata.codec_unit_metadata[i].flags & PIPE_VIDEO_CODEC_UNIT_LOCATION_FLAG_MAX_SLICE_SIZE_OVERFLOW)
244 curr_buf_ptr->status |= VA_CODED_BUF_STATUS_SLICE_OVERFLOW_MASK;
245 if (buf->extended_metadata.codec_unit_metadata[i].flags & PIPE_VIDEO_CODEC_UNIT_LOCATION_FLAG_SINGLE_NALU)
246 curr_buf_ptr->status |= VA_CODED_BUF_STATUS_SINGLE_NALU;
247
248 curr_buf_ptr = curr_buf_ptr->next;
249 }
250 }
251 }
252 } else {
253 mtx_unlock(&drv->mutex);
254 *pbuff = buf->data;
255 }
256
257 return VA_STATUS_SUCCESS;
258 }
259
260 VAStatus
vlVaUnmapBuffer(VADriverContextP ctx,VABufferID buf_id)261 vlVaUnmapBuffer(VADriverContextP ctx, VABufferID buf_id)
262 {
263 vlVaDriver *drv;
264 vlVaBuffer *buf;
265 struct pipe_resource *resource;
266
267 if (!ctx)
268 return VA_STATUS_ERROR_INVALID_CONTEXT;
269
270 drv = VL_VA_DRIVER(ctx);
271 if (!drv)
272 return VA_STATUS_ERROR_INVALID_CONTEXT;
273
274 mtx_lock(&drv->mutex);
275 buf = handle_table_get(drv->htab, buf_id);
276 if (!buf || buf->export_refcount > 0) {
277 mtx_unlock(&drv->mutex);
278 return VA_STATUS_ERROR_INVALID_BUFFER;
279 }
280
281 resource = buf->derived_surface.resource;
282 if (resource) {
283 void (*unmap_func)(struct pipe_context *pipe,
284 struct pipe_transfer *transfer);
285
286 if (!buf->derived_surface.transfer) {
287 mtx_unlock(&drv->mutex);
288 return VA_STATUS_ERROR_INVALID_BUFFER;
289 }
290
291 if (resource->target == PIPE_BUFFER)
292 unmap_func = pipe_buffer_unmap;
293 else
294 unmap_func = pipe_texture_unmap;
295
296 unmap_func(drv->pipe, buf->derived_surface.transfer);
297 buf->derived_surface.transfer = NULL;
298
299 if (buf->type == VAImageBufferType)
300 drv->pipe->flush(drv->pipe, NULL, 0);
301 }
302 mtx_unlock(&drv->mutex);
303
304 return VA_STATUS_SUCCESS;
305 }
306
307 VAStatus
vlVaDestroyBuffer(VADriverContextP ctx,VABufferID buf_id)308 vlVaDestroyBuffer(VADriverContextP ctx, VABufferID buf_id)
309 {
310 vlVaDriver *drv;
311 vlVaBuffer *buf;
312
313 if (!ctx)
314 return VA_STATUS_ERROR_INVALID_CONTEXT;
315
316 drv = VL_VA_DRIVER(ctx);
317 mtx_lock(&drv->mutex);
318 buf = handle_table_get(drv->htab, buf_id);
319 if (!buf) {
320 mtx_unlock(&drv->mutex);
321 return VA_STATUS_ERROR_INVALID_BUFFER;
322 }
323
324 if (buf->derived_surface.resource)
325 pipe_resource_reference(&buf->derived_surface.resource, NULL);
326
327 if (buf->type == VAEncCodedBufferType) {
328 VACodedBufferSegment* node = buf->data;
329 while (node) {
330 VACodedBufferSegment* next = (VACodedBufferSegment*) node->next;
331 FREE(node);
332 node = next;
333 }
334 } else {
335 FREE(buf->data);
336 }
337
338 if (buf->ctx) {
339 assert(_mesa_set_search(buf->ctx->buffers, buf));
340 _mesa_set_remove_key(buf->ctx->buffers, buf);
341 vlVaGetBufferFeedback(buf);
342 if (buf->fence && buf->ctx->decoder && buf->ctx->decoder->destroy_fence)
343 buf->ctx->decoder->destroy_fence(buf->ctx->decoder, buf->fence);
344 }
345
346 if (buf->coded_surf)
347 buf->coded_surf->coded_buf = NULL;
348
349 FREE(buf);
350 handle_table_remove(VL_VA_DRIVER(ctx)->htab, buf_id);
351 mtx_unlock(&drv->mutex);
352
353 return VA_STATUS_SUCCESS;
354 }
355
356 VAStatus
vlVaBufferInfo(VADriverContextP ctx,VABufferID buf_id,VABufferType * type,unsigned int * size,unsigned int * num_elements)357 vlVaBufferInfo(VADriverContextP ctx, VABufferID buf_id, VABufferType *type,
358 unsigned int *size, unsigned int *num_elements)
359 {
360 vlVaDriver *drv;
361 vlVaBuffer *buf;
362
363 if (!ctx)
364 return VA_STATUS_ERROR_INVALID_CONTEXT;
365
366 drv = VL_VA_DRIVER(ctx);
367 mtx_lock(&drv->mutex);
368 buf = handle_table_get(drv->htab, buf_id);
369 mtx_unlock(&drv->mutex);
370 if (!buf)
371 return VA_STATUS_ERROR_INVALID_BUFFER;
372
373 *type = buf->type;
374 *size = buf->size;
375 *num_elements = buf->num_elements;
376
377 return VA_STATUS_SUCCESS;
378 }
379
380 VAStatus
vlVaAcquireBufferHandle(VADriverContextP ctx,VABufferID buf_id,VABufferInfo * out_buf_info)381 vlVaAcquireBufferHandle(VADriverContextP ctx, VABufferID buf_id,
382 VABufferInfo *out_buf_info)
383 {
384 vlVaDriver *drv;
385 uint32_t i;
386 uint32_t mem_type;
387 vlVaBuffer *buf ;
388 struct pipe_screen *screen;
389
390 /* List of supported memory types, in preferred order. */
391 static const uint32_t mem_types[] = {
392 #ifdef _WIN32
393 VA_SURFACE_ATTRIB_MEM_TYPE_NTHANDLE,
394 VA_SURFACE_ATTRIB_MEM_TYPE_D3D12_RESOURCE,
395 #else
396 VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME,
397 #endif
398 0
399 };
400
401 if (!ctx)
402 return VA_STATUS_ERROR_INVALID_CONTEXT;
403
404 drv = VL_VA_DRIVER(ctx);
405 screen = VL_VA_PSCREEN(ctx);
406 mtx_lock(&drv->mutex);
407 buf = handle_table_get(VL_VA_DRIVER(ctx)->htab, buf_id);
408 mtx_unlock(&drv->mutex);
409
410 if (!buf)
411 return VA_STATUS_ERROR_INVALID_BUFFER;
412
413 /* Only VA surface|image like buffers are supported for now .*/
414 if (buf->type != VAImageBufferType)
415 return VA_STATUS_ERROR_UNSUPPORTED_BUFFERTYPE;
416
417 if (!out_buf_info)
418 return VA_STATUS_ERROR_INVALID_PARAMETER;
419
420 if (!out_buf_info->mem_type)
421 mem_type = mem_types[0];
422 else {
423 mem_type = 0;
424 for (i = 0; mem_types[i] != 0; i++) {
425 if (out_buf_info->mem_type & mem_types[i]) {
426 mem_type = out_buf_info->mem_type;
427 break;
428 }
429 }
430 if (!mem_type)
431 return VA_STATUS_ERROR_UNSUPPORTED_MEMORY_TYPE;
432 }
433
434 if (!buf->derived_surface.resource)
435 return VA_STATUS_ERROR_INVALID_BUFFER;
436
437 if (buf->export_refcount > 0) {
438 if (buf->export_state.mem_type != mem_type)
439 return VA_STATUS_ERROR_INVALID_PARAMETER;
440 } else {
441 VABufferInfo * const buf_info = &buf->export_state;
442
443 switch (mem_type) {
444 #ifdef _WIN32
445 case VA_SURFACE_ATTRIB_MEM_TYPE_D3D12_RESOURCE:
446 case VA_SURFACE_ATTRIB_MEM_TYPE_NTHANDLE:
447 #else
448 case VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME:
449 #endif
450 {
451 struct winsys_handle whandle;
452
453 mtx_lock(&drv->mutex);
454
455 memset(&whandle, 0, sizeof(whandle));
456 whandle.type = WINSYS_HANDLE_TYPE_FD;
457
458 #ifdef _WIN32
459 if (mem_type == VA_SURFACE_ATTRIB_MEM_TYPE_D3D12_RESOURCE)
460 whandle.type = WINSYS_HANDLE_TYPE_D3D12_RES;
461 #endif
462 if (!screen->resource_get_handle(screen, drv->pipe,
463 buf->derived_surface.resource,
464 &whandle, PIPE_HANDLE_USAGE_FRAMEBUFFER_WRITE)) {
465 mtx_unlock(&drv->mutex);
466 return VA_STATUS_ERROR_INVALID_BUFFER;
467 }
468
469 mtx_unlock(&drv->mutex);
470
471 buf_info->handle = (intptr_t)whandle.handle;
472
473 #ifdef _WIN32
474 if (mem_type == VA_SURFACE_ATTRIB_MEM_TYPE_D3D12_RESOURCE)
475 buf_info->handle = (intptr_t)whandle.com_obj;
476 #endif
477 break;
478 }
479 default:
480 return VA_STATUS_ERROR_UNSUPPORTED_MEMORY_TYPE;
481 }
482
483 buf_info->type = buf->type;
484 buf_info->mem_type = mem_type;
485 buf_info->mem_size = buf->num_elements * buf->size;
486 }
487
488 buf->export_refcount++;
489
490 *out_buf_info = buf->export_state;
491
492 return VA_STATUS_SUCCESS;
493 }
494
495 VAStatus
vlVaReleaseBufferHandle(VADriverContextP ctx,VABufferID buf_id)496 vlVaReleaseBufferHandle(VADriverContextP ctx, VABufferID buf_id)
497 {
498 vlVaDriver *drv;
499 vlVaBuffer *buf;
500
501 if (!ctx)
502 return VA_STATUS_ERROR_INVALID_CONTEXT;
503
504 drv = VL_VA_DRIVER(ctx);
505 mtx_lock(&drv->mutex);
506 buf = handle_table_get(drv->htab, buf_id);
507 mtx_unlock(&drv->mutex);
508
509 if (!buf)
510 return VA_STATUS_ERROR_INVALID_BUFFER;
511
512 if (buf->export_refcount == 0)
513 return VA_STATUS_ERROR_INVALID_BUFFER;
514
515 if (--buf->export_refcount == 0) {
516 VABufferInfo * const buf_info = &buf->export_state;
517
518 switch (buf_info->mem_type) {
519 #ifdef _WIN32
520 case VA_SURFACE_ATTRIB_MEM_TYPE_D3D12_RESOURCE:
521 // Do nothing for this case.
522 break;
523 case VA_SURFACE_ATTRIB_MEM_TYPE_NTHANDLE:
524 CloseHandle((HANDLE) buf_info->handle);
525 break;
526 #else
527 case VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME:
528 close((intptr_t)buf_info->handle);
529 break;
530 #endif
531 default:
532 return VA_STATUS_ERROR_INVALID_BUFFER;
533 }
534
535 buf_info->mem_type = 0;
536 }
537
538 return VA_STATUS_SUCCESS;
539 }
540
541 #if VA_CHECK_VERSION(1, 15, 0)
542 VAStatus
vlVaSyncBuffer(VADriverContextP ctx,VABufferID buf_id,uint64_t timeout_ns)543 vlVaSyncBuffer(VADriverContextP ctx, VABufferID buf_id, uint64_t timeout_ns)
544 {
545 vlVaDriver *drv;
546 vlVaContext *context;
547 vlVaBuffer *buf;
548
549 if (!ctx)
550 return VA_STATUS_ERROR_INVALID_CONTEXT;
551
552 drv = VL_VA_DRIVER(ctx);
553 if (!drv)
554 return VA_STATUS_ERROR_INVALID_CONTEXT;
555
556 mtx_lock(&drv->mutex);
557 buf = handle_table_get(drv->htab, buf_id);
558 if (!buf) {
559 mtx_unlock(&drv->mutex);
560 return VA_STATUS_ERROR_INVALID_BUFFER;
561 }
562
563 /* No outstanding operation: nothing to do. */
564 if (!buf->fence) {
565 mtx_unlock(&drv->mutex);
566 return VA_STATUS_SUCCESS;
567 }
568
569 context = buf->ctx;
570 if (!context || !context->decoder) {
571 mtx_unlock(&drv->mutex);
572 return VA_STATUS_ERROR_INVALID_CONTEXT;
573 }
574
575 mtx_lock(&context->mutex);
576 mtx_unlock(&drv->mutex);
577 int ret = context->decoder->fence_wait(context->decoder, buf->fence, timeout_ns);
578 mtx_unlock(&context->mutex);
579 return ret ? VA_STATUS_SUCCESS : VA_STATUS_ERROR_TIMEDOUT;
580 }
581 #endif
582
vlVaGetBufferFeedback(vlVaBuffer * buf)583 void vlVaGetBufferFeedback(vlVaBuffer *buf)
584 {
585 if (!buf->ctx || !buf->ctx->decoder || !buf->feedback)
586 return;
587
588 buf->ctx->decoder->get_feedback(buf->ctx->decoder, buf->feedback,
589 &buf->coded_size, &buf->extended_metadata);
590 buf->feedback = NULL;
591 }
592