chromium/components/viz/service/compositor_frame_fuzzer/compositor_frame_fuzzer_util.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/viz/service/compositor_frame_fuzzer/compositor_frame_fuzzer_util.h"

#include <limits>
#include <utility>

#include "base/logging.h"
#include "cc/base/math_util.h"
#include "components/viz/common/quads/compositor_render_pass_draw_quad.h"
#include "components/viz/common/quads/solid_color_draw_quad.h"
#include "components/viz/common/quads/tile_draw_quad.h"
#include "components/viz/common/resources/bitmap_allocation.h"
#include "components/viz/common/resources/resource_sizes.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/geometry/mask_filter_info.h"

namespace viz {
namespace {

// Limit on number of bytes of memory to allocate (e.g. for referenced
// bitmaps) in association with a single CompositorFrame. Currently 0.5 GiB;
// reduce this if bots are running out of memory.
constexpr uint64_t kMaxTextureMemory = 1 << 29;

// Handles inf / NaN by setting to 0.
double MakeNormal(double x) {
  return isnormal(x) ? x : 0;
}
float MakeNormal(float x) {
  return isnormal(x) ? x : 0;
}

// Normalizes value to a float in [0, 1]. Use to convert a fuzzed
// uint32 into a percentage.
float Normalize(uint32_t x) {
  return static_cast<float>(x) /
         static_cast<float>(std::numeric_limits<uint32_t>::max());
}

gfx::Size GetSizeFromProtobuf(const proto::Size& proto_size) {
  return gfx::Size(proto_size.width(), proto_size.height());
}

gfx::Rect GetRectFromProtobuf(const proto::Rect& proto_rect) {
  return gfx::Rect(proto_rect.x(), proto_rect.y(), proto_rect.width(),
                   proto_rect.height());
}

gfx::Transform GetTransformFromProtobuf(
    const proto::Transform& proto_transform) {
  gfx::Transform transform;

  // Note: There are no checks here that disallow a non-invertible transform
  // (for instance, if |scale_x| or |scale_y| is 0).
  transform.Scale(MakeNormal(proto_transform.scale_x()),
                  MakeNormal(proto_transform.scale_y()));

  transform.Rotate(MakeNormal(proto_transform.rotate()));

  transform.Translate(MakeNormal(proto_transform.translate_x()),
                      MakeNormal(proto_transform.translate_y()));

  return transform;
}

SkColor4f GetColorFromProtobuf(const proto::Color& color) {
  return SkColor4f{color.r(), color.g(), color.b(), color.a()};
}

// Mutates a gfx::Rect to ensure width and height are both at least min_size.
// Use in case e.g. a 0-width/height Rect would cause a validation error on
// deserialization.
void ExpandToMinSize(gfx::Rect* rect, int min_size) {
  if (rect->width() < min_size) {
    // grow width to min_size in +x direction
    // (may be clamped if x + min_size overflows)
    rect->set_width(min_size);
  }

  // if previous attempt failed due to overflow
  if (rect->width() < min_size) {
    // grow width to min_size in -x direction
    rect->Offset(-(min_size - rect->width()), 0);
    rect->set_width(min_size);
  }

  if (rect->height() < min_size) {
    // grow height to min_size in +y direction
    // (may be clamped if y + min_size overflows)
    rect->set_height(min_size);
  }

  // if previous attempt failed due to overflow
  if (rect->height() < min_size) {
    // grow height to min_size in -y direction
    rect->Offset(0, -(min_size - rect->height()));
    rect->set_height(min_size);
  }
}

class FuzzedCompositorFrameBuilder {
 public:
  FuzzedCompositorFrameBuilder() = default;

  FuzzedCompositorFrameBuilder(const FuzzedCompositorFrameBuilder&) = delete;
  FuzzedCompositorFrameBuilder& operator=(const FuzzedCompositorFrameBuilder&) =
      delete;

  ~FuzzedCompositorFrameBuilder() = default;

  FuzzedData Build(const proto::CompositorRenderPass& render_pass_spec);

 private:
  CompositorRenderPassId AddRenderPass(
      const proto::CompositorRenderPass& render_pass_spec);

  // Helper methods for AddRenderPass. Try* methods may return before
  // creating the quad in order to adhere to memory limits.
  void AddSolidColorDrawQuad(CompositorRenderPass* pass,
                             const gfx::Rect& rect,
                             const gfx::Rect& visible_rect,
                             const proto::DrawQuad& quad_spec);
  void TryAddTileDrawQuad(CompositorRenderPass* pass,
                          const gfx::Rect& rect,
                          const gfx::Rect& visible_rect,
                          const proto::DrawQuad& quad_spec);
  void TryAddRenderPassDrawQuad(CompositorRenderPass* pass,
                                const gfx::Rect& rect,
                                const gfx::Rect& visible_rect,
                                const proto::DrawQuad& quad_spec);

  // Configure the SharedQuadState to match the specifications in the protobuf,
  // if they are defined for this quad. Otherwise, use sensible defaults: match
  // the |rect| and |visible_rect| of the corresponding quad, and apply an
  // identity transform.
  void ConfigureSharedQuadState(SharedQuadState* shared_quad_state,
                                const proto::DrawQuad& quad_spec);

  // Records the intention to allocate enough memory for a bitmap of size
  // |size|, or returns false if the allocation would not be possible (the size
  // is 0 or the allocation would exceed kMaxMappedMemory).
  //
  // Should be called before AllocateFuzzedBitmap.
  bool TryReserveBitmapBytes(const gfx::Size& size);

  // Allocate and map memory for a bitmap filled with |color| and appends it to
  // |allocated_bitmaps|. Performs no checks to ensure that the bitmap will fit
  // in memory (see TryReserveBitmapBytes).
  FuzzedBitmap* AllocateFuzzedBitmap(const gfx::Size& size, SkColor4f color);

  // Number of bytes that have already been reserved for the allocation of
  // specific bitmaps/textures.
  uint64_t reserved_bytes_ = 0;

  CompositorRenderPassId::Generator pass_id_generator_;

  // Frame and data being built.
  FuzzedData data_;
};

FuzzedData FuzzedCompositorFrameBuilder::Build(
    const proto::CompositorRenderPass& render_pass_spec) {
  static FrameTokenGenerator next_frame_token;

  data_.frame.metadata.begin_frame_ack.frame_id = BeginFrameId(
      BeginFrameArgs::kManualSourceId, BeginFrameArgs::kStartingFrameNumber);
  data_.frame.metadata.begin_frame_ack.has_damage = true;
  data_.frame.metadata.frame_token = ++next_frame_token;
  data_.frame.metadata.device_scale_factor = 1;

  AddRenderPass(render_pass_spec);

  return std::move(data_);
}

CompositorRenderPassId FuzzedCompositorFrameBuilder::AddRenderPass(
    const proto::CompositorRenderPass& render_pass_spec) {
  auto pass = CompositorRenderPass::Create();
  gfx::Rect rp_output_rect =
      GetRectFromProtobuf(render_pass_spec.output_rect());
  gfx::Rect rp_damage_rect =
      GetRectFromProtobuf(render_pass_spec.damage_rect());

  // Handle constraints on RenderPass:
  // Ensure that |rp_output_rect| has non-zero area and that |rp_damage_rect|
  // is contained in |rp_output_rect|.
  ExpandToMinSize(&rp_output_rect, 1);
  rp_damage_rect.AdjustToFit(rp_output_rect);

  // Use an identity transform if |transform_to_root_target| is not defined.
  gfx::Transform transform_to_root_target =
      render_pass_spec.has_transform_to_root_target()
          ? GetTransformFromProtobuf(
                render_pass_spec.transform_to_root_target())
          : gfx::Transform();
  pass->SetNew(pass_id_generator_.GenerateNextId(), rp_output_rect,
               rp_damage_rect, transform_to_root_target);

  for (const proto::DrawQuad& quad_spec : render_pass_spec.quad_list()) {
    if (quad_spec.quad_case() == proto::DrawQuad::QUAD_NOT_SET) {
      continue;
    }

    gfx::Rect quad_rect = GetRectFromProtobuf(quad_spec.rect());
    gfx::Rect quad_visible_rect = GetRectFromProtobuf(quad_spec.visible_rect());

    // Handle constraints on DrawQuad:
    // Ensure that |quad_rect| has non-zero area and that |quad_visible_rect|
    // is contained in |quad_rect|.
    ExpandToMinSize(&quad_rect, 1);
    quad_visible_rect.AdjustToFit(quad_rect);

    switch (quad_spec.quad_case()) {
      case proto::DrawQuad::kSolidColorQuad: {
        AddSolidColorDrawQuad(pass.get(), quad_rect, quad_visible_rect,
                              quad_spec);
        break;
      }
      case proto::DrawQuad::kTileQuad: {
        TryAddTileDrawQuad(pass.get(), quad_rect, quad_visible_rect, quad_spec);
        break;
      }
      case proto::DrawQuad::kRenderPassQuad: {
        TryAddRenderPassDrawQuad(pass.get(), quad_rect, quad_visible_rect,
                                 quad_spec);
        break;
      }
      case proto::DrawQuad::QUAD_NOT_SET: {
        NOTREACHED_IN_MIGRATION();
      }
    }
  }
  data_.frame.render_pass_list.push_back(std::move(pass));
  return data_.frame.render_pass_list.back()->id;
}

// TODO(crbug.com/40219248): Move proto::DrawQuad to SkColor4f
void FuzzedCompositorFrameBuilder::AddSolidColorDrawQuad(
    CompositorRenderPass* pass,
    const gfx::Rect& rect,
    const gfx::Rect& visible_rect,
    const proto::DrawQuad& quad_spec) {
  auto* shared_quad_state = pass->CreateAndAppendSharedQuadState();
  ConfigureSharedQuadState(shared_quad_state, quad_spec);
  auto* quad = pass->CreateAndAppendDrawQuad<SolidColorDrawQuad>();
  quad->SetNew(shared_quad_state, rect, visible_rect,
               GetColorFromProtobuf(quad_spec.solid_color_quad().color()),
               quad_spec.solid_color_quad().force_anti_aliasing_off());
}

void FuzzedCompositorFrameBuilder::TryAddTileDrawQuad(
    CompositorRenderPass* pass,
    const gfx::Rect& rect,
    const gfx::Rect& visible_rect,
    const proto::DrawQuad& quad_spec) {
  gfx::Size tile_size =
      GetSizeFromProtobuf(quad_spec.tile_quad().texture_size());

  // Skip TileDrawQuads with bitmaps that cannot be allocated (size is 0
  // or would overflow our limit on allocated memory for this
  // CompositorFrame.)
  if (!TryReserveBitmapBytes(tile_size)) {
    VLOG(1) << "Skipping TileDrawQuad: bitmap of size " << tile_size.ToString()
            << " can't be allocated.";
    return;
  }

  FuzzedBitmap* fuzzed_bitmap = AllocateFuzzedBitmap(
      tile_size, GetColorFromProtobuf(quad_spec.tile_quad().texture_color()));
  TransferableResource transferable_resource =
      TransferableResource::MakeSoftwareSharedBitmap(
          fuzzed_bitmap->id, gpu::SyncToken(), fuzzed_bitmap->size,
          SinglePlaneFormat::kRGBA_8888);

  auto* shared_quad_state = pass->CreateAndAppendSharedQuadState();
  ConfigureSharedQuadState(shared_quad_state, quad_spec);
  auto* quad = pass->CreateAndAppendDrawQuad<TileDrawQuad>();
  quad->SetNew(
      shared_quad_state, rect, visible_rect,
      quad_spec.tile_quad().needs_blending(), transferable_resource.id,
      gfx::RectF(GetRectFromProtobuf(quad_spec.tile_quad().tex_coord_rect())),
      tile_size, quad_spec.tile_quad().is_premultiplied(),
      quad_spec.tile_quad().nearest_neighbor(),
      quad_spec.tile_quad().force_anti_aliasing_off());

  data_.frame.resource_list.push_back(transferable_resource);
}

void FuzzedCompositorFrameBuilder::TryAddRenderPassDrawQuad(
    CompositorRenderPass* pass,
    const gfx::Rect& rect,
    const gfx::Rect& visible_rect,
    const proto::DrawQuad& quad_spec) {
  // Since child RenderPasses are allocated as textures, skip
  // RenderPasses that would overflow our limit on allocated memory for
  // this CompositorFrame.
  gfx::Size render_pass_size =
      GetRectFromProtobuf(
          quad_spec.render_pass_quad().render_pass().output_rect())
          .size();

  // RenderPass texture dimensions are rounded up to a
  // multiple of 64 pixels to reduce fragmentation.
  constexpr int multiple = 64;
  if (!cc::MathUtil::VerifyRoundup(render_pass_size.width(), multiple) ||
      !cc::MathUtil::VerifyRoundup(render_pass_size.height(), multiple)) {
    VLOG(1) << "Skipping CompositorRenderPassDrawQuad: bitmap of size "
            << render_pass_size.ToString() << " can't be allocated.";
    return;
  }
  render_pass_size = gfx::Size(
      cc::MathUtil::UncheckedRoundUp(render_pass_size.width(), multiple),
      cc::MathUtil::UncheckedRoundUp(render_pass_size.height(), multiple));

  if (!TryReserveBitmapBytes(render_pass_size)) {
    VLOG(1) << "Skipping CompositorRenderPassDrawQuad: bitmap of size "
            << render_pass_size.ToString() << " can't be allocated.";
    return;
  }

  // Build the child RenderPass and add it to the frame's
  // CompositorRenderPassList.
  CompositorRenderPassId child_pass_id =
      AddRenderPass(quad_spec.render_pass_quad().render_pass());

  // Unless a tex_coord_rect is defined in the protobuf specification,
  // a good default is a rectangle covering the entire quad.
  gfx::RectF tex_coord_rect = gfx::RectF(
      quad_spec.render_pass_quad().has_tex_coord_rect()
          ? GetRectFromProtobuf(quad_spec.render_pass_quad().tex_coord_rect())
          : rect);

  auto* shared_quad_state = pass->CreateAndAppendSharedQuadState();
  ConfigureSharedQuadState(shared_quad_state, quad_spec);
  auto* quad = pass->CreateAndAppendDrawQuad<CompositorRenderPassDrawQuad>();
  quad->SetNew(shared_quad_state, rect, visible_rect, child_pass_id,
               /*mask_resource_id=*/ResourceId(),
               /*mask_uv_rect=*/gfx::RectF(),
               /*mask_texture_size=*/gfx::Size(),
               /*filters_scale=*/gfx::Vector2dF(),
               /*filters_origin=*/gfx::PointF(),
               /*tex_coord_rect=*/tex_coord_rect,
               /*force_anti_aliasing_off=*/false,
               /*backdrop_filter_quality=*/1.0);
}

void FuzzedCompositorFrameBuilder::ConfigureSharedQuadState(
    SharedQuadState* shared_quad_state,
    const proto::DrawQuad& quad_spec) {
  if (quad_spec.has_sqs()) {
    std::optional<gfx::Rect> clip_rect;
    if (quad_spec.sqs().is_clipped()) {
      clip_rect = GetRectFromProtobuf(quad_spec.sqs().clip_rect());
    }
    shared_quad_state->SetAll(
        GetTransformFromProtobuf(quad_spec.sqs().transform()),
        GetRectFromProtobuf(quad_spec.sqs().layer_rect()),
        GetRectFromProtobuf(quad_spec.sqs().visible_rect()),
        gfx::MaskFilterInfo(), clip_rect, quad_spec.sqs().are_contents_opaque(),
        Normalize(quad_spec.sqs().opacity()), SkBlendMode::kSrcOver,
        quad_spec.sqs().sorting_context_id(), /*layer_id=*/0u,
        /*fast_rounded_corner*/ false);
  } else {
    gfx::Transform transform;

    if (quad_spec.quad_case() == proto::DrawQuad::kRenderPassQuad &&
        quad_spec.render_pass_quad()
            .render_pass()
            .has_transform_to_root_target()) {
      transform = GetTransformFromProtobuf(quad_spec.render_pass_quad()
                                               .render_pass()
                                               .transform_to_root_target());
    }

    shared_quad_state->SetAll(
        transform, GetRectFromProtobuf(quad_spec.rect()),
        GetRectFromProtobuf(quad_spec.visible_rect()), gfx::MaskFilterInfo(),
        /*clip_rect=*/std::nullopt, /*are_contents_opaque=*/true,
        /*opacity=*/1.0, SkBlendMode::kSrcOver, /*sorting_context_id=*/0,
        /*layer_id=*/0u, /*fast_rounded_corner*/ false);
  }
}

bool FuzzedCompositorFrameBuilder::TryReserveBitmapBytes(
    const gfx::Size& size) {
  uint64_t bitmap_bytes;
  if (!ResourceSizes::MaybeSizeInBytes<uint64_t>(
          size, SinglePlaneFormat::kRGBA_8888, &bitmap_bytes) ||
      bitmap_bytes > kMaxTextureMemory - reserved_bytes_) {
    return false;
  }

  reserved_bytes_ += bitmap_bytes;
  return true;
}

FuzzedBitmap* FuzzedCompositorFrameBuilder::AllocateFuzzedBitmap(
    const gfx::Size& size,
    SkColor4f color) {
  SharedBitmapId shared_bitmap_id = SharedBitmap::GenerateId();
  base::MappedReadOnlyRegion shm = bitmap_allocation::AllocateSharedBitmap(
      size, SinglePlaneFormat::kRGBA_8888);

  SkBitmap bitmap;
  SkImageInfo info = SkImageInfo::MakeN32Premul(size.width(), size.height());
  bitmap.installPixels(info, shm.mapping.memory(), info.minRowBytes());
  bitmap.eraseColor(color);

  data_.allocated_bitmaps.push_back(
      {shared_bitmap_id, size, std::move(shm.region)});

  return &data_.allocated_bitmaps.back();
}

}  // namespace

FuzzedBitmap::FuzzedBitmap(const SharedBitmapId& id,
                           const gfx::Size& size,
                           base::ReadOnlySharedMemoryRegion shared_region)
    : id(id), size(size), shared_region(std::move(shared_region)) {}
FuzzedBitmap::~FuzzedBitmap() = default;
FuzzedBitmap::FuzzedBitmap(FuzzedBitmap&& other) noexcept = default;

FuzzedData::FuzzedData() = default;
FuzzedData::~FuzzedData() = default;
FuzzedData::FuzzedData(FuzzedData&& other) noexcept = default;

FuzzedData BuildFuzzedCompositorFrame(
    const proto::CompositorRenderPass& render_pass_spec) {
  return FuzzedCompositorFrameBuilder().Build(render_pass_spec);
}

}  // namespace viz