그래픽 카드를 찾는 등의 작업을 처리하기 전에, 잠시 다른 길로 돌아가 보겠습니다. 유효성 검사 레이어에 대해 살펴봅시다. Vulkan에서 무언가 잘못되면, 애플리케이션은 그냥 충돌해버릴 수 있습니다. 또는, 다른 종류나 심각도의 “잘못됨”의 경우, 모든 것이 올바르게 보여야 하는데도 검은 화면만 보일 수도 있습니다. “충돌하거나 의도대로 작동하지 않음 -> 일단 아무거나 하나 바꿔보자 -> 더 이상 충돌하지 않고 제대로 보이길 바라자” 형태의 디버깅은 아마도 가능한 가장 비생산적인 디버깅 방식일 겁니다. 그래픽 카드에서 무슨 일이 일어나고 있는지 정보를 얻을 수만 있다면 좋을 텐데요. Vulkan 드라이버 자체는 — 설계상 — 오류 검사를 거의 하지 않습니다. 드라이버의 작업이 적을수록 애플리케이션은 더 빨라집니다. 그리고 애플리케이션(즉, 프로그래머)이 완전한 제어권을 가지므로 완전한 책임을 져야 합니다. 그게 전부입니다. 정말 이게 전부일까요? 다행히도, 우리는 Vulkan 드라이버에만 국한되지 않습니다. 그 위에 추가적인 기능을 가진 “레이어”를 삽입할 수 있습니다. 여기서 우리가 사용하려는 레이어는 유효성 검사 레이어입니다. 이 레이어들은 충돌한 프로그램이나 검은 화면이라는 ‘오류 메시지’를 “다음 명령을 잘못 사용했습니다: …“와 같은 훨씬 더 도움이 되는 형태의 메시지로 바꿔줍니다. 따라서 우리의 다음 목표는 프로그램에서 유효성 검사 레이어를 켜는 것입니다.
유효성 검사 레이어는 메시지를 전달할 장소가 필요합니다. 즉, 콜백 함수를 제공해야 합니다. 이 콜백 함수는 외부 C 코드에서 호출될 수 있어야 하고 특정 인자를 가져야 하므로, extern 키워드와 다루기 까다로운 변수 타입들(예를 들어 void 포인터 같은, 외부 함수 인터페이스의 즐거움이죠)이 필요합니다:
unsafe extern "system" fn vulkan_debug_utils_callback(
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
message_type: vk::DebugUtilsMessageTypeFlagsEXT,
p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT,
_p_user_data: *mut std::ffi::c_void,
) -> vk::Bool32 {
let message = std::ffi::CStr::from_ptr((*p_callback_data).p_message);
let severity = format!("{:?}", message_severity).to_lowercase();
let ty = format!("{:?}", message_type).to_lowercase();
println!("[Debug][{}][{}] {:?}", severity, ty, message);
vk::FALSE
}
그리고 눈치채셨겠지만, ash를 자주 입력하는 것을 피하기 위해 “use ash::vk“를 포함할 것입니다. 함수의 첫 두 인자는 본질적으로 디버그 메시지의 종류에 대한 정보를 담고 있는 비트 마스크입니다. Ash는 해당 fmt::Debug 구현에 사람이 읽을 수 있는 해석을 포함하고 있습니다 (저는 터미널 메시지를 더 읽기 쉽게 만들려고 소문자로 바꾸고 있을 뿐입니다). 세 번째 인자는 원시 포인터(실제 메시지를 포함하는 무언가를 가리키는)입니다.
사실, “*const T“는 “&T“와 비슷해 보이며, 가능하면 *const T가 필요한 곳에 &T를 사용하게 될 것입니다 (*const T 타입이어야 할 값에 할당할 때). 물론, “*const T“에는 몇 가지 보장이 빠져 있지만, C로 바로 변환되기 때문에 이러한 함수 인터페이스에서 자리를 차지하고 있습니다. 마지막 인자는 지정되지 않은 타입의 포인터(결국 void가 의미하는 바입니다)이며 우리는 사용하지 않습니다.
반환 값은 “드라이버 호출을 건너뛰어야 하는가?”라는 질문에 답합니다.
이제 우리가 호출할 수 있는 함수를 준비했지만, 중요한 일이 발생할 때마다 이 함수를 호출하는 “디버그 메신저”를 만들어야 합니다. 그리고 이를 위해서는 실제로 유효성 검사 레이어를 활성화해야 하는데, 이는 인스턴스 생성 시점에 일어나야 합니다. 이전에 InstanceCreateInfo 구조체를 자세히 살펴보겠다고 약속했던가요? 바로 지금이 그럴 때입니다.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let entry = ash::Entry::new()?;
let instance_create_info = vk::InstanceCreateInfo {
..Default::default()
};
dbg!(&instance_create_info);
let instance = unsafe { entry.create_instance(&instance_create_info, None)? };
unsafe { instance.destroy_instance(None) };
Ok(())
}
여기서는 create_instance 호출에서 InstanceCreateInfo를 추출했지만, 여전히 기본 내용으로 채워두었습니다. dbg!를 사용해 그 내용을 검사합니다. ash::vk::InstanceCreateInfo 문서를 보거나 Vulkan 명세서를 읽을 수도 있습니다. (항목들이 무슨 용도인지 알고 싶다면 어쨌든 명세서를 봐야 합니다. 그리고 일반적으로 명세서는 실제로 읽어볼 가치가 있습니다.) 무엇이 있을까요?
[src/main.rs:10] &instance_create_info = InstanceCreateInfo {
s_type: INSTANCE_CREATE_INFO,
p_next: 0x0000000000000000,
flags: ,
p_application_info: 0x0000000000000000,
enabled_layer_count: 0,
pp_enabled_layer_names: 0x0000000000000000,
enabled_extension_count: 0,
pp_enabled_extension_names: 0x0000000000000000,
}
s_type: 모든 구조체가 가진 필드입니다. 타입을 인코딩합니다. Rust에서는 중복됩니다. 우리는 타입을 알고, 컴파일러도 알고 있습니다.
p_next: 모든 구조체가 가진 또 다른 항목입니다. 명세서를 작성한 똑똑한 사람들 덕분입니다. 아이디어는 이렇습니다: 나중에 이 구조체를 확장해야 할 수도 있습니다. 지금 생각하지 못했거나, 핵심 명세서보다는 일부 확장 레이어의 일부가 되어야 하는 더 특이한 사용 사례에서만 필요하기 때문입니다. 여기에 그런 확장 구조체를 가리키는 포인터를 위한 자리가 있습니다. 압도적인 대다수의 경우 NULL일 것입니다. (그리고, 이름에 있는 “p_“는 이미 이것이 포인터임을 알려줍니다…)
flags: 또 다른 흔한 필드: 설정할 수 있는 몇 가지 옵션입니다. 각 구조체마다 별도의 타입이 있습니다. InstanceCreateInfo의 플래그는 vk::InstanceCreateFlags 타입입니다. (그리고 다른 구조체들의 플래그 이름이 어떻게 될지 추측하거나 명세서에서 찾아볼 수 있습니다.) 아주 많은 구조체에서 명세서의 설명은 “flags는 향후 사용을 위해 예약됨”이라고 되어 있습니다.
이것들은 (거의 항상) “..Default::default()“로 채울 수 있는 지루한 필드들이었습니다.
p_application_info에는 프로그램 이름, API 버전 등에 대한 정보가 담긴 추가 구조체를 삽입할 수 있습니다.
pp_enabled_layer_names와 pp_enabled_extension_names가 지금 우리가 관심 있는 항목들입니다: 여기에 우리가 켜고 싶은 것들을 가리키는 포인터의 Vec을 제공합니다. 이들은 이 Vec의 길이인 숫자와 함께 제공됩니다.
먼저 ApplicationInfo를 제공해 봅시다:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let entry = ash::Entry::new()?;
let enginename = std::ffi::CString::new("UnknownGameEngine").unwrap();
let appname = std::ffi::CString::new("The Black Window").unwrap();
let app_info = vk::ApplicationInfo {
p_application_name: appname.as_ptr(),
p_engine_name: enginename.as_ptr(),
engine_version: vk::make_version(0, 42, 0),
application_version: vk::make_version(0, 0, 1),
api_version: vk::make_version(1, 0, 106),
..Default::default()
};
let instance_create_info = vk::InstanceCreateInfo {
p_application_info: &app_info,
..Default::default()
};
dbg!(&instance_create_info);
let instance = unsafe { entry.create_instance(&instance_create_info, None)? };
unsafe { instance.destroy_instance(None) };
Ok(())
}
이름들은 CString으로 제공되는데, 이는 예상하셨겠지만, C에서의 String에 해당합니다. Vec이나 Vec과 유사한 구조체들(String, CString)의 경우, ApplicationInfo 구조체에 전달하는 올바른 방법은 .as_ptr()를 사용하는 것입니다.
보아하니 우리는 애플리케이션을 만들기 위해 UnknownGameEngine 0.42.0 버전을 사용하고 있네요. 버전 번호에 너무 많은 공간을 쓰고 싶지 않기 때문에, 이 0, 42, 0은 vk::make_version에 의해 단일 u32로 결합됩니다.
이제 유효성 검사 레이어(이름: “VK_LAYER_KHRONOS_validation“)를 로드하고 DebugUtils 확장을 활성화할 시간입니다:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let entry = ash::Entry::new()?;
let enginename = std::ffi::CString::new("UnknownGameEngine").unwrap();
let appname = std::ffi::CString::new("The Black Window").unwrap();
let app_info = vk::ApplicationInfo {
p_application_name: appname.as_ptr(),
p_engine_name: enginename.as_ptr(),
engine_version: vk::make_version(0, 42, 0),
application_version: vk::make_version(0, 0, 1),
api_version: vk::make_version(1, 0, 106),
..Default::default()
};
let layer_names: Vec<std::ffi::CString> =
vec![std::ffi::CString::new("VK_LAYER_KHRONOS_validation").unwrap()];
let layer_name_pointers: Vec<*const i8> = layer_names
.iter()
.map(|layer_name| layer_name.as_ptr())
.collect();
let extension_name_pointers: Vec<*const i8> =
vec![ash::extensions::ext::DebugUtils::name().as_ptr()];
let instance_create_info = vk::InstanceCreateInfo {
p_application_info: &app_info,
pp_enabled_layer_names: layer_name_pointers.as_ptr(),
enabled_layer_count: layer_name_pointers.len() as u32,
pp_enabled_extension_names: extension_name_pointers.as_ptr(),
enabled_extension_count: extension_name_pointers.len() as u32,
..Default::default()
};
let instance = unsafe { entry.create_instance(&instance_create_info, None)? };
unsafe { instance.destroy_instance(None) };
Ok(())
}
활성화하려는 레이어의 이름을 지정했고, 이를 담고 있는 전체 벡터를 포인터 벡터로 변환하여 InstanceCreateInfo 구조체에 포함시켰습니다. 확장 기능에 대해서도 마찬가지입니다. 거기서는 ash가 이름을 제공합니다.
새로운 기능을 사용하려면, 우리가 이전에 작성한 콜백 함수로 실제로 메시지를 전달하는 디버그 메신저가 필요합니다. 그리고 디버그 메신저를 생성하려면, 첫 단계에서 Vulkan 드라이버에 대해 만들었던 “entry”와 비슷한 것이 필요합니다, 단지 DebugUtils를 위한 것입니다. 인스턴스 생성 후에 다음을 포함합니다:
let debug_utils = ash::extensions::ext::DebugUtils::new(&entry, &instance);
let debugcreateinfo = vk::DebugUtilsMessengerCreateInfoEXT {
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
| vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE
| vk::DebugUtilsMessageSeverityFlagsEXT::INFO
| vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
message_type: vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION,
pfn_user_callback: Some(vulkan_debug_utils_callback),
..Default::default()
};
let utils_messenger =
unsafe { debug_utils.create_debug_utils_messenger(&debugcreateinfo, None)? };
예고했던 대로 메신저 생성입니다. 마지막 “None”은 다시 한번 우리가 무시하는 할당 관련 부분이고, 우리가 무시하지 않는 것에 대한 설정은 어떤 CreateInfo 구조체 안에서 이루어집니다. (이름 끝의 “EXT”는 이것이 어떤 확장에 속한다는 것을 보여줍니다.) 우리는 수신하고 싶은 메시지 타입과 메시지 심각도를 지정합니다. 너무 많은 메시지를 받는다면, 예를 들어 여기서 “VERBOSE”를 제거하면 됩니다. 그리고 마지막으로 사용자 콜백 함수를 포함합니다.
실행해 봅시다! 그리고 약간의 출력이 있습니다:
[Debug][verbose][general] "Added messenger"
[Debug][info][validation] "OBJ[0x1] : CREATE DebugUtilsMessengerEXT object 0x1"
[Debug][verbose][general] "Added messenger"
[Debug][verbose][general] "Added messenger"
[Debug][info][validation] "OBJ_STAT Destroy Instance obj 0x558c867229e0 (1 total objs remain & 0 Instance objs)."
[Debug][info][validation] "OBJ_STAT Destroy Instance obj 0x558c867229e0 (1 total objs remain & 0 Instance objs)."
[Debug][verbose][general | validation] "Destroyed messenger\n"
[Debug][verbose][general | validation] "Destroyed messenger\n"
[Debug][error][validation] "Debug messengers not removed before DestroyInstance"
[Debug][error][validation] "Debug messengers not removed before DestroyInstance"
[Debug][error][validation] "Debug messengers not removed before DestroyInstance"
[Debug][error][validation] "Debug messengers not removed before DestroyInstance"
[Debug][info][general] "Unloading layer library libVkLayer_khronos_validation.so"
첫 번째 관찰: 모든 메시지(처음 두 개와 마지막 하나를 제외하고)가 중복됩니다. 왜 그런지는 모르겠지만(버그? 최적화되지 않은 설치?), 이보다 더 나쁜 일도 있을 수 있습니다. 두 번째 관찰: 실제 오류가 보고되고 있습니다. 인스턴스를 파괴하기 전에 디버그 메신저를 제거했어야 합니다. 좋습니다, 마지막 unsafe 블록에 이것을 추가합시다:
debug_utils.destroy_debug_utils_messenger(utils_messenger, None);
다시 실행하면, 오류 메시지가 사라졌을 것입니다. 이제 우리 프로그램은 다음과 같이 보입니다:
use ash::version::EntryV1_0;
use ash::version::InstanceV1_0;
use ash::vk;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let entry = ash::Entry::new()?;
let enginename = std::ffi::CString::new("UnknownGameEngine").unwrap();
let appname = std::ffi::CString::new("The Black Window").unwrap();
let app_info = vk::ApplicationInfo {
p_application_name: appname.as_ptr(),
p_engine_name: enginename.as_ptr(),
engine_version: vk::make_version(0, 42, 0),
application_version: vk::make_version(0, 0, 1),
api_version: vk::make_version(1, 0, 106),
..Default::default()
};
let layer_names: Vec<std::ffi::CString> =
vec![std::ffi::CString::new("VK_LAYER_KHRONOS_validation").unwrap()];
let layer_name_pointers: Vec<*const i8> = layer_names
.iter()
.map(|layer_name| layer_name.as_ptr())
.collect();
let extension_name_pointers: Vec<*const i8> =
vec![ash::extensions::ext::DebugUtils::name().as_ptr()];
let instance_create_info = vk::InstanceCreateInfo {
p_application_info: &app_info,
pp_enabled_layer_names: layer_name_pointers.as_ptr(),
enabled_layer_count: layer_name_pointers.len() as u32,
pp_enabled_extension_names: extension_name_pointers.as_ptr(),
enabled_extension_count: extension_name_pointers.len() as u32,
..Default::default()
};
let instance = unsafe { entry.create_instance(&instance_create_info, None)? };
let debug_utils = ash::extensions::ext::DebugUtils::new(&entry, &instance);
let debugcreateinfo = vk::DebugUtilsMessengerCreateInfoEXT {
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
| vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE
| vk::DebugUtilsMessageSeverityFlagsEXT::INFO
| vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
message_type: vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION,
pfn_user_callback: Some(vulkan_debug_utils_callback),
..Default::default()
};
let utils_messenger =
unsafe { debug_utils.create_debug_utils_messenger(&debugcreateinfo, None)? };
unsafe {
debug_utils.destroy_debug_utils_messenger(utils_messenger, None);
instance.destroy_instance(None)
};
Ok(())
}
unsafe extern "system" fn vulkan_debug_utils_callback(
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
message_type: vk::DebugUtilsMessageTypeFlagsEXT,
p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT,
_p_user_data: *mut std::ffi::c_void,
) -> vk::Bool32 {
let message = std::ffi::CStr::from_ptr((*p_callback_data).p_message);
let severity = format!("{:?}", message_severity).to_lowercase();
let ty = format!("{:?}", message_type).to_lowercase();
println!("[Debug][{}][{}] {:?}", severity, ty, message);
vk::FALSE
}
…Info 구조체를 만드는 데는 항상 성가신 부분이 있습니다: 항상 있는 “..Default::default()”, 그리고 명시적으로 제공해야 하는 Vec의 길이와 같은 중복 정보. 계속하기 전에, 이것을 더 멋지게 만들어 봅시다. 왜냐하면 (이 글을 쓰면서 막 알게 된 사실이지만) 더 멋진 방법이 있기 때문입니다: ash가 포함하고 있는 빌더를 사용할 수 있습니다:
let app_info = vk::ApplicationInfo::builder()
.application_name(&appname)
.application_version(vk::make_version(0, 0, 1))
.engine_name(&enginename)
.engine_version(vk::make_version(0, 42, 0))
.api_version(vk::make_version(1, 0, 106));
let layer_names: Vec<std::ffi::CString> =
vec![std::ffi::CString::new("VK_LAYER_KHRONOS_validation").unwrap()];
let layer_name_pointers: Vec<*const i8> = layer_names
.iter()
.map(|layer_name| layer_name.as_ptr())
.collect();
let extension_name_pointers: Vec<*const i8> =
vec![ash::extensions::ext::DebugUtils::name().as_ptr()];
let instance_create_info = vk::InstanceCreateInfo::builder()
.application_info(&app_info)
.enabled_layer_names(&layer_name_pointers)
.enabled_extension_names(&extension_name_pointers);
let instance = unsafe { entry.create_instance(&instance_create_info, None)? };
let debug_utils = ash::extensions::ext::DebugUtils::new(&entry, &instance);
let debugcreateinfo = vk::DebugUtilsMessengerCreateInfoEXT::builder()
.message_severity(
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
| vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE
| vk::DebugUtilsMessageSeverityFlagsEXT::INFO
| vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
)
.message_type(
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION,
)
.pfn_user_callback(Some(vulkan_debug_utils_callback));
let utils_messenger =
unsafe { debug_utils.create_debug_utils_messenger(&debugcreateinfo, None)? };
모든 CreateInfo{ ...}를 CreateInfo::builder()로 바꾸고, 필요한 모든 정보를 별도의 함수 호출로 설정했습니다. 다시 말해, 모든 “some_field_name: value”가 “.some_field_name(value)”로 바뀌었습니다. (포인터에 대한 주석이 사라진 점에 유의하세요: pp_enabled_layer_names 대신 .enabled_layer_names입니다.) 그리고 마지막 ..Default::default()와 Vec의 중복된 길이를 제거했습니다: enabled_layer_names를 설정하는 함수가 enabled_layer_counts도 처리해 줍니다. 또한 일부 .as_ptr() 호출도 필요 없게 되었습니다. 편리하네요.
여기서 ‘빌더 패턴’에 어느 정도 익숙하다면 주목해야 할 중요한 점이 있습니다: 마지막에 .build()를 호출하지 않는다는 것입니다. 대신, 빌드될 구조체의 참조가 필요한 곳에 빌더의 참조를 직접 전달합니다. 이는 빌더가 올바른 Deref 트레이트를 구현하고 있기 때문에 가능합니다. build()를 호출하면 구조체 내용물의 일부 라이프타임이 꼬이게 됩니다: 많은 원시 포인터가 관련되어 있고 댕글링 포인터는… 피해야 할 대상입니다. Vulkan 관련 작업에서는 .build()를 호출하지 마세요 (자신이 무엇을 하는지 정확히 알거나 더 나은 선택지가 없어 보이지 않는 한…).
디버깅 기능에 대한 작업이 끝났을까요? “예”라고 할 수도 있지만, 조금 더 완전성을 기하기 위해 다음을 관찰해 봅시다: 디버그 메신저는 인스턴스 생성 후에 생성되고 인스턴스 파괴 전에 파괴됩니다. 이는 인스턴스 생성이나 파괴에 문제가 생기면 어떤 정보도 받을 수 없다는 것을 의미합니다. 바로 이 지점에서 InstanceCreateInfo 구조체의 p_next 필드가 빛을 발합니다: 여기에 debugcreateinfo를 포함시킬 수 있습니다 (그러면 DebugUtils 확장이 그것으로 무엇을 해야 할지 알게 됩니다: 인스턴스 생성 등의 문제에 대해 우리에게 알려줄 수 있는 디버그 메신저를 생성하는 것이죠).
그래서:
let instance_create_info = vk::InstanceCreateInfo::builder()
.push_next(&mut debugcreateinfo)
.application_info(&app_info)
.enabled_layer_names(&layer_name_pointers)
.enabled_extension_names(&extension_name_pointers);
(그리고 물론, let debugcreateinfo를 더 앞부분으로 옮기고 “mut“를 삽입해야 합니다). 만약 빌더 버전을 피하고 싶다면(“직접적이고, Vulkan 명세서와의 더 명백한 대응” 느낌을 위해), 다음과 같이 작성할 수 있습니다.
p_next: &debugcreateinfo as *const vk::DebugUtilsMessengerCreateInfoEXT as *const c_void
하지만 저는 .push_next 방식을 더 선호하는 것 같네요…
이제 프로그램을 실행하면, 많은 새로운 메시지가 나타납니다. 좋습니다, 이제 디버그 설정은 끝났습니다.