diff --git a/runtime/Includes/Core/Application.inl b/runtime/Includes/Core/Application.inl index bc4ab5a..bf2c9be 100644 --- a/runtime/Includes/Core/Application.inl +++ b/runtime/Includes/Core/Application.inl @@ -9,7 +9,7 @@ Error("invalid window ptr (NULL)"); \ return; \ } \ - else if(std::find_if(m_graphics.begin(), m_graphics.end(), [win](const std::unique_ptr& gs){ return *static_cast(win) == gs->GetID(); }) == m_graphics.end()) \ + else if(std::find_if(m_graphics.begin(), m_graphics.end(), [win](const std::unique_ptr& gs){ return gs && *static_cast(win) == gs->GetID(); }) == m_graphics.end()) \ { \ Error("invalid window ptr"); \ return; \ diff --git a/runtime/Includes/Renderer/Pipelines/Graphics.h b/runtime/Includes/Renderer/Pipelines/Graphics.h index 246494a..a474f80 100644 --- a/runtime/Includes/Renderer/Pipelines/Graphics.h +++ b/runtime/Includes/Renderer/Pipelines/Graphics.h @@ -24,8 +24,8 @@ namespace mlx GraphicPipeline() = default; void Init(const GraphicPipelineDescriptor& descriptor, std::string_view debug_name); - bool BindPipeline(VkCommandBuffer command_buffer, std::size_t framebuffer_index, std::array clear) noexcept; - void EndPipeline(VkCommandBuffer command_buffer) noexcept override; + bool BindPipeline(VkCommandBuffer cmd, std::size_t framebuffer_index, std::array clear) noexcept; + void EndPipeline(VkCommandBuffer cmd) noexcept override; void Destroy() noexcept; [[nodiscard]] inline VkPipeline GetPipeline() const override { return m_pipeline; } diff --git a/runtime/Sources/Core/Logs.cpp b/runtime/Sources/Core/Logs.cpp index 47cbadf..f61ec99 100644 --- a/runtime/Sources/Core/Logs.cpp +++ b/runtime/Sources/Core/Logs.cpp @@ -38,14 +38,19 @@ namespace mlx switch(type) { - case LogType::Debug: std::cout << Ansi::blue << "[MLX Debug] " << Ansi::def << code_infos << message << std::endl; break; - case LogType::Message: std::cout << Ansi::blue << "[MLX Message] " << Ansi::def << code_infos << message << '\n'; break; - case LogType::Warning: std::cout << Ansi::magenta << "[MLX Warning] " << Ansi::def << code_infos << message << '\n'; break; - case LogType::Error: std::cerr << Ansi::red << "[MLX Error] " << Ansi::def << code_infos << message << '\n'; break; - case LogType::FatalError: std::cerr << Ansi::red << "[MLX Fatal Error] " << Ansi::def << code_infos << message << '\n'; break; + case LogType::Debug: std::cout << Ansi::blue << "[MLX Debug] "; break; + case LogType::Message: std::cout << Ansi::blue << "[MLX Message] "; break; + case LogType::Warning: std::cout << Ansi::magenta << "[MLX Warning] "; break; + case LogType::Error: std::cerr << Ansi::red << "[MLX Error] "; break; + case LogType::FatalError: std::cerr << Ansi::red << "[MLX Fatal Error] "; break; default: break; } + + const std::chrono::zoned_time current_time{ std::chrono::current_zone(), std::chrono::floor(std::chrono::system_clock::now()) }; + + std::cout << Ansi::yellow << std::format("[{0:%H:%M:%S}] ", current_time) << Ansi::def << code_infos << message << std::endl; + if(type == LogType::FatalError) { std::cout << Ansi::bg_red << "Fatal Error: emergency exit" << Ansi::bg_def << std::endl; diff --git a/runtime/Sources/Renderer/Descriptor.cpp b/runtime/Sources/Renderer/Descriptor.cpp index b0240c1..db18106 100644 --- a/runtime/Sources/Renderer/Descriptor.cpp +++ b/runtime/Sources/Renderer/Descriptor.cpp @@ -26,17 +26,9 @@ namespace mlx { MLX_PROFILE_FUNCTION(); VkDescriptorPoolSize pool_sizes[] = { - { VK_DESCRIPTOR_TYPE_SAMPLER, MAX_SETS_PER_POOL }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_SETS_PER_POOL }, - { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, MAX_SETS_PER_POOL }, - { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, MAX_SETS_PER_POOL }, - { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, MAX_SETS_PER_POOL }, - { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, MAX_SETS_PER_POOL }, { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_SETS_PER_POOL }, - { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, MAX_SETS_PER_POOL }, - { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, MAX_SETS_PER_POOL }, - { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, MAX_SETS_PER_POOL }, - { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, MAX_SETS_PER_POOL } + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, MAX_SETS_PER_POOL } }; VkDescriptorPoolCreateInfo pool_info{}; diff --git a/runtime/Sources/Renderer/Pipelines/Graphics.cpp b/runtime/Sources/Renderer/Pipelines/Graphics.cpp index 4a8024f..9036e09 100644 --- a/runtime/Sources/Renderer/Pipelines/Graphics.cpp +++ b/runtime/Sources/Renderer/Pipelines/Graphics.cpp @@ -50,8 +50,10 @@ namespace mlx kvfGPipelineBuilderSetVertexInputs(builder, binding_description, attributes_description.data(), attributes_description.size()); } - m_pipeline = kvfCreateGraphicsPipeline(RenderCore::Get().GetDevice(), m_pipeline_layout, builder, m_renderpass); - DebugLog("Vulkan: graphics pipeline created"); + m_pipeline = kvfCreateGraphicsPipeline(RenderCore::Get().GetDevice(), VK_NULL_HANDLE, m_pipeline_layout, builder, m_renderpass); + #ifdef DEBUG + DebugLog("Vulkan: graphics pipeline created %", m_debug_name); + #endif kvfDestroyGPipelineBuilder(builder); #ifdef MLX_HAS_DEBUG_UTILS_FUNCTIONS @@ -82,10 +84,10 @@ namespace mlx #endif } - bool GraphicPipeline::BindPipeline(VkCommandBuffer command_buffer, std::size_t framebuffer_index, std::array clear) noexcept + bool GraphicPipeline::BindPipeline(VkCommandBuffer cmd, std::size_t framebuffer_index, std::array clear) noexcept { MLX_PROFILE_FUNCTION(); - TransitionAttachments(command_buffer); + TransitionAttachments(cmd); VkFramebuffer fb = m_framebuffers[framebuffer_index]; VkExtent2D fb_extent = kvfGetFramebufferSize(fb); @@ -96,12 +98,12 @@ namespace mlx viewport.height = fb_extent.height; viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; - RenderCore::Get().vkCmdSetViewport(command_buffer, 0, 1, &viewport); + RenderCore::Get().vkCmdSetViewport(cmd, 0, 1, &viewport); VkRect2D scissor{}; scissor.offset = { 0, 0 }; scissor.extent = fb_extent; - RenderCore::Get().vkCmdSetScissor(command_buffer, 0, 1, &scissor); + RenderCore::Get().vkCmdSetScissor(cmd, 0, 1, &scissor); for(std::size_t i = 0; i < m_clears.size(); i++) { @@ -111,15 +113,15 @@ namespace mlx m_clears[i].color.float32[3] = clear[3]; } - kvfBeginRenderPass(m_renderpass, command_buffer, fb, fb_extent, m_clears.data(), m_clears.size()); - RenderCore::Get().vkCmdBindPipeline(command_buffer, GetPipelineBindPoint(), GetPipeline()); + kvfBeginRenderPass(m_renderpass, cmd, fb, fb_extent, m_clears.data(), m_clears.size()); + RenderCore::Get().vkCmdBindPipeline(cmd, GetPipelineBindPoint(), GetPipeline()); return true; } - void GraphicPipeline::EndPipeline(VkCommandBuffer command_buffer) noexcept + void GraphicPipeline::EndPipeline(VkCommandBuffer cmd) noexcept { MLX_PROFILE_FUNCTION(); - RenderCore::Get().vkCmdEndRenderPass(command_buffer); + RenderCore::Get().vkCmdEndRenderPass(cmd); } void GraphicPipeline::Destroy() noexcept @@ -127,25 +129,33 @@ namespace mlx MLX_PROFILE_FUNCTION(); p_vertex_shader.reset(); p_fragment_shader.reset(); - for(auto& fb : m_framebuffers) + for(auto fb : m_framebuffers) { kvfDestroyFramebuffer(RenderCore::Get().GetDevice(), fb); - DebugLog("Vulkan: framebuffer destroyed"); + #ifdef DEBUG + DebugLog("Vulkan: framebuffer destroyed in %", m_debug_name); + #endif } m_framebuffers.clear(); kvfDestroyPipelineLayout(RenderCore::Get().GetDevice(), m_pipeline_layout); m_pipeline_layout = VK_NULL_HANDLE; - DebugLog("Vulkan: graphics pipeline layout destroyed"); + #ifdef DEBUG + DebugLog("Vulkan: graphics pipeline layout destroyed %", m_debug_name); + #endif kvfDestroyRenderPass(RenderCore::Get().GetDevice(), m_renderpass); m_renderpass = VK_NULL_HANDLE; - DebugLog("Vulkan: renderpass destroyed"); + #ifdef DEBUG + DebugLog("Vulkan: renderpass destroyed for %", m_debug_name); + #endif kvfDestroyPipeline(RenderCore::Get().GetDevice(), m_pipeline); m_pipeline = VK_NULL_HANDLE; - DebugLog("Vulkan: graphics pipeline destroyed"); - + #ifdef DEBUG + DebugLog("Vulkan: graphics pipeline destroyed %", m_debug_name); + #endif + p_renderer = nullptr; m_clears.clear(); m_attachments.clear(); @@ -192,7 +202,9 @@ namespace mlx m_renderpass = kvfCreateRenderPassWithSubpassDependencies(RenderCore::Get().GetDevice(), attachments.data(), attachments.size(), GetPipelineBindPoint(), dependencies.data(), dependencies.size()); m_clears.clear(); m_clears.resize(attachments.size()); - DebugLog("Vulkan: renderpass created"); + #ifdef DEBUG + DebugLog("Vulkan: renderpass created for %", m_debug_name); + #endif if(p_renderer) { @@ -200,13 +212,17 @@ namespace mlx { attachment_views[0] = image.GetImageView(); m_framebuffers.push_back(kvfCreateFramebuffer(RenderCore::Get().GetDevice(), m_renderpass, attachment_views.data(), attachment_views.size(), { .width = image.GetWidth(), .height = image.GetHeight() })); - DebugLog("Vulkan: framebuffer created"); + #ifdef DEBUG + DebugLog("Vulkan: framebuffer created for %", m_debug_name); + #endif } } for(NonOwningPtr image : render_targets) { m_framebuffers.push_back(kvfCreateFramebuffer(RenderCore::Get().GetDevice(), m_renderpass, attachment_views.data(), attachment_views.size(), { .width = image->GetWidth(), .height = image->GetHeight() })); - DebugLog("Vulkan: framebuffer created"); + #ifdef DEBUG + DebugLog("Vulkan: framebuffer created for %", m_debug_name); + #endif } } diff --git a/runtime/Sources/Renderer/RenderPasses/2DPass.cpp b/runtime/Sources/Renderer/RenderPasses/2DPass.cpp index 1f408c0..0d1de8e 100644 --- a/runtime/Sources/Renderer/RenderPasses/2DPass.cpp +++ b/runtime/Sources/Renderer/RenderPasses/2DPass.cpp @@ -50,7 +50,7 @@ namespace mlx if(event.What() == Event::ResizeEventCode) m_pipeline.Destroy(); }; - EventBus::RegisterListener({ functor, "mlx_2d_render_pass" }); + EventBus::RegisterListener({ functor, "mlx_2d_render_pass_" + std::to_string(reinterpret_cast(this)) }); p_viewer_data_set = RenderCore::Get().GetDescriptorPoolManager().GetAvailablePool().RequestDescriptorSet(p_vertex_shader->GetShaderLayout().set_layouts[0].second, ShaderType::Vertex); p_texture_set = RenderCore::Get().GetDescriptorPoolManager().GetAvailablePool().RequestDescriptorSet(p_fragment_shader->GetShaderLayout().set_layouts[0].second, ShaderType::Fragment); @@ -76,7 +76,10 @@ namespace mlx pipeline_descriptor.color_attachments = { &render_target }; pipeline_descriptor.clear_color_attachments = false; #ifdef DEBUG - m_pipeline.Init(pipeline_descriptor, "mlx_2D_pass"); + if(renderer.GetWindow()) + m_pipeline.Init(pipeline_descriptor, "mlx_2D_pass_" + renderer.GetWindow()->GetName()); + else + m_pipeline.Init(pipeline_descriptor, "mlx_2D_pass"); #else m_pipeline.Init(pipeline_descriptor, {}); #endif diff --git a/runtime/Sources/Renderer/RenderPasses/FinalPass.cpp b/runtime/Sources/Renderer/RenderPasses/FinalPass.cpp index d9d674b..0a897a8 100644 --- a/runtime/Sources/Renderer/RenderPasses/FinalPass.cpp +++ b/runtime/Sources/Renderer/RenderPasses/FinalPass.cpp @@ -36,7 +36,7 @@ namespace mlx if(event.What() == Event::ResizeEventCode) m_pipeline.Destroy(); }; - EventBus::RegisterListener({ functor, "mlx_final_pass" }); + EventBus::RegisterListener({ functor, "mlx_final_pass_" + std::to_string(reinterpret_cast(this)) }); p_set = RenderCore::Get().GetDescriptorPoolManager().GetAvailablePool().RequestDescriptorSet(p_fragment_shader->GetShaderLayout().set_layouts[0].second, ShaderType::Fragment); } @@ -55,7 +55,10 @@ namespace mlx pipeline_descriptor.renderer = &renderer; pipeline_descriptor.no_vertex_inputs = true; #ifdef DEBUG - m_pipeline.Init(pipeline_descriptor, "mlx_final_pass"); + if(final_target) + m_pipeline.Init(pipeline_descriptor, "mlx_final_pass"); + else + m_pipeline.Init(pipeline_descriptor, "mlx_final_pass_" + renderer.GetWindow()->GetName()); #else m_pipeline.Init(pipeline_descriptor, {}); #endif diff --git a/runtime/Sources/Renderer/Swapchain.cpp b/runtime/Sources/Renderer/Swapchain.cpp index 2d6e694..d598120 100644 --- a/runtime/Sources/Renderer/Swapchain.cpp +++ b/runtime/Sources/Renderer/Swapchain.cpp @@ -27,6 +27,7 @@ namespace mlx if(m_resize) { RenderCore::Get().WaitDeviceIdle(); + Destroy(); CreateSwapchain(); EventBus::SendBroadcast(Internal::ResizeEventBroadcast{}); } @@ -76,8 +77,6 @@ namespace mlx extent = { size.x, size.y }; } while(extent.width == 0 || extent.height == 0); - Destroy(); - m_surface = p_window->CreateVulkanSurface(RenderCore::Get().GetInstance()); DebugLog("Vulkan: surface created"); m_swapchain = kvfCreateSwapchainKHR(RenderCore::Get().GetDevice(), RenderCore::Get().GetPhysicalDevice(), m_surface, extent, VK_NULL_HANDLE, false); diff --git a/third_party/kvf.h b/third_party/kvf.h index fa6a1bd..be29a0b 100755 --- a/third_party/kvf.h +++ b/third_party/kvf.h @@ -229,7 +229,7 @@ void kvfGPipelineBuilderSetVertexInputs(KvfGraphicsPipelineBuilder* builder, VkV void kvfGPipelineBuilderAddShaderStage(KvfGraphicsPipelineBuilder* builder, VkShaderStageFlagBits stage, VkShaderModule module, const char* entry); void kvfGPipelineBuilderResetShaderStages(KvfGraphicsPipelineBuilder* builder); -VkPipeline kvfCreateGraphicsPipeline(VkDevice device, VkPipelineLayout layout, KvfGraphicsPipelineBuilder* builder, VkRenderPass pass); +VkPipeline kvfCreateGraphicsPipeline(VkDevice device, VkPipelineCache cache, VkPipelineLayout layout, KvfGraphicsPipelineBuilder* builder, VkRenderPass pass); void kvfDestroyPipeline(VkDevice device, VkPipeline pipeline); void kvfCheckVk(VkResult result); @@ -413,9 +413,9 @@ typedef struct __KvfDevice { VkSurfaceCapabilitiesKHR capabilities; VkSurfaceFormatKHR* formats; - VkPresentModeKHR* presentModes; + VkPresentModeKHR* present_modes; uint32_t formats_count; - uint32_t presentModes_count; + uint32_t present_modes_count; } __KvfSwapchainSupportInternal; typedef struct __KvfSwapchain @@ -619,6 +619,7 @@ void __kvfDestroyDevice(VkDevice device) if(__kvf_internal_devices_size == 0) { KVF_FREE(__kvf_internal_devices); + __kvf_internal_devices = NULL; __kvf_internal_devices_capacity = 0; } return; @@ -703,6 +704,7 @@ __KvfDevice* __kvfGetKvfDeviceFromVkCommandBuffer(VkCommandBuffer cmd) if(__kvf_internal_swapchains_size == 0) { KVF_FREE(__kvf_internal_swapchains); + __kvf_internal_swapchains = NULL; __kvf_internal_swapchains_capacity = 0; } return; @@ -718,7 +720,6 @@ __KvfDevice* __kvfGetKvfDeviceFromVkCommandBuffer(VkCommandBuffer cmd) if(__kvf_internal_swapchains[i].swapchain == swapchain) return &__kvf_internal_swapchains[i]; } - puts("not found"); return NULL; } #endif @@ -729,7 +730,7 @@ void __kvfAddFramebufferToArray(VkFramebuffer framebuffer, VkExtent2D extent) if(__kvf_internal_framebuffers_size == __kvf_internal_framebuffers_capacity) { // Resize the dynamic array if necessary - __kvf_internal_framebuffers_capacity += 2; + __kvf_internal_framebuffers_capacity += 5; __kvf_internal_framebuffers = (__KvfFramebuffer*)KVF_REALLOC(__kvf_internal_framebuffers, __kvf_internal_framebuffers_capacity * sizeof(__KvfFramebuffer)); } @@ -760,11 +761,13 @@ void __kvfDestroyFramebuffer(VkDevice device, VkFramebuffer framebuffer) if(__kvf_internal_framebuffers_size == 0) { KVF_FREE(__kvf_internal_framebuffers); + __kvf_internal_framebuffers = NULL; __kvf_internal_framebuffers_capacity = 0; } return; } } + KVF_ASSERT(false && "could not find framebuffer"); } __KvfFramebuffer* __kvfGetKvfFramebufferFromVkFramebuffer(VkFramebuffer framebuffer) @@ -1875,12 +1878,12 @@ void kvfDestroySemaphore(VkDevice device, VkSemaphore semaphore) KVF_GET_INSTANCE_FUNCTION(vkGetPhysicalDeviceSurfaceFormatsKHR)(physical, surface, &support.formats_count, support.formats); } - KVF_GET_INSTANCE_FUNCTION(vkGetPhysicalDeviceSurfacePresentModesKHR)(physical, surface, &support.presentModes_count, NULL); - if(support.presentModes_count != 0) + KVF_GET_INSTANCE_FUNCTION(vkGetPhysicalDeviceSurfacePresentModesKHR)(physical, surface, &support.present_modes_count, NULL); + if(support.present_modes_count != 0) { - support.presentModes = (VkPresentModeKHR*)KVF_MALLOC(sizeof(VkPresentModeKHR) * support.presentModes_count); - KVF_ASSERT(support.presentModes != NULL && "allocation failed :("); - KVF_GET_INSTANCE_FUNCTION(vkGetPhysicalDeviceSurfacePresentModesKHR)(physical, surface, &support.presentModes_count, support.presentModes); + support.present_modes = (VkPresentModeKHR*)KVF_MALLOC(sizeof(VkPresentModeKHR) * support.present_modes_count); + KVF_ASSERT(support.present_modes != NULL && "allocation failed :("); + KVF_GET_INSTANCE_FUNCTION(vkGetPhysicalDeviceSurfacePresentModesKHR)(physical, surface, &support.present_modes_count, support.present_modes); } return support; } @@ -1897,13 +1900,21 @@ void kvfDestroySemaphore(VkDevice device, VkSemaphore semaphore) VkPresentModeKHR __kvfChooseSwapPresentMode(__KvfSwapchainSupportInternal* support, bool try_vsync) { - if(try_vsync == false) - return VK_PRESENT_MODE_IMMEDIATE_KHR; - for(uint32_t i = 0; i < support->presentModes_count; i++) + if(try_vsync) + return VK_PRESENT_MODE_FIFO_KHR; + bool mailbox_supported = false; + bool immediate_supported = false; + for(uint32_t i = 0; i < support->present_modes_count; i++) { - if(support->presentModes[i] == VK_PRESENT_MODE_MAILBOX_KHR) - return support->presentModes[i]; + if(support->present_modes[i] == VK_PRESENT_MODE_MAILBOX_KHR) + mailbox_supported = true; + if(support->present_modes[i] == VK_PRESENT_MODE_IMMEDIATE_KHR) + immediate_supported = true; } + if(mailbox_supported) + return VK_PRESENT_MODE_MAILBOX_KHR; + if(immediate_supported) + return VK_PRESENT_MODE_IMMEDIATE_KHR; // Best mode for low latency return VK_PRESENT_MODE_FIFO_KHR; } @@ -1920,7 +1931,7 @@ void kvfDestroySemaphore(VkDevice device, VkSemaphore semaphore) __KvfSwapchainSupportInternal support = __kvfQuerySwapchainSupport(physical, surface); VkSurfaceFormatKHR surfaceFormat = __kvfChooseSwapSurfaceFormat(&support); - VkPresentModeKHR presentMode = __kvfChooseSwapPresentMode(&support, try_vsync); + VkPresentModeKHR present_mode = __kvfChooseSwapPresentMode(&support, try_vsync); uint32_t image_count = support.capabilities.minImageCount + 1; if(support.capabilities.maxImageCount > 0 && image_count > support.capabilities.maxImageCount) @@ -1950,7 +1961,7 @@ void kvfDestroySemaphore(VkDevice device, VkSemaphore semaphore) createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; createInfo.preTransform = support.capabilities.currentTransform; createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - createInfo.presentMode = presentMode; + createInfo.presentMode = present_mode; createInfo.clipped = VK_TRUE; createInfo.oldSwapchain = old_swapchain; @@ -2968,7 +2979,7 @@ void kvfGPipelineBuilderResetShaderStages(KvfGraphicsPipelineBuilder* builder) builder->shader_stages_count = 0; } -VkPipeline kvfCreateGraphicsPipeline(VkDevice device, VkPipelineLayout layout, KvfGraphicsPipelineBuilder* builder, VkRenderPass pass) +VkPipeline kvfCreateGraphicsPipeline(VkDevice device, VkPipelineCache cache, VkPipelineLayout layout, KvfGraphicsPipelineBuilder* builder, VkRenderPass pass) { KVF_ASSERT(builder != NULL); KVF_ASSERT(device != VK_NULL_HANDLE); @@ -3020,7 +3031,7 @@ VkPipeline kvfCreateGraphicsPipeline(VkDevice device, VkPipelineLayout layout, K KVF_ASSERT(kvf_device != NULL); #endif VkPipeline pipeline; - __kvfCheckVk(KVF_GET_DEVICE_FUNCTION(vkCreateGraphicsPipelines)(device, VK_NULL_HANDLE, 1, &pipeline_info, NULL, &pipeline)); + __kvfCheckVk(KVF_GET_DEVICE_FUNCTION(vkCreateGraphicsPipelines)(device, cache, 1, &pipeline_info, NULL, &pipeline)); return pipeline; }