| // Copyright 2015 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/ipc/service/image_transport_surface_overlay_mac.h" |
| |
| #include <dawn/native/MetalBackend.h> |
| #include <dawn/webgpu_cpp.h> |
| |
| #include <memory> |
| #include <sstream> |
| #include <utility> |
| #include <variant> |
| |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/rand_util.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "components/viz/common/features.h" |
| #include "gpu/command_buffer/common/swap_buffers_complete_params.h" |
| #include "gpu/config/gpu_finch_features.h" |
| #include "gpu/ipc/service/gpu_channel_manager.h" |
| #include "gpu/ipc/service/gpu_channel_manager_delegate.h" |
| #include "ui/accelerated_widget_mac/ca_layer_tree_coordinator.h" |
| #include "ui/accelerated_widget_mac/ca_renderer_layer_tree.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| #include "ui/gfx/gpu_fence.h" |
| #include "ui/gfx/overlay_plane_data.h" |
| #include "ui/gl/ca_renderer_layer_params.h" |
| |
| #if BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_IOS_TVOS) |
| #include "gpu/ipc/common/ios/be_layer_hierarchy_transport.h" |
| #endif |
| |
| #if BUILDFLAG(SKIA_USE_DAWN) |
| #include "gpu/command_buffer/service/dawn_context_provider.h" |
| #endif |
| |
| // From ANGLE's EGL/eglext_angle.h. This should be included instead of being |
| // redefined here. |
| #ifndef EGL_ANGLE_device_metal |
| #define EGL_ANGLE_device_metal 1 |
| #define EGL_METAL_DEVICE_ANGLE 0x34A6 |
| #endif /* EGL_ANGLE_device_metal */ |
| |
| namespace gpu { |
| |
| namespace { |
| constexpr base::TimeDelta kHistogramMinTime = base::Microseconds(5); |
| constexpr base::TimeDelta kHistogramMaxTime = base::Milliseconds(16); |
| constexpr int kHistogramTimeBuckets = 50; |
| |
| // Control use of AVFoundation to draw video content. |
| BASE_FEATURE(kAVFoundationOverlays, |
| "avfoundation-overlays", |
| base::FEATURE_ENABLED_BY_DEFAULT); |
| |
| #if BUILDFLAG(IS_MAC) |
| BASE_FEATURE(kAllowCallbackWithoutPostTask, base::FEATURE_ENABLED_BY_DEFAULT); |
| |
| // Allows running completion callbacks and presnetation callbacks directly |
| // without posting tasks first. |
| bool AllowCallbackWithoutPostTask() { |
| static bool allows_no_post_task = |
| base::FeatureList::IsEnabled(kAllowCallbackWithoutPostTask) && |
| (ui::DisplayLinkMac::SupportsDisplayLinkMacInBrowser() || |
| base::FeatureList::IsEnabled(features::kEnableDrDc)); |
| return allows_no_post_task; |
| } |
| |
| // Record the delay from the system CVDisplayLink or CADisplaylink source to |
| // CrGpuMain OnVSyncPresentation(). |
| void RecordVSyncCallbackDelay(base::TimeDelta delay) { |
| UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES( |
| "GPU.Presentation.VSyncCallbackDelay", delay, |
| /*min=*/base::Microseconds(10), |
| /*max=*/base::Milliseconds(33), /*bucket_count=*/50); |
| } |
| #endif // BUILDFLAG(IS_MAC) |
| |
| id<MTLDevice> GetMTLDevice(scoped_refptr<SharedContextState> context_state) { |
| #if BUILDFLAG(SKIA_USE_DAWN) |
| if (context_state->IsGraphiteDawnMetal()) { |
| CHECK(context_state->dawn_context_provider()); |
| return dawn::native::metal::GetMTLDevice( |
| context_state->dawn_context_provider()->GetDevice().Get()); |
| } |
| #endif |
| if (context_state->GrContextIsGL()) { |
| EGLAttrib angle_device_attrib = 0; |
| if (eglQueryDisplayAttribEXT(context_state->display()->GetDisplay(), |
| EGL_DEVICE_EXT, &angle_device_attrib)) { |
| EGLDeviceEXT angle_device = |
| reinterpret_cast<EGLDeviceEXT>(angle_device_attrib); |
| EGLAttrib metal_device_attrib = 0; |
| if (eglQueryDeviceAttribEXT(angle_device, EGL_METAL_DEVICE_ANGLE, |
| &metal_device_attrib)) { |
| return (__bridge id)(void*)metal_device_attrib; |
| } |
| } |
| } |
| return nil; |
| } |
| |
| void BufferPresented(base::WeakPtr<gpu::ImageTransportSurfaceOverlayMacEGL> |
| image_transfer_weak_ptr, |
| gl::GLSurface::PresentationCallback callback, |
| const gfx::PresentationFeedback& feedback) { |
| if (!image_transfer_weak_ptr) { |
| return; |
| } |
| |
| DCHECK(!callback.is_null()); |
| std::move(callback).Run(feedback); |
| } |
| |
| } // namespace |
| |
| ImageTransportSurfaceOverlayMacEGL::ImageTransportSurfaceOverlayMacEGL( |
| scoped_refptr<SharedContextState> context_state, |
| SurfaceHandle surface_handle) |
| : weak_ptr_factory_(this) { |
| static bool av_disabled_at_command_line = |
| !base::FeatureList::IsEnabled(kAVFoundationOverlays); |
| |
| auto buffer_presented_callback = |
| base::BindRepeating(&BufferPresented, weak_ptr_factory_.GetWeakPtr()); |
| |
| auto gl_make_current_callback = |
| base::BindRepeating(&SharedContextState::MakeCurrent, context_state, |
| /*surface=*/nullptr, /*needs_gl=*/true); |
| |
| bool no_post_task_for_callback = false; |
| #if BUILDFLAG(IS_MAC) |
| no_post_task_for_callback = AllowCallbackWithoutPostTask(); |
| #endif |
| |
| ca_layer_tree_coordinator_ = std::make_unique<ui::CALayerTreeCoordinator>( |
| !av_disabled_at_command_line, std::move(buffer_presented_callback), |
| std::move(gl_make_current_callback), GetMTLDevice(context_state), |
| no_post_task_for_callback); |
| |
| #if BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_IOS_TVOS) |
| // The BELayerHierarchy needs to be created on a thread that supports |
| // libdispatch, so we proxy over to the main dispatch queue to do that. |
| CALayer* root_ca_layer = ca_layer_tree_coordinator_->root_ca_layer(); |
| __block xpc_object_t ipc_representation; |
| dispatch_sync(dispatch_get_main_queue(), ^{ |
| NSError* error = nullptr; |
| layer_hierarchy_ = [BELayerHierarchy layerHierarchyWithError:&error]; |
| layer_hierarchy_.layer = root_ca_layer; |
| ipc_representation = [layer_hierarchy_.handle createXPCRepresentation]; |
| }); |
| |
| BELayerHierarchyTransport* transport = |
| BELayerHierarchyTransport::GetInstance(); |
| CHECK(transport); |
| transport->ForwardBELayerHierarchyToBrowser(surface_handle, |
| ipc_representation); |
| #endif |
| } |
| |
| // For testing |
| ImageTransportSurfaceOverlayMacEGL::ImageTransportSurfaceOverlayMacEGL( |
| std::unique_ptr<ui::CALayerTreeCoordinator> ca_layer_tree_coordinator |
| #if BUILDFLAG(IS_MAC) |
| , |
| std::unique_ptr<ui::VSyncCallbackMac> vsync_callback_mac |
| #endif |
| ) |
| : ca_layer_tree_coordinator_(std::move(ca_layer_tree_coordinator)), |
| #if BUILDFLAG(IS_MAC) |
| vsync_callback_mac_(std::move(vsync_callback_mac)), |
| #endif |
| weak_ptr_factory_(this) { |
| } |
| |
| ImageTransportSurfaceOverlayMacEGL::~ImageTransportSurfaceOverlayMacEGL() { |
| ca_layer_tree_coordinator_.reset(); |
| |
| #if BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_IOS_TVOS) |
| // Capture and retain the BELayerHierarchy in a local __block var before |
| // dropping the member var ref. Do this before dispatch_async() to avoid a |
| // dealloc race between the block and the member var releasing the last ref. |
| __block BELayerHierarchy* layer_hierarchy = |
| std::exchange(layer_hierarchy_, nil); |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [layer_hierarchy invalidate]; |
| layer_hierarchy = nil; |
| }); |
| #endif |
| } |
| |
| void ImageTransportSurfaceOverlayMacEGL::Present( |
| SwapCompletionCallback completion_callback, |
| PresentationCallback presentation_callback, |
| gfx::FrameData data) { |
| TRACE_EVENT0("gpu", "ImageTransportSurfaceOverlayMac::Present"); |
| ca_layer_tree_coordinator_->SetCALayerErrorCode(data.ca_layer_error_code); |
| |
| // Commit the first pending frame before adding one more in Present() if there |
| // are more than supported . |
| if (ca_layer_tree_coordinator_->NumPendingSwaps() >= cap_max_pending_swaps_) { |
| TRACE_EVENT0("gpu", "Exceeds the max pending swaps. Commit now."); |
| CommitPresentedFrameToCA(); |
| } |
| |
| // Set the display HDR headroom to be used for any tone mapping to be done |
| // at the CoreAnimation level. |
| ca_layer_tree_coordinator_->GetPendingCARendererLayerTree() |
| ->SetDisplayHDRHeadroom(data.display_hdr_headroom); |
| |
| ca_layer_tree_coordinator_->Present(std::move(completion_callback), |
| std::move(presentation_callback)); |
| |
| #if BUILDFLAG(IS_MAC) |
| if (display_link_mac_ && !vsync_callback_mac_) { |
| vsync_callback_mac_ = |
| display_link_mac_->RegisterCallback(base::BindRepeating( |
| &ImageTransportSurfaceOverlayMacEGL::OnVSyncPresentation, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| bool delay_presentation_until_next_vsync = |
| features::IsVSyncAligned() || |
| (features::IsVSyncAlignedForScrolling() && data.is_handling_interaction); |
| |
| // The current frame has been added to |
| // ca_layer_tree_coordinator_->NumPendingSwaps() after calling |
| // ca_layer_tree_coordinator_->Present(). Check NumPendingSwaps() > 1 to see |
| // whether there is any previous pending frame. The current frame must wait in |
| // the queue if there is already one before this. |
| if ((features::IsVSyncAligned() || features::IsVSyncAlignedForScrolling()) && |
| ca_layer_tree_coordinator_->NumPendingSwaps() > 1) { |
| delay_presentation_until_next_vsync = true; |
| } |
| |
| if (vsync_callback_mac_) { |
| vsync_callback_mac_keep_alive_counter_ = kMaxKeepAliveCounter; |
| if (delay_presentation_until_next_vsync) { |
| // Delay CommitPresentedFrameToCA() until OnVSyncPresentation(). |
| return; |
| } |
| } |
| #endif |
| |
| CommitPresentedFrameToCA(); |
| } |
| |
| void ImageTransportSurfaceOverlayMacEGL::CommitPresentedFrameToCA() { |
| // Do a GL fence for flush to apply back-pressure before drawing. |
| { |
| base::TimeTicks start_time = base::TimeTicks::Now(); |
| ca_layer_tree_coordinator_->ApplyBackpressure(); |
| UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES( |
| "Gpu.Mac.BackpressureUs", base::TimeTicks::Now() - start_time, |
| kHistogramMinTime, kHistogramMaxTime, kHistogramTimeBuckets); |
| } |
| |
| // Update the CALayer tree in the GPU process. |
| { |
| base::TimeTicks display_time; |
| base::TimeDelta frame_interval; |
| #if BUILDFLAG(IS_MAC) |
| display_time = GetDisplaytime(base::TimeTicks::Now()); |
| frame_interval = frame_interval_; |
| #endif |
| TRACE_EVENT1("gpu", "CommitPresentedFrameToCA", "now_to_display", |
| (display_time - base::TimeTicks::Now()).InMicroseconds()); |
| ca_layer_tree_coordinator_->CommitPresentedFrameToCA(frame_interval, |
| display_time); |
| } |
| } |
| |
| bool ImageTransportSurfaceOverlayMacEGL::ScheduleCALayer( |
| const ui::CARendererLayerParams& params, |
| std::vector<gfx::MTLSharedEventFence> backpressure_fences) { |
| ca_layer_tree_coordinator_->EnqueueBackpressureFences( |
| std::move(backpressure_fences)); |
| return ca_layer_tree_coordinator_->GetPendingCARendererLayerTree() |
| ->ScheduleCALayer(params); |
| } |
| |
| bool ImageTransportSurfaceOverlayMacEGL::Resize( |
| const gfx::Size& pixel_size, |
| float scale_factor, |
| const gfx::ColorSpace& color_space, |
| bool has_alpha) { |
| ca_layer_tree_coordinator_->Resize(pixel_size, scale_factor); |
| return true; |
| } |
| |
| void ImageTransportSurfaceOverlayMacEGL::SetMaxPendingSwaps( |
| int max_pending_swaps) { |
| #if BUILDFLAG(IS_MAC) |
| cap_max_pending_swaps_ = max_pending_swaps; |
| |
| // MaxCALayerTrees is equal to the number of max_pending_swaps + one |
| // that has been displayed. |
| ca_layer_tree_coordinator_->SetMaxCALayerTrees(cap_max_pending_swaps_ + 1); |
| #endif |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| void ImageTransportSurfaceOverlayMacEGL::SetVSyncDisplayID(int64_t display_id) { |
| if (!display_link_mac_ || display_id != display_id_) { |
| vsync_callback_mac_ = nullptr; |
| |
| // Commit all pending frames before switching to the new monitor. |
| while (ca_layer_tree_coordinator_->NumPendingSwaps()) { |
| vsync_callback_mac_keep_alive_counter_ = |
| std::max(vsync_callback_mac_keep_alive_counter_, 1); |
| OnVSyncPresentation(ui::VSyncParamsMac()); |
| } |
| |
| display_link_mac_ = ui::DisplayLinkMac::GetForDisplay(display_id); |
| } |
| display_id_ = display_id; |
| } |
| |
| base::TimeTicks ImageTransportSurfaceOverlayMacEGL::GetDisplaytime( |
| base::TimeTicks latch_time) { |
| // From the CVDisplayLink params dump: |
| // |next_display_time_| ~= |current_display_time_| + |frame_interval|. |
| // params.display_time ~= params.callback_time + 1.5x |frame_interval|. |
| |
| // From the experiment, frames committed before (|current_display_time_| - 1.5 |
| // ms) will be displayed at the next display time. 1.5 ms is roughly the safe |
| // zone for the latch deadline. The result is inconsistent in the experiment |
| // if commit is too close to the display_time. |
| constexpr base::TimeDelta kLatchBufferTime = base::Microseconds(1500); |
| auto latch_deadline_for_next_display = |
| current_display_time_ - kLatchBufferTime; |
| if (latch_time < latch_deadline_for_next_display) { |
| return next_display_time_; |
| } |
| |
| // We just missed the |current_display_time|, the display will be at the next |
| // one after |next_display_time_|. |
| if (!frame_interval_.is_zero() && next_display_time_ != base::TimeTicks()) { |
| base::TimeTicks present_time = |
| latch_time.SnappedToNextTick(next_display_time_ - kLatchBufferTime, |
| frame_interval_) + |
| kLatchBufferTime + frame_interval_; |
| return present_time; |
| } |
| |
| // When there is no display_time info, just use the latch_time. |
| // This only happens at the very first frame after the browser starts, |
| return latch_time; |
| } |
| |
| // The CVDisplayLink callback on the GPU thread. |
| void ImageTransportSurfaceOverlayMacEGL::OnVSyncPresentation( |
| ui::VSyncParamsMac params) { |
| // Documentation for the CVDisplayLink display_time |
| // https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback |
| |
| base::TimeDelta callback_delay; |
| base::TimeDelta callback_timebase_to_display; |
| if (params.callback_times_valid && params.display_times_valid) { |
| callback_delay = base::TimeTicks::Now() - params.callback_timebase; |
| callback_timebase_to_display = |
| params.display_timebase - params.callback_timebase; |
| } |
| TRACE_EVENT2("gpu", "OnVSyncPresentation", "callback_timebase_to_display", |
| callback_timebase_to_display.InMicroseconds(), "callback_delay", |
| callback_delay.InMicroseconds()); |
| |
| current_display_time_ = next_display_time_; |
| |
| if (params.display_times_valid) { |
| next_display_time_ = params.display_timebase; |
| frame_interval_ = params.display_interval; |
| } |
| |
| if (params.callback_times_valid && |
| base::ShouldRecordSubsampledMetric(0.001)) { |
| RecordVSyncCallbackDelay(base::TimeTicks::Now() - params.callback_timebase); |
| } |
| |
| if (ca_layer_tree_coordinator_->NumPendingSwaps()) { |
| CommitPresentedFrameToCA(); |
| } |
| |
| vsync_callback_mac_keep_alive_counter_--; |
| |
| if (vsync_callback_mac_keep_alive_counter_ == 0) { |
| vsync_callback_mac_ = nullptr; |
| } |
| } |
| |
| #endif |
| } // namespace gpu |