chromium/ash/ambient/ui/ambient_animation_frame_rate_schedule.cc

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

#include "ash/ambient/ui/ambient_animation_frame_rate_schedule.h"

#include <iterator>
#include <limits>
#include <optional>
#include <string>
#include <vector>

#include "ash/utility/lottie_util.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "cc/paint/skottie_marker.h"
#include "third_party/re2/src/re2/re2.h"

namespace ash {
namespace {

constexpr float kMinNormalizedTimestamp = 0.f;
constexpr float kMaxNormalizedTimestamp = 1.f;
// The last section of the schedule will always have an |end_timestamp|
// (exclusive) matching this. Since the largest possible normalized timestamp is
// 1.f, this could theoretically be 1.f + <some small amount>, but just using
// the largest possible float value for this ceiling is simplest.
constexpr float kInfiniteTimestamp = std::numeric_limits<float>::max();

// Frame rate markers have names with format
// "_CrOS_Marker_Throttled_<FrameRate>fps".
//
// Fills |parsed_fps| with the "FrameRate" embedded in the name on success.
bool IsFrameRateMarker(const std::string& marker_name, int& parsed_fps) {
  static const base::NoDestructor<std::string> kMarkerPatternStr(base::StrCat(
      {kLottieCustomizableIdPrefix, R"(_Marker_Throttled_([[:digit:]]+)fps)"}));
  static const base::NoDestructor<RE2> kMarkerPattern(*kMarkerPatternStr);
  return RE2::FullMatch(marker_name, *kMarkerPattern, &parsed_fps);
}

// Returns a new AmbientAnimationFrameRateSection, or nullopt if the |marker| or
// |fps| are invalid.
std::optional<AmbientAnimationFrameRateSection>
BuildAmbientAnimationFrameRateSection(const cc::SkottieMarker& marker,
                                      int fps) {
  // Requesting a frame rate over 60 fps will not break the graphics pipeline.
  // But it is currently unnecessary and simply not supported on some displays
  // at the hardware-level, so flag an error. If theoretically, the graphics
  // pipeline is told to render at a frame rate higher than what's supported, it
  // will handle it gracefully and just render at the default frame rate.
  static constexpr int kMaxFrameRateFps = 60;
  if (fps <= 0 || fps > kMaxFrameRateFps) {
    LOG(ERROR) << "Invalid frame rate (fps) specified: " << fps;
    return std::nullopt;
  }

  if (marker.begin_time < kMinNormalizedTimestamp ||
      marker.end_time > kMaxNormalizedTimestamp ||
      marker.end_time <= marker.begin_time) {
    LOG(ERROR) << "Frame rate marker has invalid timestamps ["
               << marker.begin_time << ", " << marker.end_time << ")";
    return std::nullopt;
  }
  return AmbientAnimationFrameRateSection(marker.begin_time, marker.end_time,
                                          base::Hertz(fps));
}

}  // namespace

AmbientAnimationFrameRateSection::AmbientAnimationFrameRateSection(
    float start_timestamp,
    float end_timestamp,
    base::TimeDelta frame_interval)
    : start_timestamp(start_timestamp),
      end_timestamp(end_timestamp),
      frame_interval(frame_interval) {}

bool AmbientAnimationFrameRateSection::operator<(
    const AmbientAnimationFrameRateSection& other) const {
  return start_timestamp < other.start_timestamp;
}

bool AmbientAnimationFrameRateSection::Contains(float t) const {
  return t >= start_timestamp && t < end_timestamp;
}

bool AmbientAnimationFrameRateSection::IntersectsWith(
    const AmbientAnimationFrameRateSection& other) const {
  return start_timestamp < other.end_timestamp &&
         end_timestamp > other.start_timestamp;
}

AmbientAnimationFrameRateSchedule BuildDefaultFrameRateSchedule() {
  return {AmbientAnimationFrameRateSection(
      kMinNormalizedTimestamp, kInfiniteTimestamp, kDefaultFrameInterval)};
}

AmbientAnimationFrameRateSchedule BuildAmbientAnimationFrameRateSchedule(
    const std::vector<cc::SkottieMarker>& animation_markers) {
  AmbientAnimationFrameRateSchedule frame_rate_schedule;
  for (const cc::SkottieMarker& marker : animation_markers) {
    int fps = 0;
    if (!IsFrameRateMarker(marker.name, fps))
      continue;

    std::optional<AmbientAnimationFrameRateSection> section =
        BuildAmbientAnimationFrameRateSection(marker, fps);
    if (section) {
      frame_rate_schedule.push_back(*section);
    } else {
      LOG(ERROR) << "Found invalid marker: " << marker.name;
      return AmbientAnimationFrameRateSchedule();
    }
  }

  if (frame_rate_schedule.empty())
    return BuildDefaultFrameRateSchedule();

  frame_rate_schedule.sort();

  // A section of the animation cannot have 2 frame rates simultaneously. If
  // the animation specifies 2 overlapping frame rate markers, this should be
  // fixed in the animation.
  auto current_section = frame_rate_schedule.begin();
  auto next_section = std::next(current_section);
  for (; next_section != frame_rate_schedule.end();
       ++current_section, ++next_section) {
    if (current_section->IntersectsWith(*next_section)) {
      LOG(ERROR) << "AmbientAnimationFrameRateSection " << *current_section
                 << " intersects with " << *next_section;
      return AmbientAnimationFrameRateSchedule();
    }
  }

  // Fill in the gaps in the schedule with the default frame rate.
  current_section = frame_rate_schedule.begin();
  next_section = std::next(current_section);
  for (; next_section != frame_rate_schedule.end();
       next_section = std::next(current_section)) {
    if (current_section->end_timestamp == next_section->start_timestamp) {
      ++current_section;
    } else {
      current_section = frame_rate_schedule.emplace(
          next_section, current_section->end_timestamp,
          next_section->start_timestamp, kDefaultFrameInterval);
    }
  }
  // Corner cases: Fill in gaps at the beginning and end of the animation.
  if (frame_rate_schedule.front().start_timestamp > kMinNormalizedTimestamp) {
    frame_rate_schedule.emplace_front(
        kMinNormalizedTimestamp, frame_rate_schedule.front().start_timestamp,
        kDefaultFrameInterval);
  }
  if (frame_rate_schedule.back().end_timestamp < 1.f) {
    frame_rate_schedule.emplace_back(frame_rate_schedule.back().end_timestamp,
                                     kInfiniteTimestamp, kDefaultFrameInterval);
  }
  // The last section's |end_timestamp| should always be inf to ensure all
  // timestamps fall within it (including |kMaxNormalizedTimestamp|).
  frame_rate_schedule.back().end_timestamp = kInfiniteTimestamp;
  return frame_rate_schedule;
}

std::ostream& operator<<(std::ostream& os,
                         const AmbientAnimationFrameRateSection& section) {
  return os << "start_timestamp=" << section.start_timestamp
            << " end_timestamp=" << section.end_timestamp
            << " frame_interval=" << section.frame_interval;
}

}  // namespace ash