Compare commits
3 commits
fa26784e01
...
7ddc39b42d
Author | SHA1 | Date | |
---|---|---|---|
7ddc39b42d | |||
612f7dc8da | |||
07b384a6c7 |
3 changed files with 295 additions and 29 deletions
|
@ -1,20 +1,11 @@
|
||||||
#version 450
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 inPosition;
|
||||||
|
layout(location = 1) in vec3 inColor;
|
||||||
|
|
||||||
layout(location = 0) out vec3 fragColor;
|
layout(location = 0) out vec3 fragColor;
|
||||||
|
|
||||||
vec2 positions[3] = vec2[](
|
|
||||||
vec2( 0.0, -0.5),
|
|
||||||
vec2( 0.5, 0.5),
|
|
||||||
vec2(-0.5, 0.5)
|
|
||||||
);
|
|
||||||
|
|
||||||
vec3 colors[3] = vec3[](
|
|
||||||
vec3(1.0, 0.0, 0.0),
|
|
||||||
vec3(0.0, 1.0, 0.0),
|
|
||||||
vec3(0.0, 0.0, 1.0)
|
|
||||||
);
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
gl_Position = vec4(inPosition, 0.0, 1.0);
|
||||||
fragColor = colors[gl_VertexIndex];
|
fragColor = inColor;
|
||||||
}
|
}
|
||||||
|
|
189
src/main.rs
189
src/main.rs
|
@ -1,8 +1,11 @@
|
||||||
mod ffi;
|
mod ffi;
|
||||||
|
mod math3d;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::ffi::{c_void, CStr, CString};
|
use std::ffi::{c_void, CStr, CString};
|
||||||
|
|
||||||
|
use math3d::Vertex;
|
||||||
|
|
||||||
const WINDOW_WIDTH: i32 = 800;
|
const WINDOW_WIDTH: i32 = 800;
|
||||||
const WINDOW_HEIGHT: i32 = 600;
|
const WINDOW_HEIGHT: i32 = 600;
|
||||||
|
|
||||||
|
@ -22,6 +25,21 @@ const DYNAMIC_STATES: [ffi::VkDynamicState; 2] = [
|
||||||
const DEVICE_EXTENSIONS: [*const i8; 1] =
|
const DEVICE_EXTENSIONS: [*const i8; 1] =
|
||||||
[ffi::VK_KHR_SWAPCHAIN_EXTENSION_NAME as *const u8 as *const i8];
|
[ffi::VK_KHR_SWAPCHAIN_EXTENSION_NAME as *const u8 as *const i8];
|
||||||
|
|
||||||
|
const VERTICES: [math3d::Vertex; 3] = [
|
||||||
|
math3d::Vertex {
|
||||||
|
pos: [0.0, -0.5],
|
||||||
|
color: [1.0, 0.0, 0.0],
|
||||||
|
},
|
||||||
|
math3d::Vertex {
|
||||||
|
pos: [0.5, 0.5],
|
||||||
|
color: [0.0, 1.0, 0.0],
|
||||||
|
},
|
||||||
|
math3d::Vertex {
|
||||||
|
pos: [-0.5, 0.5],
|
||||||
|
color: [0.0, 0.0, 1.0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
fn check_validation_layer_support() -> bool {
|
fn check_validation_layer_support() -> bool {
|
||||||
let mut layer_count: u32 = 0;
|
let mut layer_count: u32 = 0;
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -188,6 +206,8 @@ struct VulkanApp {
|
||||||
render_finished_semaphore: ffi::VkSemaphore,
|
render_finished_semaphore: ffi::VkSemaphore,
|
||||||
in_flight_fence: ffi::VkFence,
|
in_flight_fence: ffi::VkFence,
|
||||||
framebuffer_resized: bool,
|
framebuffer_resized: bool,
|
||||||
|
vertex_buffer: ffi::VkBuffer,
|
||||||
|
vertex_buffer_memory: ffi::VkDeviceMemory,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VulkanApp {
|
impl VulkanApp {
|
||||||
|
@ -216,6 +236,8 @@ impl VulkanApp {
|
||||||
render_finished_semaphore: std::ptr::null_mut(),
|
render_finished_semaphore: std::ptr::null_mut(),
|
||||||
in_flight_fence: std::ptr::null_mut(),
|
in_flight_fence: std::ptr::null_mut(),
|
||||||
framebuffer_resized: false,
|
framebuffer_resized: false,
|
||||||
|
vertex_buffer: std::ptr::null_mut(),
|
||||||
|
vertex_buffer_memory: std::ptr::null_mut(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,6 +281,7 @@ impl VulkanApp {
|
||||||
.expect("Should be able to set up graphics pipeline");
|
.expect("Should be able to set up graphics pipeline");
|
||||||
self.create_framebuffers().unwrap();
|
self.create_framebuffers().unwrap();
|
||||||
self.create_command_pool().unwrap();
|
self.create_command_pool().unwrap();
|
||||||
|
self.create_vertex_buffer().unwrap();
|
||||||
self.create_command_buffer().unwrap();
|
self.create_command_buffer().unwrap();
|
||||||
self.create_sync_objects().unwrap();
|
self.create_sync_objects().unwrap();
|
||||||
}
|
}
|
||||||
|
@ -926,7 +949,19 @@ impl VulkanApp {
|
||||||
let shader_stages: [ffi::VkPipelineShaderStageCreateInfo; 2] =
|
let shader_stages: [ffi::VkPipelineShaderStageCreateInfo; 2] =
|
||||||
[vert_shader_stage_info, frag_shader_stage_info];
|
[vert_shader_stage_info, frag_shader_stage_info];
|
||||||
|
|
||||||
let vertex_input_info = Self::create_vertex_input_state_info_struct();
|
let mut vertex_input_info: ffi::VkPipelineVertexInputStateCreateInfo =
|
||||||
|
unsafe { std::mem::zeroed() };
|
||||||
|
vertex_input_info.sType =
|
||||||
|
ffi::VkStructureType_VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
||||||
|
|
||||||
|
let bind_desc = Vertex::get_binding_description();
|
||||||
|
let attr_descs = Vertex::get_attribute_descriptions();
|
||||||
|
|
||||||
|
vertex_input_info.vertexBindingDescriptionCount = 1;
|
||||||
|
vertex_input_info.vertexAttributeDescriptionCount = attr_descs.len() as u32;
|
||||||
|
|
||||||
|
vertex_input_info.pVertexBindingDescriptions = std::ptr::addr_of!(bind_desc);
|
||||||
|
vertex_input_info.pVertexAttributeDescriptions = attr_descs.as_ptr();
|
||||||
|
|
||||||
let mut input_assembly: ffi::VkPipelineInputAssemblyStateCreateInfo =
|
let mut input_assembly: ffi::VkPipelineInputAssemblyStateCreateInfo =
|
||||||
unsafe { std::mem::zeroed() };
|
unsafe { std::mem::zeroed() };
|
||||||
|
@ -1074,19 +1109,6 @@ impl VulkanApp {
|
||||||
dynamic_state
|
dynamic_state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_vertex_input_state_info_struct() -> ffi::VkPipelineVertexInputStateCreateInfo {
|
|
||||||
let mut vertex_input_info: ffi::VkPipelineVertexInputStateCreateInfo =
|
|
||||||
unsafe { std::mem::zeroed() };
|
|
||||||
vertex_input_info.sType =
|
|
||||||
ffi::VkStructureType_VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
|
||||||
vertex_input_info.vertexBindingDescriptionCount = 0;
|
|
||||||
vertex_input_info.pVertexBindingDescriptions = std::ptr::null();
|
|
||||||
vertex_input_info.vertexAttributeDescriptionCount = 0;
|
|
||||||
vertex_input_info.pVertexBindingDescriptions = std::ptr::null();
|
|
||||||
|
|
||||||
vertex_input_info
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_viewport(&self) -> ffi::VkViewport {
|
fn create_viewport(&self) -> ffi::VkViewport {
|
||||||
let mut viewport: ffi::VkViewport = unsafe { std::mem::zeroed() };
|
let mut viewport: ffi::VkViewport = unsafe { std::mem::zeroed() };
|
||||||
viewport.x = 0.0;
|
viewport.x = 0.0;
|
||||||
|
@ -1377,6 +1399,17 @@ impl VulkanApp {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let offsets: [ffi::VkDeviceSize; 1] = [0];
|
||||||
|
unsafe {
|
||||||
|
ffi::vkCmdBindVertexBuffers(
|
||||||
|
command_buffer,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
std::ptr::addr_of!(self.vertex_buffer),
|
||||||
|
offsets.as_ptr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let viewport = self.create_viewport();
|
let viewport = self.create_viewport();
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -1387,7 +1420,7 @@ impl VulkanApp {
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
ffi::vkCmdSetScissor(command_buffer, 0, 1, std::ptr::addr_of!(scissor));
|
ffi::vkCmdSetScissor(command_buffer, 0, 1, std::ptr::addr_of!(scissor));
|
||||||
ffi::vkCmdDraw(command_buffer, 3, 1, 0, 0);
|
ffi::vkCmdDraw(command_buffer, VERTICES.len() as u32, 1, 0, 0);
|
||||||
ffi::vkCmdEndRenderPass(command_buffer);
|
ffi::vkCmdEndRenderPass(command_buffer);
|
||||||
|
|
||||||
if ffi::vkEndCommandBuffer(command_buffer) != ffi::VkResult_VK_SUCCESS {
|
if ffi::vkEndCommandBuffer(command_buffer) != ffi::VkResult_VK_SUCCESS {
|
||||||
|
@ -1583,12 +1616,138 @@ impl VulkanApp {
|
||||||
pub fn set_resize_flag(&mut self) {
|
pub fn set_resize_flag(&mut self) {
|
||||||
self.framebuffer_resized = true;
|
self.framebuffer_resized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_vertex_buffer(&mut self) -> Result<(), String> {
|
||||||
|
let mut buffer_info: ffi::VkBufferCreateInfo = unsafe { std::mem::zeroed() };
|
||||||
|
|
||||||
|
buffer_info.sType = ffi::VkStructureType_VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||||
|
buffer_info.size = (std::mem::size_of::<math3d::Vertex>() * VERTICES.len()) as u64;
|
||||||
|
buffer_info.usage = ffi::VkBufferUsageFlagBits_VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
|
||||||
|
buffer_info.sharingMode = ffi::VkSharingMode_VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
|
||||||
|
let result = unsafe {
|
||||||
|
ffi::vkCreateBuffer(
|
||||||
|
self.device,
|
||||||
|
std::ptr::addr_of!(buffer_info),
|
||||||
|
std::ptr::null(),
|
||||||
|
std::ptr::addr_of_mut!(self.vertex_buffer),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if result != ffi::VkResult_VK_SUCCESS {
|
||||||
|
return Err(String::from("Failed to create vertex buffer!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mem_reqs: ffi::VkMemoryRequirements = unsafe { std::mem::zeroed() };
|
||||||
|
unsafe {
|
||||||
|
ffi::vkGetBufferMemoryRequirements(
|
||||||
|
self.device,
|
||||||
|
self.vertex_buffer,
|
||||||
|
std::ptr::addr_of_mut!(mem_reqs),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut alloc_info: ffi::VkMemoryAllocateInfo = unsafe { std::mem::zeroed() };
|
||||||
|
alloc_info.sType = ffi::VkStructureType_VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||||||
|
alloc_info.allocationSize = mem_reqs.size;
|
||||||
|
alloc_info.memoryTypeIndex = self.find_memory_type(
|
||||||
|
mem_reqs.memoryTypeBits,
|
||||||
|
ffi::VkMemoryPropertyFlagBits_VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
|
||||||
|
| ffi::VkMemoryPropertyFlagBits_VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let result = unsafe {
|
||||||
|
ffi::vkAllocateMemory(
|
||||||
|
self.device,
|
||||||
|
std::ptr::addr_of!(alloc_info),
|
||||||
|
std::ptr::null(),
|
||||||
|
std::ptr::addr_of_mut!(self.vertex_buffer_memory),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if result != ffi::VkResult_VK_SUCCESS {
|
||||||
|
return Err(String::from("Failed to allocate vertex buffer memory!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
ffi::vkBindBufferMemory(
|
||||||
|
self.device,
|
||||||
|
self.vertex_buffer,
|
||||||
|
self.vertex_buffer_memory,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data_ptr: *mut c_void = unsafe { std::mem::zeroed() };
|
||||||
|
unsafe {
|
||||||
|
ffi::vkMapMemory(
|
||||||
|
self.device,
|
||||||
|
self.vertex_buffer_memory,
|
||||||
|
0,
|
||||||
|
buffer_info.size,
|
||||||
|
0,
|
||||||
|
std::ptr::addr_of_mut!(data_ptr),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let data_ptr_vertices: &mut [math3d::Vertex; 3] =
|
||||||
|
unsafe { std::mem::transmute(data_ptr) };
|
||||||
|
*data_ptr_vertices = VERTICES;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
ffi::vkUnmapMemory(self.device, self.vertex_buffer_memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_memory_type(
|
||||||
|
&mut self,
|
||||||
|
type_filter: u32,
|
||||||
|
properties: ffi::VkMemoryPropertyFlags,
|
||||||
|
) -> Result<u32, String> {
|
||||||
|
if self.physical_device.is_null() {
|
||||||
|
return Err(String::from(
|
||||||
|
"Cannot find memory type if physical_device is null!",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mem_props: ffi::VkPhysicalDeviceMemoryProperties = unsafe { std::mem::zeroed() };
|
||||||
|
unsafe {
|
||||||
|
ffi::vkGetPhysicalDeviceMemoryProperties(
|
||||||
|
self.physical_device,
|
||||||
|
std::ptr::addr_of_mut!(mem_props),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx in 0..mem_props.memoryTypeCount {
|
||||||
|
if (type_filter & (1 << idx)) != 0
|
||||||
|
&& (mem_props.memoryTypes[idx as usize].propertyFlags & properties) == properties
|
||||||
|
{
|
||||||
|
return Ok(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(String::from("Failed to find suitable memory type!"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for VulkanApp {
|
impl Drop for VulkanApp {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.cleanup_swap_chain().unwrap();
|
self.cleanup_swap_chain().unwrap();
|
||||||
|
|
||||||
|
if !self.vertex_buffer.is_null() {
|
||||||
|
unsafe {
|
||||||
|
ffi::vkDestroyBuffer(self.device, self.vertex_buffer, std::ptr::null());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.vertex_buffer_memory.is_null() {
|
||||||
|
unsafe {
|
||||||
|
ffi::vkFreeMemory(self.device, self.vertex_buffer_memory, std::ptr::null());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !self.in_flight_fence.is_null() {
|
if !self.in_flight_fence.is_null() {
|
||||||
unsafe {
|
unsafe {
|
||||||
ffi::vkDestroyFence(self.device, self.in_flight_fence, std::ptr::null());
|
ffi::vkDestroyFence(self.device, self.in_flight_fence, std::ptr::null());
|
||||||
|
|
116
src/math3d.rs
Normal file
116
src/math3d.rs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
use crate::ffi;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub struct Vertex {
|
||||||
|
pub pos: [f32; 2],
|
||||||
|
pub color: [f32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Vertex {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pos: [0.0, 0.0],
|
||||||
|
color: [0.0, 0.0, 0.0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl Vertex {
|
||||||
|
pub fn new(pos: [f32; 2], color: [f32; 3]) -> Self {
|
||||||
|
Self { pos, color }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pos_offset() -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color_offset() -> usize {
|
||||||
|
let mut offset = std::mem::size_of::<[f32; 2]>();
|
||||||
|
let alignment = std::mem::align_of::<[f32; 3]>();
|
||||||
|
while offset % alignment != 0 {
|
||||||
|
offset += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_binding_description() -> ffi::VkVertexInputBindingDescription {
|
||||||
|
let mut bind_desc: ffi::VkVertexInputBindingDescription = unsafe { std::mem::zeroed() };
|
||||||
|
|
||||||
|
bind_desc.binding = 0;
|
||||||
|
bind_desc.stride = std::mem::size_of::<Self>() as u32;
|
||||||
|
bind_desc.inputRate = ffi::VkVertexInputRate_VK_VERTEX_INPUT_RATE_VERTEX;
|
||||||
|
|
||||||
|
bind_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_attribute_descriptions() -> [ffi::VkVertexInputAttributeDescription; 2] {
|
||||||
|
let mut attr_descs: [ffi::VkVertexInputAttributeDescription; 2] =
|
||||||
|
unsafe { std::mem::zeroed() };
|
||||||
|
|
||||||
|
attr_descs[0].binding = 0;
|
||||||
|
attr_descs[0].location = 0;
|
||||||
|
attr_descs[0].format = ffi::VkFormat_VK_FORMAT_R32G32_SFLOAT;
|
||||||
|
attr_descs[0].offset = Self::pos_offset() as u32;
|
||||||
|
|
||||||
|
attr_descs[1].binding = 0;
|
||||||
|
attr_descs[1].location = 1;
|
||||||
|
attr_descs[1].format = ffi::VkFormat_VK_FORMAT_R32G32B32_SFLOAT;
|
||||||
|
attr_descs[1].offset = Self::color_offset() as u32;
|
||||||
|
|
||||||
|
attr_descs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn offsets() {
|
||||||
|
let mut vertex = Vertex {
|
||||||
|
pos: [1.0, 2.0],
|
||||||
|
color: [3.0, 4.0, 5.0],
|
||||||
|
};
|
||||||
|
|
||||||
|
let root_ptr: *const f32 = &vertex as *const Vertex as *const f32;
|
||||||
|
|
||||||
|
let pos_offset = Vertex::pos_offset();
|
||||||
|
assert!(pos_offset + 4 <= std::mem::size_of::<Vertex>());
|
||||||
|
|
||||||
|
let pos_0_ptr = unsafe { root_ptr.byte_add(pos_offset) };
|
||||||
|
assert_eq!(unsafe { *pos_0_ptr }, vertex.pos[0]);
|
||||||
|
|
||||||
|
assert!(pos_offset + 8 <= std::mem::size_of::<Vertex>());
|
||||||
|
let pos_1_ptr = unsafe { root_ptr.byte_add(pos_offset + 4) };
|
||||||
|
assert_eq!(unsafe { *pos_1_ptr }, vertex.pos[1]);
|
||||||
|
|
||||||
|
let color_offset = Vertex::color_offset();
|
||||||
|
assert!(color_offset + 4 <= std::mem::size_of::<Vertex>());
|
||||||
|
|
||||||
|
let col_0_ptr = unsafe { root_ptr.byte_add(color_offset) };
|
||||||
|
assert_eq!(unsafe { *col_0_ptr }, vertex.color[0]);
|
||||||
|
|
||||||
|
assert!(color_offset + 8 <= std::mem::size_of::<Vertex>());
|
||||||
|
let col_1_ptr = unsafe { root_ptr.byte_add(color_offset + 4) };
|
||||||
|
assert_eq!(unsafe { *col_1_ptr }, vertex.color[1]);
|
||||||
|
|
||||||
|
assert!(color_offset + 12 <= std::mem::size_of::<Vertex>());
|
||||||
|
let col_2_ptr = unsafe { root_ptr.byte_add(color_offset + 8) };
|
||||||
|
assert_eq!(unsafe { *col_2_ptr }, vertex.color[2]);
|
||||||
|
|
||||||
|
vertex.pos[0] = 0.123;
|
||||||
|
vertex.pos[1] = 0.456;
|
||||||
|
vertex.color[0] = 0.789;
|
||||||
|
vertex.color[1] = 1.234;
|
||||||
|
vertex.color[2] = 1.567;
|
||||||
|
|
||||||
|
assert_eq!(unsafe { *pos_0_ptr }, vertex.pos[0]);
|
||||||
|
assert_eq!(unsafe { *pos_1_ptr }, vertex.pos[1]);
|
||||||
|
assert_eq!(unsafe { *col_0_ptr }, vertex.color[0]);
|
||||||
|
assert_eq!(unsafe { *col_1_ptr }, vertex.color[1]);
|
||||||
|
assert_eq!(unsafe { *col_2_ptr }, vertex.color[2]);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue