blob: 3f8d7ddc713fc13f13e22c5444de5a779274c18f [file] [log] [blame]
// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/renderer/pipeline.h"
#include "base/bind.h"
#include "base/debug/trace_event.h"
#include "base/file_path.h"
#include "base/file_util.h"
#include "base/path_service.h"
#include "cobalt/base/address_sanitizer.h"
#include "cobalt/base/cobalt_paths.h"
#include "cobalt/base/polymorphic_downcast.h"
#include "cobalt/math/rect_f.h"
#include "cobalt/render_tree/brush.h"
#include "cobalt/render_tree/dump_render_tree_to_string.h"
#include "cobalt/render_tree/rect_node.h"
#include "nb/memory_scope.h"
using cobalt::render_tree::Node;
using cobalt::render_tree::animations::AnimateNode;
namespace cobalt {
namespace renderer {
namespace {
// In order to put a bound on memory we set a maximum submission queue size that
// is empirically found to be a nice balance between animation smoothing and
// memory usage.
const size_t kMaxSubmissionQueueSize = 4u;
// How quickly the renderer time adjusts to changing submission times.
// 500ms is chosen as a default because it is fast enough that the user will not
// usually notice input lag from a slow timeline renderer, but slow enough that
// quick updates while a quick animation is playing should not jank.
const double kTimeToConvergeInMS = 500.0;
// The stack size to be used for the renderer thread. This is must be large
// enough to support recursing on the render tree.
const int kRendererThreadStackSize =
128 * 1024 + base::kAsanAdditionalStackSize;
// How many entries the rasterize periodic timer will contain before updating.
const size_t kRasterizePeriodicTimerEntriesPerUpdate = 60;
// The maxiumum numer of entries that the rasterize animations timer can contain
// before automatically updating. In the typical use case, the update will
// occur manually when the animations expire.
const size_t kRasterizeAnimationsTimerMaxEntries = 60;
void DestructSubmissionOnMessageLoop(MessageLoop* message_loop,
scoped_ptr<Submission> submission) {
TRACE_EVENT0("cobalt::renderer", "DestructSubmissionOnMessageLoop()");
if (MessageLoop::current() != message_loop) {
message_loop->DeleteSoon(FROM_HERE, submission.release());
}
}
} // namespace
Pipeline::Pipeline(const CreateRasterizerFunction& create_rasterizer_function,
const scoped_refptr<backend::RenderTarget>& render_target,
backend::GraphicsContext* graphics_context,
bool submit_even_if_render_tree_is_unchanged,
ShutdownClearMode clear_on_shutdown_mode,
const Options& options)
: rasterizer_created_event_(true, false),
render_target_(render_target),
graphics_context_(graphics_context),
rasterizer_thread_("Rasterizer"),
submission_disposal_thread_("Rasterizer Submission Disposal"),
submit_even_if_render_tree_is_unchanged_(
submit_even_if_render_tree_is_unchanged),
last_render_animations_active_(false),
rasterize_periodic_timer_("Renderer.Rasterize.Duration",
kRasterizePeriodicTimerEntriesPerUpdate,
false /*enable_entry_list_c_val*/),
ALLOW_THIS_IN_INITIALIZER_LIST(rasterize_animations_interval_timer_(
"Renderer.Rasterize.AnimationsInterval",
kRasterizeAnimationsTimerMaxEntries, true /*enable_entry_list_c_val*/,
base::Bind(&Pipeline::FrameStatsOnFlushCallback,
base::Unretained(this)))),
rasterize_animations_timer_("Renderer.Rasterize.Animations",
kRasterizeAnimationsTimerMaxEntries,
true /*enable_entry_list_c_val*/),
new_render_tree_rasterize_count_(
"Count.Renderer.Rasterize.NewRenderTree", 0,
"Total number of new render trees rasterized."),
new_render_tree_rasterize_time_(
"Time.Renderer.Rasterize.NewRenderTree", 0,
"The last time a new render tree was rasterized."),
has_active_animations_c_val_(
"Renderer.HasActiveAnimations", false,
"Is non-zero if the current render tree has active animations."),
animations_start_time_(
"Time.Renderer.Rasterize.Animations.Start", 0,
"The most recent time animations started playing."),
animations_end_time_("Time.Renderer.Rasterize.Animations.End", 0,
"The most recent time animations ended playing."),
#if defined(ENABLE_DEBUG_CONSOLE)
ALLOW_THIS_IN_INITIALIZER_LIST(dump_current_render_tree_command_handler_(
"dump_render_tree",
base::Bind(&Pipeline::OnDumpCurrentRenderTree,
base::Unretained(this)),
"Dumps the current render tree to text.",
"Dumps the current render tree either to the console if no parameter "
"is specified, or to a file with the specified filename relative to "
"the debug output folder.")),
ALLOW_THIS_IN_INITIALIZER_LIST(toggle_fps_stdout_command_handler_(
"toggle_fps_stdout",
base::Bind(&Pipeline::OnToggleFpsStdout, base::Unretained(this)),
"Toggles printing framerate stats to stdout.",
"When enabled, at the end of each animation (or every time a maximum "
"number of frames are rendered), framerate statistics are printed "
"to stdout.")),
ALLOW_THIS_IN_INITIALIZER_LIST(toggle_fps_overlay_command_handler_(
"toggle_fps_overlay",
base::Bind(&Pipeline::OnToggleFpsOverlay, base::Unretained(this)),
"Toggles rendering framerate stats to an overlay on the display.",
"Framerate statistics are rendered to a display overlay. The "
"numbers are updated at the end of each animation (or every time a "
"maximum number of frames are rendered), framerate statistics are "
"printed to stdout.")),
#endif
clear_on_shutdown_mode_(clear_on_shutdown_mode),
enable_fps_stdout_(options.enable_fps_stdout),
enable_fps_overlay_(options.enable_fps_overlay),
fps_overlay_updated_(false) {
TRACE_EVENT0("cobalt::renderer", "Pipeline::Pipeline()");
// The actual Pipeline can be constructed from any thread, but we want
// rasterizer_thread_checker_ to be associated with the rasterizer thread,
// so we detach it here and let it reattach itself to the rasterizer thread
// when CalledOnValidThread() is called on rasterizer_thread_checker_ below.
rasterizer_thread_checker_.DetachFromThread();
rasterizer_thread_.StartWithOptions(
base::Thread::Options(MessageLoop::TYPE_DEFAULT, kRendererThreadStackSize,
base::kThreadPriority_Highest));
rasterizer_thread_.message_loop()->PostTask(
FROM_HERE,
base::Bind(&Pipeline::InitializeRasterizerThread, base::Unretained(this),
create_rasterizer_function));
}
Pipeline::~Pipeline() {
TRACE_EVENT0("cobalt::renderer", "Pipeline::~Pipeline()");
// First we shutdown the submission queue. We do this as a separate step from
// rasterizer shutdown because it may post messages back to the rasterizer
// thread as it clears itself out (e.g. it may ask the rasterizer thread to
// delete textures). We wait for this shutdown to complete before proceeding
// to shutdown the rasterizer thread.
rasterizer_thread_.message_loop()->PostBlockingTask(
FROM_HERE,
base::Bind(&Pipeline::ShutdownSubmissionQueue, base::Unretained(this)));
// This potential reference to a render tree whose animations may have ended
// must be destroyed before we shutdown the rasterizer thread since it may
// contain references to render tree nodes and resources.
last_render_tree_ = NULL;
// Submit a shutdown task to the rasterizer thread so that it can shutdown
// anything that must be shutdown from that thread.
rasterizer_thread_.message_loop()->PostTask(
FROM_HERE,
base::Bind(&Pipeline::ShutdownRasterizerThread, base::Unretained(this)));
rasterizer_thread_.Stop();
}
render_tree::ResourceProvider* Pipeline::GetResourceProvider() {
rasterizer_created_event_.Wait();
return rasterizer_->GetResourceProvider();
}
void Pipeline::Submit(const Submission& render_tree_submission) {
TRACE_EVENT0("cobalt::renderer", "Pipeline::Submit()");
// Execute the actual set of the new render tree on the rasterizer tree.
rasterizer_thread_.message_loop()->PostTask(
FROM_HERE, base::Bind(&Pipeline::SetNewRenderTree, base::Unretained(this),
CollectAnimations(render_tree_submission)));
}
void Pipeline::Clear() {
TRACE_EVENT0("cobalt::renderer", "Pipeline::Clear()");
rasterizer_thread_.message_loop()->PostBlockingTask(
FROM_HERE,
base::Bind(&Pipeline::ClearCurrentRenderTree, base::Unretained(this)));
}
void Pipeline::RasterizeToRGBAPixels(
const Submission& render_tree_submission,
const RasterizationCompleteCallback& complete) {
TRACK_MEMORY_SCOPE("Renderer");
TRACE_EVENT0("cobalt::renderer", "Pipeline::RasterizeToRGBAPixels()");
if (MessageLoop::current() != rasterizer_thread_.message_loop()) {
rasterizer_thread_.message_loop()->PostTask(
FROM_HERE,
base::Bind(&Pipeline::RasterizeToRGBAPixels, base::Unretained(this),
CollectAnimations(render_tree_submission), complete));
return;
}
// Create a new target that is the same dimensions as the display target.
scoped_refptr<backend::RenderTarget> offscreen_target =
graphics_context_->CreateDownloadableOffscreenRenderTarget(
render_target_->GetSize());
// Rasterize this submission into the newly created target.
RasterizeSubmissionToRenderTarget(render_tree_submission, offscreen_target);
// Load the texture's pixel data into a CPU memory buffer and return it.
complete.Run(graphics_context_->DownloadPixelDataAsRGBA(offscreen_target),
render_target_->GetSize());
}
void Pipeline::SetNewRenderTree(const Submission& render_tree_submission) {
DCHECK(rasterizer_thread_checker_.CalledOnValidThread());
DCHECK(render_tree_submission.render_tree.get());
TRACE_EVENT0("cobalt::renderer", "Pipeline::SetNewRenderTree()");
submission_queue_->PushSubmission(render_tree_submission,
base::TimeTicks::Now());
// Start the rasterization timer if it is not yet started.
if (!rasterize_timer_) {
// We artificially limit the period between submissions to 7ms, in case a
// platform does not rate limit itself during swaps. This was changed from
// 15ms to accommodate 120fps requirements on some platforms.
rasterize_timer_.emplace(
FROM_HERE, base::TimeDelta::FromMillisecondsD(
COBALT_MINIMUM_FRAME_TIME_IN_MILLISECONDS),
base::Bind(&Pipeline::RasterizeCurrentTree, base::Unretained(this)),
true, true);
rasterize_timer_->Reset();
}
}
void Pipeline::ClearCurrentRenderTree() {
DCHECK(rasterizer_thread_checker_.CalledOnValidThread());
TRACE_EVENT0("cobalt::renderer", "Pipeline::ClearCurrentRenderTree()");
submission_queue_->Reset();
rasterize_timer_ = base::nullopt;
}
void Pipeline::RasterizeCurrentTree() {
TRACK_MEMORY_SCOPE("Renderer");
DCHECK(rasterizer_thread_checker_.CalledOnValidThread());
TRACE_EVENT0("cobalt::renderer", "Pipeline::RasterizeCurrentTree()");
base::TimeTicks now = base::TimeTicks::Now();
Submission submission = submission_queue_->GetCurrentSubmission(now);
bool is_new_render_tree = submission.render_tree != last_render_tree_;
bool has_render_tree_changed =
last_render_animations_active_ || is_new_render_tree;
// If our render tree hasn't changed from the one that was previously
// rendered and it's okay on this system to not flip the display buffer
// frequently, then we can just not do anything here.
if (!fps_overlay_updated_ && !submit_even_if_render_tree_is_unchanged_ &&
!has_render_tree_changed) {
return;
}
// Check whether the animations in the render tree that is being rasterized
// are active.
render_tree::animations::AnimateNode* animate_node =
base::polymorphic_downcast<render_tree::animations::AnimateNode*>(
submission.render_tree.get());
bool are_animations_active = animate_node->expiry() > submission.time_offset;
// If animations are going from being inactive to active, then set the c_val
// prior to starting the animation so that it's in the correct state while the
// tree is being rendered.
// Also, start the interval timer now. While the first entry only captures a
// partial interval, it's recorded to include the duration of the first
// submission. All subsequent entries will record a full interval.
if (!last_render_animations_active_ && are_animations_active) {
has_active_animations_c_val_ = true;
rasterize_animations_interval_timer_.Start(now);
}
// The rasterization is only timed with the periodic timer when the render
// tree has changed. This ensures that the frames being timed are consistent
// between platforms that submit unchanged trees and those that don't.
bool should_run_periodic_timer = has_render_tree_changed;
// The rasterization is only timed with the animations timer when there are
// animations to track. This applies when animations were active during either
// the last rasterization or the current one. The reason for including the
// last one is that if animations have just expired, then this rasterization
// produces the final state of the animated tree.
bool should_run_animations_timer =
last_render_animations_active_ || are_animations_active;
if (should_run_periodic_timer) {
rasterize_periodic_timer_.Start(now);
}
if (should_run_animations_timer) {
rasterize_animations_timer_.Start(now);
}
// Rasterize the last submitted render tree.
RasterizeSubmissionToRenderTarget(submission, render_target_);
// Update now with the post-submission time.
now = base::TimeTicks::Now();
if (should_run_periodic_timer) {
rasterize_periodic_timer_.Stop(now);
}
if (should_run_animations_timer) {
rasterize_animations_interval_timer_.Stop(now);
rasterize_animations_timer_.Stop(now);
// If animations are active, then they are guaranteed at least one more
// interval. Start the timer to record its duration.
if (are_animations_active) {
rasterize_animations_interval_timer_.Start(now);
}
}
if (is_new_render_tree) {
++new_render_tree_rasterize_count_;
new_render_tree_rasterize_time_ = now.ToInternalValue();
}
// Check for if the animations are starting or ending.
if (!last_render_animations_active_ && are_animations_active) {
animations_start_time_ = now.ToInternalValue();
} else if (last_render_animations_active_ && !are_animations_active) {
animations_end_time_ = now.ToInternalValue();
has_active_animations_c_val_ = false;
rasterize_animations_interval_timer_.Flush();
rasterize_animations_timer_.Flush();
}
last_render_animations_active_ = are_animations_active;
}
void Pipeline::RasterizeSubmissionToRenderTarget(
const Submission& submission,
const scoped_refptr<backend::RenderTarget>& render_target) {
TRACE_EVENT0("cobalt::renderer",
"Pipeline::RasterizeSubmissionToRenderTarget()");
// Keep track of the last render tree that we rendered so that we can watch
// if it changes, in which case we should reset our tracked
// |previous_animated_area_|.
if (submission.render_tree != last_render_tree_) {
last_render_tree_ = submission.render_tree;
previous_animated_area_ = base::nullopt;
last_render_time_ = base::nullopt;
}
// Animate the render tree using the submitted animations.
render_tree::animations::AnimateNode* animate_node =
base::polymorphic_downcast<render_tree::animations::AnimateNode*>(
submission.render_tree.get());
// Some animations require a GL graphics context to be current. Specifically,
// a call to SbPlayerGetCurrentFrame() may be made to get the current video
// frame to drive a video-as-an-animated-image.
rasterizer_->MakeCurrent();
render_tree::animations::AnimateNode::AnimateResults results =
animate_node->Apply(submission.time_offset);
// Calculate a bounding box around the active animations. Union it with the
// bounding box around active animations from the previous frame, and we get
// a scissor rectangle marking the dirty regions of the screen.
math::RectF animated_bounds = results.get_animation_bounds_since.Run(
last_render_time_ ? *last_render_time_ : base::TimeDelta());
math::Rect rounded_bounds = math::RoundOut(animated_bounds);
base::optional<math::Rect> redraw_area;
if (previous_animated_area_) {
redraw_area = math::UnionRects(rounded_bounds, *previous_animated_area_);
}
previous_animated_area_ = rounded_bounds;
scoped_refptr<render_tree::Node> submit_tree = results.animated;
if (enable_fps_overlay_ && fps_overlay_) {
submit_tree = fps_overlay_->AnnotateRenderTreeWithOverlay(results.animated);
}
// Rasterize the animated render tree.
rasterizer::Rasterizer::Options rasterizer_options;
rasterizer_options.dirty = redraw_area;
rasterizer_->Submit(submit_tree, render_target, rasterizer_options);
if (!submission.on_rasterized_callback.is_null()) {
submission.on_rasterized_callback.Run();
}
last_render_time_ = submission.time_offset;
}
void Pipeline::InitializeRasterizerThread(
const CreateRasterizerFunction& create_rasterizer_function) {
TRACE_EVENT0("cobalt::renderer", "Pipeline::InitializeRasterizerThread");
DCHECK(rasterizer_thread_checker_.CalledOnValidThread());
rasterizer_ = create_rasterizer_function.Run();
rasterizer_created_event_.Signal();
// Note that this is setup as high priority, but lower than the rasterizer
// thread's priority (kThreadPriority_Highest). This is to ensure that
// we never interrupt the rasterizer in order to dispose render trees, but
// at the same time we do want to prioritize cleaning them up to avoid
// large queues of pending render tree disposals.
submission_disposal_thread_.StartWithOptions(
base::Thread::Options(MessageLoop::TYPE_DEFAULT, kRendererThreadStackSize,
base::kThreadPriority_High));
submission_queue_.emplace(
kMaxSubmissionQueueSize,
base::TimeDelta::FromMillisecondsD(kTimeToConvergeInMS),
base::Bind(&DestructSubmissionOnMessageLoop,
submission_disposal_thread_.message_loop()));
}
void Pipeline::ShutdownSubmissionQueue() {
TRACE_EVENT0("cobalt::renderer", "Pipeline::ShutdownSubmissionQueue()");
DCHECK(rasterizer_thread_checker_.CalledOnValidThread());
// Stop and shutdown the raterizer timer. If we won't have a submission
// queue anymore, we won't be able to rasterize anymore.
rasterize_timer_ = base::nullopt;
// Do not retain any more references to the current render tree (which
// may refer to rasterizer resources) or animations which may refer to
// render trees.
submission_queue_ = base::nullopt;
// Shut down our submission disposer thread. This needs to happen now to
// ensure that any pending "dispose" messages are processed. Each disposal
// may result in new messages being posted to this rasterizer thread's message
// loop, and so we want to make sure these are all queued up before
// proceeding.
submission_disposal_thread_.Stop();
}
void Pipeline::ShutdownRasterizerThread() {
TRACE_EVENT0("cobalt::renderer", "Pipeline::ShutdownRasterizerThread()");
DCHECK(rasterizer_thread_checker_.CalledOnValidThread());
// Submit a black fullscreen rect node to clear the display before shutting
// down. This can be helpful if we quit while playing a video via
// punch-through, which may result in unexpected images/colors appearing for
// a flicker behind the display.
if (render_target_ && (clear_on_shutdown_mode_ == kClearToBlack)) {
rasterizer_->Submit(
new render_tree::RectNode(
math::RectF(render_target_->GetSize()),
scoped_ptr<render_tree::Brush>(new render_tree::SolidColorBrush(
render_tree::ColorRGBA(0.0f, 0.0f, 0.0f, 1.0f)))),
render_target_);
}
// Finally, destroy the rasterizer.
rasterizer_.reset();
}
#if defined(ENABLE_DEBUG_CONSOLE)
void Pipeline::OnDumpCurrentRenderTree(const std::string& message) {
if (MessageLoop::current() != rasterizer_thread_.message_loop()) {
rasterizer_thread_.message_loop()->PostTask(
FROM_HERE, base::Bind(&Pipeline::OnDumpCurrentRenderTree,
base::Unretained(this), message));
return;
}
if (!rasterize_timer_) {
LOG(INFO) << "No render tree available yet.";
return;
}
// Grab the most recent submission, animate it, and then dump the results to
// text.
Submission submission =
submission_queue_->GetCurrentSubmission(base::TimeTicks::Now());
render_tree::animations::AnimateNode* animate_node =
base::polymorphic_downcast<render_tree::animations::AnimateNode*>(
submission.render_tree.get());
render_tree::animations::AnimateNode::AnimateResults results =
animate_node->Apply(submission.time_offset);
std::string tree_dump = render_tree::DumpRenderTreeToString(results.animated);
if (message.empty() || message == "undefined") {
// If no filename was specified, send output to the console.
LOG(INFO) << tree_dump.c_str();
} else {
// If a filename was specified, dump the output to that file.
FilePath out_dir;
PathService::Get(paths::DIR_COBALT_DEBUG_OUT, &out_dir);
file_util::WriteFile(out_dir.Append(message), tree_dump.c_str(),
tree_dump.length());
}
}
void Pipeline::OnToggleFpsStdout(const std::string& message) {
if (MessageLoop::current() != rasterizer_thread_.message_loop()) {
rasterizer_thread_.message_loop()->PostTask(
FROM_HERE, base::Bind(&Pipeline::OnToggleFpsStdout,
base::Unretained(this), message));
return;
}
enable_fps_stdout_ = !enable_fps_stdout_;
}
void Pipeline::OnToggleFpsOverlay(const std::string& message) {
if (MessageLoop::current() != rasterizer_thread_.message_loop()) {
rasterizer_thread_.message_loop()->PostTask(
FROM_HERE, base::Bind(&Pipeline::OnToggleFpsOverlay,
base::Unretained(this), message));
return;
}
enable_fps_overlay_ = !enable_fps_overlay_;
}
#endif // #if defined(ENABLE_DEBUG_CONSOLE)
Submission Pipeline::CollectAnimations(
const Submission& render_tree_submission) {
// Constructing an AnimateNode will result in the tree being traversed to
// collect all sub-AnimateNodes into the new one, in order to maintain the
// invariant that a sub-tree of an AnimateNode has no AnimateNodes.
Submission collected_submission = render_tree_submission;
collected_submission.render_tree = new render_tree::animations::AnimateNode(
render_tree_submission.render_tree);
return collected_submission;
}
namespace {
void PrintFPS(const base::CValCollectionTimerStatsFlushResults& results) {
SbLogRaw(base::StringPrintf("FPS => # samples: %d, avg: %.1fms, "
"[min, max]: [%.1fms, %.1fms]\n"
" 25th : 50th : 75th : 95th pct - "
"%.1fms : %.1fms : %.1fms : %.1fms\n",
static_cast<unsigned int>(results.sample_count),
results.average.InMillisecondsF(),
results.minimum.InMillisecondsF(),
results.maximum.InMillisecondsF(),
results.percentile_25th.InMillisecondsF(),
results.percentile_50th.InMillisecondsF(),
results.percentile_75th.InMillisecondsF(),
results.percentile_95th.InMillisecondsF())
.c_str());
}
} // namespace
void Pipeline::FrameStatsOnFlushCallback(
const base::CValCollectionTimerStatsFlushResults& flush_results) {
DCHECK(rasterizer_thread_checker_.CalledOnValidThread());
if (enable_fps_overlay_) {
if (!fps_overlay_) {
fps_overlay_.emplace(rasterizer_->GetResourceProvider());
}
fps_overlay_->UpdateOverlay(flush_results);
fps_overlay_updated_ = true;
}
if (enable_fps_stdout_) {
PrintFPS(flush_results);
}
}
} // namespace renderer
} // namespace cobalt