API and Timing Model
Frame Scheduler Development
Life of a Pixel shows how a client Present request is integrated into a Scenic frame.
Scenic contract with clients
If you want to use Present2
in a
way where frame scheduling is both correct and performant, consider the
guarantees that Scenic provides surrounding:
Vsync accuracy
Scenic makes no guarantees as to the accuracy or precision of the future Vsync information it provides to clients. Scenic merely forwards whatever information it receives from the hardware which may be incorrect.
There are two identified Vsync issues, minor drift
and major disruptions, with some implications
for what Present2
clients should
do.
Minor Vsync drift
One problem is when VSync occurs (on the scale of microseconds) before the time that Scenic reports.
Assume a client receives a future Vsync time of 1000us, in a
FuturePresentationTimes
object. The client then calls
Present2
with a requested
presentation time of 1000us, expecting that its content will be displayed at
1000us. However, it is possible that the true Vsync might actually occur at
995us, and the client will effectively drop a frame as Scenic will only
attempt to display the client's content at the following Vsync.
It is therefore recommended that clients request presentation times half a Vsync interval offset from their true target time, to account for this drift in a Vsync interval independent way.
Major Vsync disruptions
Another problem is a Vsync occurring milliseconds after it was supposed to, with the following Vsync resuming the previously established pattern.
Assume the display has a 60Hz refresh rate. You can then expect Vsyncs to occur at times 0ms, 16.7ms, 33.3ms, 50ms and so on. On some hardware, a Vsync can be delayed. This might lead to Vsyncs occurring at times 0ms, 22ms, 33.3ms, 50ms and so on.
This issue happens sporadically and without warning, so there is no way to
effectively prevent it. In order to mitigate its effects, clients are
expected to take extra care to ensure that their
requested_presentation_time
s
remain monotonically increasing. If not, the client's session will be shut down.
To have your requested_presentation_time
remain monotonically increasing, add
the following logic to your scheduling code:
// Do some operations.
requested_presentation_time = CalculateNextPresentationTime();
// Lower bound the requested time.
requested_presentation_time = std::max(requested_presentation_time, previous_requested_presentation_time);
previous_requested_presentation_time = requested_presentation_time;
session->Present2(requested_presentation_time, ...);
This code is correct because the new requested time will always be greater or equal to the old requested time.
This code is performant because it will never cause you to miss a frame
unnecessarily, regardless of whatever CalculateNextPresentationTime()
does.
Present2 Best Practices Examples
These examples show how the API can be used for different use cases. The
examples assume that clients using the API have listeners registered for the
OnFramePresented
and
OnScenicEvent
event callbacks and
that session
is an initialized Scenic
Session
channel. For examples of how to set up scenic, see
Scenic examples.
Example 1
The simplest type of application creates and presents a new update every time a previous one has been presented. This is reasonable for applications with small workloads (takes less than a frame to create a frame) and no requirements to minimize latency.
void main() {
PresentNewFrame();
}
void PresentNewFrame() {
// Create a new update and enquee it.
CreateAndEnqueueNewUpdate();
// Flush enqueued events.
Present2Args args;
args.set_requested_presentation_time(0);
args.set_acquire_fences({});
args.set_release_fences({});
args.set_requested_prediction_span(0);
session->Present2(std::move(args), /*callback=*/[]{});
}
void OnFramePresented(...) {
PresentNewFrame();
}
void CreateAndEnqueueNewUpdate() {
// Enqueue commands to update the session
}
Example 2
This example demonstrates how to write an application with small input-driven
updates and, where minimizing latency is important. It creates a new small
update upon receiving an input and immediately calls Present2
attempting to
keep latency low as possible. This approach should only be used for very small
workloads since it creates some unnecessary work by not batching update creation.
int64 num_calls_left_ = 0;
bool update_pending_ = false;
main() {
session->RequestPresentationTimes(/*requested_prediction_span=*/0,
/*callback=*/[this](FuturePresentationTimes future_times){
UpdateNumCallsLeft(future_times.remaining_presents_in_flight_allowed);
};}
}
void UpdateNumCallsLeft(int64_t num_calls_left){
num_calls_left_ = num_calls_left;
}
void OnScenicEvent(Event event) {
if (IsInputEvent(event)) {
CreateAndEnqueueUpdate(std::move(event));
if (num_calls_left_ > 0) {
PresentFrame();
} else {
update_pending_ = true;
}
}
}
void PresentFrame() {
--num_calls_left_;
update_pending_ = false;
Present2Args args;
args.set_requested_presentation_time(0);
args.set_acquire_fences({});
args.set_release_fences({});
args.set_requested_prediction_span(0);
session->Present2(std::move(args),
/*callback=*/[this](FuturePresentationTimes future_times){
UpdateNumCallsLeft(future_times.remaining_presents_in_flight_allowed);
};);
}
void OnFramePresented(FramePresentedInfo info) {
UpdateNumCallsLeft(info.num_presents_allowed);
if (frame_pending_ && num_calls_left_ > 0) {
PresentFrame();
}
}
Example 3
This example demonstrates how to write an input-driven application that batches inputs.
struct TargetTimes {
zx_time_t latch_point;
zx_time_t presentation_time;
};
int64 num_calls_left_ = 0;
bool update_pending_ = false;
bool frame_pending_ = false;
zx_time_t last_targeted_time_ = 0;
std::vector<Event> unhandled_input_events_;
async_dispatcher dispatcher_;
void UpdateNumCallsLeft(int64_t num_calls_left){
num_calls_left_ = num_calls_left;
}
zx_duration_t UpdateCreationTime() {
// Return a prediction for how long an update could take to create.
}
TargetTimes FindNextPresentationTime(
std::vector<PresentationInfo> future_presentations) {
// Select the next future time to target.
zx_time_t now = time.Now();
for(auto times : future_presentations) {
if (times.latch_point > now + UpdateCreationTime()
&& times.presentation_time > last_targeted_time_) {
return {times.latch_point, times.presentation_time};
}
}
// This should never be reached.
return {now, now};
}
void CreateAndEnqueueNewUpdate(std::vector<Event> input_events) {
// Enqueue commands to update the session.
}
void OnScenicEvent(Event event) {
if (IsInputEvent(event)) {
unhandled_input_events_.push_back(std::move(event));
RequestNewFrame();
}
}
void RequestNewFrame() {
if (update_pending_) {
return;
} else {
update_pending_ = true;
ScheduleNextFrame();
}
}
void PresentFrame() {
present_pending_ = false;
session->RequestPresentationTimes(/*requested_prediction_span=*/0,
/*callback=*/[this](FuturePresentationTimes future_times){
if (future_times.remaining_presents_in_flight_allowed > 0) {
// No present calls left. Need to wait to be returned some by previous
// frames being completed in OnFramePresented(). This could happen when
// Scenic gets overwhelmed or stalled for some reason.
present_pending_ = true;
return;
}
TargetTimes target_times =
FindNextPresentationTime(future_times.future_presentations);
last_targeted_time_ = target_time.presentation_time;
// Wait until slightly before the deadline to start creating the update.
zx_time_t wakeup_time = target_times.latch_point - UpdateCreationTime();
async::PostTaskForTime(
dispatcher_,
[this, presentation_time] {
update_pending_ = false;
present_pending_ = false;
CreateAndEnqueueUpdate(std::move(unhandled_input_events_));
Present2Args args;
// We subtract a bit from our requested time (1 ms in this example)
// for two reasons:
// 1. Future presentation times aren't guaranteed to be entirely
// accurate due to hardware vsync drift and other factors.
// 2. A presetnt call is guaranteed to be presented "at or later than"
// the requested presentation time.
// This guards against against `Present2` calls getting accidentally
// delayed for an entire frame.
args.set_requested_presentation_time(
target_times.presentation_time - 1'000'000);
args.set_acquire_fences({});
args.set_release_fences({});
args.set_requested_prediction_span(0);
session->Present2(std::move(args), /*callback=*/[]{};);
},
wakeup_time);
};}
}
void OnFramePresented(FramePresentedInfo info) {
if (frame_pending_ && info.num_presents_allowed > 0) {
PresentFrame();
}
}