화면에 무언가를 그리기 위해서는(더 정확히는 위에서 본 스왑체인이 제공하는 이미지에 그리기 위해서는) 몇 가지 단계를 거쳐야 하며, 그중 일부는 설정이 가능합니다. 이 단계들을 모두 합쳐 “그래픽스 파이프라인”이라고 부릅니다. 지금은 아주 최소한의 버전을 만들어 볼 것이며, 우선 마지막 단계부터 살펴보겠습니다. 마지막 단계는 실제로 어떤 색상을 적용하는 것입니다. 우리는 물감과 붓을 든 하인들을 줄 세워놓고(물론 비유적인 표현입니다) 그림을 그리라고 지시합니다. 사실, 우리는 픽셀당 한 명의 화가를 고용합니다(그리고 그들이 실제 화가가 아니고 우리 컴퓨터가 아주 저렴한 월급으로 일해줘서 매우 기쁩니다).
각각의 화가는 무엇을 알아야 할까요?
당연히 어떤 색을 사용할지 알아야 합니다. 우리는 그들을 위한 몇 가지 지시사항을 작성합니다. (특이하게도, 아직 우리 프로그램 코드에 직접 포함할 내용은 아닙니다. 다음 코드 라인들은 Cargo.toml이 있는 폴더의 하위 폴더로 새로 만들 ‘shaders’ 폴더 안에 ‘shader.frag’라는 별도 파일로 저장할 것입니다.)
#version 450
layout (location=0) out vec4 theColour;
void main(){
theColour= vec4(1.0,0.0,0.0,1.0);
}
이것은 프래그먼트 셰이더(우리 화가들의 이름입니다)를 위한 셰이더 프로그램입니다. Vulkan은 셰이더 코드를 컴파일된 형식(SPIRV)으로 받기를 기대하지만, 우리는 GLSL로 작성하고(첫 줄은 언어 버전을 명시하며, 여기서는 4.50입니다) 사용하기 전에 컴파일할 수 있습니다.
이 셰이더에서 우리는 무엇을 하고 있나요? main 함수에서 어떤 변수(우리는 이것을 색상으로 해석할 것입니다)를 빨간색으로 설정합니다. 빨간색은 네 가지 구성 요소, 즉 1(빨강), 0(초록), 0(파랑), 1(알파, 즉 불투명)로 이루어져 있으며, [0,1] 범위로 제공됩니다. 또한 이 변수가 우리 셰이더의 출력(“out vec4 theColour“)이라는 것과 어디에 배치할지(“location=0” 부분, 나중에 좀 더 이야기할 것입니다)를 명시해야 합니다.
물론, 우리는 이 지시사항을 단순히 빨간색으로 설정하는 것보다 훨씬 더 복잡하게 만들 수 있습니다. 결국 우리 마음대로 쓸 수 있는 프로그래밍 언어가 있으니까요(몇 가지 제한이 적용되긴 하지만). 하지만, 우리는 각각의 화가(=각 프래그먼트=각 픽셀)에게 다른 프로그램을 줄 수는 없습니다.
하지만 우리는 어떤 화가들이 일하러 가서 지시를 따를지, 그리고 어떤 화가들이 그냥 놀고 있을지를 선택할 수 있습니다 — 더 정확히는 GPU가 선택하게 할 수 있습니다. (“미안, 빈센트, 오늘은 할 일이 없네”.) GPU는 어떻게 선택할까요? 글쎄요, 자기 픽셀에 무언가가 있는 모든 화가가 그림을 그립니다.
“무언가”라니요? 어떤 것, 어떤 지오메트리(geometry) 말입니다. (나중에 이야기해야 할 어떤 좌표계에서) (0.5, 0.2, 0.7)에 점이 있다고 가정해 봅시다.
그러면 이 점은 화면의 한 픽셀에 해당하고, 그 지점의 프래그먼트 셰이더가 그림을 그릴 수 있습니다. 어떤 지점에 무언가가 있는지 어떻게 알 수 있을까요? 그것이 바로 버텍스 셰이더(vertex shader)가 우리에게 알려주는 것입니다. 버텍스 셰이더가 “이 지점에 무언가가 있다”고 말하면, 그곳의 프래그먼트 셰이더가 호출됩니다.
실제로 이것은 파이프라인의 설정에 따라 조금 다릅니다. 버텍스 셰이더가 세 개의 점을 출력할 때마다, 그 점들이 이루는 삼각형에 해당하는 모든 픽셀에서 프래그먼트 셰이더가 실행될 수도 있습니다. 버텍스 셰이더는 다음과 같을 수 있습니다:
#version 450
void main() {
gl_Position = vec4(0.0,0.0,0.0,1.0);
}
출력은 gl_Position이라는 특별한 변수로 들어갑니다. 그 의미는 꽤 고정되어 있는데, 바로 앞에서 설명한 것과 같습니다. 즉, 어떤 픽셀을 그려야 하는지 알아내는 데 필요한 위치 데이터입니다. 이것 역시 네 개의 구성 요소를 가진 vec4입니다. 네 개? 3D 그래픽스에 세 개면 될 것 같은데요. 네 번째 구성 요소 “1.0”은 “이것은 방향이 아니라 점이다”라는 표시라고 생각하세요. 그 의미에 대해서는 나중에 이야기하겠습니다.
짓궂은 질문: (0,0,0), (0,0,0), (0,0,0) 세 점 사이의 삼각형 크기는 얼마일까요? 아무것도 보이지 않겠죠. 이 버텍스 셰이더는 너무 단순합니다. 삼각형을 그리려는 목표를 잠시 미루고, 단일 점 하나로 만족할 수 있습니다. 점의 크기는 얼마일까요? 글쎄요, 여러분이 알려주세요. 이 경우에는 두 번째 내장 출력 변수를 포함한다는 의미입니다:
#version 450
void main() {
gl_PointSize=2.0;
gl_Position = vec4(0.0,0.0,0.0,1.0);
}
(그리고 이것을 ‘shader.vert’라는 이름으로 shader 폴더에 저장합니다.)
실제 프로그램에서라면 애플리케이션으로부터 (위치 자체와 같은) 데이터를 전달할 것입니다. (지금까지 Vulkan을 충분히 보셨으니, 이것이 엄청난 추가 설정을 의미한다는 것을 짐작하실 수 있을 겁니다. 지금은 피하고 싶네요.) 또 다른 가능한 해결책은 버텍스 셰이더에 여러 위치를 저장하고 “셰이더가 처음 호출될 때 이것”, “두 번째 호출 때 이것”, “세 번째 호출 때 저것”을 내보내는 것입니다. (vulkan-tutorial.com의 예제와 유사한) 해당 셰이더가 궁금하시다면:
#version 450
vec4 positions[3] = vec4[](
vec4(0.0, -0.5,0.0,1.0),
vec4(0.5, 0.5,0.0,1.0),
vec4(-0.5, 0.5,0.0,1.0)
);
void main() {
gl_Position = positions[gl_VertexIndex];
}
버텍스 셰이더는 애플리케이션으로부터 데이터를 공급받는 것입니다. 짧은 파이프라인이냐고요? 네, 일부 단계는 선택 사항이며(그리고 아마도 나중에 살펴볼 것입니다), 일부 단계는 고정되어 있습니다. “버텍스 셰이더가 출력한 위치를 바탕으로 어떤 프래그먼트가 영향을 받는지 알아내는” 단계가 있었습니다. 이것이 바로 “래스터화 단계(rasterizer stage)”입니다.
이 아주 간단한 예제에서도 우리는 조작할 수 있는 몇 가지 설정들(예를 들어, 입력을 단일 점으로 해석할지 아니면 세 개를 묶어 하나의 삼각형으로 해석할지)을 건드려 보았고, 물론 생성해야 할 Vulkan 객체들(과 채워야 할 해당 CreateInfo 구조체들)이 있습니다. 이 객체들 중 하나는 vk::Pipeline입니다(놀랍지 않죠).
파이프라인은 일단 생성되면 절대 변경되지 않지만, 여러 개를 가질 수 있습니다. 두 번째로 흥미로운 것은 렌더패스(renderpass)입니다. 다소 추상적인 관점에서 보면, 우리가 위에서 본 것은 “어떤 이미지가 (어떤 그리기 방법으로든) 다른 이미지로 변환되는” 과정이었고, 심지어 (프래그먼트 셰이더에서 “layout (location=0) out ...”) 여러 다른 이미지가 관련될 수 있다는 힌트를 한 번 본 적도 있습니다.
어떤 이미지(여기서는 “어태치먼트(attachment)”라는 기술 용어가 더 적합합니다)가 어떤 용도로 사용되는지에 대한 추상적인 설명은 렌더패스에 인코딩되며, 렌더패스는 다시 여러 서브패스(subpass)로 구성될 수 있습니다. 지금은 하나의 서브패스와 단일 어태치먼트(색상으로 사용됨)만 있습니다. 어태치먼트부터 보죠 [모든 코드 수정은 “swapchain=...“과 “Ok(Aetna{...})” 사이에서 이루어집니다]:
let attachments = [vk::AttachmentDescription::builder()
.format(
surfaces
.get_formats(physical_device)?
.first()
.unwrap()
.format,
)
.load_op(vk::AttachmentLoadOp::CLEAR)
.store_op(vk::AttachmentStoreOp::STORE)
.stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
.stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.final_layout(vk::ImageLayout::PRESENT_SRC_KHR)
.samples(vk::SampleCountFlags::TYPE_1)
.build()];
(여러 개가 있을 수 있으므로 배열이 필요하고, 따라서 이전처럼 .build()를 해야 합니다.)
포맷은 스왑체인과 동일해야 합니다. 그런 다음 시작할 때 무엇을 할지 결정해야 합니다. 이전 내용을 유지할까요? 지울까요? (아니면 어차피 사용하지 않거나 모든 픽셀에 덮어쓸 것이므로 상관없을까요?) 우리는 색상을 지웁니다(clear). 렌더 패스가 끝날 때 어떤 일이 일어날지에 대해서도 비슷한 결정을 내려야 합니다. 내용을 버릴까요? 여기서는 화면에 보여주고 싶으므로 유지(store)하기를 원합니다. 그리고 패스의 시작과 끝에서 데이터가 메모리에 구성되는 방식(초기 및 최종 레이아웃)을 결정해야 합니다. 시작 시점에는 알 수 없으므로 UNDEFINED로 하고, 끝날 때는 스왑체인에 표시(present)할 수 있도록 PRESENT_SRC_KHR로 설정해야 합니다. 마지막 옵션은 픽셀당 샘플 수입니다. (앤티앨리어싱/스무딩 효과를 위해 하나의 픽셀이 충분히 작지 않다면, 픽셀의 더 작은 부분에 대해 다른 색상을 갖는 척할 수 있습니다.) 아니요, 지금은 괜찮습니다. 렌더링 중에 우리는 이 어태치먼트를 어태치먼트 번호 0으로 참조할 것입니다 (앞서 본 셰이더 참조):
let color_attachment_references = [vk::AttachmentReference {
attachment: 0,
layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
}];
(또한 패스 도중에 데이터를 메모리에 어떻게 배치할지(어떤 목적에 최적화되었는지)도 알려줍니다.) 이것으로 서브패스(설명)를 얻기에 충분한 정보가 되며, 참고로 이것은 컴퓨트 파이프라인이 아닌 그래픽스 파이프라인을 위한 것입니다:
let subpasses = [vk::SubpassDescription::builder()
.color_attachments(&color_attachment_references)
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS).build()];
여러 서브패스가 있을 수 있습니다. 그 경우, Vulkan은 (명령의 실행 순서를) 최적화하고 싶어하며, 이 서브패스들이 서로 어떻게 의존하는지 알고 싶어합니다: 서브패스 B가 (어떤 부분의) 작업을 하기 위해 서브패스 A의 어떤 부분이 완료되어야 하는가? 지금은 서브패스가 하나뿐이지만, 여전히 의존성이 있습니다: 그림을 그리기 시작하기 전에, 그릴 대상이 있어야 합니다. 이것을 다음과 같이 포함할 수 있습니다: 준비 작업(“SUBPASS_EXTERNAL”, 실제 서브패스는 아님)이 “COLOR_ATTACHMENT_OUTPUT“에 대한 작업을 마치면(어태치먼트를 준비하고 올바른 레이아웃으로 변환하면), 우리 서브패스(우리가 준비 중인 배열의 인덱스 0)는 색상 어태치먼트에 대한 자체 출력을 시작할 수 있으며, 오직 그때만 색상 어태치먼트에서 읽거나 쓸 수 있습니다:
let subpass_dependencies = [vk::SubpassDependency::builder()
.src_subpass(vk::SUBPASS_EXTERNAL)
.src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.dst_subpass(0)
.dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.dst_access_mask(
vk::AccessFlags::COLOR_ATTACHMENT_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
)
.build()];
그리고 여기서 더 설명할 것은 없습니다:
let renderpass_info = vk::RenderPassCreateInfo::builder()
.attachments(&attachments)
.subpasses(&subpasses)
.dependencies(&subpass_dependencies);
let renderpass = unsafe { logical_device.create_render_pass(&renderpass_info, None)? };
logical_device.destroy_render_pass(renderpass, None);
물론, 이 모든 것을 자체 함수로 옮기고 Aetna에 또 다른 필드를 추가해야 합니다.
fn init_renderpass(
logical_device: &ash::Device,
physical_device: vk::PhysicalDevice,
surfaces: &SurfaceDongXi,
) -> Result<vk::RenderPass, vk::Result> {
let attachments = [vk::AttachmentDescription::builder()
.format(
surfaces
.get_formats(physical_device)?
.first()
.unwrap()
.format,
)
.load_op(vk::AttachmentLoadOp::CLEAR)
.store_op(vk::AttachmentStoreOp::STORE)
.stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
.stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.final_layout(vk::ImageLayout::PRESENT_SRC_KHR)
.samples(vk::SampleCountFlags::TYPE_1)
.build()];
let color_attachment_references = [vk::AttachmentReference {
attachment: 0,
layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
}];
let subpasses = [vk::SubpassDescription::builder()
.color_attachments(&color_attachment_references)
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
.build()];
let subpass_dependencies = [vk::SubpassDependency::builder()
.src_subpass(vk::SUBPASS_EXTERNAL)
.src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.dst_subpass(0)
.dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.dst_access_mask(
vk::AccessFlags::COLOR_ATTACHMENT_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
)
.build()];
let renderpass_info = vk::RenderPassCreateInfo::builder()
.attachments(&attachments)
.subpasses(&subpasses)
.dependencies(&subpass_dependencies);
let renderpass = unsafe { logical_device.create_render_pass(&renderpass_info, None)? };
Ok(renderpass)
}
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,
}
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 swapchain = SwapchainDongXi::init(
&instance,
physical_device,
&logical_device,
&surfaces,
&queue_families,
&queues,
)?;
let renderpass = init_renderpass(&logical_device, physical_device, &surfaces)?;
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,
})
}
}
impl Drop for Aetna {
fn drop(&mut self) {
unsafe {
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)
};
}
}
렌더패스에 대한 이전 단락들에서 우리는 스왑체인의 “이미지”였던 “어태치먼트”를 다루었습니다 (그리고 제 단어들은 설명적인 것과 기술적으로 적절한 것 사이를 오갔습니다). 우리는 스왑체인으로부터 이미지 뷰(ImageView)들을 가지고 있었습니다. 우리는 어떻게든 그것들을 렌더패스의 “어태치먼트”와 연결해야 합니다. 프레임버퍼(Framebuffer)가 이 둘을 함께 가져오는 개념입니다. 우리는 SwapchainDongXi 구조체의 일부로 몇 개를 생성합니다:
fn create_framebuffers(
&mut self,
logical_device: &ash::Device,
renderpass: vk::RenderPass,
) -> Result<(), vk::Result> {
for iv in &self.imageviews {
let iview = [*iv];
let framebuffer_info = vk::FramebufferCreateInfo::builder()
.render_pass(renderpass)
.attachments(&iview)
.width(self.extent.width)
.height(self.extent.height)
.layers(1);
let fb = unsafe { logical_device.create_framebuffer(&framebuffer_info, None) }?;
self.framebuffers.push(fb);
}
Ok(())
}
주어진 정보는 렌더패스, 이미지 뷰, 그리고 스왑체인의 크기입니다 (이왕 하는 김에 구조체에 저장해 둡니다). 영향을 받는 모든 코드의 복사본(그리고 약간 더)입니다:
struct SwapchainDongXi {
swapchain_loader: ash::extensions::khr::Swapchain,
swapchain: vk::SwapchainKHR,
images: Vec<vk::Image>,
imageviews: Vec<vk::ImageView>,
framebuffers: Vec<vk::Framebuffer>,
surface_format: vk::SurfaceFormatKHR,
extent: vk::Extent2D,
}
impl SwapchainDongXi {
fn init(
instance: &ash::Instance,
physical_device: vk::PhysicalDevice,
logical_device: &ash::Device,
surfaces: &SurfaceDongXi,
queue_families: &QueueFamilies,
queues: &Queues,
) -> Result<SwapchainDongXi, vk::Result> {
let surface_capabilities = surfaces.get_capabilities(physical_device)?;
let extent = surface_capabilities.current_extent;
let surface_present_modes = surfaces.get_present_modes(physical_device)?;
let surface_format = *surfaces.get_formats(physical_device)?.first().unwrap();
let queuefamilies = [queue_families.graphics_q_index.unwrap()];
let swapchain_create_info = vk::SwapchainCreateInfoKHR::builder()
.surface(surfaces.surface)
.min_image_count(
3.max(surface_capabilities.min_image_count)
.min(surface_capabilities.max_image_count),
)
.image_format(surface_format.format)
.image_color_space(surface_format.color_space)
.image_extent(extent)
.image_array_layers(1)
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.queue_family_indices(&queuefamilies)
.pre_transform(surface_capabilities.current_transform)
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
.present_mode(vk::PresentModeKHR::FIFO);
let swapchain_loader = ash::extensions::khr::Swapchain::new(instance, logical_device);
let swapchain = unsafe { swapchain_loader.create_swapchain(&swapchain_create_info, None)? };
let swapchain_images = unsafe { swapchain_loader.get_swapchain_images(swapchain)? };
let mut swapchain_imageviews = Vec::with_capacity(swapchain_images.len());
for image in &swapchain_images {
let subresource_range = vk::ImageSubresourceRange::builder()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.base_mip_level(0)
.level_count(1)
.base_array_layer(0)
.layer_count(1);
let imageview_create_info = vk::ImageViewCreateInfo::builder()
.image(*image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::B8G8R8A8_UNORM)
.subresource_range(*subresource_range);
let imageview =
unsafe { logical_device.create_image_view(&imageview_create_info, None) }?;
swapchain_imageviews.push(imageview);
}
Ok(SwapchainDongXi {
swapchain_loader,
swapchain,
images: swapchain_images,
imageviews: swapchain_imageviews,
framebuffers: vec![],
surface_format,
extent,
})
}
fn create_framebuffers(
&mut self,
logical_device: &ash::Device,
renderpass: vk::RenderPass,
) -> Result<(), vk::Result> {
for iv in &self.imageviews {
let iview = [*iv];
let framebuffer_info = vk::FramebufferCreateInfo::builder()
.render_pass(renderpass)
.attachments(&iview)
.width(self.extent.width)
.height(self.extent.height)
.layers(1);
let fb = unsafe { logical_device.create_framebuffer(&framebuffer_info, None) }?;
self.framebuffers.push(fb);
}
Ok(())
}
unsafe fn cleanup(&mut self, logical_device: &ash::Device) {
for fb in &self.framebuffers {
logical_device.destroy_framebuffer(*fb, None);
}
for iv in &self.imageviews {
logical_device.destroy_image_view(*iv, None);
}
self.swapchain_loader
.destroy_swapchain(self.swapchain, None)
}
}
fn init_renderpass(
logical_device: &ash::Device,
physical_device: vk::PhysicalDevice,
format: vk::Format,
) -> Result<vk::RenderPass, vk::Result> {
let attachments = [vk::AttachmentDescription::builder()
.format(format)
.load_op(vk::AttachmentLoadOp::CLEAR)
.store_op(vk::AttachmentStoreOp::STORE)
.stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
.stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.final_layout(vk::ImageLayout::PRESENT_SRC_KHR)
.samples(vk::SampleCountFlags::TYPE_1)
.build()];
let color_attachment_references = [vk::AttachmentReference {
attachment: 0,
layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
}];
let subpasses = [vk::SubpassDescription::builder()
.color_attachments(&color_attachment_references)
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
.build()];
let subpass_dependencies = [vk::SubpassDependency::builder()
.src_subpass(vk::SUBPASS_EXTERNAL)
.src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.dst_subpass(0)
.dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.dst_access_mask(
vk::AccessFlags::COLOR_ATTACHMENT_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
)
.build()];
let renderpass_info = vk::RenderPassCreateInfo::builder()
.attachments(&attachments)
.subpasses(&subpasses)
.dependencies(&subpass_dependencies);
let renderpass = unsafe { logical_device.create_render_pass(&renderpass_info, None)? };
Ok(renderpass)
}
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,
}
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)?;
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,
})
}
}
좋습니다. 이제 파이프라인을 만들 시간입니다. 우리는 셰이더를 glsl 코드로 별도의 파일에 저장했습니다. Vulkan은 SPIRV를 기대하므로, 우리는 (glslc를 사용하여) 이 파일들을 .spv 파일로 컴파일하고 그것들을 로드할 수 있습니다. 또는 이 과정을 자동으로 처리하고 GLSL 코드가 담긴 파일을 직접 포함시킬 수도 있습니다. 저는 후자가 더 편리하다고 생각합니다. 이를 위해 다른 크레이트를 사용합니다:
vk-shader-macros = "0.2.2"
이것이 Cargo.toml에 추가할 새 라인입니다. 그리고 다음과 같이 버텍스 셰이더 모듈을 생성합니다:
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)? };
include_glsl! 매크로에서 “kind: vert” 정보는 중복됩니다. 다른 파일 확장자를 사용해야 할 경우에만 필요합니다. 프래그먼트 셰이더의 경우:
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)? };
셰이더에서 어떤 함수를 진입점(entry point)으로 사용할지 결정할 수 있습니다(우리는 두 경우 모두 “main”이라고 불렀고, 현재로서는 다른 것을 사용할 이유가 없습니다). 그런 다음 셰이더 모듈을 셰이더 스테이지(shader stage)로 변환하는데, 이것이 파이프라인에 포함될 것들입니다:
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()];
하지만 Vulkan이 파이프라인에 대해 알아야 할 모든 것을 알기까지는 결정해야 할 것이 훨씬 더 많습니다. 첫 번째는, 우리 애플리케이션에서 셰이더로, 더 정확히는 버텍스 셰이더로 어떤 종류의 데이터를 전달할 것인가 하는 것입니다.
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder();
아무것도 없습니다. 필요한 모든 것은 셰이더 자체에서 왔습니다. 따라서 (지금 이 특이한 상황에서는) ::builder() 뒤에 오는 메서드가 없습니다. 단일 점을 전달할 건가요, 아니면 삼각형을… ? 글쎄요, 지금은 점입니다. (만약 삼각형을 선택했다면, 무언가를 얻기 위해 버텍스 셰이더를 세 번 호출해야 했을 것이고, 매번 같은 점을 출력하도록 설정된 상황에서는 낭비처럼 보입니다…)
let input_assembly_info = vk::PipelineInputAssemblyStateCreateInfo::builder()
.topology(vk::PrimitiveTopology::POINT_LIST);
화면의 어떤 부분을 Vulkan의 내부 좌표(뷰포트)에 대응시키고 싶은가요? 그리고 특정 영역(시저) 밖의 그리기를 영구적으로 비활성화할 수 있나요?
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);
점들(버텍스 셰이더의 출력)을 어떤 프래그먼트가 칠해질지 결정하는 것으로 어떻게 변환할까요? 점의 경우 이것이 그다지 흥미롭지 않을 수 있으니, 잠시 삼각형이 있다고 가정해 봅시다. 채워진 삼각형으로 그려야 할까요? 아니면 선으로만(와이어프레임처럼)? 아니면 점으로만? (.polygon_mode(...))
선 이야기가 나와서 말인데, 선의 굵기는 얼마일까요?
특정 삼각형을 무시(“컬링”)해야 할까요? - 지금은 아니오를 선택하겠습니다. 하지만 뒷면만 보이는 모든 삼각형을 무시하는 것은 좋은 생각일 수 있습니다(그리고 약간의 작업을 절약할 수 있습니다). 만약 그렇게 하려면, 앞면과 뒷면이 무엇인지 설명해야 합니다. 예를 들어: 카메라 시점에서 A, B, C가 반시계 방향 순서일 때 삼각형 ABC를 볼 수 있습니다. (여기서 잘못된 옵션을 선택하면 “모든 것이 작동해야 하는데 아무것도 보이지 않는” 고전적인 그래픽스 오류를 쉽게 만들어낼 수 있습니다.) 여기 있습니다:
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);
우리는 또한 투명도를 어떻게 처리할지 선택할 수 있습니다. 즉, 색상을 설명하는 네 번째 값인 알파 값을 어떻게 다룰지입니다. 만약 우리 “화가”가 알파=1인 색상을 어딘가에 놓으려고 한다면, 그는 이미 거기에 있는 색상을 그냥 교체할 것입니다. 만약 알파=1/3이라면, 칠할 색상(소스 색상)의 1/3과 이미 있는 색상(대상 색상)의 2/3의 볼록 조합을 사용하는 것이 합리적으로 보입니다. 즉, αsrc+(1-α)dst (색상 및 알파 구성 요소에 대해)이며, 어떤 색상에 영향을 미칠지 결정해야 합니다:
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);
버텍스에 붙어있지 않은 다른 데이터를 파이프라인에 전달하고 싶을 수 있습니다. 만약 그렇다면, 이것들은 파이프라인 레이아웃(PipelineLayout)에 들어갑니다. 우리는 분명히 이것으로 돌아올 것이지만, 지금은 아무것도 없습니다:
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];
이번에는 “create” 명령어가 한 번에 여러 파이프라인을 생성할 것을 기대합니다(PipelineCreateInfo의 배열이 동일한 양의 파이프라인으로 변환됩니다). 프로그램에는 종종 많은 파이프라인이 있을 것입니다(작은 것 하나를 바꾸는 것은 불가능합니다; 모든 변경은 새로운 파이프라인을 생성하는 것을 의미합니다(참고: 완전히 사실은 아니지만, 현실에 충분히 가깝습니다)). 그리고 하나를 설정하는 것은 비교적 비싼 작업입니다. 따라서: 시작 시(또는 로딩 화면 동안) 많은 파이프라인을 생성하는 것이 할 일인 것 같습니다. 우리는 심지어 이전에 생성된 파이프라인(애플리케이션의 이전 실행 중에도)으로 파이프라인 캐시를 제공할 수도 있지만, 이것은 우리가 하지 않는 또 다른 것입니다: 지금은 파이프라인 하나만.
그리고 물론, 다시 정리해야 할 몇 가지 작업이 있습니다:
unsafe {
logical_device.destroy_shader_module(fragmentshader_module, None);
logical_device.destroy_shader_module(vertexshader_module, None);
}
unsafe {
logical_device.destroy_pipeline(graphicspipeline, None);
logical_device.destroy_pipeline_layout(pipelinelayout, None);
}
파이프라인 생성을 마치는 즉시 셰이더 모듈을 파괴할 수 있습니다. 파이프라인과 파이프라인 레이아웃은 프로그램이 끝날 때까지 유지합니다. 그 김에, 이것들을 별도의 구조체에 넣어봅시다.
struct Pipeline {
pipeline: vk::Pipeline,
layout: vk::PipelineLayout,
}
impl Pipeline {
fn cleanup(&self, logical_device: &ash::Device) {
unsafe {
logical_device.destroy_pipeline(self.pipeline, None);
logical_device.destroy_pipeline_layout(self.layout, None);
}
}
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,
})
}
}
그리고 당연히, 이 파이프라인을 우리 Aetna 구조체에 또 다른 필드로 추가할 것입니다.
let pipeline = Pipeline::init(&logical_device, &swapchain, &renderpass)?;
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,
})
그리고 drop 함수를 수정합니다:
impl Drop for Aetna {
fn drop(&mut self) {
unsafe {
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)
};
}
}