Files
Vulkan-Samples/samples/extensions/open_cl_interop/open_cl_interop.cpp
T
2025-09-04 10:54:47 +08:00

915 lines
39 KiB
C++

/* Copyright (c) 2023-2025, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "open_cl_interop.h"
#include "common/vk_common.h"
#include "gui.h"
#include <sstream>
#ifdef VK_USE_PLATFORM_WIN32_KHR
# include <aclapi.h>
# include <dxgi1_2.h>
#endif
#ifdef _WIN32
// On Windows, we need to enable some security settings to allow api interop
// The spec states: For handles of the following types: VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT The implementation must ensure the access rights allow read and write access to the memory.
// This class sets up the structures required for tis
class WinSecurityAttributes
{
private:
SECURITY_ATTRIBUTES security_attributes;
PSECURITY_DESCRIPTOR security_descriptor;
public:
WinSecurityAttributes();
~WinSecurityAttributes();
SECURITY_ATTRIBUTES *operator&();
};
WinSecurityAttributes::WinSecurityAttributes()
{
security_descriptor = (PSECURITY_DESCRIPTOR) calloc(1, SECURITY_DESCRIPTOR_MIN_LENGTH + 2 * sizeof(void **));
PSID *ppSID = (PSID *) ((PBYTE) security_descriptor + SECURITY_DESCRIPTOR_MIN_LENGTH);
PACL *ppACL = (PACL *) ((PBYTE) ppSID + sizeof(PSID *));
InitializeSecurityDescriptor(security_descriptor, SECURITY_DESCRIPTOR_REVISION);
SID_IDENTIFIER_AUTHORITY sid_identifier_authority = SECURITY_WORLD_SID_AUTHORITY;
AllocateAndInitializeSid(&sid_identifier_authority, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, ppSID);
EXPLICIT_ACCESS explicit_access{};
ZeroMemory(&explicit_access, sizeof(EXPLICIT_ACCESS));
explicit_access.grfAccessPermissions = STANDARD_RIGHTS_ALL | SPECIFIC_RIGHTS_ALL;
explicit_access.grfAccessMode = SET_ACCESS;
explicit_access.grfInheritance = INHERIT_ONLY;
explicit_access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
explicit_access.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
explicit_access.Trustee.ptstrName = (LPTSTR) *ppSID;
SetEntriesInAcl(1, &explicit_access, nullptr, ppACL);
SetSecurityDescriptorDacl(security_descriptor, TRUE, *ppACL, FALSE);
security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES);
security_attributes.lpSecurityDescriptor = security_descriptor;
security_attributes.bInheritHandle = TRUE;
}
SECURITY_ATTRIBUTES *WinSecurityAttributes::operator&()
{
return &security_attributes;
}
WinSecurityAttributes::~WinSecurityAttributes()
{
PSID *ppSID = (PSID *) ((PBYTE) security_descriptor + SECURITY_DESCRIPTOR_MIN_LENGTH);
PACL *ppACL = (PACL *) ((PBYTE) ppSID + sizeof(PSID *));
if (*ppSID)
{
FreeSid(*ppSID);
}
if (*ppACL)
{
LocalFree(*ppACL);
}
free(security_descriptor);
}
#endif
OpenCLInterop::OpenCLInterop()
{
zoom = -3.5f;
title = "Interoperability with OpenCL";
// To use external memory, semaphores and fences, we need to enable several extensions, both on the device as well as the instance
add_device_extension(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME);
add_device_extension(VK_KHR_EXTERNAL_SEMAPHORE_EXTENSION_NAME);
add_device_extension(VK_KHR_EXTERNAL_FENCE_EXTENSION_NAME);
// Some of the extensions are platform dependent
#ifdef _WIN32
add_device_extension(VK_KHR_EXTERNAL_MEMORY_WIN32_EXTENSION_NAME);
add_device_extension(VK_KHR_EXTERNAL_SEMAPHORE_WIN32_EXTENSION_NAME);
add_device_extension(VK_KHR_EXTERNAL_FENCE_WIN32_EXTENSION_NAME);
#else
add_device_extension(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME);
add_device_extension(VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME);
add_device_extension(VK_KHR_EXTERNAL_FENCE_FD_EXTENSION_NAME);
#endif
add_instance_extension(VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME);
add_instance_extension(VK_KHR_EXTERNAL_SEMAPHORE_CAPABILITIES_EXTENSION_NAME);
add_instance_extension(VK_KHR_EXTERNAL_FENCE_CAPABILITIES_EXTENSION_NAME);
}
OpenCLInterop::~OpenCLInterop()
{
if (has_device())
{
vkDestroyPipeline(get_device().get_handle(), pipeline, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layout, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layout, nullptr);
vkDestroyFence(get_device().get_handle(), rendering_finished_fence, nullptr);
vkDestroySemaphore(get_device().get_handle(), cl_update_vk_semaphore, nullptr);
vkDestroySemaphore(get_device().get_handle(), vk_update_cl_semaphore, nullptr);
vkDestroySampler(get_device().get_handle(), shared_image.sampler, nullptr);
vkDestroyImageView(get_device().get_handle(), shared_image.view, nullptr);
vkDestroyImage(get_device().get_handle(), shared_image.image, nullptr);
vkFreeMemory(get_device().get_handle(), shared_image.memory, nullptr);
}
if (opencl_objects.initialized)
{
clReleaseMemObject(opencl_objects.image);
clReleaseContext(opencl_objects.context);
clReleaseSemaphoreKHR(opencl_objects.cl_update_vk_semaphore);
clReleaseSemaphoreKHR(opencl_objects.vk_update_cl_semaphore);
}
unload_opencl();
}
void OpenCLInterop::render(float delta_time)
{
if (!prepared)
{
return;
}
total_time_passed += delta_time;
// Wait until the Vulkan command buffer displaying the image has finished execution, so we can start writing to it from OpenCL
vkWaitForFences(get_device().get_handle(), 1, &rendering_finished_fence, VK_TRUE, std::numeric_limits<uint64_t>::max());
vkResetFences(get_device().get_handle(), 1, &rendering_finished_fence);
ApiVulkanSample::prepare_frame();
std::vector<VkPipelineStageFlags> wait_stages{};
std::vector<VkSemaphore> wait_semaphores{};
std::vector<VkSemaphore> signal_semaphores{};
VkSubmitInfo submit_info = {};
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer];
submit_info.commandBufferCount = 1;
// As we have no way to manually signal the semaphores, we need to distinguish between the first and consecutive submits
// The first submit can't wait on the (yet) unsignaled OpenCL semaphore, so we only wait for that after the first submit
if (first_submit)
{
first_submit = false;
wait_stages = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
wait_semaphores = {semaphores.acquired_image_ready};
signal_semaphores = {semaphores.render_complete, vk_update_cl_semaphore};
}
else
{
wait_stages = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT};
wait_semaphores = {semaphores.acquired_image_ready, cl_update_vk_semaphore};
signal_semaphores = {semaphores.render_complete, vk_update_cl_semaphore};
}
submit_info.pWaitDstStageMask = wait_stages.data();
submit_info.waitSemaphoreCount = static_cast<uint32_t>(wait_semaphores.size());
submit_info.pWaitSemaphores = wait_semaphores.data();
submit_info.signalSemaphoreCount = static_cast<uint32_t>(signal_semaphores.size());
submit_info.pSignalSemaphores = signal_semaphores.data();
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, rendering_finished_fence));
ApiVulkanSample::submit_frame();
// Update the image from OpenCL
// To make sure OpenCL won't start updating the image until Vulkan has finished rendering to it, we wait for the Vulkan->OpenCL semaphore
CL_CHECK(clEnqueueWaitSemaphoresKHR(opencl_objects.command_queue, 1, &opencl_objects.vk_update_cl_semaphore, nullptr, 0, nullptr, nullptr));
// We also need to acquire the image (resource) so we can update it with OpenCL
CL_CHECK(clEnqueueAcquireExternalMemObjectsKHR(opencl_objects.command_queue, 1, &opencl_objects.image, 0, nullptr, nullptr));
std::array<size_t, 2> global_size = {shared_image.width, shared_image.height};
std::array<size_t, 2> local_size = {16, 16};
CL_CHECK(clSetKernelArg(opencl_objects.kernel, 0, sizeof(cl_mem), &opencl_objects.image));
CL_CHECK(clSetKernelArg(opencl_objects.kernel, 1, sizeof(float), &total_time_passed));
CL_CHECK(clEnqueueNDRangeKernel(opencl_objects.command_queue, opencl_objects.kernel, global_size.size(), nullptr, global_size.data(), local_size.data(), 0, nullptr, nullptr));
// Release the image (resource) to Vulkan
CL_CHECK(clEnqueueReleaseExternalMemObjectsKHR(opencl_objects.command_queue, 1, &opencl_objects.image, 0, nullptr, nullptr));
// Signal a semaphore that the next Vulkan frame can wait on (first_submit != false)
CL_CHECK(clEnqueueSignalSemaphoresKHR(opencl_objects.command_queue, 1, &opencl_objects.cl_update_vk_semaphore, nullptr, 0, nullptr, nullptr));
}
void OpenCLInterop::view_changed()
{
update_uniform_buffers();
}
void OpenCLInterop::build_command_buffers()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VkClearValue clear_values[2]{};
clear_values[0].color = default_clear_color;
clear_values[1].depthStencil = {0.0f, 0};
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.renderArea.offset.x = 0;
render_pass_begin_info.renderArea.offset.y = 0;
render_pass_begin_info.renderArea.extent.width = width;
render_pass_begin_info.renderArea.extent.height = height;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
for (int32_t i = 0; i < draw_cmd_buffers.size(); i++)
{
render_pass_begin_info.framebuffer = framebuffers[i];
VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffers[i], &command_buffer_begin_info));
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport = vkb::initializers::viewport((float) width, (float) height, 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(static_cast<int32_t>(width), static_cast<int32_t>(height), 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set, 0, nullptr);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
VkDeviceSize offsets[1] = {0};
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 0, 1, vertex_buffer->get(), offsets);
vkCmdBindIndexBuffer(draw_cmd_buffers[i], index_buffer->get_handle(), 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(draw_cmd_buffers[i], index_count, 1, 0, 0, 0);
draw_ui(draw_cmd_buffers[i]);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
void OpenCLInterop::generate_quad()
{
std::vector<VertexStructure> vertices =
{
{{1.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, 1.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, -1.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},
{{1.0f, -1.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}}};
std::vector<uint32_t> indices = {0, 1, 2, 2, 3, 0};
index_count = static_cast<uint32_t>(indices.size());
auto vertex_buffer_size = vkb::to_u32(vertices.size() * sizeof(VertexStructure));
auto index_buffer_size = vkb::to_u32(indices.size() * sizeof(uint32_t));
// Create buffers
// For the sake of simplicity we won't stage the vertex data to the gpu memory
// Vertex buffer
vertex_buffer = std::make_unique<vkb::core::BufferC>(get_device(),
vertex_buffer_size,
VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
vertex_buffer->update(vertices.data(), vertex_buffer_size);
index_buffer = std::make_unique<vkb::core::BufferC>(get_device(),
index_buffer_size,
VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
index_buffer->update(indices.data(), index_buffer_size);
}
void OpenCLInterop::setup_descriptor_pool()
{
std::vector<VkDescriptorPoolSize> pool_sizes =
{
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1)};
VkDescriptorPoolCreateInfo descriptor_pool_create_info = vkb::initializers::descriptor_pool_create_info(static_cast<uint32_t>(pool_sizes.size()),
pool_sizes.data(),
2);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
}
void OpenCLInterop::setup_descriptor_set_layout()
{
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings{
// Binding 0 : Vertex shader uniform buffer
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0),
// Binding 1 : Fragment shader image sampler
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1),
};
VkDescriptorSetLayoutCreateInfo descriptor_layout = vkb::initializers::descriptor_set_layout_create_info(set_layout_bindings.data(), vkb::to_u32(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout, nullptr, &descriptor_set_layout));
VkPipelineLayoutCreateInfo pipeline_layout_create_info = vkb::initializers::pipeline_layout_create_info(&descriptor_set_layout, 1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layout));
}
void OpenCLInterop::setup_descriptor_set()
{
VkDescriptorSetAllocateInfo alloc_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, &descriptor_set_layout, 1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &descriptor_set));
VkDescriptorBufferInfo buffer_descriptor = create_descriptor(*uniform_buffer_vs);
// Setup a descriptor image info for the current texture to be used as a combined image sampler
VkDescriptorImageInfo image_descriptor{};
image_descriptor.imageView = shared_image.view;
image_descriptor.sampler = shared_image.sampler;
image_descriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
std::vector<VkWriteDescriptorSet> write_descriptor_sets{
// Binding 0 : Vertex shader uniform buffer
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &buffer_descriptor),
// Binding 1 : Fragment shader texture sampler
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &image_descriptor),
};
vkUpdateDescriptorSets(get_device().get_handle(), vkb::to_u32(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, nullptr);
}
void OpenCLInterop::prepare_pipelines()
{
VkPipelineInputAssemblyStateCreateInfo input_assembly_state =
vkb::initializers::pipeline_input_assembly_state_create_info(
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
0,
VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state =
vkb::initializers::pipeline_rasterization_state_create_info(
VK_POLYGON_MODE_FILL,
VK_CULL_MODE_NONE,
VK_FRONT_FACE_COUNTER_CLOCKWISE,
0);
VkPipelineColorBlendAttachmentState blend_attachment_state =
vkb::initializers::pipeline_color_blend_attachment_state(
0xf,
VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state =
vkb::initializers::pipeline_color_blend_state_create_info(
1,
&blend_attachment_state);
// Note: Using Reversed depth-buffer for increased precision, so Greater depth values are kept
VkPipelineDepthStencilStateCreateInfo depth_stencil_state =
vkb::initializers::pipeline_depth_stencil_state_create_info(
VK_TRUE,
VK_TRUE,
VK_COMPARE_OP_GREATER);
VkPipelineViewportStateCreateInfo viewport_state =
vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineMultisampleStateCreateInfo multisample_state =
vkb::initializers::pipeline_multisample_state_create_info(
VK_SAMPLE_COUNT_1_BIT,
0);
std::vector<VkDynamicState> dynamic_state_enables = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic_state =
vkb::initializers::pipeline_dynamic_state_create_info(
dynamic_state_enables.data(),
vkb::to_u32(dynamic_state_enables.size()),
0);
// Load shaders
std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages{};
shader_stages[0] = load_shader("open_cl_interop", "texture_display.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("open_cl_interop", "texture_display.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
// Vertex bindings and attributes
const std::vector<VkVertexInputBindingDescription> vertex_input_bindings = {
vkb::initializers::vertex_input_binding_description(0, sizeof(VertexStructure),
VK_VERTEX_INPUT_RATE_VERTEX),
};
const std::vector<VkVertexInputAttributeDescription> vertex_input_attributes = {
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(VertexStructure, pos)),
vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32_SFLOAT, offsetof(VertexStructure, uv)),
vkb::initializers::vertex_input_attribute_description(0, 2, VK_FORMAT_R32G32B32_SFLOAT, offsetof(VertexStructure, normal)),
};
VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
vertex_input_state.vertexBindingDescriptionCount = vkb::to_u32(vertex_input_bindings.size());
vertex_input_state.pVertexBindingDescriptions = vertex_input_bindings.data();
vertex_input_state.vertexAttributeDescriptionCount = vkb::to_u32(vertex_input_attributes.size());
vertex_input_state.pVertexAttributeDescriptions = vertex_input_attributes.data();
VkGraphicsPipelineCreateInfo pipeline_create_info = vkb::initializers::pipeline_create_info(pipeline_layout, render_pass, 0);
pipeline_create_info.pVertexInputState = &vertex_input_state;
pipeline_create_info.pInputAssemblyState = &input_assembly_state;
pipeline_create_info.pRasterizationState = &rasterization_state;
pipeline_create_info.pColorBlendState = &color_blend_state;
pipeline_create_info.pMultisampleState = &multisample_state;
pipeline_create_info.pViewportState = &viewport_state;
pipeline_create_info.pDepthStencilState = &depth_stencil_state;
pipeline_create_info.pDynamicState = &dynamic_state;
pipeline_create_info.stageCount = vkb::to_u32(shader_stages.size());
pipeline_create_info.pStages = shader_stages.data();
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipeline));
}
void OpenCLInterop::prepare_uniform_buffers()
{
// Vertex shader uniform buffer block
uniform_buffer_vs = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(ubo_vs),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
}
void OpenCLInterop::update_uniform_buffers()
{
ubo_vs.projection = glm::perspective(glm::radians(60.0f), (float) width / (float) height, 0.001f, 256.0f);
glm::mat4 view_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, zoom));
ubo_vs.model = view_matrix * glm::translate(glm::mat4(1.0f), camera_pos);
ubo_vs.model = glm::rotate(ubo_vs.model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
ubo_vs.model = glm::rotate(ubo_vs.model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
ubo_vs.model = glm::rotate(ubo_vs.model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
ubo_vs.view_pos = glm::vec4(0.0f, 0.0f, -zoom, 0.0f);
uniform_buffer_vs->convert_and_update(ubo_vs);
}
// These functions wraps the platform specific functions to get platform handles for Vulkan memory objects (e.g. the memory backing the image) and semaphores
#ifdef _WIN32
HANDLE OpenCLInterop::get_vulkan_memory_handle(VkDeviceMemory memory)
{
HANDLE handle;
VkMemoryGetWin32HandleInfoKHR win32_handle_info{};
win32_handle_info.sType = VK_STRUCTURE_TYPE_MEMORY_GET_WIN32_HANDLE_INFO_KHR;
win32_handle_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR;
win32_handle_info.memory = memory;
vkGetMemoryWin32HandleKHR(get_device().get_handle(), &win32_handle_info, &handle);
return handle;
}
HANDLE OpenCLInterop::get_vulkan_semaphore_handle(VkSemaphore &sempahore)
{
HANDLE handle;
VkSemaphoreGetWin32HandleInfoKHR win32_handle_info{};
win32_handle_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_WIN32_HANDLE_INFO_KHR;
win32_handle_info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT;
win32_handle_info.semaphore = sempahore;
vkGetSemaphoreWin32HandleKHR(get_device().get_handle(), &win32_handle_info, &handle);
return handle;
}
#else
int OpenCLInterop::get_vulkan_memory_handle(VkDeviceMemory memory)
{
int fd;
VkMemoryGetFdInfoKHR fd_info{};
fd_info.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR;
fd_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
fd_info.memory = memory;
vkGetMemoryFdKHR(get_device().get_handle(), &fd_info, &fd);
return fd;
}
int OpenCLInterop::get_vulkan_semaphore_handle(VkSemaphore &sempahore)
{
int fd;
VkSemaphoreGetFdInfoKHR fd_info{};
fd_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR;
fd_info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
fd_info.semaphore = sempahore;
vkGetSemaphoreFdKHR(get_device().get_handle(), &fd_info, &fd);
return fd;
}
#endif
void OpenCLInterop::prepare_shared_image()
{
// This texture will be shared between both APIs: OpenCL fills it and Vulkan uses it for rendering
shared_image.width = 512;
shared_image.height = 512;
// We need to select the external handle type based on our target platform
// Note: Windows 8 and older requires the _KMT suffixed handle type, which we don't support in this sample
VkExternalMemoryHandleTypeFlagBits external_handle_type{};
#ifdef _WIN32
external_handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR;
#else
external_handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR;
#endif
auto device_handle = get_device().get_handle();
// Setting up Vulkan resources (image, memory, image view and sampler)
VkImageCreateInfo image_create_info = vkb::initializers::image_create_info();
image_create_info.imageType = VK_IMAGE_TYPE_2D;
image_create_info.format = VK_FORMAT_R8G8B8A8_UNORM;
image_create_info.mipLevels = 1;
image_create_info.arrayLayers = 1;
image_create_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_create_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_create_info.extent = {shared_image.width, shared_image.height, 1};
image_create_info.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
VkExternalMemoryImageCreateInfo external_memory_image_info{};
external_memory_image_info.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO;
external_memory_image_info.handleTypes = external_handle_type;
image_create_info.pNext = &external_memory_image_info;
VK_CHECK(vkCreateImage(get_device().get_handle(), &image_create_info, nullptr, &shared_image.image));
VkMemoryRequirements memory_requirements{};
vkGetImageMemoryRequirements(get_device().get_handle(), shared_image.image, &memory_requirements);
VkExportMemoryAllocateInfoKHR export_memory_allocate_info{};
export_memory_allocate_info.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO_KHR;
export_memory_allocate_info.handleTypes = external_handle_type;
#ifdef _WIN32
WinSecurityAttributes win_security_attributes;
VkExportMemoryWin32HandleInfoKHR export_memory_win32_handle_info{};
export_memory_win32_handle_info.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_WIN32_HANDLE_INFO_KHR;
export_memory_win32_handle_info.pAttributes = &win_security_attributes;
export_memory_win32_handle_info.dwAccess = DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE;
export_memory_allocate_info.pNext = &export_memory_win32_handle_info;
#endif
VkMemoryAllocateInfo memory_allocate_info = vkb::initializers::memory_allocate_info();
memory_allocate_info.pNext = &export_memory_allocate_info;
memory_allocate_info.allocationSize = memory_requirements.size;
memory_allocate_info.memoryTypeIndex = get_device().get_gpu().get_memory_type(memory_requirements.memoryTypeBits, 0);
VK_CHECK(vkAllocateMemory(device_handle, &memory_allocate_info, nullptr, &shared_image.memory));
VK_CHECK(vkBindImageMemory(device_handle, shared_image.image, shared_image.memory, 0));
// Calculate valid filter and mipmap modes
VkFilter filter = VK_FILTER_LINEAR;
VkSamplerMipmapMode mipmap_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
vkb::make_filters_valid(get_device().get_gpu().get_handle(), image_create_info.format, &filter, &mipmap_mode);
VkSamplerCreateInfo sampler_create_info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_create_info.magFilter = filter;
sampler_create_info.minFilter = filter;
sampler_create_info.mipmapMode = mipmap_mode;
sampler_create_info.maxLod = (float) 1;
sampler_create_info.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
vkCreateSampler(device_handle, &sampler_create_info, nullptr, &shared_image.sampler);
VkImageViewCreateInfo view_create_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_create_info.image = shared_image.image;
view_create_info.format = VK_FORMAT_R8G8B8A8_UNORM;
view_create_info.subresourceRange = VkImageSubresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCreateImageView(device_handle, &view_create_info, nullptr, &shared_image.view);
VkCommandBuffer copy_command = get_device().create_command_buffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
VkImageSubresourceRange subresource_range = {};
subresource_range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
subresource_range.baseMipLevel = 0;
subresource_range.levelCount = 1;
subresource_range.layerCount = 1;
VkImageMemoryBarrier image_memory_barrier = vkb::initializers::image_memory_barrier();
image_memory_barrier.image = shared_image.image;
image_memory_barrier.subresourceRange = subresource_range;
image_memory_barrier.srcAccessMask = 0;
image_memory_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
image_memory_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
image_memory_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
vkCmdPipelineBarrier(
copy_command,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0,
0, nullptr,
0, nullptr,
1, &image_memory_barrier);
get_device().flush_command_buffer(copy_command, queue, true);
// Import the image into OpenCL
std::vector<cl_mem_properties> mem_properties;
#ifdef _WIN32
HANDLE handle = get_vulkan_memory_handle(shared_image.memory);
mem_properties.push_back((cl_mem_properties) CL_EXTERNAL_MEMORY_HANDLE_OPAQUE_WIN32_KHR);
mem_properties.push_back((cl_mem_properties) handle);
#else
int fd = get_vulkan_memory_handle(shared_image.memory);
mem_properties.push_back((cl_mem_properties) CL_EXTERNAL_MEMORY_HANDLE_OPAQUE_FD_KHR);
mem_properties.push_back((cl_mem_properties) fd);
#endif
mem_properties.push_back((cl_mem_properties) CL_MEM_DEVICE_HANDLE_LIST_KHR);
mem_properties.push_back((cl_mem_properties) opencl_objects.device_id);
mem_properties.push_back((cl_mem_properties) CL_MEM_DEVICE_HANDLE_LIST_END_KHR);
mem_properties.push_back(0);
cl_image_format cl_img_fmt{};
cl_img_fmt.image_channel_order = CL_RGBA;
cl_img_fmt.image_channel_data_type = CL_UNSIGNED_INT8;
cl_image_desc cl_img_desc{};
cl_img_desc.image_width = shared_image.width;
cl_img_desc.image_height = shared_image.height;
cl_img_desc.image_type = CL_MEM_OBJECT_IMAGE2D;
cl_img_desc.image_slice_pitch = cl_img_desc.image_row_pitch * cl_img_desc.image_height;
cl_img_desc.num_mip_levels = 1;
cl_img_desc.buffer = nullptr;
int cl_result;
opencl_objects.image = clCreateImageWithProperties(opencl_objects.context,
mem_properties.data(),
CL_MEM_READ_WRITE,
&cl_img_fmt,
&cl_img_desc,
NULL,
&cl_result);
CL_CHECK(cl_result);
}
void OpenCLInterop::prepare_sync_objects()
{
// Just as the image, we also create the semaphores in Vulkan and export them
VkExportSemaphoreCreateInfoKHR export_semaphore_create_info{};
export_semaphore_create_info.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO_KHR;
#ifdef _WIN32
WinSecurityAttributes win_security_attributes;
VkExportSemaphoreWin32HandleInfoKHR export_semaphore_handle_info{};
export_semaphore_handle_info.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_WIN32_HANDLE_INFO_KHR;
export_semaphore_handle_info.pAttributes = &win_security_attributes;
export_semaphore_handle_info.dwAccess = DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE;
export_semaphore_create_info.pNext = &export_semaphore_handle_info;
export_semaphore_create_info.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT;
#else
export_semaphore_create_info.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT;
#endif
VkSemaphoreCreateInfo semaphore_create_info{};
semaphore_create_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
semaphore_create_info.pNext = &export_semaphore_create_info;
VK_CHECK(vkCreateSemaphore(get_device().get_handle(), &semaphore_create_info, nullptr, &cl_update_vk_semaphore));
VK_CHECK(vkCreateSemaphore(get_device().get_handle(), &semaphore_create_info, nullptr, &vk_update_cl_semaphore));
// We also need a fence for the Vulkan side of things, which is not shared with OpenCL
VkFenceCreateInfo fence_create_info = vkb::initializers::fence_create_info(VK_FENCE_CREATE_SIGNALED_BIT);
vkCreateFence(get_device().get_handle(), &fence_create_info, nullptr, &rendering_finished_fence);
// Import the Vulkan sempahores into OpenCL
std::vector<cl_semaphore_properties_khr> semaphore_properties{
(cl_semaphore_properties_khr) CL_SEMAPHORE_TYPE_KHR,
(cl_semaphore_properties_khr) CL_SEMAPHORE_TYPE_BINARY_KHR,
(cl_semaphore_properties_khr) CL_SEMAPHORE_DEVICE_HANDLE_LIST_KHR,
(cl_semaphore_properties_khr) opencl_objects.device_id,
(cl_semaphore_properties_khr) CL_SEMAPHORE_DEVICE_HANDLE_LIST_END_KHR,
};
// CL to VK semaphore
#ifdef _WIN32
semaphore_properties.push_back((cl_semaphore_properties_khr) CL_SEMAPHORE_HANDLE_OPAQUE_WIN32_KHR);
HANDLE handle = get_vulkan_semaphore_handle(cl_update_vk_semaphore);
semaphore_properties.push_back((cl_semaphore_properties_khr) handle);
#else
semaphore_properties.push_back((cl_semaphore_properties_khr) CL_SEMAPHORE_HANDLE_OPAQUE_FD_KHR);
int fd = get_vulkan_semaphore_handle(cl_update_vk_semaphore);
semaphore_properties.push_back((cl_semaphore_properties_khr) fd);
#endif
semaphore_properties.push_back(0);
cl_int cl_result;
opencl_objects.cl_update_vk_semaphore = clCreateSemaphoreWithPropertiesKHR(opencl_objects.context, semaphore_properties.data(), &cl_result);
CL_CHECK(cl_result);
// Remove the last two entries so we can push the next handle and zero terminator to the properties list and re-use the other values
semaphore_properties.pop_back();
semaphore_properties.pop_back();
// VK to CL semaphore
#ifdef _WIN32
handle = get_vulkan_semaphore_handle(vk_update_cl_semaphore);
semaphore_properties.push_back((cl_semaphore_properties_khr) handle);
#else
fd = get_vulkan_semaphore_handle(vk_update_cl_semaphore);
semaphore_properties.push_back((cl_semaphore_properties_khr) fd);
#endif
semaphore_properties.push_back(0);
opencl_objects.vk_update_cl_semaphore = clCreateSemaphoreWithPropertiesKHR(opencl_objects.context, semaphore_properties.data(), &cl_result);
CL_CHECK(cl_result);
}
void OpenCLInterop::prepare_opencl_resources()
{
load_opencl();
// We need to ensure that we get the same device in OpenCL as we got in Vulkan
// To do this, we compare the unique device identifier of the current Vulkan implementation with the list of available
// devices in OpenCL and then select the OpenCL platform that matches
// See https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceIDProperties.html
// Get the UUID of the current Vulkan device
VkPhysicalDeviceIDPropertiesKHR physical_device_id_propreties{};
physical_device_id_propreties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES;
VkPhysicalDeviceProperties2 physical_device_properties_2{};
physical_device_properties_2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2_KHR;
physical_device_properties_2.pNext = &physical_device_id_propreties;
vkGetPhysicalDeviceProperties2KHR(get_device().get_gpu().get_handle(), &physical_device_properties_2);
// We also need to make sure the OpenCL platform/device supports all the extensions required in this sample
std::vector<std::string> required_extensions{
// Platform independent OpenCL extensions for interop and for getting the device
"cl_khr_external_memory",
"cl_khr_external_semaphore",
// Extension required to read the uuid of a device (see below for more information on why this is required)
"cl_khr_device_uuid"};
// Platform specific OpenCL extensions for interop
#if defined(_WIN32)
required_extensions.push_back("cl_khr_external_memory_win32");
required_extensions.push_back("cl_khr_external_semaphore_win32");
#else
required_extensions.push_back("cl_khr_external_memory_opaque_fd");
required_extensions.push_back("cl_khr_external_semaphore_opaque_fd");
#endif
// Iterate over all available OpenCL platforms and find the first that fits our requirements (extensions, device UUID)
cl_uint num_platforms;
clGetPlatformIDs_ptr(0, nullptr, &num_platforms);
std::vector<cl_platform_id> platform_ids(num_platforms);
clGetPlatformIDs_ptr(num_platforms, platform_ids.data(), nullptr);
cl_platform_id selected_platform_id{nullptr};
cl_device_id selected_device_id{nullptr};
for (auto &platform_id : platform_ids)
{
cl_uint num_devices;
clGetDeviceIDs_ptr(platform_id, CL_DEVICE_TYPE_ALL, 0, nullptr, &num_devices);
std::vector<cl_device_id> device_ids(num_devices);
clGetDeviceIDs_ptr(platform_id, CL_DEVICE_TYPE_ALL, num_devices, device_ids.data(), nullptr);
// Check if this platform supports all required extensions
size_t extension_string_size = 0;
clGetPlatformInfo(platform_id, CL_PLATFORM_EXTENSIONS, 0, nullptr, &extension_string_size);
std::string extension_string(extension_string_size, ' ');
clGetPlatformInfo(platform_id, CL_PLATFORM_EXTENSIONS, extension_string_size, &extension_string[0], nullptr);
std::vector<std::string> available_extensions{};
std::stringstream extension_stream(extension_string);
std::string extension;
while (std::getline(extension_stream, extension, ' '))
{
// Remove trailing zeroes from all extensions (which otherwise may make support checks fail)
extension.erase(std::ranges::find(extension, '\0'), extension.end());
available_extensions.push_back(extension);
}
bool extensions_present = true;
for (auto &extension : required_extensions)
{
if (std::ranges::find(available_extensions, extension) == available_extensions.end())
{
extensions_present = false;
break;
}
}
if (!extensions_present)
{
continue;
}
// Check every device of this platform and see if it matches our Vulkan device UUID
selected_device_id = nullptr;
for (auto &device_id : device_ids)
{
cl_uchar uuid[CL_UUID_SIZE_KHR];
clGetDeviceInfo_ptr(device_id, CL_DEVICE_UUID_KHR, sizeof(uuid), &uuid, nullptr);
bool device_uuid_match = true;
for (uint32_t i = 0; i < CL_UUID_SIZE_KHR; i++)
{
if (uuid[i] != physical_device_id_propreties.deviceUUID[i])
{
device_uuid_match = false;
break;
}
}
if (!device_uuid_match)
{
continue;
}
// We found a device with a matching UUID, so use it
selected_device_id = device_id;
break;
}
// We found a platform that supportes the required extensions and has a device with a matching UUID
if (selected_device_id)
{
selected_platform_id = platform_id;
break;
}
}
if ((selected_platform_id == nullptr) || (selected_device_id == nullptr))
{
const std::string message{"Could not find an OpenCL platform + device that matches the required extensions and also matches the Vulkan device UUID"};
LOGE("{}", message);
throw std::runtime_error(message);
}
opencl_objects.device_id = selected_device_id;
cl_int cl_result;
opencl_objects.context = clCreateContext(NULL, 1, &opencl_objects.device_id, nullptr, nullptr, &cl_result);
CL_CHECK(cl_result);
opencl_objects.command_queue = clCreateCommandQueue(opencl_objects.context, opencl_objects.device_id, 0, &cl_result);
CL_CHECK(cl_result);
std::string kernel_source = vkb::fs::read_text_file("open_cl_interop/procedural_texture.cl");
auto kernel_source_data = kernel_source.c_str();
size_t kernel_source_size = kernel_source.size();
opencl_objects.program = clCreateProgramWithSource(opencl_objects.context, 1, &kernel_source_data, &kernel_source_size, &cl_result);
CL_CHECK(cl_result);
CL_CHECK(clBuildProgram(opencl_objects.program, 1, &opencl_objects.device_id, nullptr, nullptr, nullptr));
opencl_objects.kernel = clCreateKernel(opencl_objects.program, "generate_texture", &cl_result);
CL_CHECK(cl_result);
}
bool OpenCLInterop::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
prepare_opencl_resources();
prepare_sync_objects();
prepare_shared_image();
generate_quad();
prepare_uniform_buffers();
setup_descriptor_set_layout();
prepare_pipelines();
setup_descriptor_pool();
setup_descriptor_set();
build_command_buffers();
opencl_objects.initialized = true;
prepared = true;
return true;
}
std::unique_ptr<vkb::VulkanSampleC> create_open_cl_interop()
{
return std::make_unique<OpenCLInterop>();
}