Commit f5bbb138 authored by Klaus Weidner's avatar Klaus Weidner Committed by Commit Bot

Add SubmitFrameMissing mojo call for WebVR/WebXR

Goal is that we get a clean lifecycle for a functioning WebVR/WebXR
presentation render loop. It's started by a presenting SendVSync, calls
GetVSync to schedule the next frame, and is ended by a SubmitFrame
call. If there was nothing drawn, it uses SubmitFrameMissing instead of the
usual SubmitFrame/SubmitFrameWithTextureHandle.

In WebVR 1.1, submitFrame is a JS call, and the app can exit its animation
loop without calling it. WebXR had an analogous feature where SubmitFrame
was skipped if the framebuffer wasn't touched by drawing calls. This
made it hard to tell for the device side if a frame is done or not.

WebVR 1.1 worked around this by deferring a GetVSync call until after
SubmitFrame, but this was complex:

-  // The logic here is a bit subtle. We get called from one of the following
-  // four contexts:
-  //
-  // (a) from requestAnimationFrame if outside an animating context (i.e. the
-  //     first rAF call from inside a getVRDisplays() promise)
-  //
-  // (b) from requestAnimationFrame in an animating context if the JS code
-  //     calls rAF after submitFrame.
-  //
-  // (c) from submitFrame if that is called after rAF.
-  //
-  // (d) from ProcessScheduledAnimations if a rAF callback finishes without
-  //     submitting a frame.
-  //
-  // These cases are mutually exclusive which prevents duplicate GetVSync
-  // calls. Case (a) only applies outside an animating context
-  // (in_animation_frame_ is false), and (b,c,d) all require an animating
-  // context. While in an animating context, submitFrame is called either
-  // before rAF (b), after rAF (c), or not at all (d). If rAF isn't called at
-  // all, there won't be future frames.

This CL removes those special cases and just always calls RequestVSync from
requestAnimationFrame, collapsing cases (a) and (b) into an unconditional call.
Cases (c) and (d) are now no longer needed and removed.

The layout tests now check the SubmitFrameMissing call count.

