우리는 정점 셰이더에서 프래그먼트 셰이더로 데이터를 전달하는 방법을 보았지만, 모든 데이터를 정점 셰이더에 집어넣고 싶지는 않을 겁니다. 이제 정점 셰이더로 약간의 데이터를 보내보겠습니다. 아마도 위치부터 시작하는 것이 좋겠네요.
이전 장에 따르면, 정점 셰이더는 다음과 같은 모습이 될 것입니다:
#version 450
layout (location=0) in vec4 position;
layout (location=0) out vec4 colourdata_for_the_fragmentshader;
void main() {
gl_PointSize=10.0;
gl_Position = position;
colourdata_for_the_fragmentshader=vec4(0.4,1.0,0.5,1.0);
}
(그리고, 완전성을 위해 덧붙이자면, 프래그먼트 셰이더는 여전히 이렇습니다.
#version 450
layout (location=0) out vec4 theColour;
layout (location=0) in vec4 data_from_the_vertexshader;
void main(){
theColour= data_from_the_vertexshader;
}
)
아마 놀랍게도, 프로그램은 이미 실행됩니다. 하지만 다음과 같은 메시지와 함께 실행됩니다.
[Debug][error][validation] "Vertex shader consumes input at location 0 but not provided"
([디버그][에러][검증] "정점 셰이더가 location 0의 입력을 사용하지만, 제공되지 않았습니다")
우리는 정점 셰이더에 입력이 있을 것이라고 말했지만, 아직 러스트 프로그램에는 그 사실을 알리지 않았습니다. 어딘가에서
정점 셰이더가 이제 인자로 vec4, 즉 러스트 식으로 말하면 [f32;4]를 기대한다는 것을 알려줘야 합니다.
물론, 러스트 코드 안에 layout (location=0) out이라고 그냥 쓸 수는 없습니다. 하지만 우리가 정보를 채워 넣었던 많은 구조체들이 있었습니다. 이 “정점 셰이더를 위한 데이터”는 분명 “파이프라인”과 관련이 있습니다. 그리고 실제로, 우리 프로그램의 긴 Pipeline::init()
함수 안에…
fn init(
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/shader.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/shader.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_input_info = vk::PipelineVertexInputStateCreateInfo::builder();
let input_assembly_info = vk::PipelineInputAssemblyStateCreateInfo::builder()
.topology(vk::PrimitiveTopology::POINT_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::NONE)
.polygon_mode(vk::PolygonMode::FILL);
let multisampler_info = vk::PipelineMultisampleStateCreateInfo::builder()
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
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 pipelinelayout_info = vk::PipelineLayoutCreateInfo::builder();
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)
.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,
})
}
… 다른 많은 항목들 속에서, let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder();를 찾을 수 있습니다.
현재는 기본 PipelineVertexInputStateCreateInfo를 설정하고 있지만, 이것은 쉽게 바꿀 수 있습니다:
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder()
.vertex_attribute_descriptions(&vertex_attrib_descs)
.vertex_binding_descriptions(&vertex_binding_descs);
여기서 우리가 설정하는 것은 속성 서술자(attribute descriptions)와 바인딩 서술자(binding descriptions)입니다.
layout (location=0) in vec4 position;
와 비교했을 때, 이 정보를 담고 있는 것은 속성 서술자입니다:
let vertex_attrib_descs = [vk::VertexInputAttributeDescription {
binding: 0,
location: 0,
offset: 0,
format: vk::Format::R32G32B32A32_SFLOAT,
}];
location은 이해하기 쉽습니다: 이것은 셰이더의 location과 동일합니다.
format은 변수 타입입니다. 그리고 R32G32B32A32_SFLOAT는 [f32;4]를 이상하게 표기한 방식입니다. 둘 다 “float”을 알아볼 수 있습니다. 또한 각 컴포넌트의 크기를 비트 단위로 나타내는 32도 둘 다에서 보입니다. 컴포넌트는 네 개입니다: 한쪽에서는 숫자 4로, 다른 쪽에서는 글자와 “32”의 반복 횟수로 표시됩니다. 왜 이름이 R, G, B, A일까요? 음, 색상이라면 말이 되겠지요. 이건 색상이 아니지만, 굳이 다른 이름을 쓸 이유가 있을까요? 그래서 R, G, B, A를 씁니다. 만약 [f32;2]를 전달하고 싶다면, R32G32_SFLOAT를 사용해야 합니다 (그리고 세 개나 한 개의 컴포넌트에는 무엇을 써야 할지 추측해보세요!).
offset: 변수를 하나가 아니라 두 개 전달해야 하고, 파이프라인에 “여기 데이터가 있다”고 말한다고 상상해보세요. 파이프라인이 할 법한 합리적인 질문은 “데이터의 어느 부분, 즉 어느 변수를 어디서 찾아야 하는가?”일 것입니다. offset이 이 질문에 답합니다: 데이터의 시작 부분으로부터 오프셋(바이트 단위)이 얼마나 되는가. 우리 변수의 경우, 첫 번째이므로 오프셋은 0입니다. 만약 그 뒤에 두 번째 변수가 저장된다면, 그 변수는 오프셋 16을 가질 것입니다 (4바이트짜리 변수 네 개가 첫 번째 자리에 있으니까요) — 하지만 지금은 두 번째 변수가 없습니다.
binding: 아직 “binding” 항목에 대한 가시적인 이유는 없습니다. 하지만 셰이더에 완전히 다른 두 종류의 것을 보내고 싶다고 상상해보세요. 예를 들어, 위치와 색상이 있는데, 색상 데이터는 매우 자주 바꾸고 싶지만 위치는 오랫동안 그대로 유지된다고 합시다. 그렇다면 색상 데이터를 위치 데이터와 독립적으로 업데이트할 수 있다면 좋을 것입니다. 이럴 때 이러한 다른 종류의 정보를 다른 장소에 두고, 다른… 음, 바인딩에 “바인딩”하는 것이 유리합니다.
물론, 이러한 바인딩은 서술되어야 합니다:
let vertex_binding_descs = [vk::VertexInputBindingDescription {
binding: 0,
stride: 16,
input_rate: vk::VertexInputRate::VERTEX,
}];
우리는 어떤 바인딩인지, 그리고 이 바인딩에 있는 모든 변수들을 합친 크기가 얼마인지(우리 경우는 위에서 본 것처럼 16바이트), 즉 한 정점의 데이터 세트에서 다음 정점의 데이터 세트로 넘어갈 때의 보폭(stride)이 얼마인지를 명시합니다. 그리고 input rate가 있는데, 여기서는 이 바인딩의 데이터가 정점마다 바뀐다고 말합니다. 다른 옵션은 INSTANCE인데, 이건 인스턴스 렌더링을 다룰 때(나중에) 이야기해야 할 것입니다.
그래서, 이 줄들로
let vertex_attrib_descs = [vk::VertexInputAttributeDescription {
binding: 0,
location: 0,
offset: 0,
format: vk::Format::R32G32B32A32_SFLOAT,
}];
let vertex_binding_descs = [vk::VertexInputBindingDescription {
binding: 0,
stride: 16,
input_rate: vk::VertexInputRate::VERTEX,
}];
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder()
.vertex_attribute_descriptions(&vertex_attrib_descs)
.vertex_binding_descriptions(&vertex_binding_descs);
우리는 파이프라인에 어떤 데이터를 전달하고 싶은지 서술했습니다. 아직 실제 데이터를 주지는 않았습니다.
이제, 데이터는 GPU에 “자, 여기 변수들이야” 하는 식으로 그냥 주어지지 않습니다. 대신 먼저 “버퍼”를 만들고 모든 변수를 채워 넣은 다음, 벌칸에 “여기, 데이터는 이 버퍼에 있어”라고 말합니다.
이는 우리가 버퍼를 설정해야 한다는 것을 의미합니다. 그리고 실제로는 그것을 위해 적절한 메모리를 별도로 예약해야 합니다. 벌칸은 우리에게 메모리 처리에 대한 제어권을 주며, 우리가 그것을 처리하기를 기대합니다. 또한 (튜토리얼 코드 단계에서는 아직 중요하지 않지만) 모든 버퍼를 별도로 할당하지 않고, 하나의 큰 버퍼를 할당한 다음 그 일부를 사용하는 것이 권장됩니다. 그리고 할당 시에는 특정 정렬 요구사항을 만족시켜야 합니다…
우리는 이 전체 주제를 좀 더 쉽게 만들기 위해 벌칸을 직접 사용하지 않고, Vulkan Memory Allocator, 즉 VMA를 사용할 것입니다. Cargo.toml에 새 줄이 들어갑니다: vk-mem = "0.2.2"
(나중에 추가: 만약 vk-mem 0.2.2와 러스트 1.48.0 이상에서 'attempted to zero-initialize type 'ash::Device', which is invalid'와 같은 패닉을 경험한다면, 40장 의 시작 부분을 확인하거나, 관련 이슈를 참조하세요 — 또는 더 오래된 러스트 버전을 사용하세요.) (더 나중에 추가: 아래 코드에 약간의 변경이 필요하겠지만, vk-mem 대신 gpu-allocator를 사용하는 것이 좋을 수 있습니다.)
먼저 할당자(allocator)를 만듭니다:
let allocator_create_info = vk_mem::AllocatorCreateInfo {
physical_device,
device: logical_device.clone(),
instance: instance.clone(),
..Default::default()
};
let mut allocator = vk_mem::Allocator::new(&allocator_create_info)?;
벌칸의 모든 것이 그렇듯, 이것도 먼저 채워야 하는 CreateInfo 구조체와 함께 제공됩니다. vk-mem은 애쉬가 사용하는 멋진 ::builder() 기능을 지원하지 않으므로, 명시적인 목록과 함께 ..Default::default() 변형을 사용합니다. (그리고 어떤 이유에서인지, device와 instance를 복제해야 합니다. Copy를 구현하는 ash::vk::Instance가 아니라 ash::Instance이기 때문입니다.)
이 할당자로, 우리는 버퍼를 만듭니다:
let (buffer, allocation, allocation_info) = allocator.create_buffer(
&ash::vk::BufferCreateInfo::builder()
.size(16)
.usage(vk::BufferUsageFlags::VERTEX_BUFFER)
.build(),
&allocation_create_info,
)?;
보시다시피, BufferCreateInfo가 관련되어 있으며, 버퍼의 크기(바이트 단위)와 용도를 설정합니다. 이 경우, 정점 버퍼로 사용합니다. 그리고, 명백하게, AllocationCreateInfo가 필요합니다. 여기 있습니다:
let allocation_create_info = vk_mem::AllocationCreateInfo {
usage: vk_mem::MemoryUsage::CpuToGpu,
..Default::default()
};
여기서 우리가 서술하는 것은 우리가 원하는 메모리의 종류입니다. CpuOnly/GpuOnly/CpuToGpu/GpuToCpu? 여기서는 CpuToGpu를 선택합니다: CPU에서 이 버퍼에 직접 쓰고 싶고, GPU에서 접근하고 싶습니다.
그런 다음 버퍼를 데이터로 채웁니다:
let data_ptr = allocator.map_memory(&allocation)? as *mut f32;
let data = [-0.5f32, 0.0f32, 0.0f32, 1.0f32];
unsafe { data_ptr.copy_from_nonoverlapping(data.as_ptr(), 4) };
allocator.unmap_memory(&allocation);
먼저 메모리를 map하여 접근할 수 있게 합니다. 그런 다음 데이터를 만들고(셰이더에 전달하고 싶은 좌표들!), 복사한 다음, 마지막으로 메모리를 unmap합니다. (unmap은 map만큼 자주 호출되어야 하지만, 여기처럼 빨리 할 필요는 없습니다.)
이제 버퍼를 채웠으니, 아직 사용해야 합니다. 커맨드 버퍼 기록에 한 줄을 더 포함시킵니다:
unsafe {
logical_device.cmd_begin_render_pass(
commandbuffer,
&renderpass_begininfo,
vk::SubpassContents::INLINE,
);
logical_device.cmd_bind_pipeline(
commandbuffer,
vk::PipelineBindPoint::GRAPHICS,
pipeline.pipeline,
);
logical_device.cmd_bind_vertex_buffers(commandbuffer, 0, &[*vb], &[0]);
logical_device.cmd_draw(commandbuffer, 1, 1, 0, 0);
logical_device.cmd_end_render_pass(commandbuffer);
logical_device.end_command_buffer(commandbuffer)?;
}
logical_device.cmd_bind_vertex_buffers(commandbuffer, 0, &[*vb], &[0]); 줄이
새로운 줄이고, vb는 buffer에 대한 참조입니다. 함수 인자 중 첫 번째 0은 이 버퍼에 의해 업데이트될 바인딩입니다. (위에서 바인딩에 대해 언급했죠.) 만약 여러 바인딩을 동시에 업데이트하고 싶다면, 그것도 가능하며, 그래서 버퍼 슬라이스가 사용됩니다.
마지막 인자는 제공된 정점 버퍼의 모든 항목을 사용하고 싶지 않은 경우를 위한 오프셋을 나타냅니다.
한 가지 더 할 일이 남았습니다: 버퍼와 할당자의 파괴입니다:
self.allocator.destroy_buffer(self.buffer, &self.allocation);
self.allocator.destroy();
Aetna 구조체와 그것의 init, drop 함수, 그리고 fill_commandbuffer 함수를 변경했습니다. 여기 그 코드입니다:
fn fill_commandbuffers(
commandbuffers: &[vk::CommandBuffer],
logical_device: &ash::Device,
renderpass: &vk::RenderPass,
swapchain: &SwapchainDongXi,
pipeline: &Pipeline,
vb: &vk::Buffer,
) -> Result<(), vk::Result> {
for (i, &commandbuffer) in commandbuffers.iter().enumerate() {
let commandbuffer_begininfo = vk::CommandBufferBeginInfo::builder();
unsafe {
logical_device.begin_command_buffer(commandbuffer, &commandbuffer_begininfo)?;
}
let clearvalues = [vk::ClearValue {
color: vk::ClearColorValue {
float32: [0.0, 0.0, 0.08, 1.0],
},
}];
let renderpass_begininfo = vk::RenderPassBeginInfo::builder()
.render_pass(*renderpass)
.framebuffer(swapchain.framebuffers[i])
.render_area(vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: swapchain.extent,
})
.clear_values(&clearvalues);
unsafe {
logical_device.cmd_begin_render_pass(
commandbuffer,
&renderpass_begininfo,
vk::SubpassContents::INLINE,
);
logical_device.cmd_bind_pipeline(
commandbuffer,
vk::PipelineBindPoint::GRAPHICS,
pipeline.pipeline,
);
logical_device.cmd_bind_vertex_buffers(commandbuffer, 0, &[*vb], &[0]);
logical_device.cmd_draw(commandbuffer, 1, 1, 0, 0);
logical_device.cmd_end_render_pass(commandbuffer);
logical_device.end_command_buffer(commandbuffer)?;
}
}
Ok(())
}
struct Aetna {
window: winit::window::Window,
entry: ash::Entry,
instance: ash::Instance,
debug: std::mem::ManuallyDrop<DebugDongXi>,
surfaces: std::mem::ManuallyDrop<SurfaceDongXi>,
physical_device: vk::PhysicalDevice,
physical_device_properties: vk::PhysicalDeviceProperties,
queue_families: QueueFamilies,
queues: Queues,
device: ash::Device,
swapchain: SwapchainDongXi,
renderpass: vk::RenderPass,
pipeline: Pipeline,
pools: Pools,
commandbuffers: Vec<vk::CommandBuffer>,
allocator: vk_mem::Allocator,
buffer: vk::Buffer,
allocation: vk_mem::Allocation,
allocation_info: vk_mem::AllocationInfo,
}
impl Aetna {
fn init(window: winit::window::Window) -> Result<Aetna, Box<dyn std::error::Error>> {
let entry = ash::Entry::new()?;
let layer_names = vec!["VK_LAYER_KHRONOS_validation"];
let instance = init_instance(&entry, &layer_names)?;
let debug = DebugDongXi::init(&entry, &instance)?;
let surfaces = SurfaceDongXi::init(&window, &entry, &instance)?;
let (physical_device, physical_device_properties) =
init_physical_device_and_properties(&instance)?;
let queue_families = QueueFamilies::init(&instance, physical_device, &surfaces)?;
let (logical_device, queues) =
init_device_and_queues(&instance, physical_device, &queue_families, &layer_names)?;
let mut swapchain = SwapchainDongXi::init(
&instance,
physical_device,
&logical_device,
&surfaces,
&queue_families,
&queues,
)?;
let renderpass = init_renderpass(
&logical_device,
physical_device,
swapchain.surface_format.format,
)?;
swapchain.create_framebuffers(&logical_device, renderpass)?;
let pipeline = Pipeline::init(&logical_device, &swapchain, &renderpass)?;
let pools = Pools::init(&logical_device, &queue_families)?;
let allocator_create_info = vk_mem::AllocatorCreateInfo {
physical_device,
device: logical_device.clone(),
instance: instance.clone(),
..Default::default()
};
let mut allocator = vk_mem::Allocator::new(&allocator_create_info)?;
let allocation_create_info = vk_mem::AllocationCreateInfo {
usage: vk_mem::MemoryUsage::CpuToGpu,
..Default::default()
};
let (buffer, allocation, allocation_info) = allocator.create_buffer(
&ash::vk::BufferCreateInfo::builder()
.size(16)
.usage(vk::BufferUsageFlags::VERTEX_BUFFER)
.build(),
&allocation_create_info,
)?;
let data_ptr = allocator.map_memory(&allocation)? as *mut f32;
let data = [-0.5f32, 0.0f32, 0.0f32, 1.0f32];
unsafe { data_ptr.copy_from_nonoverlapping(data.as_ptr(), 4) };
allocator.unmap_memory(&allocation);
let commandbuffers =
create_commandbuffers(&logical_device, &pools, swapchain.amount_of_images)?;
fill_commandbuffers(
&commandbuffers,
&logical_device,
&renderpass,
&swapchain,
&pipeline,
&buffer,
)?;
Ok(Aetna {
window,
entry,
instance,
debug: std::mem::ManuallyDrop::new(debug),
surfaces: std::mem::ManuallyDrop::new(surfaces),
physical_device,
physical_device_properties,
queue_families,
queues,
device: logical_device,
swapchain,
renderpass,
pipeline,
pools,
commandbuffers,
allocator,
buffer,
allocation,
allocation_info,
})
}
}
impl Drop for Aetna {
fn drop(&mut self) {
unsafe {
self.device
.device_wait_idle()
.expect("something wrong while waiting");
self.allocator.destroy_buffer(self.buffer, &self.allocation);
self.allocator.destroy();
self.pools.cleanup(&self.device);
self.pipeline.cleanup(&self.device);
self.device.destroy_render_pass(self.renderpass, None);
self.swapchain.cleanup(&self.device);
self.device.destroy_device(None);
std::mem::ManuallyDrop::drop(&mut self.surfaces);
std::mem::ManuallyDrop::drop(&mut self.debug);
self.instance.destroy_instance(None)
};
}
}
이제 프로그램을 실행할 수 있습니다. 그리고 let data = [-0.5f32, 0.0f32, 0.0f32, 1.0f32];의 값을 바꾸면
우리가 그리는 점의 좌표가 바뀝니다.
자, 이렇게 해서 정점 셰이더로 데이터를 전달합니다. 멋지네요!
이제 더 많은 데이터를 보내봅시다.
위치 외에도 변수가 더 있었습니다: 색상과 점 크기도 있었죠.
#version 450
layout (location=0) in vec4 position;
layout (location=1) in float size;
layout (location=2) in vec4 colour;
layout (location=0) out vec4 colourdata_for_the_fragmentshader;
void main() {
gl_PointSize=size;
gl_Position = position;
colourdata_for_the_fragmentshader=colour;
}
물론, 이제는 그에 맞게 더 많은 실수를 버퍼에 전달해야 합니다 (버퍼도 더 커져야겠죠):
let (buffer, allocation, allocation_info) = allocator.create_buffer(
&ash::vk::BufferCreateInfo::builder()
.size(36)
.usage(vk::BufferUsageFlags::VERTEX_BUFFER)
.build(),
&allocation_create_info,
)?;
let data_ptr = allocator.map_memory(&allocation)? as *mut f32;
let data = [
0.1f32, -0.3f32, 0.0f32, 1.0f32, 5.0f32, 1.0f32, 1.0f32, 0.0f32, 1.0f32,
];
unsafe { data_ptr.copy_from_nonoverlapping(data.as_ptr(), 9) };
잠깐만요! 속성 서술자도 조정해야 합니다:
let vertex_attrib_descs = [
vk::VertexInputAttributeDescription {
binding: 0,
location: 0,
offset: 0,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 0,
location: 1,
offset: 16,
format: vk::Format::R32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 0,
location: 2,
offset: 20,
format: vk::Format::R32G32B32A32_SFLOAT,
},
];
모두 바인딩 0에 대해, location 1과 2에 대한 항목들이 새로 추가되었으며, 이는 셰이더의 새로운 입력 변수들에 해당합니다. 점 크기는 단 하나의
실수(Format::R32_SFLOAT)이고, 색상은 네 개의 32비트 컴포넌트를 가집니다 (여기서는 R32G32B32A32라는 이름이 아주 적절하네요). 오프셋은 각 f32 실수가 4바이트라는 것을 기억하며 간단한 계산으로 쉽게 구할 수 있습니다.
프로그램을 실행하고, data의 값들을 좀 바꿔보세요.
다음으로, 이것들을 다른 버퍼에 넣어봅시다. 예를 들어, 위치 데이터는 하나에, 색상과 크기 데이터는 다른 하나에 넣는 거죠.
좋습니다, 그럼 두 개의 버퍼를 만들어 봅시다:
let allocation_create_info = vk_mem::AllocationCreateInfo {
usage: vk_mem::MemoryUsage::CpuToGpu,
..Default::default()
};
let (buffer1, allocation1, allocation_info1) = allocator.create_buffer(
&ash::vk::BufferCreateInfo::builder()
.size(16)
.usage(vk::BufferUsageFlags::VERTEX_BUFFER)
.build(),
&allocation_create_info,
)?;
let data_ptr = allocator.map_memory(&allocation1)? as *mut f32;
let data = [0.1f32, -0.3f32, 0.0f32, 1.0f32];
unsafe { data_ptr.copy_from_nonoverlapping(data.as_ptr(), 4) };
allocator.unmap_memory(&allocation1);
let (buffer2, allocation2, allocation_info2) = allocator.create_buffer(
&ash::vk::BufferCreateInfo::builder()
.size(20)
.usage(vk::BufferUsageFlags::VERTEX_BUFFER)
.build(),
&allocation_create_info,
)?;
let data_ptr = allocator.map_memory(&allocation2)? as *mut f32;
let data = [5.0f32, 1.0f32, 1.0f32, 0.0f32, 1.0f32];
unsafe { data_ptr.copy_from_nonoverlapping(data.as_ptr(), 5) };
allocator.unmap_memory(&allocation2);
(방금 버퍼 생성 부분을 복사했고, 이전과 같은 값을 채워 넣되, 일부는 첫 번째 버퍼에, 나머지는 두 번째 버퍼에 넣었습니다.)
그리고 이것은 우리의 메인 구조체를 변경시켰습니다:
Ok(Aetna {
window,
entry,
instance,
debug: std::mem::ManuallyDrop::new(debug),
surfaces: std::mem::ManuallyDrop::new(surfaces),
physical_device,
physical_device_properties,
queue_families,
queues,
device: logical_device,
swapchain,
renderpass,
pipeline,
pools,
commandbuffers,
allocator,
buffer1,
allocation1,
allocation_info1,
buffer2,
allocation2,
allocation_info2,
})
그리고 이와 함께
struct Aetna {
window: winit::window::Window,
entry: ash::Entry,
instance: ash::Instance,
debug: std::mem::ManuallyDrop<DebugDongXi>,
surfaces: std::mem::ManuallyDrop<SurfaceDongXi>,
physical_device: vk::PhysicalDevice,
physical_device_properties: vk::PhysicalDeviceProperties,
queue_families: QueueFamilies,
queues: Queues,
device: ash::Device,
swapchain: SwapchainDongXi,
renderpass: vk::RenderPass,
pipeline: Pipeline,
pools: Pools,
commandbuffers: Vec<vk::CommandBuffer>,
allocator: vk_mem::Allocator,
buffer1: vk::Buffer,
allocation1: vk_mem::Allocation,
allocation_info1: vk_mem::AllocationInfo,
buffer2: vk::Buffer,
allocation2: vk_mem::Allocation,
allocation_info2: vk_mem::AllocationInfo,
}
와
fn drop(&mut self) {
unsafe {
self.device
.device_wait_idle()
.expect("something wrong while waiting");
self.allocator
.destroy_buffer(self.buffer1, &self.allocation1);
self.allocator
.destroy_buffer(self.buffer2, &self.allocation2);
도 변경되었습니다.
중요하게 조정해야 할 것은 서술자입니다:
let vertex_attrib_descs = [
vk::VertexInputAttributeDescription {
binding: 0,
location: 0,
offset: 0,
format: vk::Format::R32G32B32A32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 1,
offset: 0,
format: vk::Format::R32_SFLOAT,
},
vk::VertexInputAttributeDescription {
binding: 1,
location: 2,
offset: 4,
format: vk::Format::R32G32B32A32_SFLOAT,
},
];
두 번째와 세 번째 속성을 binding: 1로 옮겼습니다. 이는 그들의 오프셋을 다시 계산해야 한다는 것을 의미합니다: 이제 각 데이터 요소의 시작점에서부터의 오프셋이 됩니다.
하지만 location은 변경되지 않았습니다. 모든 정점 셰이더 입력 바인딩은 하나의 네임스페이스를 공유합니다. (이는 이 변경을 위해 정점 셰이더를 변경할 필요가 없다는 의미이기도 합니다.)
계속하기 전에, 코드 정리를 좀 합시다. 두 개의 버퍼 때문에 Aetna에 여섯 개의 새로운 멤버 변수를 추가하는 것은 마음에 들지 않습니다.
버퍼와 vk_mem 관련 정보를 담는 별도의 Buffer 구조체를 도입합시다:
struct Buffer {
buffer: vk::Buffer,
allocation: vk_mem::Allocation,
allocation_info: vk_mem::AllocationInfo,
}
전체 생성 과정을 new 함수로 추출합니다:
impl Buffer {
fn new(
allocator: &vk_mem::Allocator,
size_in_bytes: u64,
usage: vk::BufferUsageFlags,
memory_usage: vk_mem::MemoryUsage,
) -> Result<Buffer, vk_mem::error::Error> {
let allocation_create_info = vk_mem::AllocationCreateInfo {
usage: memory_usage,
..Default::default()
};
let (buffer, allocation, allocation_info) = allocator.create_buffer(
&ash::vk::BufferCreateInfo::builder()
.size(size_in_bytes)
.usage(usage)
.build(),
&allocation_create_info,
)?;
Ok(Buffer {
buffer,
allocation,
allocation_info,
})
}
}
그리고 버퍼를 채우는 것도 자체 함수가 됩니다. 데이터 타입에 대해 제네릭으로 만듭니다:
fn fill<T: Sized>(
&self,
allocator: &vk_mem::Allocator,
data: &[T],
) -> Result<(), vk_mem::error::Error> {
let data_ptr = allocator.map_memory(&self.allocation)? as *mut T;
unsafe { data_ptr.copy_from_nonoverlapping(data.as_ptr(), data.len()) };
allocator.unmap_memory(&self.allocation);
Ok(())
}
그런 다음 이 두 버퍼를 만들고 채운 후에 Aetna에 vec![buffer1,buffer2]를 넣습니다. 이 섹션의 거의 완전한 코드 덩어리는 다음과 같습니다:
fn fill_commandbuffers(
commandbuffers: &[vk::CommandBuffer],
logical_device: &ash::Device,
renderpass: &vk::RenderPass,
swapchain: &SwapchainDongXi,
pipeline: &Pipeline,
vb1: &vk::Buffer,
vb2: &vk::Buffer,
) -> Result<(), vk::Result> {
for (i, &commandbuffer) in commandbuffers.iter().enumerate() {
let commandbuffer_begininfo = vk::CommandBufferBeginInfo::builder();
unsafe {
logical_device.begin_command_buffer(commandbuffer, &commandbuffer_begininfo)?;
}
let clearvalues = [vk::ClearValue {
color: vk::ClearColorValue {
float32: [0.0, 0.0, 0.08, 1.0],
},
}];
let renderpass_begininfo = vk::RenderPassBeginInfo::builder()
.render_pass(*renderpass)
.framebuffer(swapchain.framebuffers[i])
.render_area(vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: swapchain.extent,
})
.clear_values(&clearvalues);
unsafe {
logical_device.cmd_begin_render_pass(
commandbuffer,
&renderpass_begininfo,
vk::SubpassContents::INLINE,
);
logical_device.cmd_bind_pipeline(
commandbuffer,
vk::PipelineBindPoint::GRAPHICS,
pipeline.pipeline,
);
logical_device.cmd_bind_vertex_buffers(commandbuffer, 0, &[*vb1], &[0]);
logical_device.cmd_bind_vertex_buffers(commandbuffer, 1, &[*vb2], &[0]);
logical_device.cmd_draw(commandbuffer, 1, 1, 0, 0);
logical_device.cmd_end_render_pass(commandbuffer);
logical_device.end_command_buffer(commandbuffer)?;
}
}
Ok(())
}
struct Buffer {
buffer: vk::Buffer,
allocation: vk_mem::Allocation,
allocation_info: vk_mem::AllocationInfo,
}
impl Buffer {
fn new(
allocator: &vk_mem::Allocator,
size_in_bytes: u64,
usage: vk::BufferUsageFlags,
memory_usage: vk_mem::MemoryUsage,
) -> Result<Buffer, vk_mem::error::Error> {
let allocation_create_info = vk_mem::AllocationCreateInfo {
usage: memory_usage,
..Default::default()
};
let (buffer, allocation, allocation_info) = allocator.create_buffer(
&ash::vk::BufferCreateInfo::builder()
.size(size_in_bytes)
.usage(usage)
.build(),
&allocation_create_info,
)?;
Ok(Buffer {
buffer,
allocation,
allocation_info,
})
}
fn fill<T: Sized>(
&self,
allocator: &vk_mem::Allocator,
data: &[T],
) -> Result<(), vk_mem::error::Error> {
let data_ptr = allocator.map_memory(&self.allocation)? as *mut T;
unsafe { data_ptr.copy_from_nonoverlapping(data.as_ptr(), data.len()) };
allocator.unmap_memory(&self.allocation);
Ok(())
}
}
struct Aetna {
window: winit::window::Window,
entry: ash::Entry,
instance: ash::Instance,
debug: std::mem::ManuallyDrop<DebugDongXi>,
surfaces: std::mem::ManuallyDrop<SurfaceDongXi>,
physical_device: vk::PhysicalDevice,
physical_device_properties: vk::PhysicalDeviceProperties,
queue_families: QueueFamilies,
queues: Queues,
device: ash::Device,
swapchain: SwapchainDongXi,
renderpass: vk::RenderPass,
pipeline: Pipeline,
pools: Pools,
commandbuffers: Vec<vk::CommandBuffer>,
allocator: vk_mem::Allocator,
buffers: Vec<Buffer>,
}
impl Aetna {
fn init(window: winit::window::Window) -> Result<Aetna, Box<dyn std::error::Error>> {
let entry = ash::Entry::new()?;
let layer_names = vec!["VK_LAYER_KHRONOS_validation"];
let instance = init_instance(&entry, &layer_names)?;
let debug = DebugDongXi::init(&entry, &instance)?;
let surfaces = SurfaceDongXi::init(&window, &entry, &instance)?;
let (physical_device, physical_device_properties) =
init_physical_device_and_properties(&instance)?;
let queue_families = QueueFamilies::init(&instance, physical_device, &surfaces)?;
let (logical_device, queues) =
init_device_and_queues(&instance, physical_device, &queue_families, &layer_names)?;
let mut swapchain = SwapchainDongXi::init(
&instance,
physical_device,
&logical_device,
&surfaces,
&queue_families,
&queues,
)?;
let renderpass = init_renderpass(
&logical_device,
physical_device,
swapchain.surface_format.format,
)?;
swapchain.create_framebuffers(&logical_device, renderpass)?;
let pipeline = Pipeline::init(&logical_device, &swapchain, &renderpass)?;
let pools = Pools::init(&logical_device, &queue_families)?;
let allocator_create_info = vk_mem::AllocatorCreateInfo {
physical_device,
device: logical_device.clone(),
instance: instance.clone(),
..Default::default()
};
let allocator = vk_mem::Allocator::new(&allocator_create_info)?;
let buffer1 = Buffer::new(
&allocator,
16,
vk::BufferUsageFlags::VERTEX_BUFFER,
vk_mem::MemoryUsage::CpuToGpu,
)?;
buffer1.fill(&allocator, &[0.4f32, -0.2f32, 0.0f32, 1.0f32])?;
let buffer2 = Buffer::new(
&allocator,
20,
vk::BufferUsageFlags::VERTEX_BUFFER,
vk_mem::MemoryUsage::CpuToGpu,
)?;
buffer2.fill(&allocator, &[15.0f32, 0.0f32, 1.0f32, 0.0f32, 1.0f32])?;
let commandbuffers =
create_commandbuffers(&logical_device, &pools, swapchain.amount_of_images)?;
fill_commandbuffers(
&commandbuffers,
&logical_device,
&renderpass,
&swapchain,
&pipeline,
&buffer1.buffer,
&buffer2.buffer,
)?;
Ok(Aetna {
window,
entry,
instance,
debug: std::mem::ManuallyDrop::new(debug),
surfaces: std::mem::ManuallyDrop::new(surfaces),
physical_device,
physical_device_properties,
queue_families,
queues,
device: logical_device,
swapchain,
renderpass,
pipeline,
pools,
commandbuffers,
allocator,
buffers: vec![buffer1, buffer2],
})
}
}
impl Drop for Aetna {
fn drop(&mut self) {
unsafe {
self.device
.device_wait_idle()
.expect("something wrong while waiting");
for b in &self.buffers {
self.allocator
.destroy_buffer(b.buffer, &b.allocation)
.expect("problem with buffer destruction");
}
self.allocator.destroy();
self.pools.cleanup(&self.device);
self.pipeline.cleanup(&self.device);
self.device.destroy_render_pass(self.renderpass, None);
self.swapchain.cleanup(&self.device);
self.device.destroy_device(None);
std::mem::ManuallyDrop::drop(&mut self.surfaces);
std::mem::ManuallyDrop::drop(&mut self.debug);
self.instance.destroy_instance(None)
};
}
}