|
|
@ -725,15 +725,38 @@ void BlockLinearUnswizzle3DPass::Unswizzle( |
|
|
: VK_IMAGE_LAYOUT_UNDEFINED; |
|
|
: VK_IMAGE_LAYOUT_UNDEFINED; |
|
|
|
|
|
|
|
|
scheduler.RequestOutsideRenderPassOperationContext(); |
|
|
scheduler.RequestOutsideRenderPassOperationContext(); |
|
|
|
|
|
|
|
|
|
|
|
scheduler.Record([dst_image = image.Handle(), aspect = image.AspectMask(), initial_prior_layout](vk::CommandBuffer cmdbuf) { |
|
|
|
|
|
if (dst_image == VK_NULL_HANDLE) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const VkAccessFlags src_access = initial_prior_layout == VK_IMAGE_LAYOUT_GENERAL |
|
|
|
|
|
? (VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT) : 0u; |
|
|
|
|
|
const VkPipelineStageFlags src_stage = initial_prior_layout == VK_IMAGE_LAYOUT_GENERAL |
|
|
|
|
|
? (VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT) : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; |
|
|
|
|
|
|
|
|
|
|
|
const VkImageMemoryBarrier pre_barrier{ |
|
|
|
|
|
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|
|
|
|
|
.pNext = nullptr, |
|
|
|
|
|
.srcAccessMask = src_access, |
|
|
|
|
|
.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, |
|
|
|
|
|
.oldLayout = initial_prior_layout, |
|
|
|
|
|
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
|
|
|
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.image = dst_image, |
|
|
|
|
|
.subresourceRange = {aspect, 0, 1, 0, 1}, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
cmdbuf.PipelineBarrier(src_stage, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, pre_barrier); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
for (u32 z_offset = 0; z_offset < z_count; z_offset += MAX_BATCH_SLICES) { |
|
|
for (u32 z_offset = 0; z_offset < z_count; z_offset += MAX_BATCH_SLICES) { |
|
|
const u32 current_chunk_slices = (std::min)(MAX_BATCH_SLICES, z_count - z_offset); |
|
|
const u32 current_chunk_slices = (std::min)(MAX_BATCH_SLICES, z_count - z_offset); |
|
|
const u32 current_z_src = z_src_start + z_offset; |
|
|
const u32 current_z_src = z_src_start + z_offset; |
|
|
const u32 current_z_dst = z_image_start + z_offset; |
|
|
const u32 current_z_dst = z_image_start + z_offset; |
|
|
|
|
|
|
|
|
const VkImageLayout prior_layout = (z_offset == 0) |
|
|
|
|
|
? initial_prior_layout |
|
|
|
|
|
: VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; |
|
|
|
|
|
|
|
|
|
|
|
bool chunk_has_data = slice_has_data.empty(); |
|
|
bool chunk_has_data = slice_has_data.empty(); |
|
|
if (!chunk_has_data) { |
|
|
if (!chunk_has_data) { |
|
|
const u32 z_src_end = current_z_src + current_chunk_slices; |
|
|
const u32 z_src_end = current_z_src + current_chunk_slices; |
|
|
@ -747,12 +770,33 @@ void BlockLinearUnswizzle3DPass::Unswizzle( |
|
|
|
|
|
|
|
|
if (chunk_has_data) { |
|
|
if (chunk_has_data) { |
|
|
UnswizzleChunk(image, swizzled, sw, params, blocks_x, blocks_y, |
|
|
UnswizzleChunk(image, swizzled, sw, params, blocks_x, blocks_y, |
|
|
current_z_src, current_z_dst, current_chunk_slices, prior_layout); |
|
|
|
|
|
|
|
|
current_z_src, current_z_dst, current_chunk_slices); |
|
|
} else { |
|
|
} else { |
|
|
UnswizzleZeroChunk(image, blocks_x, blocks_y, bytes_per_block, |
|
|
UnswizzleZeroChunk(image, blocks_x, blocks_y, bytes_per_block, |
|
|
current_z_dst, current_chunk_slices, prior_layout); |
|
|
|
|
|
|
|
|
current_z_dst, current_chunk_slices); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
scheduler.Record([dst_image = image.Handle(), aspect = image.AspectMask()](vk::CommandBuffer cmdbuf) { |
|
|
|
|
|
if (dst_image == VK_NULL_HANDLE) return; |
|
|
|
|
|
|
|
|
|
|
|
const VkImageMemoryBarrier post_barrier{ |
|
|
|
|
|
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|
|
|
|
|
.pNext = nullptr, |
|
|
|
|
|
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, |
|
|
|
|
|
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, |
|
|
|
|
|
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
|
|
|
|
|
.newLayout = VK_IMAGE_LAYOUT_GENERAL, |
|
|
|
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.image = dst_image, |
|
|
|
|
|
.subresourceRange = {aspect, 0, 1, 0, 1}, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, |
|
|
|
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, |
|
|
|
|
|
0, post_barrier); |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void BlockLinearUnswizzle3DPass::UnswizzleChunk( |
|
|
void BlockLinearUnswizzle3DPass::UnswizzleChunk( |
|
|
@ -761,10 +805,10 @@ void BlockLinearUnswizzle3DPass::UnswizzleChunk( |
|
|
const VideoCommon::SwizzleParameters& sw, |
|
|
const VideoCommon::SwizzleParameters& sw, |
|
|
const BlockLinearSwizzle3DParams& params, |
|
|
const BlockLinearSwizzle3DParams& params, |
|
|
u32 blocks_x, u32 blocks_y, |
|
|
u32 blocks_x, u32 blocks_y, |
|
|
u32 z_src, u32 z_dst, u32 z_count, |
|
|
|
|
|
VkImageLayout prior_image_layout) |
|
|
|
|
|
|
|
|
u32 z_src, u32 z_dst, u32 z_count) |
|
|
{ |
|
|
{ |
|
|
BlockLinearUnswizzle3DPushConstants pc{}; |
|
|
BlockLinearUnswizzle3DPushConstants pc{}; |
|
|
|
|
|
|
|
|
pc.origin[0] = params.origin[0]; |
|
|
pc.origin[0] = params.origin[0]; |
|
|
pc.origin[1] = params.origin[1]; |
|
|
pc.origin[1] = params.origin[1]; |
|
|
pc.origin[2] = z_src; // Current chunk's Z start
|
|
|
pc.origin[2] = z_src; // Current chunk's Z start
|
|
|
@ -789,7 +833,7 @@ void BlockLinearUnswizzle3DPass::UnswizzleChunk( |
|
|
compute_pass_descriptor_queue.Acquire(scheduler, 3); |
|
|
compute_pass_descriptor_queue.Acquire(scheduler, 3); |
|
|
compute_pass_descriptor_queue.AddBuffer(swizzled.buffer, |
|
|
compute_pass_descriptor_queue.AddBuffer(swizzled.buffer, |
|
|
sw.buffer_offset + swizzled.offset, |
|
|
sw.buffer_offset + swizzled.offset, |
|
|
image.guest_size_bytes - sw.buffer_offset); |
|
|
|
|
|
|
|
|
VK_WHOLE_SIZE); |
|
|
compute_pass_descriptor_queue.AddBuffer(*image.compute_unswizzle_buffer, 0, |
|
|
compute_pass_descriptor_queue.AddBuffer(*image.compute_unswizzle_buffer, 0, |
|
|
image.compute_unswizzle_buffer_size); |
|
|
image.compute_unswizzle_buffer_size); |
|
|
|
|
|
|
|
|
@ -805,13 +849,6 @@ void BlockLinearUnswizzle3DPass::UnswizzleChunk( |
|
|
static_cast<VkDeviceSize>(blocks_x) * blocks_y * bytes_per_block; |
|
|
static_cast<VkDeviceSize>(blocks_x) * blocks_y * bytes_per_block; |
|
|
const VkDeviceSize barrier_size = output_slice_size * z_count; |
|
|
const VkDeviceSize barrier_size = output_slice_size * z_count; |
|
|
|
|
|
|
|
|
const VkAccessFlags src_access = |
|
|
|
|
|
(prior_image_layout == VK_IMAGE_LAYOUT_UNDEFINED) |
|
|
|
|
|
? VkAccessFlags{} |
|
|
|
|
|
: (prior_image_layout == VK_IMAGE_LAYOUT_GENERAL) |
|
|
|
|
|
? static_cast<VkAccessFlags>(VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT) |
|
|
|
|
|
: static_cast<VkAccessFlags>(VK_ACCESS_TRANSFER_WRITE_BIT); |
|
|
|
|
|
|
|
|
|
|
|
const VkBuffer out_buffer = *image.compute_unswizzle_buffer; |
|
|
const VkBuffer out_buffer = *image.compute_unswizzle_buffer; |
|
|
const VkImage dst_image = image.Handle(); |
|
|
const VkImage dst_image = image.Handle(); |
|
|
const VkImageAspectFlags aspect = image.AspectMask(); |
|
|
const VkImageAspectFlags aspect = image.AspectMask(); |
|
|
@ -820,10 +857,8 @@ void BlockLinearUnswizzle3DPass::UnswizzleChunk( |
|
|
|
|
|
|
|
|
scheduler.Record([this, set, descriptor_data, pc, gx, gy, gz, |
|
|
scheduler.Record([this, set, descriptor_data, pc, gx, gy, gz, |
|
|
z_dst, z_count, barrier_size, |
|
|
z_dst, z_count, barrier_size, |
|
|
prior_image_layout, src_access, |
|
|
|
|
|
out_buffer, dst_image, aspect, |
|
|
out_buffer, dst_image, aspect, |
|
|
image_width, image_height |
|
|
|
|
|
](vk::CommandBuffer cmdbuf) { |
|
|
|
|
|
|
|
|
image_width, image_height](vk::CommandBuffer cmdbuf) { |
|
|
|
|
|
|
|
|
if (dst_image == VK_NULL_HANDLE || out_buffer == VK_NULL_HANDLE) { |
|
|
if (dst_image == VK_NULL_HANDLE || out_buffer == VK_NULL_HANDLE) { |
|
|
return; |
|
|
return; |
|
|
@ -848,26 +883,7 @@ void BlockLinearUnswizzle3DPass::UnswizzleChunk( |
|
|
.size = barrier_size, |
|
|
.size = barrier_size, |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// Image layout transition
|
|
|
|
|
|
const VkImageMemoryBarrier pre_barrier{ |
|
|
|
|
|
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|
|
|
|
|
.pNext = nullptr, |
|
|
|
|
|
.srcAccessMask = src_access, |
|
|
|
|
|
.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, |
|
|
|
|
|
.oldLayout = prior_image_layout, |
|
|
|
|
|
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
|
|
|
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.image = dst_image, |
|
|
|
|
|
.subresourceRange = {aspect, 0, 1, 0, 1}, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// Single barrier handles both buffer and image
|
|
|
|
|
|
cmdbuf.PipelineBarrier( |
|
|
|
|
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, |
|
|
|
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, |
|
|
|
|
|
0, nullptr, buffer_barrier, pre_barrier |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, buffer_barrier); |
|
|
|
|
|
|
|
|
// Copy chunk to correct Z position in image
|
|
|
// Copy chunk to correct Z position in image
|
|
|
const VkBufferImageCopy copy{ |
|
|
const VkBufferImageCopy copy{ |
|
|
@ -880,27 +896,6 @@ void BlockLinearUnswizzle3DPass::UnswizzleChunk( |
|
|
}; |
|
|
}; |
|
|
cmdbuf.CopyBufferToImage(out_buffer, dst_image, |
|
|
cmdbuf.CopyBufferToImage(out_buffer, dst_image, |
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, copy); |
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, copy); |
|
|
|
|
|
|
|
|
// Post-copy transition
|
|
|
|
|
|
const VkImageMemoryBarrier post_barrier{ |
|
|
|
|
|
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|
|
|
|
|
.pNext = nullptr, |
|
|
|
|
|
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, |
|
|
|
|
|
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, |
|
|
|
|
|
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
|
|
|
|
|
.newLayout = VK_IMAGE_LAYOUT_GENERAL, |
|
|
|
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.image = dst_image, |
|
|
|
|
|
.subresourceRange = {aspect, 0, 1, 0, 1}, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
cmdbuf.PipelineBarrier( |
|
|
|
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, |
|
|
|
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, |
|
|
|
|
|
0, |
|
|
|
|
|
nullptr, nullptr, post_barrier |
|
|
|
|
|
); |
|
|
|
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -1006,8 +1001,7 @@ void BlockLinearUnswizzle3DPass::UnswizzleZeroChunk( |
|
|
Image& image, |
|
|
Image& image, |
|
|
u32 blocks_x, u32 blocks_y, |
|
|
u32 blocks_x, u32 blocks_y, |
|
|
u32 bytes_per_block, |
|
|
u32 bytes_per_block, |
|
|
u32 z_dst, u32 z_count, |
|
|
|
|
|
VkImageLayout prior_image_layout) |
|
|
|
|
|
|
|
|
u32 z_dst, u32 z_count) |
|
|
{ |
|
|
{ |
|
|
ASSERT(image.has_compute_unswizzle_buffer); |
|
|
ASSERT(image.has_compute_unswizzle_buffer); |
|
|
|
|
|
|
|
|
@ -1024,17 +1018,8 @@ void BlockLinearUnswizzle3DPass::UnswizzleZeroChunk( |
|
|
static_cast<VkDeviceSize>(blocks_x) * blocks_y * bytes_per_block; |
|
|
static_cast<VkDeviceSize>(blocks_x) * blocks_y * bytes_per_block; |
|
|
const VkDeviceSize fill_size = output_slice_bytes * z_count; |
|
|
const VkDeviceSize fill_size = output_slice_bytes * z_count; |
|
|
|
|
|
|
|
|
const VkAccessFlags src_access = |
|
|
|
|
|
(prior_image_layout == VK_IMAGE_LAYOUT_UNDEFINED) |
|
|
|
|
|
? VkAccessFlags{} |
|
|
|
|
|
: (prior_image_layout == VK_IMAGE_LAYOUT_GENERAL) |
|
|
|
|
|
? static_cast<VkAccessFlags>(VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT) |
|
|
|
|
|
: static_cast<VkAccessFlags>(VK_ACCESS_TRANSFER_WRITE_BIT); |
|
|
|
|
|
|
|
|
|
|
|
scheduler.Record([out_buffer, dst_image, aspect, z_dst, z_count, |
|
|
scheduler.Record([out_buffer, dst_image, aspect, z_dst, z_count, |
|
|
fill_size, prior_image_layout, src_access, |
|
|
|
|
|
image_width, image_height |
|
|
|
|
|
](vk::CommandBuffer cmdbuf) { |
|
|
|
|
|
|
|
|
fill_size, image_width, image_height](vk::CommandBuffer cmdbuf) { |
|
|
|
|
|
|
|
|
if (dst_image == VK_NULL_HANDLE || out_buffer == VK_NULL_HANDLE) { |
|
|
if (dst_image == VK_NULL_HANDLE || out_buffer == VK_NULL_HANDLE) { |
|
|
return; |
|
|
return; |
|
|
@ -1052,25 +1037,13 @@ void BlockLinearUnswizzle3DPass::UnswizzleZeroChunk( |
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
.buffer = out_buffer, |
|
|
.buffer = out_buffer, |
|
|
.offset = 0, |
|
|
.offset = 0, |
|
|
.size = fill_size, |
|
|
|
|
|
}; |
|
|
|
|
|
const VkImageMemoryBarrier pre_barrier{ |
|
|
|
|
|
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|
|
|
|
|
.pNext = nullptr, |
|
|
|
|
|
.srcAccessMask = src_access, |
|
|
|
|
|
.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, |
|
|
|
|
|
.oldLayout = prior_image_layout, |
|
|
|
|
|
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
|
|
|
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.image = dst_image, |
|
|
|
|
|
.subresourceRange = {aspect, 0, 1, 0, 1}, |
|
|
|
|
|
|
|
|
.size = fill_size, |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
cmdbuf.PipelineBarrier( |
|
|
cmdbuf.PipelineBarrier( |
|
|
|
|
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, |
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, |
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, |
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, |
|
|
|
|
|
0, nullptr, buffer_barrier, pre_barrier |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
0, buffer_barrier); |
|
|
|
|
|
|
|
|
// Copy the zeroed buffer region into the correct Z position of the image.
|
|
|
// Copy the zeroed buffer region into the correct Z position of the image.
|
|
|
const VkBufferImageCopy copy{ |
|
|
const VkBufferImageCopy copy{ |
|
|
@ -1082,26 +1055,7 @@ void BlockLinearUnswizzle3DPass::UnswizzleZeroChunk( |
|
|
.imageExtent = {image_width, image_height, z_count}, |
|
|
.imageExtent = {image_width, image_height, z_count}, |
|
|
}; |
|
|
}; |
|
|
cmdbuf.CopyBufferToImage(out_buffer, dst_image, |
|
|
cmdbuf.CopyBufferToImage(out_buffer, dst_image, |
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, copy); |
|
|
|
|
|
|
|
|
|
|
|
// Transition image to GENERAL for subsequent shader reads/writes.
|
|
|
|
|
|
const VkImageMemoryBarrier post_barrier{ |
|
|
|
|
|
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|
|
|
|
|
.pNext = nullptr, |
|
|
|
|
|
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, |
|
|
|
|
|
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, |
|
|
|
|
|
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
|
|
|
|
|
.newLayout = VK_IMAGE_LAYOUT_GENERAL, |
|
|
|
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|
|
|
|
|
.image = dst_image, |
|
|
|
|
|
.subresourceRange = {aspect, 0, 1, 0, 1}, |
|
|
|
|
|
}; |
|
|
|
|
|
cmdbuf.PipelineBarrier( |
|
|
|
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, |
|
|
|
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, |
|
|
|
|
|
0, nullptr, nullptr, post_barrier |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, copy); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|