이제 화면에 그림을 표시할 수 있게 되었으니, 텍스트를 그리는 문제를 다뤄봅시다. 문자열을 올바른 모양으로 바꾸는 것은 간단하지 않은 작업이므로, 다른 라이브러리가 이미 이 작업을 수행했다고 가정하겠습니다.
각 글자에 대해 어디에 그릴지 위치, 크기(너비와 높이, 픽셀 단위), 그리고 어떤 픽셀이 글자에 속하는지를 알려주는 배열이 있다고 가정해 봅시다. (예를 들어, “o”의 매우 조잡한 근사치로 “3x3, [1,1,1,1,0,1,1,1,1]” 같은 것입니다. 다만, 픽셀이 부분적으로 글자에 포함되는 경우를 처리하기 위해 0-1 대신 0-255를 사용할 것입니다.) 또한 텍스트(각 글자)에 색상을 부여할 수도 있습니다.
이는 우리가 사용하는 텍스처에 작은 차이(더 작고 다른 색상 형식)를 만들고, 물론 셰이더에도 차이를 만듭니다. 하지만 그리기 자체는 지난 두 장과 거의 비슷해야 합니다. (텍스트를 다루는 다른 방법들도 있다는 점을 언급하고 싶습니다. 특히 일부 준비 단계를 GPU로 옮기는 경우 그렇지만, 지금은 가장 쉬운 방법에 충실하고자 합니다. — 나중에 기회가 된다면 다른 장에서 이 주제를 다시 다룰 수도 있습니다.)
셰이더에는 무엇이 필요할까요?
이전에 다루었던 일반적이고 복잡할 수 있는 모델들과는 달리, 모든 “인스턴스”는 단지 4개의 정점으로만 구성될 것이며, 인스턴스 렌더링이나 인덱스 렌더링이 큰 성능 향상을 가져오지 않을 것입니다(오히려 오버헤드 때문에 성능 저하가 있을 수도 있습니다). 이것이 큰 역할을 하는 것은 아니지만, 텍스트는 이전의 Model 구조체에 포함되지 않을 만큼 충분히 특별하다고 생각합니다.
먼저 새로운 text.rs 파일에 들어갈 첫 줄로, 적절한 텍스처 구조체를 만들어 봅시다:
pub struct TextTexture {
vk_image: vk::Image,
pub imageview: vk::ImageView,
pub sampler: vk::Sampler,
allocation: vk_mem::Allocation,
allocation_info: vk_mem::AllocationInfo,
}
이번에는 RgbaImage를 포함하지 않습니다. u8 슬라이스와 픽셀 단위의 너비 및 높이로부터 이 텍스처들을 생성하는 방법은 다음과 같습니다 (그리고 이전 텍스처 생성과의 차이점은 작아야 합니다: data가 이미지에서 가져오는 것이 아니라 인자로 전달됩니다):
impl TextTexture {
pub fn from_u8s(
data: &[u8],
width: u32,
height: u32,
device: &ash::Device,
allocator: &vk_mem::Allocator,
commandpool_graphics: &vk::CommandPool,
graphics_queue: &vk::Queue,
) -> Result<Self, Box<dyn std::error::Error>> {
let img_create_info = vk::ImageCreateInfo::builder()
.image_type(vk::ImageType::TYPE_2D)
.extent(vk::Extent3D {
width,
height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.format(vk::Format::R8_SRGB)
.samples(vk::SampleCountFlags::TYPE_1)
.usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED);
let alloc_create_info = vk_mem::AllocationCreateInfo {
usage: vk_mem::MemoryUsage::GpuOnly,
..Default::default()
};
let (vk_image, allocation, allocation_info) = allocator
.create_image(&img_create_info, &alloc_create_info)
.expect("creating vkImage for texture");
let view_create_info = vk::ImageViewCreateInfo::builder()
.image(vk_image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::R8_SRGB)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
level_count: 1,
layer_count: 1,
..Default::default()
});
let imageview = unsafe { device.create_image_view(&view_create_info, None) }
.expect("image view creation");
let sampler_info = vk::SamplerCreateInfo::builder()
.mag_filter(vk::Filter::LINEAR)
.min_filter(vk::Filter::LINEAR);
let sampler =
unsafe { device.create_sampler(&sampler_info, None) }.expect("sampler creation");
let mut buffer = Buffer::new(
allocator,
data.len() as u64,
vk::BufferUsageFlags::TRANSFER_SRC,
vk_mem::MemoryUsage::CpuToGpu,
)?;
buffer.fill(allocator, &data);
let commandbuf_allocate_info = vk::CommandBufferAllocateInfo::builder()
.command_pool(*commandpool_graphics)
.command_buffer_count(1);
let copycmdbuffer =
unsafe { device.allocate_command_buffers(&commandbuf_allocate_info) }.unwrap()[0];
let cmdbegininfo = vk::CommandBufferBeginInfo::builder()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
unsafe { device.begin_command_buffer(copycmdbuffer, &cmdbegininfo) }?;
let barrier = vk::ImageMemoryBarrier::builder()
.image(vk_image)
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
device.cmd_pipeline_barrier(
copycmdbuffer,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
let image_subresource = vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::COLOR,
mip_level: 0,
base_array_layer: 0,
layer_count: 1,
};
let region = vk::BufferImageCopy {
buffer_offset: 0,
buffer_row_length: 0,
buffer_image_height: 0,
image_offset: vk::Offset3D { x: 0, y: 0, z: 0 },
image_extent: vk::Extent3D {
width,
height,
depth: 1,
},
image_subresource,
..Default::default()
};
unsafe {
device.cmd_copy_buffer_to_image(
copycmdbuffer,
buffer.buffer,
vk_image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&[region],
);
}
let barrier = vk::ImageMemoryBarrier::builder()
.image(vk_image)
.src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.dst_access_mask(vk::AccessFlags::SHADER_READ)
.old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.build();
unsafe {
device.cmd_pipeline_barrier(
copycmdbuffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
)
};
unsafe { device.end_command_buffer(copycmdbuffer) }?;
let submit_infos = [vk::SubmitInfo::builder()
.command_buffers(&[copycmdbuffer])
.build()];
let fence = unsafe { device.create_fence(&vk::FenceCreateInfo::default(), None) }?;
unsafe { device.queue_submit(*graphics_queue, &submit_infos, fence) }?;
unsafe { device.wait_for_fences(&[fence], true, std::u64::MAX) }?;
unsafe { device.destroy_fence(fence, None) };
allocator.destroy_buffer(buffer.buffer, &buffer.allocation)?;
unsafe { device.free_command_buffers(*commandpool_graphics, &[copycmdbuffer]) };
Ok(TextTexture {
vk_image,
imageview,
sampler,
allocation,
allocation_info,
})
}
역자 주: 원문의 Ok(Texture { image, ... }) 부분은 TextTexture 구조체 정의와 맞지 않아 Ok(TextTexture { ... })로 수정되었습니다.
그리기를 위해 다음 셰이더를 사용합시다:
정점 셰이더 shaders/text.vert:
#version 450
layout (location=0) in vec3 in_position;
layout (location=1) in vec2 in_texcoord;
layout (location=2) in vec3 in_colour;
layout (location=3) in uint in_texture_id;
layout (location=0) out vec2 out_texcoord;
layout (location=1) out vec3 out_colour;
layout (location=2) out uint out_texture_id;
void main() {
gl_Position=vec4(in_position,1.0);
out_texcoord=in_texcoord;
out_colour=in_colour;
out_texture_id=in_texture_id;
}
그리고 shaders/text.frag:
#version 450
#extension GL_EXT_nonuniform_qualifier : require
layout (location=0) out vec4 theColour;
layout (location=0) in vec2 texcoord;
layout (location=1) in vec3 colour;
layout (location=2) flat in uint texture_id;
layout(set=0,binding=0) uniform sampler2D lettertextures[];
void main(){
theColour=vec4(colour,texture(lettertextures[texture_id],texcoord));
}
이들은 매우 최소한의 코드이며, 이제는 아마 설명이 필요 없을 것입니다.
물론, 이 셰이더들로 구성된 파이프라인이 필요합니다:
Pipeline::init_text():
pub fn init_text(
logical_device: &ash::Device,
swapchain: &SwapchainDongXi,
renderpass: &vk::RenderPass,
) -> Result<Pipeline, vk::Result> {
let vertexshader_createinfo = vk::ShaderModuleCreateInfo::builder().code(
vk_shader_macros::include_glsl!("./shaders/text.vert", kind: vert),
);
let vertexshader_module =
unsafe { logical_device.create_shader_module(&vertexshader_createinfo, None)? };
let fragmentshader_createinfo = vk::ShaderModuleCreateInfo::builder()
.code(vk_shader_macros::include_glsl!("./shaders/text.frag"));
let fragmentshader_module =
unsafe { logical_device.create_shader_module(&fragmentshader_createinfo, None)? };
let mainfunctionname = std::ffi::CString::new("main").unwrap();
let vertexshader_stage = vk::PipelineShaderStageCreateInfo::builder()
.stage(vk::ShaderStageFlags::VERTEX)
.module(vertexshader_module)
.name(&mainfunctionname);
let fragmentshader_stage = vk::PipelineShaderStageCreateInfo::builder()
.stage(vk::ShaderStageFlags::FRAGMENT)
.module(fragmentshader_module)
.name(&mainfunctionname);
let shader_stages = vec![vertexshader_stage.build(), fragmentshader_stage.build()];
let vertex_attrib_descs = [
vk::VertexInputAttributeDescription {
binding: 0,
location: 0,
offset: 0,
format: vk::Format::R32G32B32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 0,
location: 1,
offset: 12,
format: vk::Format::R32G32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 0,
location: 2,
offset: 20,
format: vk::Format::R32G32B32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 0,
location: 3,
offset: 32,
format: vk::Format::R8G8B8A8_UINT,
},
];
let vertex_binding_descs = [vk::VertexInputBindingDescription {
binding: 0,
stride: 36,
input_rate: vk::VertexInputRate::VERTEX,
}];
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder()
.vertex_attribute_descriptions(&vertex_attrib_descs)
.vertex_binding_descriptions(&vertex_binding_descs);
let input_assembly_info = vk::PipelineInputAssemblyStateCreateInfo::builder()
.topology(vk::PrimitiveTopology::TRIANGLE_LIST);
let viewports = [vk::Viewport {
x: 0.,
y: 0.,
width: swapchain.extent.width as f32,
height: swapchain.extent.height as f32,
min_depth: 0.,
max_depth: 1.,
}];
let scissors = [vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: swapchain.extent,
}];
let viewport_info = vk::PipelineViewportStateCreateInfo::builder()
.viewports(&viewports)
.scissors(&scissors);
let rasterizer_info = vk::PipelineRasterizationStateCreateInfo::builder()
.line_width(1.0)
.front_face(vk::FrontFace::COUNTER_CLOCKWISE)
.cull_mode(vk::CullModeFlags::BACK)
.polygon_mode(vk::PolygonMode::FILL);
let multisampler_info = vk::PipelineMultisampleStateCreateInfo::builder()
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
let depth_stencil_info = vk::PipelineDepthStencilStateCreateInfo::builder()
.depth_test_enable(true)
.depth_write_enable(true)
.depth_compare_op(vk::CompareOp::LESS_OR_EQUAL);
let colourblend_attachments = [vk::PipelineColorBlendAttachmentState::builder()
.blend_enable(true)
.src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
.dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
.color_blend_op(vk::BlendOp::ADD)
.src_alpha_blend_factor(vk::BlendFactor::SRC_ALPHA)
.dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
.alpha_blend_op(vk::BlendOp::ADD)
.color_write_mask(
vk::ColorComponentFlags::R
| vk::ColorComponentFlags::G
| vk::ColorComponentFlags::B
| vk::ColorComponentFlags::A,
)
.build()];
let colourblend_info =
vk::PipelineColorBlendStateCreateInfo::builder().attachments(&colourblend_attachments);
let descriptorset_layout_binding_descs = [vk::DescriptorSetLayoutBinding::builder()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(MAXIMAL_NUMBER_OF_TEXTURES)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.build()];
let descriptor_binding_flags = [vk::DescriptorBindingFlags::VARIABLE_DESCRIPTOR_COUNT];
let mut descriptorset_layout_binding_flags =
vk::DescriptorSetLayoutBindingFlagsCreateInfo::builder()
.binding_flags(&descriptor_binding_flags);
let descriptorset_layout_info = vk::DescriptorSetLayoutCreateInfo::builder()
.bindings(&descriptorset_layout_binding_descs)
.push_next(&mut descriptorset_layout_binding_flags);
let descriptorsetlayout = unsafe {
logical_device.create_descriptor_set_layout(&descriptorset_layout_info, None)
}?;
let desclayouts = vec![descriptorsetlayout];
let pipelinelayout_info = vk::PipelineLayoutCreateInfo::builder().set_layouts(&desclayouts);
let pipelinelayout =
unsafe { logical_device.create_pipeline_layout(&pipelinelayout_info, None) }?;
let pipeline_info = vk::GraphicsPipelineCreateInfo::builder()
.stages(&shader_stages)
.vertex_input_state(&vertex_input_info)
.input_assembly_state(&input_assembly_info)
.viewport_state(&viewport_info)
.rasterization_state(&rasterizer_info)
.multisample_state(&multisampler_info)
.depth_stencil_state(&depth_stencil_info)
.color_blend_state(&colourblend_info)
.layout(pipelinelayout)
.render_pass(*renderpass)
.subpass(0);
let graphicspipeline = unsafe {
logical_device
.create_graphics_pipelines(
vk::PipelineCache::null(),
&[pipeline_info.build()],
None,
)
.expect("A problem with the pipeline creation")
}[0];
unsafe {
logical_device.destroy_shader_module(fragmentshader_module, None);
logical_device.destroy_shader_module(vertexshader_module, None);
}
Ok(Pipeline {
pipeline: graphicspipeline,
layout: pipelinelayout,
descriptor_set_layouts: desclayouts,
})
}
이제 글자를 어디서 가져올까요? 모든 ASCII 문자의 모양을 하드코딩하거나, 모든 글자가 이미 그려진 하나의 큰 텍스처를 사용할 수 있습니다. 이는 알파벳 언어에 대해서는 실현 가능하며, 적은 수의 다른 글꼴(그리고 어쩌면 크기)만 사용하고 싶을 때도 그렇습니다. 또는 폰트 파일을 로드하고 문자열과 텍스트 크기를 주면 글자를 어디에 배치하고 어떻게 생겼는지 알려주는 다른 크레이트를 사용할 수도 있습니다. 저는 fontdue를 사용하고 싶습니다; Cargo.toml을 확장해 봅시다:
fontdue = "0.2.4"
그리고 텍스트 관련 모든 것을 처리하는 구조체를 만들기 시작할 수 있습니다:
pub struct AllText {
fonts: Vec<fontdue::Font>,
}
걱정 마세요, 필드는 더 추가될 겁니다. 하지만 먼저 폰트를 로드할 수 있게 만들어 봅시다:
pub fn load_font<P: AsRef<std::path::Path>>(
&mut self,
path: P,
) -> Result<usize, Box<dyn std::error::Error>> {
let fontdata = std::fs::read(path)?;
let font = fontdue::Font::from_bytes(fontdata, fontdue::FontSettings::default())?;
self.fonts.push(font);
Ok(self.fonts.len() - 1)
}
폰트가 있으니, 글자를 준비할 수 있습니다. 아마 이런 식으로요:
pub struct Letter {
colour: [f32; 3],
position_and_shape: fontdue::layout::GlyphPosition,
}
보시다시피, 저는 글자에 색상을 부여하고 싶습니다. position_and_shape에는 다른 모든 중요한 정보(fontdue가 이미 의도한 대로), 특히 위치와 모양을 저장합니다.
다음과 같은 방식으로 글자를 생성할 것입니다. (여기서 text는 Aetna의 멤버가 되는 AllText 구조체입니다.) TextStyle은 문자열, 텍스트 크기, 그리고 사용할 폰트의 인덱스를 포함합니다(후자 때문에 create_letters가 AllText의 메서드가 됩니다). create_letters의 두 번째 인자는 색상입니다:
let letters = aetna.text.create_letters(
&[
&fontdue::layout::TextStyle::new("Hello ", 35.0, 0),
&fontdue::layout::TextStyle::new("world!", 40.0, 0),
],
[0., 1., 0.],
);
함수는 다음과 같습니다. 가장 좋은 설명은 아마도: fontdue가 작업을 처리하게 두는 것입니다.
pub fn create_letters(
&self,
styles: &[&fontdue::layout::TextStyle],
colour: [f32; 3],
) -> Vec<Letter> {
let mut layout = fontdue::layout::Layout::new();
let mut output = Vec::new();
let settings = fontdue::layout::LayoutSettings {
..fontdue::layout::LayoutSettings::default()
};
layout.layout_horizontal(&self.fonts, styles, &settings, &mut output);
let mut letters: Vec<Letter> = vec![];
for glyph in output {
letters.push(Letter {
colour,
position_and_shape: glyph,
});
}
letters
}
좋습니다, 이제 글자가 생겼습니다. 이것들을 화면에 표시할 수 있는 무언가로 바꿔야 합니다. 결국, 이는 텍스처(이를 위한 함수는 이미 준비했습니다)와 정점 버퍼를 생성해야 한다는 것을 의미합니다.
AllText 구조체를 향상시켜 봅시다. 폰트 외에도 텍스처를 넣습니다. 만약 글자가 두 번 나타나면(같은 크기와 폰트로), 두 번째 텍스처를 만들지 않고 기존 것을 재사용할 것입니다. 이를 위해 GlyphRasterConfig(즉, 문자, 크기, 폰트)를 해당 텍스처에 매핑하는 HashMap을 포함합니다.
또한 (글자로부터 생성될) 정점 데이터와 정점 버퍼, 그리고 파이프라인과 디스크립터도 포함합니다:
pub struct AllText {
vertexdata: Vec<TextVertexData>,
vertexbuffer: Option<Buffer>,
textures: Vec<TextTexture>,
texture_ids: HashMap<fontdue::layout::GlyphRasterConfig, u32>,
fonts: Vec<fontdue::Font>,
pipeline: Option<Pipeline>,
desc_pool: Option<vk::DescriptorPool>,
descriptorsets: Vec<vk::DescriptorSet>,
}
정점 형식에 관해서는:
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct TextVertexData {
pub position: [f32; 3],
pub texcoord: [f32; 2],
pub colour: [f32; 3],
pub texture_id: u32,
}
글자를 정점 위치로 변환하는 작업은 이 함수에서 일어납니다:
pub fn create_vertexdata(
&mut self,
letters: Vec<Letter>,
position: (u32, u32), //in pixels
window: &winit::window::Window,
device: &ash::Device,
allocator: &vk_mem::Allocator,
commandpool_graphics: &vk::CommandPool,
graphics_queue: &vk::Queue,
swapchain: &SwapchainDongXi,
) {
let screensize = window.inner_size();
let mut need_texture_update = false;
let mut vertexdata = Vec::with_capacity(6 * letters.len());
for l in letters {
let id_option = self.texture_ids.get(&l.position_and_shape.key);
let id;
if id_option.is_none() {
let (metrics, bitmap) = self.fonts[l.position_and_shape.key.font_index]
.rasterize(l.position_and_shape.key.c, l.position_and_shape.key.px);
need_texture_update = true;
id = self
.new_texture_from_u8s(
&bitmap,
metrics.width as u32,
metrics.height as u32,
device,
allocator,
commandpool_graphics,
graphics_queue,
)
.unwrap() as u32;
self.texture_ids.insert(l.position_and_shape.key, id);
} else {
id = *id_option.unwrap() as u32;
}
let left =
2. * (l.position_and_shape.x + position.0 as f32) / screensize.width as f32 - 1.0;
let right = 2.
* (l.position_and_shape.x + position.0 as f32 + l.position_and_shape.width as f32)
/ screensize.width as f32
- 1.0;
let top = 2.
* (-l.position_and_shape.y + position.1 as f32
- l.position_and_shape.height as f32)
/ screensize.height as f32
- 1.0;
let bottom =
2. * (-l.position_and_shape.y + position.1 as f32) / screensize.height as f32 - 1.0;
let v1 = TextVertexData {
position: [left, top, 0.],
texcoord: [0., 0.],
colour: l.colour,
texture_id: id,
};
let v2 = TextVertexData {
position: [left, bottom, 0.],
texcoord: [0., 1.],
colour: l.colour,
texture_id: id,
};
let v3 = TextVertexData {
position: [right, top, 0.],
texcoord: [1., 0.],
colour: l.colour,
texture_id: id,
};
let v4 = TextVertexData {
position: [right, bottom, 0.],
texcoord: [1., 1.],
colour: l.colour,
texture_id: id,
};
vertexdata.push(v1);
vertexdata.push(v2);
vertexdata.push(v3);
vertexdata.push(v3);
vertexdata.push(v2);
vertexdata.push(v4);
}
self.vertexdata.append(&mut vertexdata);
if need_texture_update {
self.update_textures(swapchain, device);
}
}
글자에 이미 텍스처가 있다면, 주어진 좌표와 fontdue에서 얻은 정보로 정점 위치만 계산합니다. 글자가 새로운 것이라면, 새로운 텍스처를 만듭니다.
pub fn new_texture_from_u8s(
&mut self,
data: &[u8],
width: u32,
height: u32,
device: &ash::Device,
allocator: &vk_mem::Allocator,
commandpool_graphics: &vk::CommandPool,
graphics_queue: &vk::Queue,
) -> Result<usize, Box<dyn std::error::Error>> {
let new_texture = TextTexture::from_u8s(
data,
width,
height,
device,
allocator,
commandpool_graphics,
graphics_queue,
)?;
let new_id = self.textures.len();
self.textures.push(new_texture);
Ok(new_id)
}
(이것은 모든 인자를 이 장의 첫 번째 함수로 전달하는 것으로 귀결됩니다) 그리고 마지막으로 텍스처를 업데이트합니다:
pub fn update_textures(
&mut self,
swapchain: &SwapchainDongXi,
device: &ash::Device,
) -> Result<(), Box<dyn std::error::Error>> {
let amount = self.textures.len() as u32;
if let Some(pool) = self.desc_pool {
unsafe {
device.destroy_descriptor_pool(pool, None);
}
}
let pool_sizes = [vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: crate::renderpass_and_pipeline::MAXIMAL_NUMBER_OF_TEXTURES
* swapchain.amount_of_images,
}];
let descriptor_pool_info = vk::DescriptorPoolCreateInfo::builder()
.max_sets(swapchain.amount_of_images)
.pool_sizes(&pool_sizes);
let descriptor_pool =
unsafe { device.create_descriptor_pool(&descriptor_pool_info, None) }?;
self.desc_pool = Some(descriptor_pool);
let desc_layouts_text = vec![
self.pipeline.as_ref().unwrap().descriptor_set_layouts[0];
swapchain.amount_of_images as usize
];
let descriptor_set_allocate_info_text = vk::DescriptorSetAllocateInfo::builder()
.descriptor_pool(descriptor_pool)
.set_layouts(&desc_layouts_text);
let descriptor_sets_text =
unsafe { device.allocate_descriptor_sets(&descriptor_set_allocate_info_text) }?;
for i in 0..swapchain.amount_of_images {
let imageinfos: Vec<vk::DescriptorImageInfo> = self
.textures
.iter()
.map(|t| vk::DescriptorImageInfo {
image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
image_view: t.imageview,
sampler: t.sampler,
..Default::default()
})
.collect();
let descriptorwrite_image = vk::WriteDescriptorSet::builder()
.dst_set(descriptor_sets_text[i as usize])
.dst_binding(0)
.dst_array_element(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&imageinfos)
.build();
unsafe {
device.update_descriptor_sets(&[descriptorwrite_image], &[]);
}
}
self.descriptorsets = descriptor_sets_text;
Ok(())
}
create_vertexdata 함수는 TextVertexData만 생성했으므로, 아직 사용 가능한 버퍼로 바꿔야 합니다:
pub fn update_vertexbuffer(
&mut self,
allocator: &vk_mem::Allocator,
) -> Result<(), vk_mem::error::Error> {
if self.vertexdata.is_empty() {
return Ok(());
}
if let Some(buffer) = &mut self.vertexbuffer {
buffer.fill(allocator, &self.vertexdata)?;
Ok(())
} else {
let bytes = (self.vertexdata.len() * std::mem::size_of::<TextVertexData>()) as u64;
let mut buffer = Buffer::new(
&allocator,
bytes,
vk::BufferUsageFlags::VERTEX_BUFFER,
vk_mem::MemoryUsage::CpuToGpu,
)?;
buffer.fill(allocator, &self.vertexdata)?;
self.vertexbuffer = Some(buffer);
Ok(())
}
}
그리고, AllText를 생성하는 방법도 있어야 합니다. 이때, 표준 폰트를 이미 결정합니다:
pub fn new<P: AsRef<std::path::Path>>(
device: &ash::Device,
swapchain: &SwapchainDongXi,
renderpass: &vk::RenderPass,
standardfont: P,
) -> Result<AllText, Box<dyn std::error::Error>> {
let pip = Pipeline::init_text(device, swapchain, renderpass)?;
let mut all_text = AllText {
vertexdata: vec![],
vertexbuffer: None,
textures: vec![],
texture_ids: HashMap::new(),
fonts: vec![],
pipeline: Some(pip),
desc_pool: None,
descriptorsets: vec![],
};
all_text.load_font(standardfont);
Ok(all_text)
}
그리고 정리(cleanup):
pub fn cleanup(&mut self, device: &ash::Device, allocator: &vk_mem::Allocator) {
let p = self.pipeline.take();
if let Some(pip) = p {
pip.cleanup(device);
}
let p = self.desc_pool.take();
if let Some(pool) = p {
unsafe {
device.destroy_descriptor_pool(pool, None);
}
}
let b = self.vertexbuffer.take();
if let Some(buf) = b {
unsafe { allocator.destroy_buffer(buf.buffer, &buf.allocation) };
}
for texture in &self.textures {
unsafe {
device.destroy_sampler(texture.sampler, None);
device.destroy_image_view(texture.imageview, None);
}
allocator.destroy_image(texture.vk_image, &texture.allocation);
}
}
한 부분이 명백히 빠져있습니다: 언젠가 텍스트를 그려야 합니다. 이를 위한 메서드를 준비합시다:
파이프라인, 디스크립터 셋, 정점 버퍼를 설정하고 그립니다. (참고로, index는 메인 이벤트 루프의 RedrawRequested 분기에서 얻는 이미지 인덱스를 가리킵니다.)
pub fn draw(
&self,
logical_device: &ash::Device,
commandbuffer: vk::CommandBuffer,
index: usize,
) {
if let Some(pipeline) = &self.pipeline {
if self.descriptorsets.len() > index {
unsafe {
logical_device.cmd_bind_pipeline(
commandbuffer,
vk::PipelineBindPoint::GRAPHICS,
pipeline.pipeline,
);
logical_device.cmd_bind_descriptor_sets(
commandbuffer,
vk::PipelineBindPoint::GRAPHICS,
pipeline.layout,
0,
&[self.descriptorsets[index]],
&[],
);
}
if let Some(vertexbuffer) = &self.vertexbuffer {
unsafe {
logical_device.cmd_bind_vertex_buffers(
commandbuffer,
0,
&[vertexbuffer.buffer],
&[0],
);
logical_device.cmd_draw(
commandbuffer,
self.vertexdata.len() as u32,
1, //instance count
0,
0,
);
}
}
}
}
}
aetna::update_commandbuffer에 한 줄을 포함시킵니다 (self.text.draw에 관한 줄):
for m in &self.models {
m.draw(&self.device, commandbuffer);
}
self.text.draw(&self.device, commandbuffer, index);
self.device.cmd_end_render_pass(commandbuffer);
self.device.end_command_buffer(commandbuffer)?;
}
Ok(())
}
그리고 aetna는 (앞서 언급했듯이) text라는 새 필드를 얻고, 다음과 같이 초기화합니다:
let text = AllText::new(
&logical_device,
&swapchain,
&renderpass,
"../fonts/Roboto-Regular.ttf",
)?;
(또는 원하는 다른 폰트로). 또한 Aetna::drop에 새 줄이 추가됩니다:
self.text.cleanup(&self.device, &self.allocator);
main()의 시작 부분 근처에서 (aetna를 생성한 후 물론):
let letters = aetna.text.create_letters(
&[
&fontdue::layout::TextStyle::new("Hello ", 35.0, 0),
&fontdue::layout::TextStyle::new("world!", 40.0, 0),
&fontdue::layout::TextStyle::new("(and smaller)", 8.0, 0),
],
[0., 1., 0.],
);
aetna.text.create_vertexdata(
letters,
(100, 200),
&aetna.window,
&aetna.device,
&aetna.allocator,
&aetna.pools.commandpool_graphics,
&aetna.queues.graphics_queue,
&aetna.swapchain,
);
aetna.text.update_vertexbuffer(&aetna.allocator);
let letters2 = aetna.text.create_letters(
&[
&fontdue::layout::TextStyle::new("Hello ", 35.0, 0),
&fontdue::layout::TextStyle::new("world!", 40.0, 0),
&fontdue::layout::TextStyle::new("(and smaller)", 8.0, 0),
],
[0.6, 0.6, 0.6],
);
aetna.text.create_vertexdata(
letters2,
(100, 400),
&aetna.window,
&aetna.device,
&aetna.allocator,
&aetna.pools.commandpool_graphics,
&aetna.queues.graphics_queue,
&aetna.swapchain,
);
aetna.text.update_vertexbuffer(&aetna.allocator);
작동합니다. 그리고 괜찮아 보입니다. 비결은 제가 꽤 큰 텍스트 크기를 사용하고 있다는 것입니다. 8포인트 문자열에서는 조금 덜 선명합니다. 아마도 “글자당 단일 텍스처” 접근 방식이 충분히 잘 작동하지 않거나 (또는 반올림 오류를 보정하기 위해 픽셀에서 Vulkan 좌표로 변환할 때 어딘가에 0.5 픽셀을 추가하면 더 나아질 수도 있습니다).
디스크립터 풀은 (최대 텍스처 수만큼) 많은 디스크립터 셋을 포함합니다. update_textures를 보세요:
let pool_sizes = [vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: crate::renderpass_and_pipeline::MAXIMAL_NUMBER_OF_TEXTURES
* swapchain.amount_of_images,
}];
이것은 파이프라인 생성 시 결정한 내용에 따라야 했기 때문입니다. 만약 MAXIMAL_NUMBER_OF_TEXTURES 대신 amount를 사용하면 유효성 검사 레이어에서 불평합니다.
대신, 더 낮은 숫자로 시작하고 현재 풀 크기를 초과할 때마다 파이프라인을 다시 생성할 수 있습니다. 이는 파이프라인을 다시 생성하는 것을 의미합니다(비싸다고 알려진 작업입니다. 하지만 이전에 사용한 적 없는 글자가 얼마나 자주 필요할까요?).
AllText에 새 필드를 추가합니다:
texture_amount: usize,
이것을 0으로 초기화합니다. 그리고 초기화에 대해 말하자면, pipeline을 None으로 설정할 수 있습니다. (그러면 new는 (self와) 표준 폰트만 인자로 필요하게 됩니다.)
이제 update_textures에서 파이프라인 생성이 (적어도 때때로) 일어날 것입니다:
pub fn update_textures(
&mut self,
swapchain: &SwapchainDongXi,
device: &ash::Device,
renderpass: &vk::RenderPass,
) -> Result<(), Box<dyn std::error::Error>> {
let amount = self.textures.len();
if amount > self.texture_amount {
self.texture_amount = amount;
let p = self.pipeline.take();
if let Some(pip) = p {
pip.cleanup(device);
}
}
if self.pipeline.is_none() {
let pip = Pipeline::init_text(device, swapchain, renderpass, amount as u32)?;
self.pipeline = Some(pip);
}
if let Some(pool) = self.desc_pool {
unsafe {
device.destroy_descriptor_pool(pool, None);
}
}
let pool_sizes = [vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: amount as u32 * swapchain.amount_of_images,
}];
마지막 줄의 디스크립터 카운트에 주목하세요. 추가 참고: 이 함수는 renderpass를 추가 인자로 필요로 합니다 (이 때문에 aetna의 renderpass 필드를 pub로 만들어야 합니다). Pipeline::init_text 함수는 텍스처 수를 인자로 받습니다. 함수 본문 변경 사항:
let descriptorset_layout_binding_descs = [vk::DescriptorSetLayoutBinding::builder()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(amount_of_textures) //MAXIMAL_NUMBER_OF_TEXTURES)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.build()];
좋습니다. (이것이 성능에 큰 영향을 미칠지는 모르겠습니다. 하지만 기분은 더 좋습니다.)
창 크기를 조절하면 프로그램이 많은 유효성 검사 오류를 뱉어냅니다.
이를 처리하기 위해, aetna::recreate_swapchain에 다음 줄을 삽입합니다:
self.text.clear_pipeline(&self.device, &self.allocator);
self.text
.update_textures(&self.swapchain, &self.device, &self.renderpass);
여기서 AllText::cleanup()을 두 개로 분리했습니다:
pub fn clear_pipeline(&mut self, device: &ash::Device, allocator: &vk_mem::Allocator) {
let p = self.pipeline.take();
if let Some(pip) = p {
pip.cleanup(device);
}
}
pub fn cleanup(&mut self, device: &ash::Device, allocator: &vk_mem::Allocator) {
self.clear_pipeline(device, allocator);
let p = self.desc_pool.take();
if let Some(pool) = p {
unsafe {
device.destroy_descriptor_pool(pool, None);
}
}
let b = self.vertexbuffer.take();
if let Some(buf) = b {
unsafe { allocator.destroy_buffer(buf.buffer, &buf.allocation) };
}
for texture in &self.textures {
unsafe {
device.destroy_sampler(texture.sampler, None);
device.destroy_image_view(texture.imageview, None);
}
allocator.destroy_image(texture.vk_image, &texture.allocation);
}
}
스왑체인이 갱신될 때 파이프라인은 정리되고 (update_textures 중에) 다시 생성되어야 하지만, 모든 텍스처를 파괴할 필요는 없습니다.
또한 지난 장들의 텍스처도 같은 문제가 있습니다.
[Debug][error][validation] "vkUpdateDescriptorSets() failed write update validation for VkDescriptorSet 0x2b000000002b[] with error: Cannot call vkUpdateDescriptorSets() to perform write update on VkDescriptorSet VkDescriptorSet 0x2b000000002b[] allocated with VkDescriptorSetLayout VkDescriptorSetLayout 0x1d000000001d[] which has been destroyed. The Vulkan spec states: dstSet must be a valid VkDescriptorSet handle (https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VUID-VkWriteDescriptorSet-dstSet-00320)"
[Debug][error][validation] "descriptorSet #1 being bound is not compatible with overlapping descriptorSetLayout at index 1 of VkPipelineLayout 0x1020000000102[] due to: VkDescriptorSetLayout 0x1010000000101[] from pipeline layout has 1 total descriptors, but VkDescriptorSetLayout 0x1d000000001d[], which is bound, has 1048576 total descriptors.. The Vulkan spec states: Each element of pDescriptorSets must have been allocated with a VkDescriptorSetLayout that matches (is the same as, or identically defined as) the VkDescriptorSetLayout at set n in layout, where n is the sum of firstSet and the index into pDescriptorSets (https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VUID-vkCmdBindDescriptorSets-pDescriptorSets-00358)"
실제로, 이 불일치는 recreate_swapchain의 한 줄 때문입니다.
self.pipeline = Pipeline::init_textured(&self.device, &self.swapchain, &self.renderpass)?;
이여야 하는데 Pipeline::init(&...) 이었습니다 (여기서 파이프라인을 조정하는 것을 잊었습니다).
그럼에도 불구하고, 여전히:
[Debug][error][validation] "vkUpdateDescriptorSets() failed write update validation for VkDescriptorSet 0x2b000000002b[] with error: Cannot call vkUpdateDescriptorSets() to perform write update on VkDescriptorSet VkDescriptorSet 0x2b000000002b[] allocated with VkDescriptorSetLayout VkDescriptorSetLayout 0x1d000000001d[] which has been destroyed. The Vulkan spec states: dstSet must be a valid VkDescriptorSet handle (https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VUID-VkWriteDescriptorSet-dstSet-00320)"
맞습니다: pipeline::cleanup()이 디스크립터 셋을 파괴합니다. 텍스트에서는 디스크립터 셋이 text::update_textures() 중에 어차피 갱신되기 때문에 눈치채지 못했습니다.
좋습니다, recreate_swapchain에 또 다른 변경 사항:
self.pipeline = Pipeline::init_textured(&self.device, &self.swapchain, &self.renderpass)?;
unsafe {
self.device.reset_descriptor_pool(
self.descriptor_pool,
ash::vk::DescriptorPoolResetFlags::empty(),
);
}
let desc_layouts_texture =
vec![self.pipeline.descriptor_set_layouts[1]; self.swapchain.amount_of_images as usize];
let descriptor_set_allocate_info_texture = vk::DescriptorSetAllocateInfo::builder()
.descriptor_pool(self.descriptor_pool)
.set_layouts(&desc_layouts_texture);
self.descriptor_sets_texture = unsafe {
self.device
.allocate_descriptor_sets(&descriptor_set_allocate_info_texture)
}?;
let desc_layouts_camera =
vec![self.pipeline.descriptor_set_layouts[0]; self.swapchain.amount_of_images as usize];
let descriptor_set_allocate_info_camera = vk::DescriptorSetAllocateInfo::builder()
.descriptor_pool(self.descriptor_pool)
.set_layouts(&desc_layouts_camera);
self.descriptor_sets_camera = unsafe {
self.device
.allocate_descriptor_sets(&descriptor_set_allocate_info_camera)
}?;
for descset in &self.descriptor_sets_camera {
let buffer_infos = [vk::DescriptorBufferInfo {
buffer: self.uniformbuffer.buffer,
offset: 0,
range: 128,
}];
let desc_sets_write = [vk::WriteDescriptorSet::builder()
.dst_set(*descset)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
.buffer_info(&buffer_infos)
.build()];
unsafe { self.device.update_descriptor_sets(&desc_sets_write, &[]) };
}
(이것의 대부분은 초기화에서 복사해왔습니다.) 새로운 것은 self.device.reset_descriptor_pool 명령어입니다. 이것은 디스크립터 풀을 리셋하여 그 안의 모든 디스크립터 셋을 해제하고 재활용합니다.
새로운 오류:
[Debug][error][validation] "vkUpdateDescriptorSets() failed write update validation for VkDescriptorSet 0x12f000000012f[] with error: Cannot call vkUpdateDescriptorSets() to perform write update on VkDescriptorSet VkDescriptorSet 0x12f000000012f[] allocated with VkDescriptorSetLayout VkDescriptorSetLayout 0x12b000000012b[] that is in use by a command buffer. The Vulkan spec states: All submitted commands that refer to any element of pDescriptorSets must have completed execution (https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VUID-vkFreeDescriptorSets-pDescriptorSets-00309)"
이게 뭘까요?
알고 보니, 업데이트할 디스크립터 셋을 선택할 때 잘못된 인덱스를 사용하고 있었습니다. 한 줄 수정:
let descriptorwrite_image = vk::WriteDescriptorSet::builder()
//.dst_set(aetna.descriptor_sets_texture[aetna.swapchain.current_image])
.dst_set(aetna.descriptor_sets_texture[image_index as usize])
.dst_binding(0)
.dst_array_element(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&imageinfos)
.build();
AllText의 전반적인 디자인은 최적이 아닙니다: 아직 글자를 제거할 수 없습니다. 또한 항상 create_letters, create_vertexdata, update_vertexbuffer를 차례로 (수동으로) 호출해야 하는 것은 성가십니다.
하지만 이를 수정하는 것은 다음 기회로 미루겠습니다 (어쩌면 영원히 안 할지도 모릅니다).