Also added a check for a WebXR exclusive session ending in the middle of a
frame. (The layout tests revealed this since OnEndFrame no longer exits early
on a clean framebuffer.)

Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel;master.tryserver.chromium.linux:linux_layout_tests_slimming_paint_v2
Change-Id: I5722097d421ca9448760e696ea379895a1320199
Reviewed-on: https://chromium-review.googlesource.com/996614Reviewed-by: default avatarIan Vollick <vollick@chromium.org>
Reviewed-by: default avatarBrandon Jones <bajones@chromium.org>
Reviewed-by: default avatarMartin Barbella <mbarbella@chromium.org>
Reviewed-by: default avatarBill Orr <billorr@chromium.org>
Commit-Queue: Klaus Weidner <klausw@chromium.org>
Cr-Commit-Position: refs/heads/master@{#548518}
parent 521940b7
......@@ -304,6 +304,12 @@ void MailboxToSurfaceBridge::GenSyncToken(gpu::SyncToken* out_sync_token) {
gl_->GenSyncTokenCHROMIUM(out_sync_token->GetData());
}
void MailboxToSurfaceBridge::WaitSyncToken(const gpu::SyncToken& sync_token) {
TRACE_EVENT0("gpu", __FUNCTION__);
DCHECK(IsConnected());
gl_->WaitSyncTokenCHROMIUM(sync_token.GetConstData());
}
void MailboxToSurfaceBridge::WaitForClientGpuFence(gfx::GpuFence* gpu_fence) {
TRACE_EVENT0("gpu", __FUNCTION__);
DCHECK(IsConnected());
......
......@@ -61,6 +61,8 @@ class MailboxToSurfaceBridge {
void GenSyncToken(gpu::SyncToken* out_sync_token);
void WaitSyncToken(const gpu::SyncToken& sync_token);
// Copies a GpuFence from the local context to the GPU process,
// and issues a server wait for it.
void WaitForClientGpuFence(gfx::GpuFence*);
......
......@@ -390,9 +390,7 @@ void VrShellGl::CreateOrResizeWebVRSurface(const gfx::Size& size) {
}
}
void VrShellGl::SubmitFrame(int16_t frame_index,
const gpu::MailboxHolder& mailbox,
base::TimeDelta time_waited) {
bool VrShellGl::IsSubmitFrameExpected(int16_t frame_index) {
TRACE_EVENT0("gpu", "VrShellGl::SubmitWebVRFrame");
// submit_client_ could be null when we exit presentation, if there were
......@@ -400,15 +398,39 @@ void VrShellGl::SubmitFrame(int16_t frame_index,
// will clean up state in blink, so it doesn't wait for
// OnSubmitFrameTransferred or OnSubmitFrameRendered.
if (!submit_client_.get())
return;
return false;
if (frame_index < 0 ||
!webvr_frame_oustanding_[frame_index % kPoseRingBufferSize]) {
mojo::ReportBadMessage("SubmitFrame called with an invalid frame_index");
binding_.Close();
return;
return false;
}
// Frame looks valid.
return true;
}
void VrShellGl::SubmitFrameMissing(int16_t frame_index,
const gpu::SyncToken& sync_token) {
if (!IsSubmitFrameExpected(frame_index))
return;
// Renderer didn't submit a frame. Wait for the sync token to ensure
// that any mailbox_bridge_ operations for the next frame happen after
// whatever drawing the Renderer may have done before exiting.
if (mailbox_bridge_ready_)
mailbox_bridge_->WaitSyncToken(sync_token);
DVLOG(2) << __FUNCTION__ << ": recycle unused animating frame";
webvr_frame_oustanding_[frame_index % kPoseRingBufferSize] = false;
}
void VrShellGl::SubmitFrame(int16_t frame_index,
const gpu::MailboxHolder& mailbox,
base::TimeDelta time_waited) {
if (!IsSubmitFrameExpected(frame_index))
return;
// The JavaScript wait time is supplied externally and not trustworthy. Clamp
// to a reasonable range to avoid math errors.
......
......@@ -175,8 +175,11 @@ class VrShellGl : public device::mojom::VRPresentationProvider {
void OnVSync(base::TimeTicks frame_time);
bool IsSubmitFrameExpected(int16_t frame_index);
// VRPresentationProvider
void GetVSync(GetVSyncCallback callback) override;
void SubmitFrameMissing(int16_t frame_index, const gpu::SyncToken&) override;
void SubmitFrame(int16_t frame_index,
const gpu::MailboxHolder& mailbox,
base::TimeDelta time_waited) override;
......
......@@ -60,6 +60,12 @@ void OculusRenderLoop::CleanUp() {
binding_.Close();
}
void OpenVRRenderLoop::SubmitFrameMissing(int16_t frame_index,
const gpu::SyncToken& sync_token) {
// Nothing to do. It's OK to start the next frame even if the current
// one didn't get sent to the ovrSession.
}
void OculusRenderLoop::SubmitFrame(int16_t frame_index,
const gpu::MailboxHolder& mailbox,
base::TimeDelta time_waited) {
......
......@@ -36,6 +36,7 @@ class OculusRenderLoop : public base::Thread, mojom::VRPresentationProvider {
base::WeakPtr<OculusRenderLoop> GetWeakPtr();
// VRPresentationProvider overrides:
void SubmitFrameMissing(int16_t frame_index, const gpu::SyncToken&) override;
void SubmitFrame(int16_t frame_index,
const gpu::MailboxHolder& mailbox,
base::TimeDelta time_waited) override;
......
......@@ -55,6 +55,12 @@ OpenVRRenderLoop::~OpenVRRenderLoop() {
Stop();
}
void OpenVRRenderLoop::SubmitFrameMissing(int16_t frame_index,
const gpu::SyncToken& sync_token) {
// Nothing to do. It's OK to start the next frame even if the current
// one didn't get sent to OpenVR.
}
void OpenVRRenderLoop::SubmitFrame(int16_t frame_index,
const gpu::MailboxHolder& mailbox,
base::TimeDelta time_waited) {
......
......@@ -35,6 +35,7 @@ class OpenVRRenderLoop : public base::Thread, mojom::VRPresentationProvider {
base::WeakPtr<OpenVRRenderLoop> GetWeakPtr();
// VRPresentationProvider overrides:
void SubmitFrameMissing(int16_t frame_index, const gpu::SyncToken&) override;
void SubmitFrame(int16_t frame_index,
const gpu::MailboxHolder& mailbox,
base::TimeDelta time_waited) override;
......
......@@ -245,6 +245,16 @@ interface VRPresentationProvider {
UpdateLayerBounds(int16 frame_id, gfx.mojom.RectF left_bounds,
gfx.mojom.RectF right_bounds, gfx.mojom.Size source_size);
// Call this if the animation loop exited without submitting a frame to
// ensure that every GetVSync has a matching Submit call. This happens for
// WebXR if there were no drawing operations to the opaque framebuffer, and
// for WebVR 1.1 if the application didn't call SubmitFrame. Usable with any
// VRDisplayFrameTransportMethod. This path does *not* call the
// SubmitFrameClient methods such as OnSubmitFrameTransferred. This is
// intended to help separate frames while presenting, it may or may not
// be called for the last animating frame when presentation ends.
SubmitFrameMissing(int16 frame_id, gpu.mojom.SyncToken sync_token);
// VRDisplayFrameTransportMethod SUBMIT_AS_MAILBOX_HOLDER
SubmitFrame(int16 frame_id, gpu.mojom.MailboxHolder mailbox_holder,
mojo_base.mojom.TimeDelta time_waited);
......
......@@ -10,7 +10,7 @@
<script>
let fakeDisplays = fakeVRDisplays();
vr_test( (t) => {
vr_test( (t, mock_service) => {
return navigator.getVRDisplays().then( (displays) => {
let display = displays[0];
......@@ -33,31 +33,51 @@ vr_test( (t) => {
}
}
function getSubmitFrameCount() {
return mock_service.mockVRDisplays_[0].getSubmitFrameCount();
}
function getMissingFrameCount() {
return mock_service.mockVRDisplays_[0].getMissingFrameCount();
}
function onFrame1() {
assert_equals(getSubmitFrameCount(), 0);
assert_equals(getMissingFrameCount(), 0);
// case (b): submit frame first, then rAF
display.submitFrame();
display.requestAnimationFrame(onFrame2);
}
function onFrame2() {
assert_equals(getSubmitFrameCount(), 1);
assert_equals(getMissingFrameCount(), 0);
// case (c): rAF first, then submit frame
display.requestAnimationFrame(onFrame3);
display.submitFrame();
}
function onFrame3(time) {
assert_equals(getSubmitFrameCount(), 2);
assert_equals(getMissingFrameCount(), 0);
// case (d): don't submit a frame.
display.requestAnimationFrame(onFrame4);
}
function onFrame4(time) {
// If we get here, we're done.
assert_equals(getSubmitFrameCount(), 2);
assert_equals(getMissingFrameCount(), 1);
t.done();
}
function startPresentation() {
assert_equals(getSubmitFrameCount(), 0);
assert_equals(getMissingFrameCount(), 0);
display.requestPresent([{ source : webglCanvas }]).then( () => {
t.step( () => {
assert_equals(getSubmitFrameCount(), 0);
assert_equals(getMissingFrameCount(), 0);
// case (a): in requestPresent promise, outside animating context.
assert_true(display.isPresenting);
display.requestAnimationFrame(onFrame1);
......
......@@ -43,6 +43,10 @@ class MockVRDisplay {
return this.presentation_provider_.submit_frame_count_;
}
getMissingFrameCount() {
return this.presentation_provider_.missing_frame_count_;
}
forceActivate(reason) {
this.displayClient_.onActivate(reason);
}
......@@ -69,6 +73,7 @@ class MockVRPresentationProvider {
this.binding_ = new mojo.Binding(device.mojom.VRPresentationProvider, this);
this.pose_ = null;
this.submit_frame_count_ = 0;
this.missing_frame_count_ = 0;
}
bind(client, request) {
......@@ -77,6 +82,10 @@ class MockVRPresentationProvider {
this.binding_.bind(request);
}
submitFrameMissing(frameId, syncToken) {
this.missing_frame_count_++;
}
submitFrame(frameId, mailboxHolder, timeWaited) {
this.submit_frame_count_++;
......
......@@ -36,6 +36,11 @@ function getSubmitFrameCount() {
return mockVRService.mockVRDisplays_[0].getSubmitFrameCount();
}
// Returns the missing (not submitted) frame count for the first display
function getMissingFrameCount() {
return mockVRService.mockVRDisplays_[0].getMissingFrameCount();
}
function addInputSource(input_source) {
return mockVRService.mockVRDisplays_[0].addInputSource(input_source);
}
......@@ -293,6 +298,10 @@ class MockDevice {
return this.presentation_provider_.submit_frame_count_;
}
getMissingFrameCount() {
return this.presentation_provider_.missing_frame_count_;
}
forceActivate(reason) {
this.displayClient_.onActivate(reason);
}
......@@ -329,6 +338,7 @@ class MockVRPresentationProvider {
this.pose_ = null;
this.next_frame_id_ = 0;
this.submit_frame_count_ = 0;
this.missing_frame_count_ = 0;
this.input_sources_ = [];
this.next_input_source_index_ = 1;
......@@ -340,6 +350,10 @@ class MockVRPresentationProvider {
this.binding_.bind(request);
}
submitFrameMissing(frameId, mailboxHolder, timeWaited) {
this.missing_frame_count_++;
}
submitFrame(frameId, mailboxHolder, timeWaited) {
this.submit_frame_count_++;
......
......@@ -17,6 +17,8 @@ xr_session_promise_test( (session) => new Promise((resolve, reject) => {
session.baseLayer = webglLayer;
function onSkipFrame(time, xrFrame) {
assert_equals(getSubmitFrameCount(), 0);
assert_equals(getMissingFrameCount(), 0);
// No GL commands issued.
session.requestAnimationFrame(onDrawToCanvas);
}
......@@ -24,15 +26,17 @@ xr_session_promise_test( (session) => new Promise((resolve, reject) => {
function onDrawToCanvas(time, xrFrame) {
// Ensure the previous step did not submit a frame.
assert_equals(getSubmitFrameCount(), 0);
assert_equals(getMissingFrameCount(), 1);
// Clear the canvas.
// Clear the canvas, but don't touch the framebuffer.
gl.clear(gl.COLOR_BUFFER_BIT);
session.requestAnimationFrame(onDrawToFramebuffer);
}
function onDrawToFramebuffer(time, xrFrame) {
// Ensure the previous step did not submit a frame.
// Ensure both previous steps did not submit frames.
assert_equals(getSubmitFrameCount(), 0);
assert_equals(getMissingFrameCount(), 2);
// Clear the VRWebGLLayer framebuffer.
gl.bindFramebuffer(gl.FRAMEBUFFER, webglLayer.framebuffer);
......@@ -41,6 +45,7 @@ xr_session_promise_test( (session) => new Promise((resolve, reject) => {
// After the function returns ensure the frame was submitted.
window.setTimeout(() => {
assert_equals(getSubmitFrameCount(), 1);
assert_equals(getMissingFrameCount(), 2);
// Finished test.
resolve();
}, 100);
......
......@@ -244,27 +244,6 @@ void VRDisplay::RequestVSync() {
if (pending_presenting_vsync_)
return;
// The logic here is a bit subtle. We get called from one of the following
// four contexts:
//
// (a) from requestAnimationFrame if outside an animating context (i.e. the
// first rAF call from inside a getVRDisplays() promise)
//
// (b) from requestAnimationFrame in an animating context if the JS code
// calls rAF after submitFrame.
//
// (c) from submitFrame if that is called after rAF.
//
// (d) from ProcessScheduledAnimations if a rAF callback finishes without
// submitting a frame.
//
// These cases are mutually exclusive which prevents duplicate GetVSync
// calls. Case (a) only applies outside an animating context
// (in_animation_frame_ is false), and (b,c,d) all require an animating
// context. While in an animating context, submitFrame is called either
// before rAF (b), after rAF (c), or not at all (d). If rAF isn't called at
// all, there won't be future frames.
pending_magic_window_vsync_ = false;
pending_presenting_vsync_ = true;
vr_presentation_provider_->GetVSync(
......@@ -281,13 +260,8 @@ int VRDisplay::requestAnimationFrame(V8FrameRequestCallback* callback) {
return 0;
pending_vrdisplay_raf_ = true;
// We want to delay the GetVSync call while presenting to ensure it doesn't
// arrive earlier than frame submission, but other than that we want to call
// it as early as possible. See comments inside RequestVSync() for more
// details on the applicable cases.
if (!is_presenting_ || !in_animation_frame_ || did_submit_this_frame_) {
RequestVSync();
}
RequestVSync();
FrameRequestCallbackCollection::V8FrameCallback* frame_callback =
FrameRequestCallbackCollection::V8FrameCallback::Create(callback);
frame_callback->SetUseLegacyTimeBase(false);
......@@ -756,8 +730,6 @@ void VRDisplay::submitFrame() {
// Reset our frame id, since anything we'd want to do (resizing/etc) can
// no-longer happen to this frame.
vr_frame_id_ = -1;
// If we were deferring a rAF-triggered vsync request, do this now.
RequestVSync();
// If preserveDrawingBuffer is false, must clear now. Normally this
// happens as part of compositing, but that's not active while
......@@ -908,10 +880,19 @@ void VRDisplay::ProcessScheduledAnimations(double timestamp) {
pending_vrdisplay_raf_ = false;
did_submit_this_frame_ = false;
scripted_animation_controller_->ServiceScriptedAnimations(timestamp);
// requestAnimationFrame may have deferred RequestVSync, call it now to
// cover the case where no frame was submitted, or where presentation ended
// while servicing the scripted animation.
RequestVSync();
// If presenting and the script didn't call SubmitFrame, let the device
// side know so that it can cleanly reuse resources and make appropriate
// timing decisions. Note that is_presenting_ could become false during
// an animation loop due to reentrant mojo processing in SubmitFrame,
// so there's no guarantee that this is called for the last animating
// frame. That's OK since the sync token placed by FrameSubmitMissing
// is only intended to separate frames while presenting.
if (is_presenting_ && !did_submit_this_frame_) {
DCHECK(frame_transport_);
DCHECK(context_gl_);
frame_transport_->FrameSubmitMissing(vr_presentation_provider_.get(),
context_gl_, vr_frame_id_);
}
}
if (pending_pose_)
frame_pose_ = std::move(pending_pose_);
......
......@@ -150,7 +150,6 @@ void XRFrameProvider::OnPresentationProviderConnectionError() {
pending_exclusive_session_resolver_->Reject(exception);
pending_exclusive_session_resolver_ = nullptr;
}
presentation_provider_.reset();
if (vsync_connection_failed_)
return;
......@@ -321,7 +320,7 @@ void XRFrameProvider::ProcessScheduledFrame(double timestamp) {
}
}
void XRFrameProvider::SubmitWebGLLayer(XRWebGLLayer* layer) {
void XRFrameProvider::SubmitWebGLLayer(XRWebGLLayer* layer, bool was_changed) {
DCHECK(layer);
DCHECK(layer->session() == exclusive_session_);
DCHECK(presentation_provider_);
......@@ -330,6 +329,14 @@ void XRFrameProvider::SubmitWebGLLayer(XRWebGLLayer* layer) {
WebGLRenderingContextBase* webgl_context = layer->context();
if (!was_changed) {
// Just tell the device side that there was no submitted frame instead
// of executing the implicit end-of-frame submit.
frame_transport_->FrameSubmitMissing(presentation_provider_.get(),
webgl_context->ContextGL(), frame_id_);
return;
}
frame_transport_->FramePreImage(webgl_context->ContextGL());
std::unique_ptr<viz::SingleReleaseCallback> image_release_callback;
......
......@@ -35,7 +35,7 @@ class XRFrameProvider final
void OnNonExclusiveVSync(double timestamp);
void SubmitWebGLLayer(XRWebGLLayer*);
void SubmitWebGLLayer(XRWebGLLayer*, bool was_changed);
void UpdateWebGLLayerViewports(XRWebGLLayer*);
void Dispose();
......
......@@ -329,7 +329,10 @@ void XRSession::OnFrame(
AutoReset<bool> resolving(&resolving_frame_, true);
callback_collection_.ExecuteCallbacks(this, presentation_frame);
frame_base_layer->OnFrameEnd();
// The session might have ended in the middle of the frame. Only call
// OnFrameEnd if it's still valid.
if (!ended_)
frame_base_layer->OnFrameEnd();
}
}
......
......@@ -254,17 +254,19 @@ void XRWebGLLayer::OnFrameStart() {
void XRWebGLLayer::OnFrameEnd() {
framebuffer_->MarkOpaqueBufferComplete(false);
// Exit early if the framebuffer contents have not changed.
if (!framebuffer_->HaveContentsChanged())
return;
// Submit the frame to the XR compositor.
if (session()->exclusive()) {
session()->device()->frameProvider()->SubmitWebGLLayer(this);
// Always call submit, but notify if the contents were changed or not.
session()->device()->frameProvider()->SubmitWebGLLayer(
this, framebuffer_->HaveContentsChanged());
} else if (session()->outputContext()) {
ImageBitmap* image_bitmap =
ImageBitmap::Create(TransferToStaticBitmapImage(nullptr));
session()->outputContext()->SetImage(image_bitmap);
// Nothing to do if the framebuffer contents have not changed.
if (framebuffer_->HaveContentsChanged()) {
ImageBitmap* image_bitmap =
ImageBitmap::Create(TransferToStaticBitmapImage(nullptr));
session()->outputContext()->SetImage(image_bitmap);
}
}
}
......
......@@ -66,6 +66,16 @@ void XRFrameTransport::CallPreviousFrameCallback() {
}
}
void XRFrameTransport::FrameSubmitMissing(
device::mojom::blink::VRPresentationProvider* vr_presentation_provider,
gpu::gles2::GLES2Interface* gl,
int16_t vr_frame_id) {
TRACE_EVENT0("gpu", __FUNCTION__);
gpu::SyncToken sync_token;
gl->GenSyncTokenCHROMIUM(sync_token.GetData());
vr_presentation_provider->SubmitFrameMissing(vr_frame_id, sync_token);
}
void XRFrameTransport::FrameSubmit(
device::mojom::blink::VRPresentationProvider* vr_presentation_provider,
gpu::gles2::GLES2Interface* gl,
......
......@@ -53,6 +53,10 @@ class PLATFORM_EXPORT XRFrameTransport final
int16_t vr_frame_id,
bool needs_copy);
void FrameSubmitMissing(device::mojom::blink::VRPresentationProvider*,
gpu::gles2::GLES2Interface*,
int16_t vr_frame_id);
virtual void Trace(blink::Visitor*);
private:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment