| // Copyright 2017 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/compositing/paint_chunks_to_cc_layer.h" |
| |
| #include "cc/paint/display_item_list.h" |
| #include "cc/paint/paint_op_buffer.h" |
| #include "cc/paint/render_surface_filters.h" |
| #include "third_party/blink/renderer/platform/graphics/compositing/chunk_to_layer_mapper.h" |
| #include "third_party/blink/renderer/platform/graphics/graphics_context.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/display_item_list.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/drawing_display_item.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_chunk.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_chunk_subset.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/property_tree_state.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/raster_invalidation_tracking.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // TODO(crbug.com/853357): This is a temporary work-around for a bug. Unalias on |
| // a node should always succeed if the node is not null. The code below assumes |
| // that the node is not null, so we should be calling Unalias directly. However, |
| // because of the referenced bug, which triggers a DCHECK, the node can in fact |
| // sometimes be null. This turns the bug symptom from a DCHECK to a |
| // null-dereference, which crashes release. |
| template <typename NodeType> |
| const NodeType* Unalias(const NodeType* node) { |
| return node ? node->Unalias() : nullptr; |
| } |
| |
| class ConversionContext { |
| public: |
| ConversionContext(const PropertyTreeState& layer_state, |
| const gfx::Vector2dF& layer_offset, |
| const FloatSize& visual_rect_subpixel_offset, |
| cc::DisplayItemList& cc_list) |
| : layer_state_(layer_state), |
| layer_offset_(layer_offset), |
| current_transform_(Unalias(layer_state.Transform())), |
| current_clip_(Unalias(layer_state.Clip())), |
| current_effect_(Unalias(layer_state.Effect())), |
| chunk_to_layer_mapper_(layer_state_, |
| layer_offset_, |
| visual_rect_subpixel_offset), |
| cc_list_(cc_list) {} |
| ~ConversionContext(); |
| |
| // The main function of this class. It converts a list of paint chunks into |
| // non-pair display items, and paint properties associated with them are |
| // implemented by paired display items. |
| // This is done by closing and opening paired items to adjust the current |
| // property state to the chunk's state when each chunk is consumed. |
| // Note that the clip/effect state is "lazy" in the sense that it stays |
| // in whatever state the last chunk left with, and only adjusted when |
| // a new chunk is consumed. The class implemented a few helpers to manage |
| // state switching so that paired display items are nested properly. |
| // |
| // State management example (transform tree omitted). |
| // Corresponds to unit test PaintChunksToCcLayerTest.InterleavedClipEffect: |
| // Clip tree: C0 <-- C1 <-- C2 <-- C3 <-- C4 |
| // Effect tree: E0(clip=C0) <-- E1(clip=C2) <-- E2(clip=C4) |
| // Layer state: C0, E0 |
| // Paint chunks: P0(C3, E0), P1(C4, E2), P2(C3, E1), P3(C4, E0) |
| // Initialization: |
| // The current state is initalized with the layer state, and starts with |
| // an empty state stack. |
| // current_clip = C0 |
| // current_effect = E0 |
| // state_stack = [] |
| // When P0 is consumed, C1, C2 and C3 need to be applied to the state: |
| // Output: Begin_C1 Begin_C2 Begin_C3 Draw_P0 |
| // current_clip = C3 |
| // state_stack = [C0, C1, C2] |
| // When P1 is consumed, C3 needs to be closed before E1 can be entered, |
| // then C3 and C4 need to be entered before E2 can be entered: |
| // Output: End_C3 Begin_E1 Begin_C3 Begin_C4 Begin_E2 Draw_P1 |
| // current_clip = C4 |
| // current_effect = E2 |
| // state_stack = [C0, C1, E0, C2, C3, E1] |
| // When P2 is consumed, E2 then C4 need to be exited: |
| // Output: End_E2 End_C4 Draw_P2 |
| // current_clip = C3 |
| // current_effect = E1 |
| // state_stack = [C0, C1, E0, C2] |
| // When P3 is consumed, C3 must exit before E1 can be exited, then we can |
| // enter C3 and C4: |
| // Output: End_C3 End_E1 Enter_C3 Enter_C4 Draw_P3 |
| // current_clip = C4 |
| // current_effect = E0 |
| // state_stack = [C0, C1, C2, C3] |
| // At last, close all pushed states to balance pairs (this happens when the |
| // context object is destructed): |
| // Output: End_C4 End_C3 End_C2 End_C1 |
| void Convert(const PaintChunkSubset&, const DisplayItemList&); |
| |
| private: |
| // Adjust the translation of the whole display list relative to layer offset. |
| // It's only called if we actually paint anything. |
| void TranslateForLayerOffsetOnce(); |
| |
| // Switch the current property tree state to the chunk's state. It's only |
| // called if we actually paint anything, and should execute for a chunk |
| // only once. |
| void SwitchToChunkState(const PaintChunk&); |
| |
| // Switch the current clip to the target state, staying in the same effect. |
| // It is no-op if the context is already in the target state. |
| // Otherwise zero or more clips will be popped from or pushed onto the |
| // current state stack. |
| // INPUT: |
| // The target clip must be a descendant of the input clip of current effect. |
| // OUTPUT: |
| // The current transform may be changed. |
| // The current clip will change to the target clip. |
| // The current effect will not change. |
| void SwitchToClip(const ClipPaintPropertyNode*); |
| |
| // Switch the current effect to the target state. |
| // It is no-op if the context is already in the target state. |
| // Otherwise zero or more effect effects will be popped from or pushed onto |
| // the state stack. As effects getting popped from the stack, clips applied |
| // on top of them will be popped as well. Also clips will be pushed at |
| // appropriate steps to apply output clip to newly pushed effects. |
| // INPUT: |
| // The target effect must be a descendant of the layer's effect. |
| // OUTPUT: |
| // The current transform may be changed. |
| // The current clip may be changed, and is guaranteed to be a descendant of |
| // the output clip of the target effect. |
| // The current effect will change to the target effect. |
| void SwitchToEffect(const EffectPaintPropertyNode*); |
| |
| // Switch the current transform to the target state. |
| void SwitchToTransform(const TransformPaintPropertyNode*); |
| // End the transform state that is estalished by SwitchToTransform(). |
| // Called when the next chunk has different property tree state and when we |
| // have processed all chunks. |
| void EndTransform(); |
| |
| // Applies combined transform from |current_transform_| to |target_transform| |
| // This function doesn't change |current_transform_|. |
| void ApplyTransform(const TransformPaintPropertyNode* target_transform) { |
| if (target_transform == current_transform_) |
| return; |
| auto sk_matrix = GetSkMatrix(target_transform); |
| if (!sk_matrix.isIdentity()) |
| cc_list_.push<cc::ConcatOp>(sk_matrix); |
| } |
| |
| SkMatrix GetSkMatrix( |
| const TransformPaintPropertyNode* target_transform) const { |
| return SkMatrix(TransformationMatrix::ToSkMatrix44( |
| GeometryMapper::SourceToDestinationProjection(target_transform, |
| current_transform_))); |
| } |
| |
| void AppendRestore(size_t n) { |
| cc_list_.StartPaint(); |
| while (n--) |
| cc_list_.push<cc::RestoreOp>(); |
| cc_list_.EndPaintOfPairedEnd(); |
| } |
| |
| // Starts an effect state by adjusting clip and transform state, applying |
| // the effect as a SaveLayer[Alpha]Op (whose bounds will be updated in |
| // EndEffect()), and updating the current state. |
| void StartEffect(const EffectPaintPropertyNode*); |
| // Ends the effect on the top of the state stack if the stack is not empty, |
| // and update the bounds of the SaveLayer[Alpha]Op of the effect. |
| void EndEffect(); |
| void UpdateEffectBounds(const FloatRect&, const TransformPaintPropertyNode*); |
| |
| // Starts a clip state by adjusting the transform state, applying |
| // |combined_clip_rect| which is combined from one or more consecutive clips, |
| // and updating the current state. |lowest_combined_clip_node| is the lowest |
| // node of the combined clips. |
| void StartClip(const FloatRoundedRect& combined_clip_rect, |
| const ClipPaintPropertyNode* lowest_combined_clip_node); |
| // Pop one clip state from the top of the stack. |
| void EndClip(); |
| // Pop clip states from the top of the stack until the top is an effect state |
| // or the stack is empty. |
| void EndClips(); |
| |
| // State stack. |
| // The size of the stack is the number of nested paired items that are |
| // currently nested. Note that this is a "restore stack", i.e. the top |
| // element does not represent the current state, but the state prior to |
| // applying the last paired begin. |
| struct StateEntry { |
| // Remembers the type of paired begin that caused a state to be saved. |
| // This is for checking integrity of the algorithm. |
| enum PairedType { kClip, kEffect } type; |
| int saved_count; |
| |
| const TransformPaintPropertyNode* transform; |
| const ClipPaintPropertyNode* clip; |
| const EffectPaintPropertyNode* effect; |
| // See ConversionContext::previous_transform_. |
| const TransformPaintPropertyNode* previous_transform; |
| }; |
| void PushState(StateEntry::PairedType, int saved_count); |
| void PopState(); |
| Vector<StateEntry> state_stack_; |
| |
| const PropertyTreeState& layer_state_; |
| gfx::Vector2dF layer_offset_; |
| bool translated_for_layer_offset_ = false; |
| |
| const TransformPaintPropertyNode* current_transform_; |
| const ClipPaintPropertyNode* current_clip_; |
| const EffectPaintPropertyNode* current_effect_; |
| |
| // The previous transform state before SwitchToTransform() within the current |
| // clip/effect state. When the next chunk's transform is different from the |
| // current transform we should restore to this transform using EndTransform() |
| // which will set this field to nullptr. When a new clip/effect state starts, |
| // the value of this field will be saved into the state stack and set to |
| // nullptr. When the clip/effect state ends, this field will be restored to |
| // the saved value. |
| const TransformPaintPropertyNode* previous_transform_ = nullptr; |
| |
| // This structure accumulates bounds of all chunks under an effect. When an |
| // effect starts, we emit a SaveLayer[Alpha]Op with null bounds starts, and |
| // push a new |EffectBoundsInfo| onto |effect_bounds_stack_|. When the effect |
| // ends, we update the bounds of the op. |
| struct EffectBoundsInfo { |
| // The id of the SaveLayer[Alpha]Op for this effect. It's recorded when we |
| // push the op for this effect, and used when this effect ends in |
| // UpdateSaveLayerBounds(). |
| size_t save_layer_id; |
| // The transform space when the SaveLayer[Alpha]Op was emitted. |
| const TransformPaintPropertyNode* transform; |
| // Records the bounds of the effect which initiated the entry. Note that |
| // the effect is not |this->effect| (which is the previous effect), but the |
| // |current_effect_| when this entry is the top of the stack. |
| FloatRect bounds; |
| }; |
| Vector<EffectBoundsInfo> effect_bounds_stack_; |
| |
| ChunkToLayerMapper chunk_to_layer_mapper_; |
| |
| cc::DisplayItemList& cc_list_; |
| }; |
| |
| ConversionContext::~ConversionContext() { |
| // End all states. |
| while (state_stack_.size()) { |
| if (state_stack_.back().type == StateEntry::kEffect) |
| EndEffect(); |
| else |
| EndClip(); |
| } |
| EndTransform(); |
| if (translated_for_layer_offset_) |
| AppendRestore(1); |
| } |
| |
| void ConversionContext::TranslateForLayerOffsetOnce() { |
| if (translated_for_layer_offset_ || layer_offset_.IsZero()) |
| return; |
| |
| cc_list_.StartPaint(); |
| cc_list_.push<cc::SaveOp>(); |
| cc_list_.push<cc::TranslateOp>(-layer_offset_.x(), -layer_offset_.y()); |
| cc_list_.EndPaintOfPairedBegin(); |
| translated_for_layer_offset_ = true; |
| } |
| |
| void ConversionContext::SwitchToChunkState(const PaintChunk& chunk) { |
| chunk_to_layer_mapper_.SwitchToChunk(chunk); |
| |
| const auto& chunk_state = chunk.properties; |
| SwitchToEffect(chunk_state.Effect()); |
| SwitchToClip(chunk_state.Clip()); |
| SwitchToTransform(chunk_state.Transform()); |
| } |
| |
| // Tries to combine a clip node's clip rect into |combined_clip_rect|. |
| // Returns whether the clip has been combined. |
| static bool CombineClip(const ClipPaintPropertyNode* clip, |
| FloatRoundedRect& combined_clip_rect) { |
| // Don't combine into a clip with clip path. |
| if (clip->Parent()->ClipPath()) |
| return false; |
| |
| // Don't combine clips in different transform spaces. |
| const auto* transform_space = clip->LocalTransformSpace(); |
| const auto* parent_transform_space = clip->Parent()->LocalTransformSpace(); |
| if (transform_space != parent_transform_space && |
| (transform_space->Parent() != parent_transform_space || |
| !transform_space->Matrix().IsIdentity())) |
| return false; |
| |
| // Don't combine two rounded clip rects. |
| bool clip_is_rounded = clip->ClipRect().IsRounded(); |
| bool combined_is_rounded = combined_clip_rect.IsRounded(); |
| if (clip_is_rounded && combined_is_rounded) |
| return false; |
| |
| // If one is rounded and the other contains the rounded bounds, use the |
| // rounded as the combined. |
| if (combined_is_rounded) |
| return clip->ClipRect().Rect().Contains(combined_clip_rect.Rect()); |
| if (clip_is_rounded) { |
| if (combined_clip_rect.Rect().Contains(clip->ClipRect().Rect())) { |
| combined_clip_rect = clip->ClipRect(); |
| return true; |
| } |
| return false; |
| } |
| |
| // The combined is the intersection if both are rectangular. |
| DCHECK(!combined_is_rounded && !clip_is_rounded); |
| combined_clip_rect = FloatRoundedRect( |
| Intersection(combined_clip_rect.Rect(), clip->ClipRect().Rect())); |
| return true; |
| } |
| |
| void ConversionContext::SwitchToClip(const ClipPaintPropertyNode* target_clip) { |
| target_clip = Unalias(target_clip); |
| if (target_clip == current_clip_) |
| return; |
| |
| // Step 1: Exit all clips until the lowest common ancestor is found. |
| const ClipPaintPropertyNode* lca_clip = |
| LowestCommonAncestor(*target_clip, *current_clip_).Unalias(); |
| while (current_clip_ != lca_clip) { |
| if (!state_stack_.size() || state_stack_.back().type != StateEntry::kClip) { |
| #if DCHECK_IS_ON() |
| DLOG(ERROR) << "Error: Chunk has a clip that escaped its layer's or " |
| "effect's clip." |
| << "\ntarget_clip:\n" |
| << target_clip->ToTreeString().Utf8().data() |
| << "current_clip_:\n" |
| << current_clip_->ToTreeString().Utf8().data(); |
| #endif |
| // This bug is known to happen in SPv1 due to some clip-escaping corner |
| // cases that are very difficult to fix in legacy architecture. |
| // In SPv2 this should never happen. |
| if (RuntimeEnabledFeatures::SlimmingPaintV2Enabled()) |
| NOTREACHED(); |
| break; |
| } |
| current_clip_ = Unalias(current_clip_->Parent()); |
| StateEntry& previous_state = state_stack_.back(); |
| if (current_clip_ == lca_clip) { |
| // |lca_clip| is an intermediate clip in a series of combined clips. |
| // Jump to the first of the combined clips. |
| current_clip_ = lca_clip = previous_state.clip; |
| } |
| if (current_clip_ == previous_state.clip) |
| EndClip(); |
| } |
| |
| if (target_clip == current_clip_) |
| return; |
| |
| // Step 2: Collect all clips between the target clip and the current clip. |
| // At this point the current clip must be an ancestor of the target. |
| Vector<const ClipPaintPropertyNode*, 1u> pending_clips; |
| for (const ClipPaintPropertyNode* clip = target_clip; clip != current_clip_; |
| clip = Unalias(clip->Parent())) { |
| // This should never happen unless the DCHECK in step 1 failed. |
| if (!clip) |
| break; |
| pending_clips.push_back(clip); |
| } |
| |
| // Step 3: Now apply the list of clips in top-down order. |
| DCHECK(pending_clips.size()); |
| auto pending_combined_clip_rect = pending_clips.back()->ClipRect(); |
| const auto* lowest_combined_clip_node = pending_clips.back(); |
| for (size_t i = pending_clips.size() - 1; i--;) { |
| const auto* sub_clip = pending_clips[i]; |
| if (CombineClip(sub_clip, pending_combined_clip_rect)) { |
| // Continue to combine. |
| lowest_combined_clip_node = sub_clip; |
| } else { |
| // |sub_clip| can't be combined to previous clips. Output the current |
| // combined clip, and start new combination. |
| StartClip(pending_combined_clip_rect, lowest_combined_clip_node); |
| pending_combined_clip_rect = sub_clip->ClipRect(); |
| lowest_combined_clip_node = sub_clip; |
| } |
| } |
| StartClip(pending_combined_clip_rect, lowest_combined_clip_node); |
| |
| DCHECK_EQ(current_clip_, target_clip); |
| } |
| |
| void ConversionContext::StartClip( |
| const FloatRoundedRect& combined_clip_rect, |
| const ClipPaintPropertyNode* lowest_combined_clip_node) { |
| DCHECK_EQ(lowest_combined_clip_node, Unalias(lowest_combined_clip_node)); |
| auto* local_transform = |
| Unalias(lowest_combined_clip_node->LocalTransformSpace()); |
| if (local_transform != current_transform_) |
| EndTransform(); |
| cc_list_.StartPaint(); |
| cc_list_.push<cc::SaveOp>(); |
| ApplyTransform(local_transform); |
| const bool antialias = true; |
| if (combined_clip_rect.IsRounded()) { |
| cc_list_.push<cc::ClipRRectOp>(combined_clip_rect, SkClipOp::kIntersect, |
| antialias); |
| } else { |
| cc_list_.push<cc::ClipRectOp>(combined_clip_rect.Rect(), |
| SkClipOp::kIntersect, antialias); |
| } |
| if (lowest_combined_clip_node->ClipPath()) { |
| cc_list_.push<cc::ClipPathOp>( |
| lowest_combined_clip_node->ClipPath()->GetSkPath(), |
| SkClipOp::kIntersect, antialias); |
| } |
| cc_list_.EndPaintOfPairedBegin(); |
| |
| PushState(StateEntry::kClip, 1); |
| current_clip_ = lowest_combined_clip_node; |
| current_transform_ = local_transform; |
| } |
| |
| void ConversionContext::SwitchToEffect( |
| const EffectPaintPropertyNode* target_effect) { |
| target_effect = Unalias(target_effect); |
| if (target_effect == current_effect_) |
| return; |
| |
| // Step 1: Exit all effects until the lowest common ancestor is found. |
| const EffectPaintPropertyNode* lca_effect = |
| LowestCommonAncestor(*target_effect, *current_effect_).Unalias(); |
| while (current_effect_ != lca_effect) { |
| // This EndClips() and the later EndEffect() pop to the parent effect. |
| EndClips(); |
| #if DCHECK_IS_ON() |
| DCHECK(state_stack_.size()) |
| << "Error: Chunk has an effect that escapes layer's effect.\n" |
| << "target_effect:\n" |
| << target_effect->ToTreeString().Utf8().data() << "current_effect_:\n" |
| << current_effect_->ToTreeString().Utf8().data(); |
| #endif |
| if (!state_stack_.size()) |
| break; |
| EndEffect(); |
| } |
| |
| // Step 2: Collect all effects between the target effect and the current |
| // effect. At this point the current effect must be an ancestor of the target. |
| Vector<const EffectPaintPropertyNode*, 1u> pending_effects; |
| for (const EffectPaintPropertyNode* effect = target_effect; |
| effect != current_effect_; effect = Unalias(effect->Parent())) { |
| // This should never happen unless the DCHECK in step 1 failed. |
| if (!effect) |
| break; |
| pending_effects.push_back(effect); |
| } |
| |
| // Step 3: Now apply the list of effects in top-down order. |
| for (size_t i = pending_effects.size(); i--;) { |
| const EffectPaintPropertyNode* sub_effect = pending_effects[i]; |
| DCHECK_EQ(current_effect_, Unalias(sub_effect->Parent())); |
| StartEffect(sub_effect); |
| } |
| } |
| |
| void ConversionContext::StartEffect(const EffectPaintPropertyNode* effect) { |
| DCHECK_EQ(effect, Unalias(effect)); |
| |
| // Before each effect can be applied, we must enter its output clip first, |
| // or exit all clips if it doesn't have one. |
| if (effect->OutputClip()) |
| SwitchToClip(effect->OutputClip()); |
| else |
| EndClips(); |
| |
| int saved_count = 0; |
| size_t save_layer_id = kNotFound; |
| |
| // Adjust transform first. Though a non-filter effect itself doesn't depend on |
| // the transform, switching to the target transform before SaveLayer[Alpha]Op |
| // will help the rasterizer optimize a non-filter SaveLayer[Alpha]Op/ |
| // DrawRecord/Restore sequence into a single DrawRecord which is much faster. |
| // This also avoids multiple Save/Concat/.../Restore pairs for multiple |
| // consecutive effects in the same transform space, by issuing only one pair |
| // around all of the effects. |
| SwitchToTransform(effect->LocalTransformSpace()); |
| |
| // We always create separate effect nodes for normal effects and filter |
| // effects, so we can handle them separately. |
| bool has_filter = !effect->Filter().IsEmpty(); |
| bool has_opacity = effect->Opacity() != 1.f; |
| bool has_other_effects = effect->BlendMode() != SkBlendMode::kSrcOver || |
| effect->GetColorFilter() != kColorFilterNone; |
| DCHECK(!has_filter || !(has_opacity || has_other_effects)); |
| |
| // TODO(crbug.com/904592): Add support for non-composited backdrop-filter |
| // here. |
| |
| // Apply effects. |
| cc_list_.StartPaint(); |
| if (!has_filter) { |
| // TODO(ajuma): This should really be rounding instead of flooring the |
| // alpha value, but that breaks slimming paint reftests. |
| auto alpha = |
| static_cast<uint8_t>(gfx::ToFlooredInt(255 * effect->Opacity())); |
| if (has_other_effects) { |
| PaintFlags flags; |
| flags.setBlendMode(effect->BlendMode()); |
| flags.setAlpha(alpha); |
| flags.setColorFilter(GraphicsContext::WebCoreColorFilterToSkiaColorFilter( |
| effect->GetColorFilter())); |
| save_layer_id = cc_list_.push<cc::SaveLayerOp>(nullptr, &flags); |
| } else { |
| constexpr bool preserve_lcd_text_requests = false; |
| save_layer_id = cc_list_.push<cc::SaveLayerAlphaOp>( |
| nullptr, alpha, preserve_lcd_text_requests); |
| } |
| saved_count++; |
| } else { |
| // Handle filter effect. |
| FloatPoint filter_origin = effect->FiltersOrigin(); |
| if (filter_origin != FloatPoint()) { |
| cc_list_.push<cc::SaveOp>(); |
| cc_list_.push<cc::TranslateOp>(filter_origin.X(), filter_origin.Y()); |
| saved_count++; |
| } |
| // The size parameter is only used to computed the origin of zoom |
| // operation, which we never generate. |
| gfx::SizeF empty; |
| PaintFlags filter_flags; |
| filter_flags.setImageFilter(cc::RenderSurfaceFilters::BuildImageFilter( |
| effect->Filter().AsCcFilterOperations(), empty)); |
| save_layer_id = cc_list_.push<cc::SaveLayerOp>(nullptr, &filter_flags); |
| saved_count++; |
| if (filter_origin != FloatPoint()) |
| cc_list_.push<cc::TranslateOp>(-filter_origin.X(), -filter_origin.Y()); |
| } |
| cc_list_.EndPaintOfPairedBegin(); |
| |
| DCHECK_GT(saved_count, 0); |
| DCHECK_LE(saved_count, 2); |
| DCHECK_NE(save_layer_id, kNotFound); |
| |
| // Adjust state and push previous state onto effect stack. |
| // TODO(trchen): Change input clip to expansion hint once implemented. |
| const ClipPaintPropertyNode* input_clip = current_clip_; |
| PushState(StateEntry::kEffect, saved_count); |
| effect_bounds_stack_.emplace_back( |
| EffectBoundsInfo{save_layer_id, current_transform_}); |
| current_clip_ = input_clip; |
| current_effect_ = effect; |
| } |
| |
| void ConversionContext::UpdateEffectBounds( |
| const FloatRect& bounds, |
| const TransformPaintPropertyNode* transform) { |
| if (effect_bounds_stack_.IsEmpty() || bounds.IsEmpty()) |
| return; |
| |
| auto& effect_bounds_info = effect_bounds_stack_.back(); |
| FloatRect mapped_bounds = bounds; |
| GeometryMapper::SourceToDestinationRect( |
| transform, effect_bounds_info.transform, mapped_bounds); |
| effect_bounds_info.bounds.Unite(mapped_bounds); |
| } |
| |
| void ConversionContext::EndEffect() { |
| const auto& previous_state = state_stack_.back(); |
| DCHECK_EQ(previous_state.type, StateEntry::kEffect); |
| DCHECK(Unalias(current_effect_->Parent()) == previous_state.effect); |
| DCHECK_EQ(current_clip_, previous_state.clip); |
| |
| DCHECK(effect_bounds_stack_.size()); |
| const auto& bounds_info = effect_bounds_stack_.back(); |
| FloatRect bounds = bounds_info.bounds; |
| if (!bounds.IsEmpty()) { |
| if (current_effect_->Filter().IsEmpty()) { |
| cc_list_.UpdateSaveLayerBounds(bounds_info.save_layer_id, bounds); |
| } else { |
| // The bounds for the SaveLayer[Alpha]Op should be the source bounds |
| // before the filter is applied, in the space of the TranslateOp which was |
| // emitted before the SaveLayer[Alpha]Op. |
| auto save_layer_bounds = bounds; |
| save_layer_bounds.MoveBy(-current_effect_->FiltersOrigin()); |
| cc_list_.UpdateSaveLayerBounds(bounds_info.save_layer_id, |
| save_layer_bounds); |
| // We need to propagate the filtered bounds to the parent. |
| bounds = current_effect_->MapRect(bounds); |
| } |
| } |
| |
| effect_bounds_stack_.pop_back(); |
| EndTransform(); |
| // Propagate the bounds to the parent effect. |
| UpdateEffectBounds(bounds, current_transform_); |
| PopState(); |
| } |
| |
| void ConversionContext::EndClips() { |
| while (state_stack_.size() && state_stack_.back().type == StateEntry::kClip) |
| EndClip(); |
| } |
| |
| void ConversionContext::EndClip() { |
| DCHECK_EQ(state_stack_.back().type, StateEntry::kClip); |
| DCHECK_EQ(state_stack_.back().effect, current_effect_); |
| EndTransform(); |
| PopState(); |
| } |
| |
| void ConversionContext::PushState(StateEntry::PairedType type, |
| int saved_count) { |
| state_stack_.emplace_back(StateEntry{type, saved_count, current_transform_, |
| current_clip_, current_effect_, |
| previous_transform_}); |
| previous_transform_ = nullptr; |
| } |
| |
| void ConversionContext::PopState() { |
| DCHECK_EQ(nullptr, previous_transform_); |
| |
| const auto& previous_state = state_stack_.back(); |
| AppendRestore(previous_state.saved_count); |
| current_transform_ = previous_state.transform; |
| previous_transform_ = previous_state.previous_transform; |
| current_clip_ = previous_state.clip; |
| current_effect_ = previous_state.effect; |
| state_stack_.pop_back(); |
| } |
| |
| void ConversionContext::SwitchToTransform( |
| const TransformPaintPropertyNode* target_transform) { |
| target_transform = Unalias(target_transform); |
| if (target_transform == current_transform_) |
| return; |
| |
| EndTransform(); |
| if (target_transform == current_transform_) |
| return; |
| |
| auto sk_matrix = GetSkMatrix(target_transform); |
| if (sk_matrix.isIdentity()) |
| return; |
| |
| cc_list_.StartPaint(); |
| cc_list_.push<cc::SaveOp>(); |
| cc_list_.push<cc::ConcatOp>(sk_matrix); |
| cc_list_.EndPaintOfPairedBegin(); |
| previous_transform_ = current_transform_; |
| current_transform_ = target_transform; |
| } |
| |
| void ConversionContext::EndTransform() { |
| if (!previous_transform_) |
| return; |
| |
| cc_list_.StartPaint(); |
| cc_list_.push<cc::RestoreOp>(); |
| cc_list_.EndPaintOfPairedEnd(); |
| current_transform_ = previous_transform_; |
| previous_transform_ = nullptr; |
| } |
| |
| void ConversionContext::Convert(const PaintChunkSubset& paint_chunks, |
| const DisplayItemList& display_items) { |
| for (const auto& chunk : paint_chunks) { |
| const auto& chunk_state = chunk.properties; |
| bool switched_to_chunk_state = false; |
| |
| for (const auto& item : display_items.ItemsInPaintChunk(chunk)) { |
| DCHECK(item.IsDrawing()); |
| auto record = |
| static_cast<const DrawingDisplayItem&>(item).GetPaintRecord(); |
| // If we have an empty paint record, then we would prefer not to draw it. |
| // However, if we also have a non-root effect, it means that the filter |
| // applied might draw something even if the record is empty. We need to |
| // "draw" this record in order to ensure that the effect has correct |
| // visual rects. |
| if ((!record || record->size() == 0) && |
| chunk_state.Effect() == &EffectPaintPropertyNode::Root()) { |
| continue; |
| } |
| |
| TranslateForLayerOffsetOnce(); |
| if (!switched_to_chunk_state) { |
| SwitchToChunkState(chunk); |
| switched_to_chunk_state = true; |
| } |
| |
| cc_list_.StartPaint(); |
| if (record && record->size() != 0) |
| cc_list_.push<cc::DrawRecordOp>(std::move(record)); |
| cc_list_.EndPaintOfUnpaired( |
| chunk_to_layer_mapper_.MapVisualRect(item.VisualRect())); |
| } |
| UpdateEffectBounds(chunk.bounds, chunk_state.Transform()); |
| } |
| } |
| |
| } // unnamed namespace |
| |
| void PaintChunksToCcLayer::ConvertInto( |
| const PaintChunkSubset& paint_chunks, |
| const PropertyTreeState& layer_state, |
| const gfx::Vector2dF& layer_offset, |
| const FloatSize& visual_rect_subpixel_offset, |
| const DisplayItemList& display_items, |
| cc::DisplayItemList& cc_list) { |
| ConversionContext(layer_state, layer_offset, visual_rect_subpixel_offset, |
| cc_list) |
| .Convert(paint_chunks, display_items); |
| } |
| |
| scoped_refptr<cc::DisplayItemList> PaintChunksToCcLayer::Convert( |
| const PaintChunkSubset& paint_chunks, |
| const PropertyTreeState& layer_state, |
| const gfx::Vector2dF& layer_offset, |
| const DisplayItemList& display_items, |
| cc::DisplayItemList::UsageHint hint, |
| RasterUnderInvalidationCheckingParams* under_invalidation_checking_params) { |
| auto cc_list = base::MakeRefCounted<cc::DisplayItemList>(hint); |
| ConvertInto(paint_chunks, layer_state, layer_offset, FloatSize(), |
| display_items, *cc_list); |
| |
| if (under_invalidation_checking_params) { |
| auto& params = *under_invalidation_checking_params; |
| PaintRecorder recorder; |
| recorder.beginRecording(params.interest_rect); |
| // Create a complete cloned list for under-invalidation checking. We can't |
| // use cc_list because it is not finalized yet. |
| auto list_clone = base::MakeRefCounted<cc::DisplayItemList>( |
| cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer); |
| ConvertInto(paint_chunks, layer_state, layer_offset, FloatSize(), |
| display_items, *list_clone); |
| recorder.getRecordingCanvas()->drawPicture(list_clone->ReleaseAsRecord()); |
| params.tracking.CheckUnderInvalidations(params.debug_name, |
| recorder.finishRecordingAsPicture(), |
| params.interest_rect); |
| if (auto record = params.tracking.UnderInvalidationRecord()) { |
| cc_list->StartPaint(); |
| cc_list->push<cc::DrawRecordOp>(std::move(record)); |
| cc_list->EndPaintOfUnpaired(params.interest_rect); |
| } |
| } |
| |
| cc_list->Finalize(); |
| return cc_list; |
| } |
| |
| } // namespace blink |