|
|
@ -7,6 +7,10 @@ |
|
|
|
#include <stdlib.h> |
|
|
|
#include <stdlib.h> |
|
|
|
#include <stdbool.h> |
|
|
|
#include <stdbool.h> |
|
|
|
#include <cglm/cglm.h> |
|
|
|
#include <cglm/cglm.h> |
|
|
|
|
|
|
|
#include <cglm/affine.h> |
|
|
|
|
|
|
|
#include <cglm/cam.h> |
|
|
|
|
|
|
|
#include <time.h> |
|
|
|
|
|
|
|
#include <cglm/util.h> |
|
|
|
|
|
|
|
|
|
|
|
#define WIDTH 2560 |
|
|
|
#define WIDTH 2560 |
|
|
|
#define HEIGHT 1440 |
|
|
|
#define HEIGHT 1440 |
|
|
@ -56,6 +60,8 @@ typedef struct ApplicationState { |
|
|
|
//describes how to access an image and allows access to it
|
|
|
|
//describes how to access an image and allows access to it
|
|
|
|
VkImageView* swapchain_image_views; |
|
|
|
VkImageView* swapchain_image_views; |
|
|
|
VkRenderPass render_pass; |
|
|
|
VkRenderPass render_pass; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorSetLayout descriptor_set_layout; |
|
|
|
VkPipelineLayout pipeline_layout; |
|
|
|
VkPipelineLayout pipeline_layout; |
|
|
|
|
|
|
|
|
|
|
|
VkPipeline graphics_pipeline; |
|
|
|
VkPipeline graphics_pipeline; |
|
|
@ -76,6 +82,12 @@ typedef struct ApplicationState { |
|
|
|
|
|
|
|
|
|
|
|
VkBuffer index_buffer; |
|
|
|
VkBuffer index_buffer; |
|
|
|
VkDeviceMemory index_buffer_memory; |
|
|
|
VkDeviceMemory index_buffer_memory; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VkBuffer uniform_buffers[MAX_FRAMES_IN_FLIGHT]; |
|
|
|
|
|
|
|
VkDeviceMemory uniform_buffer_memory[MAX_FRAMES_IN_FLIGHT]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorPool descriptor_pool; |
|
|
|
|
|
|
|
VkDescriptorSet descriptor_sets[2]; |
|
|
|
} ApplicationState; |
|
|
|
} ApplicationState; |
|
|
|
|
|
|
|
|
|
|
|
typedef struct QueueFamilyIndices { |
|
|
|
typedef struct QueueFamilyIndices { |
|
|
@ -101,6 +113,12 @@ const uint16_t indices[] = { |
|
|
|
0, 1, 2, 2, 3, 0 |
|
|
|
0, 1, 2, 2, 3, 0 |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct UBO { |
|
|
|
|
|
|
|
mat4 model; |
|
|
|
|
|
|
|
mat4 view; |
|
|
|
|
|
|
|
mat4 proj; |
|
|
|
|
|
|
|
} UBO; |
|
|
|
|
|
|
|
|
|
|
|
void create_window(ApplicationState* state) { |
|
|
|
void create_window(ApplicationState* state) { |
|
|
|
glfwInit(); |
|
|
|
glfwInit(); |
|
|
|
|
|
|
|
|
|
|
@ -656,13 +674,33 @@ void create_render_pass(ApplicationState* state) { |
|
|
|
render_pass_info.pDependencies = &dependency; |
|
|
|
render_pass_info.pDependencies = &dependency; |
|
|
|
|
|
|
|
|
|
|
|
if (vkCreateRenderPass(state->device, &render_pass_info, NULL, &state->render_pass) != VK_SUCCESS) { |
|
|
|
if (vkCreateRenderPass(state->device, &render_pass_info, NULL, &state->render_pass) != VK_SUCCESS) { |
|
|
|
printf("failed to create render pass"); |
|
|
|
printf("failed to create render pass\n"); |
|
|
|
exit(1); |
|
|
|
exit(1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
printf("created render pass\n"); |
|
|
|
printf("created render pass\n"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Descriptor set layouts specify the types of resources that are going to be accessed by the pipeline
|
|
|
|
|
|
|
|
void create_descriptor_set_layout(ApplicationState* state) { |
|
|
|
|
|
|
|
VkDescriptorSetLayoutBinding ubo_layout_binding = {0}; |
|
|
|
|
|
|
|
ubo_layout_binding.binding = 0; |
|
|
|
|
|
|
|
ubo_layout_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; |
|
|
|
|
|
|
|
ubo_layout_binding.descriptorCount = 1; |
|
|
|
|
|
|
|
ubo_layout_binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorSetLayoutCreateInfo layout_info = {0}; |
|
|
|
|
|
|
|
layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; |
|
|
|
|
|
|
|
layout_info.bindingCount = 1; |
|
|
|
|
|
|
|
layout_info.pBindings = &ubo_layout_binding; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (vkCreateDescriptorSetLayout(state->device, &layout_info, NULL, &state->descriptor_set_layout)) { |
|
|
|
|
|
|
|
printf("could not create descriptor set layout\n"); |
|
|
|
|
|
|
|
exit(1); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
printf("created descriptor set layout\n"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//now we need to setup a graphics pipeline
|
|
|
|
//now we need to setup a graphics pipeline
|
|
|
|
//the usual stages of a pipeline are:
|
|
|
|
//the usual stages of a pipeline are:
|
|
|
|
//
|
|
|
|
//
|
|
|
@ -815,8 +853,8 @@ void create_graphics_pipeline(ApplicationState* state) { |
|
|
|
//used for things such as uniforms
|
|
|
|
//used for things such as uniforms
|
|
|
|
VkPipelineLayoutCreateInfo pipeline_layout_create_info = {0}; |
|
|
|
VkPipelineLayoutCreateInfo pipeline_layout_create_info = {0}; |
|
|
|
pipeline_layout_create_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; |
|
|
|
pipeline_layout_create_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; |
|
|
|
pipeline_layout_create_info.setLayoutCount = 0; |
|
|
|
pipeline_layout_create_info.setLayoutCount = 1; |
|
|
|
pipeline_layout_create_info.pSetLayouts = NULL; |
|
|
|
pipeline_layout_create_info.pSetLayouts = &state->descriptor_set_layout; |
|
|
|
pipeline_layout_create_info.pushConstantRangeCount = 0; |
|
|
|
pipeline_layout_create_info.pushConstantRangeCount = 0; |
|
|
|
pipeline_layout_create_info.pPushConstantRanges = NULL; |
|
|
|
pipeline_layout_create_info.pPushConstantRanges = NULL; |
|
|
|
|
|
|
|
|
|
|
@ -1030,6 +1068,72 @@ void create_index_buffer(ApplicationState* state) { |
|
|
|
vkFreeMemory(state->device, staging_buffer_memory, NULL); |
|
|
|
vkFreeMemory(state->device, staging_buffer_memory, NULL); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void create_uniform_buffers(ApplicationState* state) { |
|
|
|
|
|
|
|
VkDeviceSize buffer_size = sizeof(UBO); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { |
|
|
|
|
|
|
|
create_buffer(state, buffer_size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, |
|
|
|
|
|
|
|
&state->uniform_buffers[i], &state->uniform_buffer_memory[i]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void create_descriptor_pool(ApplicationState* state) { |
|
|
|
|
|
|
|
VkDescriptorPoolSize pool_size = {0}; |
|
|
|
|
|
|
|
pool_size.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; |
|
|
|
|
|
|
|
pool_size.descriptorCount = MAX_FRAMES_IN_FLIGHT; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorPoolCreateInfo pool_info = {0}; |
|
|
|
|
|
|
|
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; |
|
|
|
|
|
|
|
pool_info.poolSizeCount = 1; |
|
|
|
|
|
|
|
pool_info.pPoolSizes = &pool_size; |
|
|
|
|
|
|
|
pool_info.maxSets = MAX_FRAMES_IN_FLIGHT; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (vkCreateDescriptorPool(state->device, &pool_info, NULL, &state->descriptor_pool) != VK_SUCCESS) { |
|
|
|
|
|
|
|
printf("failed to create descriptor pool\n"); |
|
|
|
|
|
|
|
exit(1); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
printf("created descriptor pool\n"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void create_descriptor_sets(ApplicationState* state) { |
|
|
|
|
|
|
|
VkDescriptorSetLayout layouts[2] = {state->descriptor_set_layout, state->descriptor_set_layout}; |
|
|
|
|
|
|
|
VkDescriptorSetAllocateInfo alloc_info = {0}; |
|
|
|
|
|
|
|
alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; |
|
|
|
|
|
|
|
alloc_info.descriptorPool = state->descriptor_pool; |
|
|
|
|
|
|
|
alloc_info.descriptorSetCount = MAX_FRAMES_IN_FLIGHT; |
|
|
|
|
|
|
|
alloc_info.pSetLayouts = layouts; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (vkAllocateDescriptorSets(state->device, &alloc_info, state->descriptor_sets) != VK_SUCCESS) { |
|
|
|
|
|
|
|
printf("failed to allocate descriptor sets\n"); |
|
|
|
|
|
|
|
exit(1); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
printf("allocated descriptor sets\n"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { |
|
|
|
|
|
|
|
VkDescriptorBufferInfo buffer_info = {0}; |
|
|
|
|
|
|
|
buffer_info.buffer = state->uniform_buffers[i]; |
|
|
|
|
|
|
|
buffer_info.offset = 0; |
|
|
|
|
|
|
|
buffer_info.range = sizeof(UBO); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VkWriteDescriptorSet descriptor_write = {0}; |
|
|
|
|
|
|
|
descriptor_write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; |
|
|
|
|
|
|
|
descriptor_write.dstSet = state->descriptor_sets[i]; |
|
|
|
|
|
|
|
descriptor_write.dstBinding = 0; |
|
|
|
|
|
|
|
descriptor_write.dstArrayElement = 0; |
|
|
|
|
|
|
|
descriptor_write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; |
|
|
|
|
|
|
|
descriptor_write.descriptorCount = 1; |
|
|
|
|
|
|
|
descriptor_write.pBufferInfo = &buffer_info; |
|
|
|
|
|
|
|
descriptor_write.pImageInfo = NULL; |
|
|
|
|
|
|
|
descriptor_write.pTexelBufferView = NULL; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vkUpdateDescriptorSets(state->device, 1, &descriptor_write, 0, NULL); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
printf("created descriptor sets\n"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void create_command_buffer(ApplicationState* state) { |
|
|
|
void create_command_buffer(ApplicationState* state) { |
|
|
|
VkCommandBufferAllocateInfo command_buf_allocate_info = {0}; |
|
|
|
VkCommandBufferAllocateInfo command_buf_allocate_info = {0}; |
|
|
|
command_buf_allocate_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; |
|
|
|
command_buf_allocate_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; |
|
|
@ -1046,7 +1150,7 @@ void create_command_buffer(ApplicationState* state) { |
|
|
|
printf("created command buffer\n"); |
|
|
|
printf("created command buffer\n"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void record_command_buffer(ApplicationState* state, VkCommandBuffer command_buffer, uint32_t image_index) { |
|
|
|
void record_command_buffer(ApplicationState* state, VkCommandBuffer command_buffer, uint32_t image_index, uint32_t current_frame) { |
|
|
|
VkCommandBufferBeginInfo begin_info = {0}; |
|
|
|
VkCommandBufferBeginInfo begin_info = {0}; |
|
|
|
begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; |
|
|
|
begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; |
|
|
|
begin_info.flags = 0; |
|
|
|
begin_info.flags = 0; |
|
|
@ -1100,6 +1204,8 @@ void record_command_buffer(ApplicationState* state, VkCommandBuffer command_buff |
|
|
|
scissor.extent = state->swapchain_extent; |
|
|
|
scissor.extent = state->swapchain_extent; |
|
|
|
vkCmdSetScissor(command_buffer, 0, 1, &scissor); |
|
|
|
vkCmdSetScissor(command_buffer, 0, 1, &scissor); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vkCmdBindDescriptorSets(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, state->pipeline_layout, 0, 1, &state->descriptor_sets[current_frame], 0, NULL); |
|
|
|
|
|
|
|
|
|
|
|
//vertex count, instance count, first vertex and first instance
|
|
|
|
//vertex count, instance count, first vertex and first instance
|
|
|
|
vkCmdDrawIndexed(command_buffer, ARRSIZE(indices), 1, 0, 0, 0); |
|
|
|
vkCmdDrawIndexed(command_buffer, ARRSIZE(indices), 1, 0, 0, 0); |
|
|
|
|
|
|
|
|
|
|
@ -1140,15 +1246,45 @@ void init_vulkan(ApplicationState* state) { |
|
|
|
create_swap_chain(state); |
|
|
|
create_swap_chain(state); |
|
|
|
create_image_views(state); |
|
|
|
create_image_views(state); |
|
|
|
create_render_pass(state); |
|
|
|
create_render_pass(state); |
|
|
|
|
|
|
|
create_descriptor_set_layout(state); |
|
|
|
create_graphics_pipeline(state); |
|
|
|
create_graphics_pipeline(state); |
|
|
|
create_framebuffers(state); |
|
|
|
create_framebuffers(state); |
|
|
|
create_command_pool(state); |
|
|
|
create_command_pool(state); |
|
|
|
create_vertex_buffer(state); |
|
|
|
create_vertex_buffer(state); |
|
|
|
create_index_buffer(state); |
|
|
|
create_index_buffer(state); |
|
|
|
|
|
|
|
create_uniform_buffers(state); |
|
|
|
|
|
|
|
create_descriptor_pool(state); |
|
|
|
|
|
|
|
printf("creating descriptor sets\n"); |
|
|
|
|
|
|
|
create_descriptor_sets(state); |
|
|
|
create_command_buffer(state); |
|
|
|
create_command_buffer(state); |
|
|
|
create_sync_objects(state); |
|
|
|
create_sync_objects(state); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void update_uniform_buffer(ApplicationState* state, uint32_t current_frame) { |
|
|
|
|
|
|
|
uint64_t time = clock() / (CLOCKS_PER_SEC / 1000); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
UBO ubo = {0}; |
|
|
|
|
|
|
|
glm_mat4_identity(ubo.model); |
|
|
|
|
|
|
|
glm_mat4_identity(ubo.view); |
|
|
|
|
|
|
|
glm_mat4_identity(ubo.proj); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
glm_mat4_identity(ubo.model); |
|
|
|
|
|
|
|
vec3 axis = {0.0f, 1.0f, 1.0f}; |
|
|
|
|
|
|
|
glm_rotate(ubo.model, time * glm_rad(90.0f), axis); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vec3 eye = {2.0f, 2.0f, 2.0f}; |
|
|
|
|
|
|
|
vec3 center = {0.0f, 0.0f, 0.0f}; |
|
|
|
|
|
|
|
vec3 up = {0.0f, 0.0f, 1.0f}; |
|
|
|
|
|
|
|
glm_lookat(eye, center, up, ubo.view); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
glm_perspective(glm_rad(45.0f), state->swapchain_extent.width / (float)state->swapchain_extent.height, 0.1f, 10.f, ubo.proj); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void* data; |
|
|
|
|
|
|
|
vkMapMemory(state->device, state->uniform_buffer_memory[current_frame], 0, sizeof(UBO), 0, &data); |
|
|
|
|
|
|
|
memcpy(data, &ubo, sizeof(UBO)); |
|
|
|
|
|
|
|
vkUnmapMemory(state->device, state->uniform_buffer_memory[current_frame]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void draw_frame(ApplicationState* state, uint32_t current_frame) { |
|
|
|
void draw_frame(ApplicationState* state, uint32_t current_frame) { |
|
|
|
//wait for the previous frame to be rendered
|
|
|
|
//wait for the previous frame to be rendered
|
|
|
|
vkWaitForFences(state->device, 1, &state->in_flight_frames[current_frame], VK_TRUE, UINT64_MAX); |
|
|
|
vkWaitForFences(state->device, 1, &state->in_flight_frames[current_frame], VK_TRUE, UINT64_MAX); |
|
|
@ -1158,9 +1294,11 @@ void draw_frame(ApplicationState* state, uint32_t current_frame) { |
|
|
|
//get images from the swapchain and signal when the image is ready
|
|
|
|
//get images from the swapchain and signal when the image is ready
|
|
|
|
vkAcquireNextImageKHR(state->device, state->swapchain, UINT64_MAX, state->image_available_semaph[current_frame], VK_NULL_HANDLE, &image_index); |
|
|
|
vkAcquireNextImageKHR(state->device, state->swapchain, UINT64_MAX, state->image_available_semaph[current_frame], VK_NULL_HANDLE, &image_index); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
update_uniform_buffer(state, current_frame); |
|
|
|
|
|
|
|
|
|
|
|
//reset and setup the commands we want to run
|
|
|
|
//reset and setup the commands we want to run
|
|
|
|
vkResetCommandBuffer(state->command_buffer[current_frame], 0); |
|
|
|
vkResetCommandBuffer(state->command_buffer[current_frame], 0); |
|
|
|
record_command_buffer(state, state->command_buffer[current_frame], image_index); |
|
|
|
record_command_buffer(state, state->command_buffer[current_frame], image_index, current_frame); |
|
|
|
|
|
|
|
|
|
|
|
//specify what to wait for before execution can begin
|
|
|
|
//specify what to wait for before execution can begin
|
|
|
|
VkSubmitInfo submit_info = {0}; |
|
|
|
VkSubmitInfo submit_info = {0}; |
|
|
@ -1214,6 +1352,9 @@ void terminate(ApplicationState* state) { |
|
|
|
vkDestroyFence(state->device, state->in_flight_frames[i], NULL); |
|
|
|
vkDestroyFence(state->device, state->in_flight_frames[i], NULL); |
|
|
|
vkDestroySemaphore(state->device, state->image_available_semaph[i], NULL); |
|
|
|
vkDestroySemaphore(state->device, state->image_available_semaph[i], NULL); |
|
|
|
vkDestroySemaphore(state->device, state->render_finished_sempah[i], NULL); |
|
|
|
vkDestroySemaphore(state->device, state->render_finished_sempah[i], NULL); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vkDestroyBuffer(state->device, state->uniform_buffers[i], NULL); |
|
|
|
|
|
|
|
vkFreeMemory(state->device, state->uniform_buffer_memory[i], NULL); |
|
|
|
} |
|
|
|
} |
|
|
|
vkDestroyCommandPool(state->device, state->commandPool, NULL); |
|
|
|
vkDestroyCommandPool(state->device, state->commandPool, NULL); |
|
|
|
for (int i = 0; i < state->swapchain_image_count; i++) { |
|
|
|
for (int i = 0; i < state->swapchain_image_count; i++) { |
|
|
@ -1222,6 +1363,8 @@ void terminate(ApplicationState* state) { |
|
|
|
vkDestroyPipeline(state->device, state->graphics_pipeline, NULL); |
|
|
|
vkDestroyPipeline(state->device, state->graphics_pipeline, NULL); |
|
|
|
vkDestroyRenderPass(state->device, state->render_pass, NULL); |
|
|
|
vkDestroyRenderPass(state->device, state->render_pass, NULL); |
|
|
|
vkDestroyPipelineLayout(state->device, state->pipeline_layout, NULL); |
|
|
|
vkDestroyPipelineLayout(state->device, state->pipeline_layout, NULL); |
|
|
|
|
|
|
|
vkDestroyDescriptorPool(state->device, state->descriptor_pool, NULL); |
|
|
|
|
|
|
|
vkDestroyDescriptorSetLayout(state->device, state->descriptor_set_layout, NULL); |
|
|
|
for (int i = 0; i < state->swapchain_image_count; i++) { |
|
|
|
for (int i = 0; i < state->swapchain_image_count; i++) { |
|
|
|
vkDestroyImageView(state->device, state->swapchain_image_views[i], NULL); |
|
|
|
vkDestroyImageView(state->device, state->swapchain_image_views[i], NULL); |
|
|
|
} |
|
|
|
} |
|
|
|