blob: 0c3f63dc92ddc0f67904c925a33d8ee45bb0bc83 [file] [log] [blame]
// 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 "platform/graphics/compositing/PaintChunksToCcLayer.h"
#include "cc/paint/display_item_list.h"
#include "cc/paint/paint_op_buffer.h"
#include "cc/paint/render_surface_filters.h"
#include "platform/graphics/GraphicsContext.h"
#include "platform/graphics/paint/DisplayItemList.h"
#include "platform/graphics/paint/DrawingDisplayItem.h"
#include "platform/graphics/paint/GeometryMapper.h"
#include "platform/graphics/paint/PaintChunk.h"
#include "platform/graphics/paint/PropertyTreeState.h"
#include "platform/graphics/paint/RasterInvalidationTracking.h"
namespace blink {
namespace {
constexpr gfx::Rect g_large_rect(-200000, -200000, 400000, 400000);
void AppendDisplayItemToCcDisplayItemList(const DisplayItem& display_item,
cc::DisplayItemList& list) {
DCHECK(display_item.IsDrawing());
sk_sp<const PaintRecord> record =
static_cast<const DrawingDisplayItem&>(display_item).GetPaintRecord();
if (!record)
return;
list.StartPaint();
list.push<cc::DrawRecordOp>(std::move(record));
// TODO(trchen): Pass correct visual rect here.
// The visual rect of the item can be used by cc to skip replaying items
// that can't be seen. To workaround a space conversion bug, the optimization
// is suppressed by passing a large rect.
list.EndPaintOfUnpaired(g_large_rect);
}
void AppendRestore(cc::DisplayItemList& list, size_t n) {
list.StartPaint();
while (n--)
list.push<cc::RestoreOp>();
list.EndPaintOfPairedEnd();
}
class ConversionContext {
public:
ConversionContext(const PropertyTreeState& layer_state,
cc::DisplayItemList& cc_list)
: current_transform_(layer_state.Transform()),
current_clip_(layer_state.Clip()),
current_effect_(layer_state.Effect()),
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 Vector<const PaintChunk*>&, const DisplayItemList&);
private:
// 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*);
const TransformPaintPropertyNode* current_transform_;
const ClipPaintPropertyNode* current_clip_;
const EffectPaintPropertyNode* current_effect_;
// 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 useful for emitting corresponding paired end.
enum class PairedType : char { kClip, kEffect } type;
const TransformPaintPropertyNode* transform;
const ClipPaintPropertyNode* clip;
const EffectPaintPropertyNode* effect;
};
Vector<StateEntry> state_stack_;
cc::DisplayItemList& cc_list_;
};
ConversionContext::~ConversionContext() {
for (size_t i = state_stack_.size(); i--;) {
if (state_stack_[i].type == StateEntry::PairedType::kClip) {
AppendRestore(cc_list_, 1);
} else {
DCHECK_EQ(StateEntry::PairedType::kEffect, state_stack_[i].type);
AppendRestore(cc_list_, 2);
}
}
}
void ConversionContext::SwitchToClip(const ClipPaintPropertyNode* 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_);
while (current_clip_ != lca_clip) {
DCHECK(state_stack_.size() &&
state_stack_.back().type == StateEntry::PairedType::kClip)
<< "Error: Chunk has a clip that escaped its effect's clip.";
if (!state_stack_.size() ||
state_stack_.back().type != StateEntry::PairedType::kClip)
break;
StateEntry& previous_state = state_stack_.back();
current_transform_ = previous_state.transform;
current_clip_ = previous_state.clip;
DCHECK_EQ(previous_state.effect, current_effect_);
state_stack_.pop_back();
AppendRestore(cc_list_, 1);
}
// 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 = 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.
for (size_t i = pending_clips.size(); i--;) {
const ClipPaintPropertyNode* sub_clip = pending_clips[i];
DCHECK_EQ(current_clip_, sub_clip->Parent());
// Step 3a: Switch CTM to the clip's local space then apply clip.
cc_list_.StartPaint();
cc_list_.push<cc::SaveOp>();
const TransformPaintPropertyNode* target_transform =
sub_clip->LocalTransformSpace();
if (current_transform_ != target_transform) {
cc_list_.push<cc::ConcatOp>(
static_cast<SkMatrix>(TransformationMatrix::ToSkMatrix44(
GeometryMapper::SourceToDestinationProjection(
target_transform, current_transform_))));
}
cc_list_.push<cc::ClipRectOp>(
static_cast<SkRect>(sub_clip->ClipRect().Rect()), SkClipOp::kIntersect,
false);
if (sub_clip->ClipRect().IsRounded()) {
cc_list_.push<cc::ClipRRectOp>(static_cast<SkRRect>(sub_clip->ClipRect()),
SkClipOp::kIntersect, true);
}
cc_list_.EndPaintOfPairedBegin();
// Step 3b: Adjust state and push previous state onto clip stack.
state_stack_.emplace_back(StateEntry{StateEntry::PairedType::kClip,
current_transform_, current_clip_,
current_effect_});
current_transform_ = target_transform;
current_clip_ = sub_clip;
}
}
void ConversionContext::SwitchToEffect(
const EffectPaintPropertyNode* 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_);
while (current_effect_ != lca_effect) {
DCHECK(state_stack_.size()) << "Error: Chunk layerized into a layer with "
"an effect that's too deep.";
if (!state_stack_.size())
break;
StateEntry& previous_state = state_stack_.back();
if (previous_state.type == StateEntry::PairedType::kClip) {
AppendRestore(cc_list_, 1);
} else {
DCHECK_EQ(StateEntry::PairedType::kEffect, previous_state.type);
AppendRestore(cc_list_, 2);
}
current_transform_ = previous_state.transform;
current_clip_ = previous_state.clip;
current_effect_ = previous_state.effect;
state_stack_.pop_back();
}
// 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 = 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_, sub_effect->Parent());
// Step 3a: Before each effect can be applied, we must enter its output
// clip first, or exit all clips if it doesn't have one.
if (sub_effect->OutputClip()) {
SwitchToClip(sub_effect->OutputClip());
} else {
while (state_stack_.size() &&
state_stack_.back().type == StateEntry::PairedType::kClip) {
StateEntry& previous_state = state_stack_.back();
current_transform_ = previous_state.transform;
current_clip_ = previous_state.clip;
DCHECK_EQ(previous_state.effect, current_effect_);
state_stack_.pop_back();
AppendRestore(cc_list_, 1);
}
}
// Step 3b: Apply non-spatial effects first, adjust CTM, then apply spatial
// effects. Strictly speaking the CTM shall be appled first, it is done
// in this particular order only to save one SaveOp.
// TODO(trchen): Omit one of the SaveLayerOp if no-op.
cc_list_.StartPaint();
cc::PaintFlags flags;
flags.setBlendMode(sub_effect->BlendMode());
// TODO(ajuma): This should really be rounding instead of flooring the
// alpha value, but that breaks slimming paint reftests.
flags.setAlpha(
static_cast<uint8_t>(gfx::ToFlooredInt(255 * sub_effect->Opacity())));
flags.setColorFilter(GraphicsContext::WebCoreColorFilterToSkiaColorFilter(
sub_effect->GetColorFilter()));
cc_list_.push<cc::SaveLayerOp>(nullptr, &flags);
const TransformPaintPropertyNode* target_transform =
sub_effect->LocalTransformSpace();
if (current_transform_ != target_transform) {
cc_list_.push<cc::ConcatOp>(
static_cast<SkMatrix>(TransformationMatrix::ToSkMatrix44(
GeometryMapper::SourceToDestinationProjection(
target_transform, current_transform_))));
}
FloatPoint filter_origin = sub_effect->PaintOffset();
if (filter_origin != FloatPoint())
cc_list_.push<cc::TranslateOp>(filter_origin.X(), filter_origin.Y());
// The size parameter is only used to computed the origin of zoom
// operation, which we never generate.
gfx::SizeF empty;
cc::PaintFlags filter_flags;
filter_flags.setImageFilter(cc::RenderSurfaceFilters::BuildImageFilter(
sub_effect->Filter().AsCcFilterOperations(), empty));
cc_list_.push<cc::SaveLayerOp>(nullptr, &filter_flags);
if (filter_origin != FloatPoint())
cc_list_.push<cc::TranslateOp>(-filter_origin.X(), -filter_origin.Y());
cc_list_.EndPaintOfPairedBegin();
// Step 3c: 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_;
state_stack_.emplace_back(StateEntry{StateEntry::PairedType::kEffect,
current_transform_, current_clip_,
current_effect_});
current_transform_ = target_transform;
current_clip_ = input_clip;
current_effect_ = sub_effect;
}
}
void ConversionContext::Convert(const Vector<const PaintChunk*>& paint_chunks,
const DisplayItemList& display_items) {
for (auto chunk_it = paint_chunks.begin(); chunk_it != paint_chunks.end();
chunk_it++) {
const PaintChunk& chunk = **chunk_it;
const PropertyTreeState& chunk_state = chunk.properties.property_tree_state;
SwitchToEffect(chunk_state.Effect());
SwitchToClip(chunk_state.Clip());
bool transformed = chunk_state.Transform() != current_transform_;
if (transformed) {
cc_list_.StartPaint();
cc_list_.push<cc::SaveOp>();
cc_list_.push<cc::ConcatOp>(
static_cast<SkMatrix>(TransformationMatrix::ToSkMatrix44(
GeometryMapper::SourceToDestinationProjection(
chunk_state.Transform(), current_transform_))));
cc_list_.EndPaintOfPairedBegin();
}
for (const auto& item : display_items.ItemsInPaintChunk(chunk))
AppendDisplayItemToCcDisplayItemList(item, cc_list_);
if (transformed)
AppendRestore(cc_list_, 1);
}
}
} // unnamed namespace
void PaintChunksToCcLayer::ConvertInto(
const Vector<const PaintChunk*>& paint_chunks,
const PropertyTreeState& layer_state,
const gfx::Vector2dF& layer_offset,
const DisplayItemList& display_items,
cc::DisplayItemList& cc_list) {
bool need_translate = !layer_offset.IsZero();
if (need_translate) {
cc_list.StartPaint();
cc_list.push<cc::SaveOp>();
cc_list.push<cc::TranslateOp>(-layer_offset.x(), -layer_offset.y());
cc_list.EndPaintOfPairedBegin();
}
ConversionContext(layer_state, cc_list).Convert(paint_chunks, display_items);
if (need_translate)
AppendRestore(cc_list, 1);
}
scoped_refptr<cc::DisplayItemList> PaintChunksToCcLayer::Convert(
const Vector<const PaintChunk*>& 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, 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, 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(g_large_rect);
}
}
cc_list->Finalize();
return cc_list;
}
} // namespace blink