Missing vkCmdBeginRendering and vkCmdEndRendering on MacOS

APIs for dynamic rendering in Vulkan.

Recently, I was refactoring some Vulkan rendering code to adopt dynamic rendering, which has been promoted to core Vulkan by the official tutorial (https://docs.vulkan.org/tutorial/latest/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.html).

I successfully updated most of the rendering pipeline and resolved all validation errors reported by the Vulkan validation layers. However, the application consistently crashed with a segmentation fault at runtime. After spending a significant amount of time debugging, I traced the issue back to the following code:

1
2
3
4
vkCmdBeginRendering(command_buffer.buffer(), &rendering_info);
// Some rendering work
// ...
vkCmdEndRendering(command_buffer.buffer());

The program compiled without any issues because the Vulkan loader was able to resolve the symbols vkCmdBeginRendering and vkCmdEndRendering at link time. However, these calls were invalid at runtime. The logical device could not dispatch the commands to the driver, which ultimately caused the crash.

This behavior occurs because macOS does not provide a native Vulkan loader or driver. Instead, all Vulkan calls are routed through MoltenVK and a non-native loader. Unlike native Vulkan implementations on Windows and Linux, MoltenVK strictly enforces Vulkan’s runtime dispatch rules and does not export many core dispatcher functions by default.

As a result, the correct solution is to explicitly load the dynamic rendering function pointers using vkGetDeviceProcAddr:

1
2
3
4
5
vkCmdBeginRendering = reinterpret_cast<PFN_vkCmdBeginRendering>(
vkGetDeviceProcAddr(device, "vkCmdBeginRenderingKHR"));

vkCmdEndRendering =
reinterpret_cast<PFN_vkCmdEndRendering>(vkGetDeviceProcAddr(device, "vkCmdEndRenderingKHR"));

I initially attempted to load the non-KHR entry points instead, but this still resulted in a segmentation fault:

1
2
3
4
5
vkCmdBeginRendering = reinterpret_cast<PFN_vkCmdBeginRendering>(
vkGetDeviceProcAddr(device, "vkCmdBeginRendering"));

vkCmdEndRendering =
reinterpret_cast<PFN_vkCmdEndRendering>(vkGetDeviceProcAddr(device, "vkCmdEndRendering"));

Based on this behavior, it appears that on macOS (MoltenVK), the KHR variants should be preferred, with a fallback to the core entry points only if necessary. A robust loading strategy therefore looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
VulkanRenderingCommands LoadDynamicRenderingCommands(VkDevice device) {
VulkanRenderingCommands cmds{};

cmds.vkCmdBeginRendering = reinterpret_cast<PFN_vkCmdBeginRendering>(
vkGetDeviceProcAddr(device, "vkCmdBeginRenderingKHR"));
if (!cmds.vkCmdBeginRendering) {
cmds.vkCmdBeginRendering = reinterpret_cast<PFN_vkCmdBeginRendering>(
vkGetDeviceProcAddr(device, "vkCmdBeginRendering"));
}

cmds.vkCmdEndRendering =
reinterpret_cast<PFN_vkCmdEndRendering>(vkGetDeviceProcAddr(device, "vkCmdEndRenderingKHR"));
if (!cmds.vkCmdEndRendering) {
cmds.vkCmdEndRendering =
reinterpret_cast<PFN_vkCmdEndRendering>(vkGetDeviceProcAddr(device, "vkCmdEndRendering"));
}

return cmds;
}

This approach ensures correct behavior across platforms, while remaining compliant with Vulkan’s runtime dispatch model—particularly on macOS, where direct calls to promoted core commands may not be safely callable.

Author

Joe Chu

Posted on

2025-12-30

Updated on

2025-12-30

Licensed under

Comments