Vulkan 초기화: Buffer creation
Vulkan은 매우 명시적이고 직접적인 제어를 제공하는 저수준 graphics 및 compute API로, 개발자가 하드웨어 동작의 거의 모든 단계를 직접 지정해야 하는 것이 특징입니다. OpenGL과 달리, 드라이버나 런타임이 자동으로 많은 일을 처리하지 않으며, 개발자는 hardware capability, device feature, 확장 지원 여부 등을 직접 설정하고 각 함수에 전달해야 합니다.
Instance
Vulkan API의 첫 진입점은 Instance
입니다. 애플리케이션이 Vulkan 기능을 사용하려면 가장 먼저 Instance
객체를 생성해야 하며, 이는 Vulkan 시스템 전체의 전역 문맥으로 작동합니다. Instance
를 만들 때, 원하는 경우 검증 레이어를 활성화하거나 필요한 확장(VK_KHR_surface 등)을 등록할 수 있습니다. 또 드라이버 에러 발생 시 사용할 각종 로깅 기능도 Instance
에 연결합니다. 일반적으로 하나의 애플리케이션당 단일 Instance
를 만들어서 사용하고, 이 Instance
가 전체 프로세스 동안 Vulkan 관련 모든 객체의 기반이 됩니다.
다음 코드는 InstanceCreateFlags::ENUMERATE_PORTABILITY
확장이 설정된 Instance
를 생성하는 작업을 수행합니다.
use vulkano::VulkanLibrary;
use vulkano::instance::{Instance, InstanceCreateFlags, InstanceCreateInfo};
let library = VulkanLibrary::new().expect("no local Vulkan library/DLL");
let instance = Instance::new(
library,
InstanceCreateInfo {
flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
..Default::default()
},
)
.expect("failed to create instance");
PhysicalDevice
Vulkan에서는 시스템에 존재하는 GPU와 해당 GPU 기능 목록을 가져올 수 있으며, 이 때 사용되는 객체가 PhysicalDevice
입니다. PhysicalDevice
는 시스템에 존재하는 실제 GPU를 의미하며, 하나의 PC에는 한 개 혹은 여러 개의 GPU가 존재할 수 있습니다. 예를 들어, 게이밍 PC에는 고성능 d-GPU가 단독으로 있거나, 노트북처럼 저전력 i-GPU와 고성능 d-GPU가 동시에 구성될 수도 있습니다. 만약 여러 GPU가 존재한다면, 애플리케이션이 어느 GPU를 사용할지 사용자가 직접 선택할 수 있습니다. 사용 가능한 VRAM 크기, 지원 확장, 고급 기능 지원 여부 등 GPU의 세부 사양 역시 PhysicalDevice
객체를 통해 질의할 수 있습니다. 고급 엔진에서는 이런 정보를 바탕으로 최적의 GPU를 동적으로 선정하기도 합니다.
다음 코드는 사용 가능한 PhysicalDevice
목록 중 가장 첫 번째 장치를 반환합니다.
let physical_device = instance
.enumerate_physical_devices()
.expect("could not enumerate devices")
.next()
.expect("no devices available");
Queue
CPU에서 실행되는 프로그램에서 여러 스레드를 사용할 수 있는 것처럼, GPU에서도 여러 작업을 병렬로 실행할 수 있습니다. Vulkan에서 CPU 스레드에 해당하는 개념은 Queue
이며, PhysicalDevice
는 어떠한 종류의 Queue
가 사용 가능한지에 대한 정보를 QueueFamily
를 통해 제공합니다.
for family in physical_device.queue_family_properties() {
println!("Found a queue family with {:?} queue(s)", family.queue_count);
}
어떠한 장치가 작업을 수행하도록 하려면 해당 작업을 특정 Queue
에 제출해야 합니다. Queue
에 따라 graphics, compute, transfer 등의 작업을 모두 지원하거나 일부를 지원하므로, 이를 잘 고려해야 합니다.
Device
마지막으로 Device
객체는 PhysicalDevice
를 실제로 제어할 수 있도록 만들어주는 논리적 장치입니다. 대부분의 Vulkan 명령은 Device
핸들을 요구하며, 디버깅·초기화와 관련된 소수의 기능만이 Instance
/PhysicalDevice
단계에서 실행됩니다.
Device
를 생성하기 위해서는 해당 장치로 어떤 작업을 수행할 것인지를 나타내는 Queue
가 필요합니다. 다음 코드는 graphics 작업을 지원하는 QueueFamily
를 찾아 index를 반환합니다.
use vulkano::device::QueueFlags;
let queue_family_index = physical_device
.queue_family_properties()
.iter()
.enumerate()
.position(|(_queue_family_index, queue_family_properties)| {
queue_family_properties.queue_flags.contains(QueueFlags::GRAPHICS)
})
.expect("couldn't find a graphical queue family") as u32;
반환된 index를 사용해 Device
를 생성합니다.
use vulkano::device::{Device, DeviceCreateInfo, QueueCreateInfo};
let (device, mut queues) = Device::new(
physical_device,
DeviceCreateInfo {
// here we pass the desired queue family to use by index
queue_create_infos: vec![QueueCreateInfo {
queue_family_index,
..Default::default()
}],
..Default::default()
}는
)
.expect("failed to create device");
Device::new
는 Device
객체와 실제 명령을 제출하게 될 Queue
객체에 대한 iterator를 반환합니다. 위의 코드는 QueueCreateInfo
에 한 가지 종류의 Queue
를 요청했기 때문에 바로 unwrap할 수 있습니다.
let queue = queues.next().unwrap();
#version 460
layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
layout(set = 0, binding = 0) buffer Data {
uint data[];
} buf;
void main() {
uint idx = gl_GlobalInvocationID.x;
buf.data[idx] *= 12;
}