2021-06-24 12:09:35 +01:00
|
|
|
|
/*
|
|
|
|
|
* Copyright © Microsoft Corporation
|
|
|
|
|
*
|
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
|
|
* copy of this software and associated documentation files (the "Software"),
|
|
|
|
|
* to deal in the Software without restriction, including without limitation
|
|
|
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
|
|
|
* and/or sell copies of the Software, and to permit persons to whom the
|
|
|
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
|
|
|
*
|
|
|
|
|
* The above copyright notice and this permission notice (including the next
|
|
|
|
|
* paragraph) shall be included in all copies or substantial portions of the
|
|
|
|
|
* Software.
|
|
|
|
|
*
|
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
|
|
|
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
|
|
|
* IN THE SOFTWARE.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "dzn_private.h"
|
|
|
|
|
|
|
|
|
|
#include "vk_alloc.h"
|
|
|
|
|
#include "vk_debug_report.h"
|
|
|
|
|
#include "vk_util.h"
|
|
|
|
|
|
|
|
|
|
static D3D12_QUERY_HEAP_TYPE
|
|
|
|
|
dzn_query_pool_get_heap_type(VkQueryType in)
|
|
|
|
|
{
|
|
|
|
|
switch (in) {
|
|
|
|
|
case VK_QUERY_TYPE_OCCLUSION: return D3D12_QUERY_HEAP_TYPE_OCCLUSION;
|
|
|
|
|
case VK_QUERY_TYPE_PIPELINE_STATISTICS: return D3D12_QUERY_HEAP_TYPE_PIPELINE_STATISTICS;
|
|
|
|
|
case VK_QUERY_TYPE_TIMESTAMP: return D3D12_QUERY_HEAP_TYPE_TIMESTAMP;
|
|
|
|
|
default: unreachable("Unsupported query type");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
D3D12_QUERY_TYPE
|
2022-04-05 16:41:20 +01:00
|
|
|
|
dzn_query_pool_get_query_type(const struct dzn_query_pool *qpool,
|
2021-06-24 12:09:35 +01:00
|
|
|
|
VkQueryControlFlags flags)
|
|
|
|
|
{
|
|
|
|
|
switch (qpool->heap_type) {
|
|
|
|
|
case D3D12_QUERY_HEAP_TYPE_OCCLUSION:
|
|
|
|
|
return flags & VK_QUERY_CONTROL_PRECISE_BIT ?
|
|
|
|
|
D3D12_QUERY_TYPE_OCCLUSION : D3D12_QUERY_TYPE_BINARY_OCCLUSION;
|
|
|
|
|
case D3D12_QUERY_HEAP_TYPE_PIPELINE_STATISTICS: return D3D12_QUERY_TYPE_PIPELINE_STATISTICS;
|
|
|
|
|
case D3D12_QUERY_HEAP_TYPE_TIMESTAMP: return D3D12_QUERY_TYPE_TIMESTAMP;
|
|
|
|
|
default: unreachable("Unsupported query type");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2022-04-05 16:41:20 +01:00
|
|
|
|
dzn_query_pool_destroy(struct dzn_query_pool *qpool,
|
2021-06-24 12:09:35 +01:00
|
|
|
|
const VkAllocationCallbacks *alloc)
|
|
|
|
|
{
|
|
|
|
|
if (!qpool)
|
|
|
|
|
return;
|
|
|
|
|
|
2022-04-05 16:41:20 +01:00
|
|
|
|
struct dzn_device *device = container_of(qpool->base.device, struct dzn_device, vk);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
|
|
|
|
|
if (qpool->collect_map)
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Resource_Unmap(qpool->collect_buffer, 0, NULL);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
|
|
|
|
|
if (qpool->collect_buffer)
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Resource_Release(qpool->collect_buffer);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
|
|
|
|
|
if (qpool->resolve_buffer)
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Resource_Release(qpool->resolve_buffer);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
|
|
|
|
|
if (qpool->heap)
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12QueryHeap_Release(qpool->heap);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
|
|
|
|
|
for (uint32_t q = 0; q < qpool->query_count; q++) {
|
|
|
|
|
if (qpool->queries[q].fence)
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Fence_Release(qpool->queries[q].fence);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mtx_destroy(&qpool->queries_lock);
|
|
|
|
|
vk_object_base_finish(&qpool->base);
|
|
|
|
|
vk_free2(&device->vk.alloc, alloc, qpool);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static VkResult
|
2022-04-05 16:41:20 +01:00
|
|
|
|
dzn_query_pool_create(struct dzn_device *device,
|
2021-06-24 12:09:35 +01:00
|
|
|
|
const VkQueryPoolCreateInfo *info,
|
|
|
|
|
const VkAllocationCallbacks *alloc,
|
|
|
|
|
VkQueryPool *out)
|
|
|
|
|
{
|
|
|
|
|
VK_MULTIALLOC(ma);
|
2022-04-05 16:41:20 +01:00
|
|
|
|
VK_MULTIALLOC_DECL(&ma, struct dzn_query_pool, qpool, 1);
|
|
|
|
|
VK_MULTIALLOC_DECL(&ma, struct dzn_query, queries, info->queryCount);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
|
|
|
|
|
if (!vk_multialloc_zalloc2(&ma, &device->vk.alloc, alloc,
|
|
|
|
|
VK_SYSTEM_ALLOCATION_SCOPE_OBJECT))
|
|
|
|
|
return vk_error(device, VK_ERROR_OUT_OF_HOST_MEMORY);
|
|
|
|
|
|
|
|
|
|
vk_object_base_init(&device->vk, &qpool->base, VK_OBJECT_TYPE_QUERY_POOL);
|
|
|
|
|
|
|
|
|
|
mtx_init(&qpool->queries_lock, mtx_plain);
|
|
|
|
|
qpool->query_count = info->queryCount;
|
|
|
|
|
qpool->queries = queries;
|
|
|
|
|
|
|
|
|
|
D3D12_QUERY_HEAP_DESC desc = { 0 };
|
|
|
|
|
qpool->heap_type = desc.Type = dzn_query_pool_get_heap_type(info->queryType);
|
|
|
|
|
desc.Count = info->queryCount;
|
|
|
|
|
desc.NodeMask = 0;
|
|
|
|
|
|
|
|
|
|
HRESULT hres =
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Device1_CreateQueryHeap(device->dev, &desc,
|
2022-04-06 14:04:34 +01:00
|
|
|
|
&IID_ID3D12QueryHeap,
|
|
|
|
|
&qpool->heap);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
if (FAILED(hres)) {
|
|
|
|
|
dzn_query_pool_destroy(qpool, alloc);
|
|
|
|
|
return vk_error(device, VK_ERROR_OUT_OF_DEVICE_MEMORY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (info->queryType) {
|
|
|
|
|
case VK_QUERY_TYPE_OCCLUSION:
|
|
|
|
|
case VK_QUERY_TYPE_TIMESTAMP:
|
|
|
|
|
qpool->query_size = sizeof(uint64_t);
|
|
|
|
|
break;
|
|
|
|
|
case VK_QUERY_TYPE_PIPELINE_STATISTICS:
|
|
|
|
|
qpool->pipeline_statistics = info->pipelineStatistics;
|
|
|
|
|
qpool->query_size = sizeof(D3D12_QUERY_DATA_PIPELINE_STATISTICS);
|
|
|
|
|
break;
|
|
|
|
|
default: unreachable("Unsupported query type");
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-25 16:08:50 +00:00
|
|
|
|
D3D12_HEAP_PROPERTIES hprops;
|
|
|
|
|
ID3D12Device1_GetCustomHeapProperties(device->dev, &hprops, 0,
|
|
|
|
|
D3D12_HEAP_TYPE_DEFAULT);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
D3D12_RESOURCE_DESC rdesc = {
|
|
|
|
|
.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER,
|
|
|
|
|
.Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT,
|
|
|
|
|
.Width = info->queryCount * qpool->query_size,
|
|
|
|
|
.Height = 1,
|
|
|
|
|
.DepthOrArraySize = 1,
|
|
|
|
|
.MipLevels = 1,
|
|
|
|
|
.Format = DXGI_FORMAT_UNKNOWN,
|
|
|
|
|
.SampleDesc = { .Count = 1, .Quality = 0 },
|
|
|
|
|
.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
|
|
|
|
|
.Flags = D3D12_RESOURCE_FLAG_NONE,
|
|
|
|
|
};
|
|
|
|
|
|
2022-03-25 16:08:50 +00:00
|
|
|
|
hres = ID3D12Device1_CreateCommittedResource(device->dev, &hprops,
|
|
|
|
|
D3D12_HEAP_FLAG_NONE,
|
|
|
|
|
&rdesc,
|
|
|
|
|
D3D12_RESOURCE_STATE_COPY_DEST,
|
|
|
|
|
NULL,
|
2022-04-06 14:04:34 +01:00
|
|
|
|
&IID_ID3D12Resource,
|
|
|
|
|
&qpool->resolve_buffer);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
if (FAILED(hres)) {
|
|
|
|
|
dzn_query_pool_destroy(qpool, alloc);
|
|
|
|
|
return vk_error(device, VK_ERROR_OUT_OF_DEVICE_MEMORY);
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Device1_GetCustomHeapProperties(device->dev, &hprops, 0,
|
|
|
|
|
D3D12_HEAP_TYPE_READBACK);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
rdesc.Width = info->queryCount * (qpool->query_size + sizeof(uint64_t));
|
2022-03-25 16:08:50 +00:00
|
|
|
|
hres = ID3D12Device1_CreateCommittedResource(device->dev, &hprops,
|
|
|
|
|
D3D12_HEAP_FLAG_NONE,
|
|
|
|
|
&rdesc,
|
|
|
|
|
D3D12_RESOURCE_STATE_COPY_DEST,
|
|
|
|
|
NULL,
|
2022-04-06 14:04:34 +01:00
|
|
|
|
&IID_ID3D12Resource,
|
|
|
|
|
&qpool->collect_buffer);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
if (FAILED(hres)) {
|
|
|
|
|
dzn_query_pool_destroy(qpool, alloc);
|
|
|
|
|
return vk_error(device, VK_ERROR_OUT_OF_DEVICE_MEMORY);
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-25 16:08:50 +00:00
|
|
|
|
hres = ID3D12Resource_Map(qpool->collect_buffer, 0, NULL, (void **)&qpool->collect_map);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
if (FAILED(hres)) {
|
|
|
|
|
dzn_query_pool_destroy(qpool, alloc);
|
|
|
|
|
return vk_error(device, VK_ERROR_OUT_OF_HOST_MEMORY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memset(qpool->collect_map, 0, rdesc.Width);
|
|
|
|
|
|
|
|
|
|
*out = dzn_query_pool_to_handle(qpool);
|
|
|
|
|
return VK_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t
|
2022-04-05 16:41:20 +01:00
|
|
|
|
dzn_query_pool_get_result_offset(const struct dzn_query_pool *qpool, uint32_t query)
|
2021-06-24 12:09:35 +01:00
|
|
|
|
{
|
|
|
|
|
return query * qpool->query_size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t
|
2022-04-05 16:41:20 +01:00
|
|
|
|
dzn_query_pool_get_result_size(const struct dzn_query_pool *qpool, uint32_t query_count)
|
2021-06-24 12:09:35 +01:00
|
|
|
|
{
|
|
|
|
|
return query_count * qpool->query_size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t
|
2022-04-05 16:41:20 +01:00
|
|
|
|
dzn_query_pool_get_availability_offset(const struct dzn_query_pool *qpool, uint32_t query)
|
2021-06-24 12:09:35 +01:00
|
|
|
|
{
|
|
|
|
|
return (qpool->query_count * qpool->query_size) + (sizeof(uint64_t) * query);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL
|
|
|
|
|
dzn_CreateQueryPool(VkDevice device,
|
|
|
|
|
const VkQueryPoolCreateInfo *pCreateInfo,
|
|
|
|
|
const VkAllocationCallbacks *pAllocator,
|
|
|
|
|
VkQueryPool *pQueryPool)
|
|
|
|
|
{
|
|
|
|
|
return dzn_query_pool_create(dzn_device_from_handle(device),
|
|
|
|
|
pCreateInfo, pAllocator, pQueryPool);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VKAPI_ATTR void VKAPI_CALL
|
|
|
|
|
dzn_DestroyQueryPool(VkDevice device,
|
|
|
|
|
VkQueryPool queryPool,
|
|
|
|
|
const VkAllocationCallbacks *pAllocator)
|
|
|
|
|
{
|
|
|
|
|
dzn_query_pool_destroy(dzn_query_pool_from_handle(queryPool), pAllocator);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VKAPI_ATTR void VKAPI_CALL
|
|
|
|
|
dzn_ResetQueryPool(VkDevice device,
|
|
|
|
|
VkQueryPool queryPool,
|
|
|
|
|
uint32_t firstQuery,
|
|
|
|
|
uint32_t queryCount)
|
|
|
|
|
{
|
|
|
|
|
VK_FROM_HANDLE(dzn_query_pool, qpool, queryPool);
|
|
|
|
|
|
|
|
|
|
mtx_lock(&qpool->queries_lock);
|
|
|
|
|
for (uint32_t q = 0; q < queryCount; q++) {
|
2022-04-05 16:41:20 +01:00
|
|
|
|
struct dzn_query *query = &qpool->queries[firstQuery + q];
|
2021-06-24 12:09:35 +01:00
|
|
|
|
|
|
|
|
|
query->fence_value = 0;
|
|
|
|
|
if (query->fence) {
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Fence_Release(query->fence);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
query->fence = NULL;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
mtx_lock(&qpool->queries_lock);
|
|
|
|
|
|
|
|
|
|
memset((uint8_t *)qpool->collect_map + dzn_query_pool_get_result_offset(qpool, firstQuery),
|
|
|
|
|
0, queryCount * qpool->query_size);
|
|
|
|
|
memset((uint8_t *)qpool->collect_map + dzn_query_pool_get_availability_offset(qpool, firstQuery),
|
|
|
|
|
0, queryCount * sizeof(uint64_t));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VKAPI_ATTR VkResult VKAPI_CALL
|
|
|
|
|
dzn_GetQueryPoolResults(VkDevice device,
|
|
|
|
|
VkQueryPool queryPool,
|
|
|
|
|
uint32_t firstQuery,
|
|
|
|
|
uint32_t queryCount,
|
|
|
|
|
size_t dataSize,
|
|
|
|
|
void *pData,
|
|
|
|
|
VkDeviceSize stride,
|
|
|
|
|
VkQueryResultFlags flags)
|
|
|
|
|
{
|
|
|
|
|
VK_FROM_HANDLE(dzn_query_pool, qpool, queryPool);
|
|
|
|
|
|
|
|
|
|
uint32_t step = (flags & VK_QUERY_RESULT_64_BIT) ?
|
|
|
|
|
sizeof(uint64_t) : sizeof(uint32_t);
|
|
|
|
|
VkResult result = VK_SUCCESS;
|
|
|
|
|
|
|
|
|
|
for (uint32_t q = 0; q < queryCount; q++) {
|
2022-04-05 16:41:20 +01:00
|
|
|
|
struct dzn_query *query = &qpool->queries[q + firstQuery];
|
2021-06-24 12:09:35 +01:00
|
|
|
|
|
|
|
|
|
uint8_t *dst_ptr = (uint8_t *)pData + (stride * q);
|
|
|
|
|
uint8_t *src_ptr =
|
|
|
|
|
(uint8_t *)qpool->collect_map +
|
|
|
|
|
dzn_query_pool_get_result_offset(qpool, firstQuery + q);
|
|
|
|
|
uint64_t available = 0;
|
|
|
|
|
|
|
|
|
|
if (flags & VK_QUERY_RESULT_WAIT_BIT) {
|
2022-03-25 12:15:44 +00:00
|
|
|
|
ID3D12Fence *query_fence = NULL;
|
2021-06-24 12:09:35 +01:00
|
|
|
|
uint64_t query_fence_val = 0;
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
mtx_lock(&qpool->queries_lock);
|
2022-03-25 12:15:44 +00:00
|
|
|
|
if (query->fence) {
|
|
|
|
|
query_fence = query->fence;
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Fence_AddRef(query_fence);
|
2022-03-25 12:15:44 +00:00
|
|
|
|
}
|
2021-06-24 12:09:35 +01:00
|
|
|
|
query_fence_val = query->fence_value;
|
|
|
|
|
mtx_unlock(&qpool->queries_lock);
|
|
|
|
|
|
2022-03-25 12:15:44 +00:00
|
|
|
|
if (query_fence)
|
2021-06-24 12:09:35 +01:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
/* Check again in 10ms.
|
|
|
|
|
* FIXME: decrease the polling period if it happens to hurt latency.
|
|
|
|
|
*/
|
|
|
|
|
Sleep(10);
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Fence_SetEventOnCompletion(query_fence, query_fence_val, NULL);
|
|
|
|
|
ID3D12Fence_Release(query_fence);
|
2021-06-24 12:09:35 +01:00
|
|
|
|
available = UINT64_MAX;
|
|
|
|
|
} else {
|
2022-03-25 12:15:44 +00:00
|
|
|
|
ID3D12Fence *query_fence = NULL;
|
2021-06-24 12:09:35 +01:00
|
|
|
|
mtx_lock(&qpool->queries_lock);
|
2022-03-25 12:15:44 +00:00
|
|
|
|
if (query->fence) {
|
|
|
|
|
query_fence = query->fence;
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Fence_AddRef(query_fence);
|
2022-03-25 12:15:44 +00:00
|
|
|
|
}
|
2021-06-24 12:09:35 +01:00
|
|
|
|
uint64_t query_fence_val = query->fence_value;
|
|
|
|
|
mtx_unlock(&qpool->queries_lock);
|
|
|
|
|
|
2022-03-25 12:15:44 +00:00
|
|
|
|
if (query_fence) {
|
2022-03-25 16:08:50 +00:00
|
|
|
|
if (ID3D12Fence_GetCompletedValue(query_fence) >= query_fence_val)
|
2022-03-25 12:15:44 +00:00
|
|
|
|
available = UINT64_MAX;
|
2022-03-25 16:08:50 +00:00
|
|
|
|
ID3D12Fence_Release(query_fence);
|
2022-03-25 12:15:44 +00:00
|
|
|
|
}
|
2021-06-24 12:09:35 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (qpool->heap_type != D3D12_QUERY_HEAP_TYPE_PIPELINE_STATISTICS) {
|
|
|
|
|
if (available)
|
|
|
|
|
memcpy(dst_ptr, src_ptr, step);
|
|
|
|
|
else if (flags & VK_QUERY_RESULT_PARTIAL_BIT)
|
|
|
|
|
memset(dst_ptr, 0, step);
|
|
|
|
|
|
|
|
|
|
dst_ptr += step;
|
|
|
|
|
} else {
|
|
|
|
|
for (uint32_t c = 0; c < sizeof(D3D12_QUERY_DATA_PIPELINE_STATISTICS) / sizeof(uint64_t); c++) {
|
|
|
|
|
if (!(BITFIELD_BIT(c) & qpool->pipeline_statistics))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (available)
|
|
|
|
|
memcpy(dst_ptr, src_ptr + (c * sizeof(uint64_t)), step);
|
|
|
|
|
else if (flags & VK_QUERY_RESULT_PARTIAL_BIT)
|
|
|
|
|
memset(dst_ptr, 0, step);
|
|
|
|
|
|
|
|
|
|
dst_ptr += step;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (flags & VK_QUERY_RESULT_WITH_AVAILABILITY_BIT)
|
|
|
|
|
memcpy(dst_ptr, &available, step);
|
|
|
|
|
|
|
|
|
|
if (!available && !(flags & VK_QUERY_RESULT_PARTIAL_BIT))
|
|
|
|
|
result = VK_NOT_READY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|