blob: ffbcb78365436e5ee47fc840249511c6d490933e [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 "garnet/lib/ui/gfx/engine/default_frame_scheduler.h"
#include <lib/async/cpp/task.h>
#include <lib/async/default.h>
#include <lib/async/time.h>
#include <trace/event.h>
#include <zircon/syscalls.h>
#include "garnet/lib/ui/gfx/displays/display.h"
#include "garnet/lib/ui/gfx/engine/frame_timings.h"
#include "lib/fxl/logging.h"
namespace scenic_impl {
namespace gfx {
DefaultFrameScheduler::DefaultFrameScheduler(const Display* display)
: dispatcher_(async_get_default_dispatcher()),
display_(display),
weak_factory_(this) {
outstanding_frames_.reserve(kMaxOutstandingFrames);
}
DefaultFrameScheduler::~DefaultFrameScheduler() {}
void DefaultFrameScheduler::OnFrameRendered(const FrameTimings& timings) {
TRACE_INSTANT("gfx", "DefaultFrameScheduler::OnFrameRendered",
TRACE_SCOPE_PROCESS, "Timestamp",
timings.rendering_finished_time(), "Frame number",
timings.frame_number());
currently_rendering_ = false;
if (render_pending_) {
RequestFrame();
}
}
void DefaultFrameScheduler::SetRenderContinuously(bool render_continuously) {
render_continuously_ = render_continuously;
if (render_continuously_) {
RequestFrame();
}
}
zx_time_t DefaultFrameScheduler::PredictRequiredFrameRenderTime() const {
// TODO(MZ-400): more sophisticated prediction. This might require more info,
// e.g. about how many compositors will be rendering scenes, at what
// resolutions, etc.
constexpr zx_time_t kHardcodedPrediction = 8'000'000; // 8ms
return kHardcodedPrediction;
}
std::pair<zx_time_t, zx_time_t>
DefaultFrameScheduler::ComputePresentationAndWakeupTimesForTargetTime(
const zx_time_t requested_presentation_time) const {
const zx_time_t last_vsync_time = display_->GetLastVsyncTime();
const zx_duration_t vsync_interval = display_->GetVsyncInterval();
const zx_time_t now = async_now(dispatcher_);
const zx_duration_t required_render_time = PredictRequiredFrameRenderTime();
// Compute the number of full vsync intervals between the last vsync and the
// requested presentation time. Notes:
// - The requested time might be earlier than the last vsync time,
// for example when client content is a bit late.
// - We subtract a nanosecond before computing the number of intervals, to
// avoid an off-by-one error in the common case where a client computes a
// a desired presentation time based on a previously-received actual
// presentation time.
uint64_t num_intervals =
1 + (requested_presentation_time <= last_vsync_time
? 0
: (requested_presentation_time - last_vsync_time - 1) /
vsync_interval);
// Compute the target vsync/presentation time, and the time we would need to
// start rendering to meet the target.
zx_time_t target_presentation_time =
last_vsync_time + (num_intervals * vsync_interval);
zx_time_t wakeup_time = target_presentation_time - required_render_time;
// Handle startup-time corner case: since monotonic clock starts at 0, there
// will be underflow when required_render_time > target_presentation_time,
// resulting in a *very* late wakeup time.
while (required_render_time > target_presentation_time) {
target_presentation_time += vsync_interval;
wakeup_time = target_presentation_time - required_render_time;
}
// If it's too late to start rendering, drop a frame.
while (wakeup_time < now) {
// TODO(MZ-400): This is insufficient. It prevents Scenic from
// overcommitting but it doesn't prevent apparent jank. For example,
// consider apps like hello_scenic that don't render the next frame
// until they receive the async response to the present call, which contains
// the actual presentation time. Currently, it won't receive that response
// until the defered frame is rendered, and so the animated content will be
// at the wrong position.
//
// One solution is to evaluate animation inside Scenic. However, there will
// probably always be apps that use the "hello_scenic pattern". To
// support this, the app needs to be notified of the dropped frame so that
// it can make any necessary updates before the next frame is rendered.
//
// This seems simple enough, but it is tricky to specify/implement:
//
// It is critical to maintain the invariant that receiving the Present()
// response means that your commands enqueued before that Present() were
// actually applied in the session. Currently, we only do this when
// rendering a frame, but we would have to do it earlier. Also, is this
// even the right invariant? See discussion in session.fidl
//
// But when do we do it? Immediately? That might be well before the
// desired presentation time; is that a problem? Do we sleep twice, once to
// wake up and apply commands without rendering, and again to render?
//
// If we do that, what presentation time should we return to clients, given
// that our only access to Vsync times is via an event signaled by Magma?
// (probably we could just extrapolate from the previous vsync, assuming
// that there is one, but this is nevertheless complexity to consider).
//
// Other complications will arise as when we try to address this.
// Try it and see!
// Drop a frame.
target_presentation_time += vsync_interval;
wakeup_time += vsync_interval;
}
#if SCENIC_IGNORE_VSYNC
return std::make_pair(now, now);
#else
return std::make_pair(target_presentation_time, wakeup_time);
#endif
}
void DefaultFrameScheduler::RequestFrame() {
FXL_CHECK(!updatable_sessions_.empty() || render_continuously_ ||
render_pending_);
// Logging the first few frames to find common startup bugs.
if (frame_number_ < 5) {
FXL_LOG(INFO) << "DefaultFrameScheduler::RequestFrame";
}
auto requested_presentation_time =
render_continuously_ || render_pending_
? 0
: updatable_sessions_.top().requested_presentation_time;
auto next_times = ComputePresentationAndWakeupTimesForTargetTime(
requested_presentation_time);
auto new_presentation_time = next_times.first;
auto new_wakeup_time = next_times.second;
// If there is no render waiting we should schedule a frame.
// Likewise, if newly predicted wake up time is earlier than the current one
// then we need to reschedule the next wake up.
if (!frame_render_task_.is_pending() || new_wakeup_time < wakeup_time_) {
frame_render_task_.Cancel();
wakeup_time_ = new_wakeup_time;
next_presentation_time_ = new_presentation_time;
frame_render_task_.PostForTime(dispatcher_, zx::time(wakeup_time_));
}
}
void DefaultFrameScheduler::MaybeRenderFrame(async_dispatcher_t*,
async::TaskBase*, zx_status_t) {
auto presentation_time = next_presentation_time_;
TRACE_DURATION("gfx", "FrameScheduler::MaybeRenderFrame", "presentation_time",
presentation_time);
// Logging the first few frames to find common startup bugs.
if (frame_number_ < 5) {
FXL_LOG(INFO)
<< "DefaultFrameScheduler::MaybeRenderFrame presentation_time="
<< presentation_time << " wakeup_time=" << wakeup_time_
<< " frame_number=" << frame_number_;
}
// TODO(MZ-400): Check whether there is enough time to render. If not, drop
// a frame and reschedule. It would be nice to simply bump the presentation
// time, but then there is a danger of rendering too fast, and actually
// presenting 1 vsync before the bumped presentation time. The safest way to
// avoid this is to start rendering after the vsync that we want to skip.
//
// TODO(MZ-400): If there isn't enough time to render, why did this happen?
// One possiblity is that we woke up later than expected, and another is an
// increase in the estimated time required to render the frame (well, not
// currently: the estimate is a hard-coded constant. Figure out what
// happened, and log it appropriately.
// TODO(SCN-124): We should derive an appropriate value from the rendering
// targets, in particular giving priority to couple to the display refresh
// (vsync).
auto presentation_interval = display_->GetVsyncInterval();
if (delegate_.frame_renderer && delegate_.session_updater) {
FXL_DCHECK(outstanding_frames_.size() < kMaxOutstandingFrames);
const zx_time_t now = async_now(dispatcher_);
auto frame_timings = fxl::MakeRefCounted<FrameTimings>(
this, ++frame_number_, presentation_time);
TRACE_INSTANT("gfx", "Render start", TRACE_SCOPE_PROCESS, "Timestamp", now,
"Frame number", frame_number_);
// Apply all updates
bool any_updates_were_applied =
ApplyScheduledSessionUpdates(frame_timings->frame_number(),
presentation_time, presentation_interval);
if (!any_updates_were_applied && !render_pending_ &&
!render_continuously_) {
return;
}
if (currently_rendering_) {
render_pending_ = true;
return;
}
// Logging the first few frames to find common startup bugs.
if (frame_number_ < 5) {
FXL_LOG(INFO)
<< "DefaultFrameScheduler: calling RenderFrame presentation_time="
<< presentation_time << " frame_number=" << frame_number_;
}
// Render the frame
currently_rendering_ = delegate_.frame_renderer->RenderFrame(
frame_timings, presentation_time, presentation_interval);
if (currently_rendering_) {
outstanding_frames_.push_back(frame_timings);
render_pending_ = false;
}
}
// If necessary, schedule another frame.
if (!updatable_sessions_.empty()) {
RequestFrame();
}
}
void DefaultFrameScheduler::ScheduleUpdateForSession(
zx_time_t presentation_time, scenic_impl::SessionId session_id) {
updatable_sessions_.push({.session_id = session_id,
.requested_presentation_time = presentation_time});
RequestFrame();
}
bool DefaultFrameScheduler::ApplyScheduledSessionUpdates(
uint64_t frame_number, zx_time_t presentation_time,
zx_duration_t presentation_interval) {
FXL_DCHECK(delegate_.session_updater);
// Logging the first few frames to find common startup bugs.
if (frame_number_ < 5) {
FXL_LOG(INFO) << "DefaultFrameScheduler::ApplyScheduledSessionUpdates "
"presentation_time="
<< presentation_time << " frame_number=" << frame_number_;
}
TRACE_DURATION("gfx", "ApplyScheduledSessionUpdates", "time",
presentation_time, "interval", presentation_interval);
std::vector<SessionUpdate> sessions_to_update;
while (!updatable_sessions_.empty()) {
auto& top = updatable_sessions_.top();
if (top.requested_presentation_time > presentation_time) {
break;
}
sessions_to_update.push_back(std::move(top));
updatable_sessions_.pop();
}
bool needs_render = delegate_.session_updater->UpdateSessions(
std::move(sessions_to_update), frame_number, presentation_time,
presentation_interval);
return needs_render;
}
void DefaultFrameScheduler::OnFramePresented(const FrameTimings& timings) {
FXL_DCHECK(!outstanding_frames_.empty());
// TODO(MZ-400): how should we handle this case? It is theoretically
// possible, but if if it happens then it means that the EventTimestamper is
// receiving signals out-of-order and is therefore generating bogus data.
FXL_DCHECK(outstanding_frames_[0].get() == &timings) << "out-of-order.";
if (timings.frame_was_dropped()) {
TRACE_INSTANT("gfx", "FrameDropped", TRACE_SCOPE_PROCESS, "frame_number",
timings.frame_number());
} else {
// Log trace data.
// TODO(MZ-400): just pass the whole Frame to a listener.
int64_t target_vs_actual_usecs =
static_cast<int64_t>(timings.actual_presentation_time() -
timings.target_presentation_time()) /
1000;
zx_time_t now = async_now(dispatcher_);
FXL_DCHECK(now >= timings.actual_presentation_time());
uint64_t elapsed_since_presentation_usecs =
static_cast<int64_t>(now - timings.actual_presentation_time()) / 1000;
TRACE_INSTANT("gfx", "FramePresented", TRACE_SCOPE_PROCESS, "frame_number",
timings.frame_number(), "presentation time (usecs)",
timings.actual_presentation_time() / 1000,
"target time missed by (usecs)", target_vs_actual_usecs,
"elapsed time since presentation (usecs)",
elapsed_since_presentation_usecs);
}
// Pop the front Frame off the queue.
for (size_t i = 1; i < outstanding_frames_.size(); ++i) {
outstanding_frames_[i - 1] = std::move(outstanding_frames_[i]);
}
outstanding_frames_.resize(outstanding_frames_.size() - 1);
if (render_continuously_) {
RequestFrame();
}
}
} // namespace gfx
} // namespace scenic_impl