| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cc/paint/display_item_list.h" |
| |
| #include <stddef.h> |
| |
| #include <map> |
| #include <string> |
| |
| #include "base/trace_event/trace_event.h" |
| #include "base/trace_event/traced_value.h" |
| #include "cc/base/math_util.h" |
| #include "cc/debug/picture_debug_util.h" |
| #include "cc/paint/solid_color_analyzer.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkPictureRecorder.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| #include "ui/gfx/geometry/rect_f.h" |
| #include "ui/gfx/geometry/skia_conversions.h" |
| |
| namespace cc { |
| |
| namespace { |
| |
| bool GetCanvasClipBounds(SkCanvas* canvas, gfx::Rect* clip_bounds) { |
| SkRect canvas_clip_bounds; |
| if (!canvas->getLocalClipBounds(&canvas_clip_bounds)) |
| return false; |
| *clip_bounds = ToEnclosingRect(gfx::SkRectToRectF(canvas_clip_bounds)); |
| return true; |
| } |
| |
| template <typename Function> |
| void IterateTextContent(const PaintOpBuffer& buffer, |
| const Function& yield, |
| const gfx::Rect& rect) { |
| if (!buffer.has_draw_text_ops()) |
| return; |
| for (const PaintOp& op : PaintOpBuffer::Iterator(&buffer)) { |
| if (op.GetType() == PaintOpType::DrawTextBlob) { |
| yield(static_cast<const DrawTextBlobOp&>(op), rect); |
| } else if (op.GetType() == PaintOpType::DrawRecord) { |
| IterateTextContent(*static_cast<const DrawRecordOp&>(op).record.get(), |
| yield, rect); |
| } |
| } |
| } |
| |
| template <typename Function> |
| void IterateTextContentByOffsets(const PaintOpBuffer& buffer, |
| const std::vector<size_t>& offsets, |
| const std::vector<gfx::Rect>& rects, |
| const Function& yield) { |
| DCHECK(buffer.has_draw_text_ops()); |
| DCHECK_EQ(rects.size(), offsets.size()); |
| size_t index = 0; |
| for (const PaintOp& op : PaintOpBuffer::OffsetIterator(&buffer, &offsets)) { |
| if (op.GetType() == PaintOpType::DrawTextBlob) { |
| yield(static_cast<const DrawTextBlobOp&>(op), rects[index]); |
| } else if (op.GetType() == PaintOpType::DrawRecord) { |
| IterateTextContent(*static_cast<const DrawRecordOp&>(op).record.get(), |
| yield, rects[index]); |
| } |
| ++index; |
| } |
| } |
| } // namespace |
| |
| DisplayItemList::DisplayItemList(UsageHint usage_hint) |
| : usage_hint_(usage_hint) { |
| if (usage_hint_ == kTopLevelDisplayItemList) { |
| visual_rects_.reserve(1024); |
| offsets_.reserve(1024); |
| paired_begin_stack_.reserve(32); |
| } |
| } |
| |
| DisplayItemList::~DisplayItemList() = default; |
| |
| void DisplayItemList::Raster(SkCanvas* canvas, |
| ImageProvider* image_provider) const { |
| DCHECK(usage_hint_ == kTopLevelDisplayItemList); |
| gfx::Rect canvas_playback_rect; |
| if (!GetCanvasClipBounds(canvas, &canvas_playback_rect)) |
| return; |
| |
| std::vector<size_t> offsets; |
| rtree_.Search(canvas_playback_rect, &offsets); |
| paint_op_buffer_.Playback(canvas, PlaybackParams(image_provider), &offsets); |
| } |
| |
| void DisplayItemList::CaptureContent(const gfx::Rect& rect, |
| std::vector<NodeInfo>* content) const { |
| if (!paint_op_buffer_.has_draw_text_ops()) |
| return; |
| std::vector<size_t> offsets; |
| std::vector<gfx::Rect> rects; |
| rtree_.Search(rect, &offsets, &rects); |
| IterateTextContentByOffsets( |
| paint_op_buffer_, offsets, rects, |
| [content](const DrawTextBlobOp& op, const gfx::Rect& rect) { |
| // Only union the rect if the current is the same as the last one. |
| if (!content->empty() && content->back().node_id == op.node_id) |
| content->back().visual_rect.Union(rect); |
| else |
| content->emplace_back(op.node_id, rect); |
| }); |
| } |
| |
| double DisplayItemList::AreaOfDrawText(const gfx::Rect& rect) const { |
| if (!paint_op_buffer_.has_draw_text_ops()) |
| return 0; |
| std::vector<size_t> offsets; |
| std::vector<gfx::Rect> rects; |
| rtree_.Search(rect, &offsets, &rects); |
| DCHECK_EQ(offsets.size(), rects.size()); |
| |
| double area = 0; |
| size_t index = 0; |
| for (const PaintOp& op : |
| PaintOpBuffer::OffsetIterator(&paint_op_buffer_, &offsets)) { |
| if (op.GetType() == PaintOpType::DrawTextBlob || |
| // Don't walk into the record because the visual rect is already the |
| // bounding box of the sub paint operations. This works for most paint |
| // results for text generated by blink. |
| (op.GetType() == PaintOpType::DrawRecord && |
| static_cast<const DrawRecordOp&>(op).record->has_draw_text_ops())) { |
| area += static_cast<double>(rects[index].width()) * rects[index].height(); |
| } |
| ++index; |
| } |
| return area; |
| } |
| |
| void DisplayItemList::EndPaintOfPairedEnd() { |
| #if DCHECK_IS_ON() |
| DCHECK(IsPainting()); |
| DCHECK_LT(current_range_start_, paint_op_buffer_.size()); |
| current_range_start_ = kNotPainting; |
| #endif |
| if (usage_hint_ == kToBeReleasedAsPaintOpBuffer) |
| return; |
| |
| DCHECK(paired_begin_stack_.size()); |
| size_t last_begin_index = paired_begin_stack_.back().first_index; |
| size_t last_begin_count = paired_begin_stack_.back().count; |
| DCHECK_GT(last_begin_count, 0u); |
| |
| // Copy the visual rect at |last_begin_index| to all indices that constitute |
| // the begin item. Note that because we possibly reallocate the |
| // |visual_rects_| buffer below, we need an actual copy instead of a const |
| // reference which can become dangling. |
| auto visual_rect = visual_rects_[last_begin_index]; |
| for (size_t i = 1; i < last_begin_count; ++i) |
| visual_rects_[i + last_begin_index] = visual_rect; |
| paired_begin_stack_.pop_back(); |
| |
| // Copy the visual rect of the matching begin item to the end item(s). |
| visual_rects_.resize(paint_op_buffer_.size(), visual_rect); |
| |
| // The block that ended needs to be included in the bounds of the enclosing |
| // block. |
| GrowCurrentBeginItemVisualRect(visual_rect); |
| } |
| |
| void DisplayItemList::Finalize() { |
| TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("cc.debug"), |
| "DisplayItemList::Finalize"); |
| #if DCHECK_IS_ON() |
| // If this fails a call to StartPaint() was not ended. |
| DCHECK(!IsPainting()); |
| // If this fails we had more calls to EndPaintOfPairedBegin() than |
| // to EndPaintOfPairedEnd(). |
| DCHECK(paired_begin_stack_.empty()); |
| DCHECK_EQ(visual_rects_.size(), offsets_.size()); |
| #endif |
| |
| if (usage_hint_ == kTopLevelDisplayItemList) { |
| rtree_.Build(visual_rects_, |
| [](const std::vector<gfx::Rect>& rects, size_t index) { |
| return rects[index]; |
| }, |
| [this](const std::vector<gfx::Rect>& rects, size_t index) { |
| // Ignore the given rects, since the payload comes from |
| // offsets. However, the indices match, so we can just index |
| // into offsets. |
| return offsets_[index]; |
| }); |
| } |
| paint_op_buffer_.ShrinkToFit(); |
| visual_rects_.clear(); |
| visual_rects_.shrink_to_fit(); |
| offsets_.clear(); |
| offsets_.shrink_to_fit(); |
| paired_begin_stack_.shrink_to_fit(); |
| } |
| |
| void DisplayItemList::EmitTraceSnapshot() const { |
| bool include_items; |
| TRACE_EVENT_CATEGORY_GROUP_ENABLED( |
| TRACE_DISABLED_BY_DEFAULT("cc.debug.display_items"), &include_items); |
| TRACE_EVENT_OBJECT_SNAPSHOT_WITH_ID( |
| TRACE_DISABLED_BY_DEFAULT("cc.debug.display_items") "," |
| TRACE_DISABLED_BY_DEFAULT("cc.debug.picture") "," |
| TRACE_DISABLED_BY_DEFAULT("devtools.timeline.picture"), |
| "cc::DisplayItemList", TRACE_ID_LOCAL(this), |
| CreateTracedValue(include_items)); |
| } |
| |
| std::string DisplayItemList::ToString() const { |
| base::trace_event::TracedValueJSON value; |
| AddToValue(&value, true); |
| return value.ToFormattedJSON(); |
| } |
| |
| std::unique_ptr<base::trace_event::TracedValue> |
| DisplayItemList::CreateTracedValue(bool include_items) const { |
| auto state = std::make_unique<base::trace_event::TracedValue>(); |
| AddToValue(state.get(), include_items); |
| return state; |
| } |
| |
| void DisplayItemList::AddToValue(base::trace_event::TracedValue* state, |
| bool include_items) const { |
| state->BeginDictionary("params"); |
| |
| gfx::Rect bounds; |
| if (rtree_.has_valid_bounds()) { |
| bounds = rtree_.GetBoundsOrDie(); |
| } else { |
| // For tracing code, just use the entire positive quadrant if the |rtree_| |
| // has invalid bounds. |
| bounds = gfx::Rect(INT_MAX, INT_MAX); |
| } |
| |
| if (include_items) { |
| state->BeginArray("items"); |
| |
| PlaybackParams params(nullptr, SkM44()); |
| std::map<size_t, gfx::Rect> visual_rects = rtree_.GetAllBoundsForTracing(); |
| for (const PaintOp& op : PaintOpBuffer::Iterator(&paint_op_buffer_)) { |
| state->BeginDictionary(); |
| state->SetString("name", PaintOpTypeToString(op.GetType())); |
| |
| MathUtil::AddToTracedValue( |
| "visual_rect", |
| visual_rects[paint_op_buffer_.GetOpOffsetForTracing(op)], state); |
| |
| SkPictureRecorder recorder; |
| SkCanvas* canvas = recorder.beginRecording(gfx::RectToSkRect(bounds)); |
| op.Raster(canvas, params); |
| sk_sp<SkPicture> picture = recorder.finishRecordingAsPicture(); |
| |
| if (picture->approximateOpCount()) { |
| std::string b64_picture; |
| PictureDebugUtil::SerializeAsBase64(picture.get(), &b64_picture); |
| state->SetString("skp64", b64_picture); |
| } |
| |
| state->EndDictionary(); |
| } |
| |
| state->EndArray(); // "items". |
| } |
| |
| MathUtil::AddToTracedValue("layer_rect", bounds, state); |
| state->EndDictionary(); // "params". |
| |
| { |
| SkPictureRecorder recorder; |
| SkCanvas* canvas = recorder.beginRecording(gfx::RectToSkRect(bounds)); |
| canvas->translate(-bounds.x(), -bounds.y()); |
| canvas->clipRect(gfx::RectToSkRect(bounds)); |
| Raster(canvas); |
| sk_sp<SkPicture> picture = recorder.finishRecordingAsPicture(); |
| |
| std::string b64_picture; |
| PictureDebugUtil::SerializeAsBase64(picture.get(), &b64_picture); |
| state->SetString("skp64", b64_picture); |
| } |
| } |
| |
| void DisplayItemList::GenerateDiscardableImagesMetadata() { |
| DCHECK(usage_hint_ == kTopLevelDisplayItemList); |
| |
| gfx::Rect bounds; |
| if (rtree_.has_valid_bounds()) { |
| bounds = rtree_.GetBoundsOrDie(); |
| } else { |
| // Bounds are only used to size an SkNoDrawCanvas, pass INT_MAX. |
| bounds = gfx::Rect(INT_MAX, INT_MAX); |
| } |
| |
| image_map_.Generate(&paint_op_buffer_, bounds); |
| } |
| |
| void DisplayItemList::Reset() { |
| #if DCHECK_IS_ON() |
| DCHECK(!IsPainting()); |
| DCHECK(paired_begin_stack_.empty()); |
| #endif |
| |
| rtree_.Reset(); |
| image_map_.Reset(); |
| paint_op_buffer_.Reset(); |
| visual_rects_.clear(); |
| visual_rects_.shrink_to_fit(); |
| offsets_.clear(); |
| offsets_.shrink_to_fit(); |
| paired_begin_stack_.clear(); |
| paired_begin_stack_.shrink_to_fit(); |
| } |
| |
| sk_sp<PaintRecord> DisplayItemList::ReleaseAsRecord() { |
| sk_sp<PaintRecord> record = |
| sk_make_sp<PaintOpBuffer>(std::move(paint_op_buffer_)); |
| |
| Reset(); |
| return record; |
| } |
| |
| bool DisplayItemList::GetColorIfSolidInRect(const gfx::Rect& rect, |
| SkColor4f* color, |
| int max_ops_to_analyze) { |
| DCHECK(usage_hint_ == kTopLevelDisplayItemList); |
| std::vector<size_t>* offsets_to_use = nullptr; |
| std::vector<size_t> offsets; |
| if (rtree_.has_valid_bounds() && !rect.Contains(rtree_.GetBoundsOrDie())) { |
| rtree_.Search(rect, &offsets); |
| offsets_to_use = &offsets; |
| } |
| |
| absl::optional<SkColor4f> solid_color = |
| SolidColorAnalyzer::DetermineIfSolidColor( |
| &paint_op_buffer_, rect, max_ops_to_analyze, offsets_to_use); |
| if (solid_color) { |
| *color = *solid_color; |
| return true; |
| } |
| return false; |
| } |
| |
| namespace { |
| |
| absl::optional<DisplayItemList::DirectlyCompositedImageResult> |
| DirectlyCompositedImageResultForPaintOpBuffer(const PaintOpBuffer& op_buffer) { |
| // A PaintOpBuffer for an image may have 1 (a DrawImageRect or a DrawRecord |
| // that recursively contains a PaintOpBuffer for an image) or 4 paint |
| // operations: |
| // (1) Save |
| // (2) Translate which applies an offset of the image in the layer |
| // or Concat with a transformation rotating the image by +/-90 degrees for |
| // image orientation |
| // (3) DrawImageRect or DrawRecord (see the 1 operation case above) |
| // (4) Restore |
| // The following algorithm also supports Translate and Concat in the same |
| // PaintOpBuffer (i.e. 5 operations). |
| constexpr size_t kMaxDrawImageOps = 5; |
| if (op_buffer.size() > kMaxDrawImageOps) |
| return absl::nullopt; |
| |
| bool transpose_image_size = false; |
| absl::optional<DisplayItemList::DirectlyCompositedImageResult> result; |
| for (const PaintOp& op : PaintOpBuffer::Iterator(&op_buffer)) { |
| switch (op.GetType()) { |
| case PaintOpType::Save: |
| case PaintOpType::Restore: |
| case PaintOpType::Translate: |
| break; |
| case PaintOpType::Concat: { |
| // We only expect a single transformation. If we see another one, then |
| // this image won't be eligible for directly compositing. |
| if (transpose_image_size) |
| return absl::nullopt; |
| // The transformation must be before the DrawImageRect operation. |
| if (result) |
| return absl::nullopt; |
| |
| const ConcatOp& concat_op = static_cast<const ConcatOp&>(op); |
| if (!MathUtil::SkM44Preserves2DAxisAlignment(concat_op.matrix)) |
| return absl::nullopt; |
| |
| // If the image has been rotated +/-90 degrees we'll need to transpose |
| // the width and height dimensions to account for the same transform |
| // applying when the layer bounds were calculated. Since we already |
| // know that the transformation preserves axis alignment, we only |
| // need to confirm that this is not a scaling operation. |
| transpose_image_size = (concat_op.matrix.rc(0, 0) == 0); |
| break; |
| } |
| case PaintOpType::DrawImageRect: { |
| if (result) |
| return absl::nullopt; |
| const auto& draw_image_rect_op = |
| static_cast<const DrawImageRectOp&>(op); |
| const SkRect& src = draw_image_rect_op.src; |
| const SkRect& dst = draw_image_rect_op.dst; |
| if (src.isEmpty() || dst.isEmpty()) |
| return absl::nullopt; |
| result.emplace(); |
| result->default_raster_scale = gfx::Vector2dF( |
| src.width() / dst.width(), src.height() / dst.height()); |
| // Ensure the layer will use nearest neighbor when drawn by the display |
| // compositor, if required. |
| result->nearest_neighbor = |
| draw_image_rect_op.flags.getFilterQuality() == |
| PaintFlags::FilterQuality::kNone; |
| break; |
| } |
| case PaintOpType::DrawRecord: |
| if (result) |
| return absl::nullopt; |
| result = DirectlyCompositedImageResultForPaintOpBuffer( |
| *static_cast<const DrawRecordOp&>(op).record); |
| if (!result) |
| return absl::nullopt; |
| break; |
| default: |
| // Disqualify the layer as a directly composited image if any other |
| // paint op is detected. |
| return absl::nullopt; |
| } |
| } |
| |
| if (result && transpose_image_size) |
| result->default_raster_scale.Transpose(); |
| return result; |
| } |
| |
| } // anonymous namespace |
| |
| absl::optional<DisplayItemList::DirectlyCompositedImageResult> |
| DisplayItemList::GetDirectlyCompositedImageResult() const { |
| return DirectlyCompositedImageResultForPaintOpBuffer(paint_op_buffer_); |
| } |
| |
| } // namespace cc |