검증 레이어(3장 참조)는 훌륭한 도우미입니다. 하지만 때로는 “프로그램이 깨져서 완전히 충돌하거나 멈춘다”는 의미의 문제가 아니라, “음, 화면에 뭔가 나오긴 하는데, 이건 명백히 틀렸어”와 같은 문제가 발생하기도 합니다. 가끔은 “시각적 디버깅”이 도움이 될 때가 있습니다. 좌표(또는 잘못되었을 수 있는 다른 어떤 것)를 색상으로 변환하고, 화면이 너무 녹색으로 보이는지(또는 어떤 색상 값이 잘못되었음을 나타내는지) 확인하고 그 원인을 추측하는 방법이죠.
하지만 때로는 셰이더 코드 안에 println!("이 값은: {}, 틀렸나요?", value);나 dbg!(coordinate[2]); 같은 코드를 넣고 그로부터 오류의 원인을 추론하고 싶을 때가 있습니다. 바로 이때 Debug Printf 기능이 유용합니다.
시작점으로 다음과 같은 정점 셰이더가 있다고 가정해 봅시다:
#version 450
layout (location = 0) in vec2 inpos;
void main(){
gl_Position=vec4(inpos,0.0,1.0);
}
그리고 다음과 같은 프래그먼트 셰이더가 있습니다:
#version 450
layout (location = 0) out vec4 outcolour;
void main(){
outcolour=vec4(0.2,0.2,0.2,1.0);
}
이 셰이더들은 [0.0, 0.0, 1.0, 1.0, 1.0, 0.0]의 내용을 가진 정점 버퍼와 함께 호출됩니다. 더 나아가, 우리는 정점 셰이더에서 정점 좌표를 출력하고 싶다고 가정해 봅시다.
(셰이더는 의도적으로 단순하게 만들었습니다. 셰이더 자체가 중요한 것이 아니라 필요한 설정이 중요합니다. 또한, 이번에는 전체 코드를 보여드리지는 않겠습니다. 주된 이유는 제가 그동안 렌더러의 상당 부분을 재작성했기 때문입니다(돌이켜보면, 몇몇 구조적인 결정들이 완전히 마음에 들지는 않았습니다. 하지만 제가 작성했던 챕터들은 저에게 유용했습니다). 그러나 재작성된 코드나 그 결과를 문서화할 시간이 부족하다고 느낍니다. 만약 일부 코드가 이전 챕터와 약간만 유사하다면 그것이 이유일 수 있습니다. 반면에, 필요한 단계들은 어렵지 않고, 올바른 위치를 쉽게 찾을 수 있을 것입니다. 그리고 작은 독립적인 챕터가 아무것도 없는 것보다는 나을 겁니다.)
셰이더에 출력 명령어를 포함시켜 봅시다:
debugPrintfEXT("Hello World! %f, %f", inpos[0], inpos[1]);
이 명령어는 C 언어의 printf와 유사하게 작동합니다.
GLSL 명세의 관련 부분에 따르면:
이 함수는 가변 인자를 가지며, 첫 번째 인자는 반드시
리터럴 문자열이어야 합니다. 다른 인자들은 어떤 타입이든 될 수 있으며,
문자열의 형식 지정자 순서에 의해 표시된 타입과 일치해야 합니다.
인자에는 타입 변환이나 오버로드 매칭 규칙이 적용되지 않습니다.
형식 지정자의 해석은 클라이언트 API에 의해 명시됩니다.
형식 지정자 집합은 구현에 따라 다르지만, 최소한 "%d"와 "%i"(int),
"%u"(uint), 그리고 "%f"(float)는 포함해야 합니다.
자, 다시 작업으로 돌아갑시다. 왜냐하면: 아직 작동하지 않거든요.
다음 단계: 셰이더에서 이 GLSL 확장을 활성화해야 합니다:
#extension GL_EXT_debug_printf:enable
전체적으로 우리의 정점 셰이더는 다음과 같이 됩니다.
#version 450
#extension GL_EXT_debug_printf:enable
layout (location = 0) in vec2 inpos;
void main(){
debugPrintfEXT("Hello World! %f, %f", inpos[0], inpos[1]);
gl_Position=vec4(inpos,0.0,1.0);
}
이것으로 셰이더 쪽의 작업은 끝났습니다. 하지만 우리 프로그램은 아직 이것으로 무엇을 해야 할지 모릅니다. Vulkan도 마찬가지인 것 같습니다:
[Debug][error][validation] "Validation Error: [ VUID-VkShaderModuleCreateInfo-pCode-04147 ] Object 0: handle = 0x5594d0188138, type = VK_OBJECT_TYPE_DEVICE; | MessageID =0x3d492883 | vkCreateShaderModule(): The SPIR-V Extension (SPV_KHR_non_semantic_info) was declared, but none of the requirements were met to use it. The Vulkan spec states: If pCode declares any of the SPIR-V extensions listed in the SPIR-V Environment appendix, one of the corresponding requirements must be satisfied (https://vulkan.lunarg.com/doc/view/1.2.170.0~rc2/linux/1.2-extensions/vkspec.html#VUID-VkShaderModuleCreateInfo-pCode-04147)"
우리는 일부 확장이 부족합니다. 그럼, 활성화해 봅시다:
let device_extension_name_pointers: Vec<*const i8> = vec![
ash::extensions::khr::Swapchain::name().as_ptr(),
ash::vk::KhrShaderNonSemanticInfoFn::name().as_ptr(),
];
이 코드는 장치(device)를 생성하는 부분, 즉 다음 호출 이전에 위치합니다.
let logical_device =
unsafe { instance.create_device(physical_device, &device_create_info, None)? };
우리는 이미 Swapchain 확장을 사용하고 있었고, 이제 Khr::ShaderNonSemanticInfo를 추가합니다. 왜 InfoFn이고, 왜 ash::extensions::khr::ShaderNonSemanticInfo가 아니라 ash::vk::...일까요? 제가 보기에는, 이 확장이 ash에 다른 확장들과 같은 방식으로 완전히 통합되지 않은 것 같습니다. (공정하게 말하면, 실제 구현이 빠진 것 같지는 않고, 이 확장은 주로 이름을 제공하는 함수로 구성된 것 같습니다.)
또한 인스턴스를 생성할 때, 올바른 확장을 활성화해야 합니다. 따라서, 다음 코드에서
let mut 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 instance = unsafe {
entry.create_instance(
&vk::InstanceCreateInfo::builder()
.push_next(&mut debugcreateinfo)
.push_next(&mut validation_features)
.enabled_layer_names(&layers)
.enabled_extension_names(&extensions)
.application_info(
&vk::ApplicationInfo::builder()
.api_version(vk::make_api_version(0, 1, 2, 203)),
),
None,
)
}
.expect("Unable to create Vulkan instance.");
다음을 추가합니다.
let mut validation_features = vk::ValidationFeaturesEXT::builder()
.enabled_validation_features(&[vk::ValidationFeatureEnableEXT::DEBUG_PRINTF]);
그리고 이것을 InstanceCreateInfo에 푸시합니다:
let mut validation_features = vk::ValidationFeaturesEXT::builder()
.enabled_validation_features(&[vk::ValidationFeatureEnableEXT::DEBUG_PRINTF]);
let mut 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 instance = unsafe {
entry.create_instance(
&vk::InstanceCreateInfo::builder()
.push_next(&mut debugcreateinfo)
.push_next(&mut validation_features)
.enabled_layer_names(&layers)
.enabled_extension_names(&extensions)
.application_info(
&vk::ApplicationInfo::builder()
.api_version(vk::make_api_version(0, 1, 2, 203)),
),
None,
)
}
.expect("Unable to create Vulkan instance.");
이래도 아직 ‘작동하지 않습니다’. Debug Printf의 메시지는 INFO 수준인데, 우리는 이 수준을 주석 처리해야 하기 때문입니다. 이 수준은 검증 레이어 출력에 약간의 ‘노이즈’를 유발할 수 있으므로, 디버그 콜백 함수를 조정해 봅시다.
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();
if severity == "info" {
let msg = message.to_str().expect("An error occurred in Vulkan debug utils callback. What kind of not-String are you handing me?");
if msg.contains("DEBUG-PRINTF") {
let msg = msg
.to_string()
.replace("Validation Information: [ UNASSIGNED-DEBUG-PRINTF ]", "");
println!("[Debug][printf] {:?}", msg);
}
} else {
println!("[Debug][{}][{}] {:?}", severity, ty, message);
}
vk::FALSE
}
(새로운 부분은 “info” 수준의 경우를 처리하는 것입니다.)
그리고 결과는:
[Debug][printf] " Object 0: handle = 0x5628d3d7e408, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x92394c89 | Hello World! 0.000000, 0.000000"
[Debug][printf] " Object 0: handle = 0x5628d3d7e408, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x92394c89 | Hello World! 1.000000, 1.000000"
[Debug][printf] " Object 0: handle = 0x5628d3d7e408, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x92394c89 | Hello World! 1.000000, 0.000000"
[Debug][printf] " Object 0: handle = 0x5628d3d7e408, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x92394c89 | Hello World! 0.000000, 0.000000"
[Debug][printf] " Object 0: handle = 0x5628d3d7e408, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x92394c89 | Hello World! 1.000000, 1.000000"
[Debug][printf] " Object 0: handle = 0x5628d3d7e408, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x92394c89 | Hello World! 1.000000, 0.000000"
[Debug][printf] " Object 0: handle = 0x5628d3d7e408, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x92394c89 | Hello World! 0.000000, 0.000000"
[Debug][printf] " Object 0: handle = 0x5628d3d7e408, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x92394c89 | Hello World! 1.000000, 1.000000"
[Debug][printf] " Object 0: handle = 0x5628d3d7e408, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x92394c89 | Hello World! 1.000000, 0.000000"
... 등등.
그리는 정점이 3개뿐이라는 게 다행이네요… 순수한 debugPrintfEXT 호출은, 특히 프래그먼트 셰이더에서는, 꽤 구체적인 if 문 뒤에 숨기지 않는 한 금방 지저분해질 수 있다고 생각합니다.
하지만 보세요, 이제 셰이더에서 텍스트를 출력할 수 있고, GPU가 또 다른 방식으로 우리에게 이야기하게 할 수 있습니다.
또한, http://anki3d.org/debugprintf-vulkan/과 https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/master/docs/debug_printf.md에 감사를 표합니다.
[계속] (아마 또 시간이 좀 걸릴 겁니다)