| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/platform/graphics/canvas_resource_dispatcher.h" |
| |
| #include <utility> |
| |
| #include "base/debug/stack_trace.h" |
| #include "base/single_thread_task_runner.h" |
| #include "components/viz/common/quads/compositor_frame.h" |
| #include "components/viz/common/quads/texture_draw_quad.h" |
| #include "components/viz/common/resources/resource_format.h" |
| #include "components/viz/common/resources/single_release_callback.h" |
| #include "services/viz/public/mojom/compositing/frame_timing_details.mojom-blink.h" |
| #include "services/viz/public/mojom/hit_test/hit_test_region_list.mojom-blink.h" |
| #include "third_party/blink/public/common/thread_safe_browser_interface_broker_proxy.h" |
| #include "third_party/blink/public/mojom/frame_sinks/embedded_frame_sink.mojom-blink.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/public/platform/web_graphics_context_3d_provider.h" |
| #include "third_party/blink/renderer/platform/graphics/canvas_resource.h" |
| #include "third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.h" |
| #include "third_party/blink/renderer/platform/graphics/offscreen_canvas_placeholder.h" |
| #include "third_party/blink/renderer/platform/instrumentation/histogram.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h" |
| #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" |
| #include "ui/gfx/mojom/presentation_feedback.mojom-blink.h" |
| |
| namespace blink { |
| |
| enum { |
| kMaxPendingCompositorFrames = 2, |
| kMaxUnreclaimedPlaceholderFrames = 3, |
| }; |
| |
| struct CanvasResourceDispatcher::FrameResource { |
| FrameResource() = default; |
| ~FrameResource() { |
| if (release_callback) |
| release_callback->Run(sync_token, is_lost); |
| } |
| |
| // TODO(junov): What does this do? |
| bool spare_lock = true; |
| |
| std::unique_ptr<viz::SingleReleaseCallback> release_callback; |
| gpu::SyncToken sync_token; |
| bool is_lost = false; |
| }; |
| |
| CanvasResourceDispatcher::CanvasResourceDispatcher( |
| CanvasResourceDispatcherClient* client, |
| uint32_t client_id, |
| uint32_t sink_id, |
| int canvas_id, |
| const IntSize& size) |
| : frame_sink_id_(viz::FrameSinkId(client_id, sink_id)), |
| size_(size), |
| change_size_for_next_commit_(false), |
| needs_begin_frame_(false), |
| placeholder_canvas_id_(canvas_id), |
| num_unreclaimed_frames_posted_(0), |
| client_(client) { |
| // Frameless canvas pass an invalid |frame_sink_id_|; don't create mojo |
| // channel for this special case. |
| if (!frame_sink_id_.is_valid()) |
| return; |
| |
| DCHECK(!sink_.is_bound()); |
| mojo::Remote<mojom::blink::EmbeddedFrameSinkProvider> provider; |
| Platform::Current()->GetBrowserInterfaceBroker()->GetInterface( |
| provider.BindNewPipeAndPassReceiver()); |
| |
| DCHECK(provider); |
| provider->CreateCompositorFrameSink(frame_sink_id_, |
| receiver_.BindNewPipeAndPassRemote(), |
| sink_.BindNewPipeAndPassReceiver()); |
| provider->ConnectToEmbedder(frame_sink_id_, |
| surface_embedder_.BindNewPipeAndPassReceiver()); |
| } |
| |
| CanvasResourceDispatcher::~CanvasResourceDispatcher() = default; |
| |
| namespace { |
| |
| void UpdatePlaceholderImage( |
| int placeholder_canvas_id, |
| scoped_refptr<blink::CanvasResource> canvas_resource, |
| viz::ResourceId resource_id) { |
| DCHECK(IsMainThread()); |
| OffscreenCanvasPlaceholder* placeholder_canvas = |
| OffscreenCanvasPlaceholder::GetPlaceholderCanvasById( |
| placeholder_canvas_id); |
| if (placeholder_canvas) { |
| placeholder_canvas->SetOffscreenCanvasResource(std::move(canvas_resource), |
| resource_id); |
| } |
| } |
| |
| void UpdatePlaceholderDispatcher( |
| base::WeakPtr<CanvasResourceDispatcher> dispatcher, |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner, |
| int placeholder_canvas_id) { |
| OffscreenCanvasPlaceholder* placeholder_canvas = |
| OffscreenCanvasPlaceholder::GetPlaceholderCanvasById( |
| placeholder_canvas_id); |
| // Note that the placeholder canvas may be destroyed when this post task get |
| // to executed. |
| if (placeholder_canvas) |
| placeholder_canvas->SetOffscreenCanvasDispatcher(dispatcher, task_runner); |
| } |
| |
| } // namespace |
| |
| void CanvasResourceDispatcher::PostImageToPlaceholderIfNotBlocked( |
| scoped_refptr<CanvasResource> canvas_resource, |
| viz::ResourceId resource_id) { |
| if (placeholder_canvas_id_ == kInvalidPlaceholderCanvasId) { |
| ReclaimResourceInternal(resource_id); |
| return; |
| } |
| // Determines whether the main thread may be blocked. If unblocked, post |
| // |canvas_resource|. Otherwise, save it but do not post it. |
| if (num_unreclaimed_frames_posted_ < kMaxUnreclaimedPlaceholderFrames) { |
| this->PostImageToPlaceholder(std::move(canvas_resource), resource_id); |
| num_unreclaimed_frames_posted_++; |
| } else { |
| DCHECK(num_unreclaimed_frames_posted_ == kMaxUnreclaimedPlaceholderFrames); |
| if (latest_unposted_image_) { |
| // The previous unposted resource becomes obsolete now. |
| ReclaimResourceInternal(latest_unposted_resource_id_); |
| } |
| |
| latest_unposted_image_ = std::move(canvas_resource); |
| latest_unposted_resource_id_ = resource_id; |
| } |
| } |
| |
| void CanvasResourceDispatcher::PostImageToPlaceholder( |
| scoped_refptr<CanvasResource> canvas_resource, |
| viz::ResourceId resource_id) { |
| // After this point, |canvas_resource| can only be used on the main thread, |
| // until it is returned. |
| canvas_resource->Transfer(); |
| PostCrossThreadTask( |
| *Thread::MainThread()->Scheduler()->CompositorTaskRunner(), FROM_HERE, |
| CrossThreadBindOnce(UpdatePlaceholderImage, placeholder_canvas_id_, |
| std::move(canvas_resource), resource_id)); |
| } |
| |
| void CanvasResourceDispatcher::DispatchFrameSync( |
| scoped_refptr<CanvasResource> canvas_resource, |
| base::TimeTicks commit_start_time, |
| const SkIRect& damage_rect, |
| bool needs_vertical_flip, |
| bool is_opaque) { |
| TRACE_EVENT0("blink", "CanvasResourceDispatcher::DispatchFrameSync"); |
| viz::CompositorFrame frame; |
| if (!PrepareFrame(std::move(canvas_resource), commit_start_time, damage_rect, |
| needs_vertical_flip, is_opaque, &frame)) { |
| return; |
| } |
| |
| pending_compositor_frames_++; |
| WTF::Vector<viz::ReturnedResource> resources; |
| sink_->SubmitCompositorFrameSync( |
| parent_local_surface_id_allocator_.GetCurrentLocalSurfaceId(), |
| std::move(frame), base::nullopt, 0, &resources); |
| DidReceiveCompositorFrameAck(resources); |
| } |
| |
| void CanvasResourceDispatcher::DispatchFrame( |
| scoped_refptr<CanvasResource> canvas_resource, |
| base::TimeTicks commit_start_time, |
| const SkIRect& damage_rect, |
| bool needs_vertical_flip, |
| bool is_opaque) { |
| TRACE_EVENT0("blink", "CanvasResourceDispatcher::DispatchFrame"); |
| viz::CompositorFrame frame; |
| if (!PrepareFrame(std::move(canvas_resource), commit_start_time, damage_rect, |
| needs_vertical_flip, is_opaque, &frame)) { |
| return; |
| } |
| |
| pending_compositor_frames_++; |
| sink_->SubmitCompositorFrame( |
| parent_local_surface_id_allocator_.GetCurrentLocalSurfaceId(), |
| std::move(frame), base::nullopt, 0); |
| } |
| |
| bool CanvasResourceDispatcher::PrepareFrame( |
| scoped_refptr<CanvasResource> canvas_resource, |
| base::TimeTicks commit_start_time, |
| const SkIRect& damage_rect, |
| bool needs_vertical_flip, |
| bool is_opaque, |
| viz::CompositorFrame* frame) { |
| TRACE_EVENT0("blink", "CanvasResourceDispatcher::PrepareFrame"); |
| if (!canvas_resource || !VerifyImageSize(canvas_resource->Size())) { |
| return false; |
| } |
| |
| next_resource_id_++; |
| |
| // For frameless canvas, we don't get a valid frame_sink_id and should drop. |
| if (!frame_sink_id_.is_valid()) { |
| PostImageToPlaceholderIfNotBlocked(std::move(canvas_resource), |
| next_resource_id_); |
| return false; |
| } |
| |
| // TODO(crbug.com/652931): update the device_scale_factor |
| frame->metadata.device_scale_factor = 1.0f; |
| if (!current_begin_frame_ack_.frame_id.IsSequenceValid()) { |
| // TODO(eseckler): This shouldn't be necessary when OffscreenCanvas no |
| // longer submits CompositorFrames without prior BeginFrame. |
| current_begin_frame_ack_ = viz::BeginFrameAck::CreateManualAckWithDamage(); |
| } else { |
| current_begin_frame_ack_.has_damage = true; |
| } |
| frame->metadata.begin_frame_ack = current_begin_frame_ack_; |
| |
| frame->metadata.frame_token = ++next_frame_token_; |
| |
| const gfx::Rect bounds(size_.Width(), size_.Height()); |
| constexpr viz::CompositorRenderPassId kRenderPassId{1}; |
| constexpr bool is_clipped = false; |
| auto pass = |
| viz::CompositorRenderPass::Create(/*shared_quad_state_list_size=*/1u, |
| /*quad_list_size=*/1u); |
| pass->SetNew(kRenderPassId, bounds, |
| gfx::Rect(damage_rect.x(), damage_rect.y(), damage_rect.width(), |
| damage_rect.height()), |
| gfx::Transform()); |
| |
| viz::SharedQuadState* sqs = pass->CreateAndAppendSharedQuadState(); |
| sqs->SetAll(gfx::Transform(), bounds, bounds, gfx::MaskFilterInfo(), bounds, |
| is_clipped, is_opaque, 1.f, SkBlendMode::kSrcOver, 0); |
| |
| viz::TransferableResource resource; |
| auto frame_resource = std::make_unique<FrameResource>(); |
| |
| bool nearest_neighbor = |
| canvas_resource->FilterQuality() == kNone_SkFilterQuality; |
| |
| canvas_resource->PrepareTransferableResource( |
| &resource, &frame_resource->release_callback, kVerifiedSyncToken); |
| const unsigned resource_id = next_resource_id_; |
| resource.id = resource_id; |
| |
| resources_.insert(resource_id, std::move(frame_resource)); |
| |
| // TODO(crbug.com/869913): add unit testing for this. |
| const gfx::Size canvas_resource_size(canvas_resource->Size()); |
| |
| PostImageToPlaceholderIfNotBlocked(std::move(canvas_resource), resource_id); |
| |
| frame->resource_list.push_back(std::move(resource)); |
| |
| viz::TextureDrawQuad* quad = |
| pass->CreateAndAppendDrawQuad<viz::TextureDrawQuad>(); |
| |
| const bool needs_blending = !is_opaque; |
| // TODO(crbug.com/645993): this should be inherited from WebGL context's |
| // creation settings. |
| constexpr bool kPremultipliedAlpha = true; |
| constexpr gfx::PointF uv_top_left(0.f, 0.f); |
| constexpr gfx::PointF uv_bottom_right(1.f, 1.f); |
| constexpr float vertex_opacity[4] = {1.f, 1.f, 1.f, 1.f}; |
| |
| // Accelerated resources have the origin of coordinates in the upper left |
| // corner while canvases have it in the lower left corner. The DrawQuad is |
| // marked as vertically flipped unless someone else has done the flip for us. |
| const bool yflipped = |
| SharedGpuContext::IsGpuCompositingEnabled() && needs_vertical_flip; |
| quad->SetAll(sqs, bounds, bounds, needs_blending, resource_id, |
| canvas_resource_size, kPremultipliedAlpha, uv_top_left, |
| uv_bottom_right, SK_ColorTRANSPARENT, vertex_opacity, yflipped, |
| nearest_neighbor, /*secure_output_only=*/false, |
| gfx::ProtectedVideoType::kClear); |
| |
| frame->render_pass_list.push_back(std::move(pass)); |
| |
| if (change_size_for_next_commit_ || |
| !parent_local_surface_id_allocator_.HasValidLocalSurfaceId()) { |
| parent_local_surface_id_allocator_.GenerateId(); |
| surface_embedder_->SetLocalSurfaceId( |
| parent_local_surface_id_allocator_.GetCurrentLocalSurfaceId()); |
| change_size_for_next_commit_ = false; |
| } |
| |
| return true; |
| } |
| |
| void CanvasResourceDispatcher::DidReceiveCompositorFrameAck( |
| const WTF::Vector<viz::ReturnedResource>& resources) { |
| ReclaimResources(resources); |
| pending_compositor_frames_--; |
| DCHECK_GE(pending_compositor_frames_, 0); |
| } |
| |
| void CanvasResourceDispatcher::SetNeedsBeginFrame(bool needs_begin_frame) { |
| if (needs_begin_frame_ == needs_begin_frame) |
| return; |
| needs_begin_frame_ = needs_begin_frame; |
| if (!suspend_animation_) |
| SetNeedsBeginFrameInternal(); |
| } |
| |
| void CanvasResourceDispatcher::SetSuspendAnimation(bool suspend_animation) { |
| if (suspend_animation_ == suspend_animation) |
| return; |
| suspend_animation_ = suspend_animation; |
| if (needs_begin_frame_) |
| SetNeedsBeginFrameInternal(); |
| } |
| |
| void CanvasResourceDispatcher::SetNeedsBeginFrameInternal() { |
| if (sink_) |
| sink_->SetNeedsBeginFrame(needs_begin_frame_ && !suspend_animation_); |
| } |
| |
| bool CanvasResourceDispatcher::HasTooManyPendingFrames() const { |
| return pending_compositor_frames_ >= kMaxPendingCompositorFrames; |
| } |
| |
| void CanvasResourceDispatcher::OnBeginFrame( |
| const viz::BeginFrameArgs& begin_frame_args, |
| const WTF::HashMap<uint32_t, viz::FrameTimingDetails>&) { |
| current_begin_frame_ack_ = viz::BeginFrameAck(begin_frame_args, false); |
| if (HasTooManyPendingFrames() || |
| (begin_frame_args.type == viz::BeginFrameArgs::MISSED && |
| base::TimeTicks::Now() > begin_frame_args.deadline)) { |
| sink_->DidNotProduceFrame(current_begin_frame_ack_); |
| return; |
| } |
| |
| // TODO(fserb): should EnqueueMicrotask BeginFrame(). |
| // We usually never get to BeginFrame if we are on RAF mode. But it could |
| // still happen that begin frame gets requested and we don't have a frame |
| // anymore, so we shouldn't let the compositor wait. |
| bool submitted_frame = Client() && Client()->BeginFrame(); |
| if (!submitted_frame) { |
| sink_->DidNotProduceFrame(current_begin_frame_ack_); |
| } |
| |
| // TODO(fserb): Update this with the correct value if we are on RAF submit. |
| current_begin_frame_ack_.frame_id.sequence_number = |
| viz::BeginFrameArgs::kInvalidFrameNumber; |
| } |
| |
| void CanvasResourceDispatcher::ReclaimResources( |
| const WTF::Vector<viz::ReturnedResource>& resources) { |
| for (const auto& resource : resources) { |
| auto it = resources_.find(resource.id); |
| |
| DCHECK(it != resources_.end()); |
| if (it == resources_.end()) |
| continue; |
| |
| it->value->sync_token = resource.sync_token; |
| it->value->is_lost = resource.lost; |
| ReclaimResourceInternal(it); |
| } |
| } |
| |
| void CanvasResourceDispatcher::ReclaimResource(viz::ResourceId resource_id) { |
| ReclaimResourceInternal(resource_id); |
| |
| num_unreclaimed_frames_posted_--; |
| |
| // The main thread has become unblocked recently and we have an image that |
| // have not been posted yet. |
| if (latest_unposted_image_) { |
| DCHECK(num_unreclaimed_frames_posted_ == |
| kMaxUnreclaimedPlaceholderFrames - 1); |
| PostImageToPlaceholderIfNotBlocked(std::move(latest_unposted_image_), |
| latest_unposted_resource_id_); |
| latest_unposted_resource_id_ = 0; |
| } |
| } |
| |
| bool CanvasResourceDispatcher::VerifyImageSize(const IntSize image_size) { |
| return image_size == size_; |
| } |
| |
| void CanvasResourceDispatcher::Reshape(const IntSize& size) { |
| if (size_ != size) { |
| size_ = size; |
| change_size_for_next_commit_ = true; |
| } |
| } |
| |
| void CanvasResourceDispatcher::DidAllocateSharedBitmap( |
| base::ReadOnlySharedMemoryRegion region, |
| const gpu::Mailbox& id) { |
| if (sink_) |
| sink_->DidAllocateSharedBitmap(std::move(region), id); |
| } |
| |
| void CanvasResourceDispatcher::DidDeleteSharedBitmap(const gpu::Mailbox& id) { |
| if (sink_) |
| sink_->DidDeleteSharedBitmap(id); |
| } |
| |
| void CanvasResourceDispatcher::SetFilterQuality( |
| SkFilterQuality filter_quality) { |
| if (Client()) |
| Client()->SetFilterQualityInResource(filter_quality); |
| } |
| |
| void CanvasResourceDispatcher::SetPlaceholderCanvasDispatcher( |
| int placeholder_canvas_id) { |
| scoped_refptr<base::SingleThreadTaskRunner> dispatcher_task_runner = |
| Thread::Current()->GetTaskRunner(); |
| |
| // If the offscreencanvas is in the same tread as the canvas, we will update |
| // the canvas resource dispatcher directly. So Offscreen Canvas can behave in |
| // a more synchronous way when it's on the main thread. |
| if (IsMainThread()) { |
| UpdatePlaceholderDispatcher(this->GetWeakPtr(), dispatcher_task_runner, |
| placeholder_canvas_id); |
| } else { |
| PostCrossThreadTask( |
| *Thread::MainThread()->Scheduler()->CompositorTaskRunner(), FROM_HERE, |
| CrossThreadBindOnce(UpdatePlaceholderDispatcher, this->GetWeakPtr(), |
| WTF::Passed(std::move(dispatcher_task_runner)), |
| placeholder_canvas_id)); |
| } |
| } |
| |
| void CanvasResourceDispatcher::ReclaimResourceInternal( |
| viz::ResourceId resource_id) { |
| auto it = resources_.find(resource_id); |
| if (it != resources_.end()) |
| ReclaimResourceInternal(it); |
| } |
| |
| void CanvasResourceDispatcher::ReclaimResourceInternal( |
| const ResourceMap::iterator& it) { |
| if (it->value->spare_lock) { |
| it->value->spare_lock = false; |
| return; |
| } |
| resources_.erase(it); |
| } |
| |
| } // namespace blink |