| // Copyright 2016 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "gpu/command_buffer/service/vulkan_in_process_context_provider.h" | |
| #include <algorithm> | |
| #include <string_view> | |
| #include <utility> | |
| #include "base/compiler_specific.h" | |
| #include "base/memory_coordinator/memory_consumer.h" | |
| #include "base/memory_coordinator/memory_coordinator_features.h" | |
| #include "base/memory_coordinator/memory_limit.h" | |
| #include "base/memory_coordinator/traits.h" | |
| #include "base/memory_coordinator/utils.h" | |
| #include "base/numerics/safe_conversions.h" | |
| #include "gpu/config/gpu_finch_features.h" | |
| #include "gpu/vulkan/buildflags.h" | |
| #include "gpu/vulkan/vma_wrapper.h" | |
| #include "gpu/vulkan/vulkan_device_queue.h" | |
| #include "gpu/vulkan/vulkan_fence_helper.h" | |
| #include "gpu/vulkan/vulkan_function_pointers.h" | |
| #include "gpu/vulkan/vulkan_implementation.h" | |
| #include "gpu/vulkan/vulkan_instance.h" | |
| #include "gpu/vulkan/vulkan_util.h" | |
| #include "skia/buildflags.h" | |
| #include "third_party/skia/include/gpu/ganesh/GrDirectContext.h" | |
| #include "third_party/skia/include/gpu/ganesh/vk/GrVkDirectContext.h" | |
| #include "third_party/skia/include/gpu/graphite/Context.h" | |
| #include "third_party/skia/include/gpu/graphite/vk/VulkanGraphiteContext.h" | |
| #include "third_party/skia/include/gpu/vk/VulkanBackendContext.h" | |
| #include "third_party/skia/include/gpu/vk/VulkanExtensions.h" | |
| #include "third_party/skia/include/gpu/vk/VulkanTypes.h" | |
| namespace gpu { | |
| namespace { | |
| // Setting this limit to 0 practically forces sync at every submit. | |
| constexpr uint32_t kSyncCpuMemoryLimitAtMemoryPressureCritical = 0; | |
| constexpr base::MemoryConsumerTraits kMemoryConsumerTraits( | |
| // Deferred Vulkan resources can accumulate to a moderate amount of memory. | |
| base::MemoryConsumerTraits::EstimatedMemoryUsage::kMedium, | |
| // Callback only updates atomics; the deferred resources are not read. | |
| base::MemoryConsumerTraits::ReleaseMemoryDataAccess::kNone, | |
| // Syncing GPU work does not lose user state or cached data. | |
| base::MemoryConsumerTraits::InformationRetention::kLossless, | |
| // Pre-determined by AsyncMemoryConsumerRegistration on the GPU thread. | |
| base::MemoryConsumerTraits::ExecutionType::kAsynchronous); | |
| skgpu::VulkanBackendContext GetBackendContextParams( | |
| VulkanImplementation* vulkan_implementation_, | |
| VulkanDeviceQueue* device_queue_, | |
| skgpu::VulkanExtensions& vk_extensions) { | |
| skgpu::VulkanBackendContext backend_context; | |
| backend_context.fInstance = device_queue_->GetVulkanInstance(); | |
| backend_context.fPhysicalDevice = device_queue_->GetVulkanPhysicalDevice(); | |
| backend_context.fDevice = device_queue_->GetVulkanDevice(); | |
| backend_context.fQueue = device_queue_->GetVulkanQueue(); | |
| backend_context.fGraphicsQueueIndex = device_queue_->GetVulkanQueueIndex(); | |
| backend_context.fMaxAPIVersion = vulkan_implementation_->GetVulkanInstance() | |
| ->vulkan_info() | |
| .used_api_version; | |
| backend_context.fMemoryAllocator = device_queue_->GetSkiaVkMemoryAllocator(); | |
| skgpu::VulkanGetProc get_proc = [](const char* proc_name, VkInstance instance, | |
| VkDevice device) { | |
| if (device) { | |
| // Using vkQueue*Hook for all vkQueue* methods here to make both chrome | |
| // side access and skia side access to the same queue thread safe. | |
| // vkQueue*Hook routes all skia side access to the same | |
| // VulkanFunctionPointers vkQueue* api which chrome uses and is under the | |
| // lock. | |
| std::string_view proc_name_view(proc_name); | |
| if (proc_name_view == "vkCreateGraphicsPipelines") { | |
| return reinterpret_cast<PFN_vkVoidFunction>( | |
| &gpu::CreateGraphicsPipelinesHook); | |
| } else if (proc_name_view == "vkQueueSubmit") { | |
| return reinterpret_cast<PFN_vkVoidFunction>( | |
| &gpu::VulkanQueueSubmitHook); | |
| } else if (proc_name_view == "vkQueueWaitIdle") { | |
| return reinterpret_cast<PFN_vkVoidFunction>( | |
| &gpu::VulkanQueueWaitIdleHook); | |
| } else if (proc_name_view == "vkQueuePresentKHR") { | |
| return reinterpret_cast<PFN_vkVoidFunction>( | |
| &gpu::VulkanQueuePresentKHRHook); | |
| } | |
| return vkGetDeviceProcAddr(device, proc_name); | |
| } | |
| return vkGetInstanceProcAddr(instance, proc_name); | |
| }; | |
| const auto& instance_extensions = vulkan_implementation_->GetVulkanInstance() | |
| ->vulkan_info() | |
| .enabled_instance_extensions; | |
| std::vector<const char*> device_extensions; | |
| device_extensions.reserve(device_queue_->enabled_extensions().size()); | |
| for (const auto& extension : device_queue_->enabled_extensions()) { | |
| device_extensions.push_back(extension.data()); | |
| } | |
| vk_extensions.init(get_proc, | |
| vulkan_implementation_->GetVulkanInstance()->vk_instance(), | |
| device_queue_->GetVulkanPhysicalDevice(), | |
| instance_extensions.size(), instance_extensions.data(), | |
| device_extensions.size(), device_extensions.data()); | |
| backend_context.fVkExtensions = &vk_extensions; | |
| backend_context.fDeviceFeatures2 = | |
| &device_queue_->enabled_device_features_2(); | |
| backend_context.fGetProc = get_proc; | |
| backend_context.fProtectedContext = GrProtected::kNo; | |
| return backend_context; | |
| } | |
| } // namespace | |
| // static | |
| scoped_refptr<VulkanInProcessContextProvider> | |
| VulkanInProcessContextProvider::Create( | |
| VulkanImplementation* vulkan_implementation, | |
| uint32_t heap_memory_limit, | |
| uint32_t sync_cpu_memory_limit, | |
| const bool is_thread_safe, | |
| const GPUInfo* gpu_info, | |
| base::TimeDelta cooldown_duration_at_memory_pressure_critical) { | |
| scoped_refptr<VulkanInProcessContextProvider> context_provider( | |
| new VulkanInProcessContextProvider( | |
| vulkan_implementation, heap_memory_limit, sync_cpu_memory_limit, | |
| cooldown_duration_at_memory_pressure_critical)); | |
| if (!context_provider->Initialize(gpu_info, is_thread_safe)) { | |
| return nullptr; | |
| } | |
| return context_provider; | |
| } | |
| scoped_refptr<VulkanInProcessContextProvider> | |
| VulkanInProcessContextProvider::CreateForCompositorGpuThread( | |
| VulkanImplementation* vulkan_implementation, | |
| std::unique_ptr<VulkanDeviceQueue> vulkan_device_queue, | |
| uint32_t sync_cpu_memory_limit, | |
| base::TimeDelta cooldown_duration_at_memory_pressure_critical) { | |
| if (!vulkan_implementation) { | |
| return nullptr; | |
| } | |
| scoped_refptr<VulkanInProcessContextProvider> context_provider( | |
| new VulkanInProcessContextProvider( | |
| vulkan_implementation, /*heap_memory_limit=*/0, sync_cpu_memory_limit, | |
| cooldown_duration_at_memory_pressure_critical)); | |
| context_provider->InitializeForCompositorGpuThread( | |
| std::move(vulkan_device_queue)); | |
| return context_provider; | |
| } | |
| VulkanInProcessContextProvider::VulkanInProcessContextProvider( | |
| VulkanImplementation* vulkan_implementation, | |
| uint32_t heap_memory_limit, | |
| uint32_t sync_cpu_memory_limit, | |
| base::TimeDelta cooldown_duration_at_memory_pressure_critical) | |
| : vulkan_implementation_(vulkan_implementation), | |
| heap_memory_limit_(heap_memory_limit), | |
| sync_cpu_memory_limit_(sync_cpu_memory_limit > 0 | |
| ? std::make_optional(sync_cpu_memory_limit) | |
| : std::nullopt), | |
| cooldown_duration_at_memory_pressure_critical_( | |
| cooldown_duration_at_memory_pressure_critical), | |
| critical_memory_pressure_expiration_time_(base::TimeTicks()), | |
| active_sync_cpu_memory_limit_(sync_cpu_memory_limit) { | |
| memory_consumer_registration_ = | |
| std::make_unique<base::AsyncMemoryConsumerRegistration>( | |
| "VulkanInProcessContextProvider", kMemoryConsumerTraits, this); | |
| } | |
| VulkanInProcessContextProvider::~VulkanInProcessContextProvider() { | |
| Destroy(); | |
| } | |
| bool VulkanInProcessContextProvider::Initialize(const GPUInfo* gpu_info, | |
| const bool is_thread_safe) { | |
| DCHECK(!device_queue_); | |
| const auto& instance_extensions = vulkan_implementation_->GetVulkanInstance() | |
| ->vulkan_info() | |
| .enabled_instance_extensions; | |
| uint32_t flags = VulkanDeviceQueue::GRAPHICS_QUEUE_FLAG; | |
| constexpr std::string_view surface_extension_name( | |
| VK_KHR_SURFACE_EXTENSION_NAME); | |
| for (const auto* extension : instance_extensions) { | |
| if (surface_extension_name == extension) { | |
| flags |= VulkanDeviceQueue::PRESENTATION_SUPPORT_QUEUE_FLAG; | |
| break; | |
| } | |
| } | |
| device_queue_ = | |
| CreateVulkanDeviceQueue(vulkan_implementation_, flags, gpu_info, | |
| heap_memory_limit_, is_thread_safe); | |
| if (!device_queue_) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| void VulkanInProcessContextProvider::InitializeForCompositorGpuThread( | |
| std::unique_ptr<VulkanDeviceQueue> vulkan_device_queue) { | |
| DCHECK(!device_queue_); | |
| DCHECK(vulkan_device_queue); | |
| device_queue_ = std::move(vulkan_device_queue); | |
| } | |
| bool VulkanInProcessContextProvider::InitializeGraphiteContext( | |
| const skgpu::graphite::ContextOptions& options) { | |
| #if BUILDFLAG(SKIA_USE_GRAPHITE_VULKAN) | |
| skgpu::VulkanExtensions vk_extensions; | |
| auto backend_context = GetBackendContextParams( | |
| vulkan_implementation_, device_queue_.get(), vk_extensions); | |
| auto graphite_context = | |
| skgpu::graphite::ContextFactory::MakeVulkan(backend_context, options); | |
| if (!graphite_context) { | |
| return false; | |
| } | |
| graphite_context_ = std::make_unique<gpu::GraphiteSharedContext>( | |
| std::move(graphite_context), nullptr, /*is_thread_safe=*/false, | |
| features::SkiaGraphiteMaxPendingRecordings(), | |
| features::SkiaGraphiteMaxTimeBetweenSubmits(), this); | |
| return true; | |
| #else | |
| return false; | |
| #endif // BUILDFLAG(SKIA_USE_GRAPHITE_VULKAN) | |
| } | |
| bool VulkanInProcessContextProvider::InitializeGrContext( | |
| const GrContextOptions& context_options) { | |
| skgpu::VulkanExtensions vk_extensions; | |
| auto backend_context = GetBackendContextParams( | |
| vulkan_implementation_, device_queue_.get(), vk_extensions); | |
| gr_context_ = GrDirectContexts::MakeVulkan(backend_context, context_options); | |
| return gr_context_ != nullptr; | |
| } | |
| void VulkanInProcessContextProvider::Destroy() { | |
| if (device_queue_) { | |
| // Destroy |fence_helper| will wait idle on the device queue, and then run | |
| // all enqueued cleanup tasks. | |
| auto* fence_helper = device_queue_->GetFenceHelper(); | |
| fence_helper->Destroy(); | |
| } | |
| if (gr_context_) { | |
| // releaseResourcesAndAbandonContext() will wait on GPU to finish all works, | |
| // execute pending flush done callbacks and release all resources. | |
| gr_context_->releaseResourcesAndAbandonContext(); | |
| gr_context_.reset(); | |
| } else if (graphite_context_) { | |
| graphite_context_.reset(); | |
| } | |
| if (device_queue_) { | |
| device_queue_->Destroy(); | |
| device_queue_.reset(); | |
| } | |
| } | |
| VulkanImplementation* | |
| VulkanInProcessContextProvider::GetVulkanImplementation() { | |
| return vulkan_implementation_; | |
| } | |
| VulkanDeviceQueue* VulkanInProcessContextProvider::GetDeviceQueue() { | |
| return device_queue_.get(); | |
| } | |
| GrDirectContext* VulkanInProcessContextProvider::GetGrContext() { | |
| return gr_context_.get(); | |
| } | |
| gpu::GraphiteSharedContext* | |
| VulkanInProcessContextProvider::GetGraphiteContext() { | |
| return graphite_context_.get(); | |
| } | |
| GrVkSecondaryCBDrawContext* | |
| VulkanInProcessContextProvider::GetGrSecondaryCBDrawContext() { | |
| return nullptr; | |
| } | |
| void VulkanInProcessContextProvider::EnqueueSecondaryCBSemaphores( | |
| std::vector<VkSemaphore> semaphores) { | |
| NOTREACHED(); | |
| } | |
| void VulkanInProcessContextProvider::EnqueueSecondaryCBPostSubmitTask( | |
| base::OnceClosure closure) { | |
| NOTREACHED(); | |
| } | |
| void VulkanInProcessContextProvider::FlushBackend() { | |
| // Do nothing as this is Windows specific. | |
| } | |
| void VulkanInProcessContextProvider::MarkContextLost( | |
| gpu::error::ContextLostReason reason) { | |
| context_lost_.store(true, std::memory_order::relaxed); | |
| } | |
| bool VulkanInProcessContextProvider::IsContextLost() const { | |
| return context_lost_.load(std::memory_order::relaxed); | |
| } | |
| void VulkanInProcessContextProvider::ReportProgress() { | |
| // TODO(crbug.com/552951905): Implement ProgressReporter hook. | |
| } | |
| base::OnceClosure | |
| VulkanInProcessContextProvider::CreateSubmitCleanupCallback() { | |
| // Associate all cleanup tasks enqueued so far with the upcoming submit so | |
| // they run once the GPU has finished with the resources they reference. | |
| return device_queue_->GetFenceHelper()->CreateExternalCallback(); | |
| } | |
| std::optional<uint32_t> VulkanInProcessContextProvider::GetSyncCpuMemoryLimit() | |
| const { | |
| // Return nullopt to indicate that there's no limit. | |
| if (!sync_cpu_memory_limit_) { | |
| return std::nullopt; | |
| } | |
| if (base::FeatureList::IsEnabled(base::kStatefulMemoryPressure)) { | |
| return active_sync_cpu_memory_limit_.load(std::memory_order_relaxed); | |
| } | |
| return base::TimeTicks::Now() < | |
| critical_memory_pressure_expiration_time_.load( | |
| std::memory_order_relaxed) | |
| ? std::optional<uint32_t>( | |
| kSyncCpuMemoryLimitAtMemoryPressureCritical) | |
| : sync_cpu_memory_limit_; | |
| } | |
| uint32_t VulkanInProcessContextProvider::GetCurrentGpuMemoryUsage() const { | |
| if (device_queue_) { | |
| return base::saturated_cast<uint32_t>( | |
| vma::GetTotalAllocatedAndUsedMemory(device_queue_->vma_allocator()) | |
| .first); | |
| } | |
| return 0; | |
| } | |
| void VulkanInProcessContextProvider::OnUpdateMemoryLimit() { | |
| if (!sync_cpu_memory_limit_) { | |
| return; | |
| } | |
| if (base::FeatureList::IsEnabled(base::kStatefulMemoryPressure)) { | |
| // We cap the ratio to 1.0 to ensure that we never exceed the | |
| // user-provided sync_cpu_memory_limit, even if the memory coordinator | |
| // allows for more memory usage (ratio > 1.0). | |
| base::MemoryLimit capped_memory_limit = | |
| std::min(base::MemoryLimit::NoPressureThreshold(), memory_limit()); | |
| uint32_t target_limit = capped_memory_limit.Scale(*sync_cpu_memory_limit_); | |
| uint32_t new_limit = target_limit; | |
| uint32_t current_active = | |
| active_sync_cpu_memory_limit_.load(std::memory_order_relaxed); | |
| if (target_limit <= current_active) { | |
| // Limit decreased: cap at current usage to prevent growth without | |
| // triggering immediate eviction. | |
| new_limit = std::max(GetCurrentGpuMemoryUsage(), target_limit); | |
| } | |
| active_sync_cpu_memory_limit_.store(new_limit, std::memory_order_relaxed); | |
| } | |
| } | |
| void VulkanInProcessContextProvider::OnReleaseMemory() { | |
| if (!sync_cpu_memory_limit_) { | |
| return; | |
| } | |
| if (base::FeatureList::IsEnabled(base::kStatefulMemoryPressure)) { | |
| // We cap the ratio to 1.0 to ensure that we never exceed the | |
| // user-provided sync_cpu_memory_limit, even if the memory coordinator | |
| // allows for more memory usage (ratio > 1.0). | |
| base::MemoryLimit capped_memory_limit = | |
| std::min(base::MemoryLimit::NoPressureThreshold(), memory_limit()); | |
| uint32_t new_sync_cpu_memory_limit = | |
| capped_memory_limit.Scale(*sync_cpu_memory_limit_); | |
| active_sync_cpu_memory_limit_.store(new_sync_cpu_memory_limit, | |
| std::memory_order_relaxed); | |
| return; | |
| } | |
| if (memory_limit() > base::MemoryLimit::CriticalPressureThreshold()) { | |
| return; | |
| } | |
| critical_memory_pressure_expiration_time_.store( | |
| base::TimeTicks::Now() + cooldown_duration_at_memory_pressure_critical_, | |
| std::memory_order_relaxed); | |
| } | |
| } // namespace gpu |