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

Reland "Add SubmitFrameMissing mojo call for WebVR/WebXR"

This is a reland of f5bbb138

Original change's description:
> 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/996614
> Reviewed-by: Ian Vollick <vollick@chromium.org>
> Reviewed-by: Brandon Jones <bajones@chromium.org>
> Reviewed-by: Martin Barbella <mbarbella@chromium.org>
> Reviewed-by: Bill Orr <billorr@chromium.org>
> Commit-Queue: Klaus Weidner <klausw@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#548518}

Change-Id: I14b7ac8fc661cfb91a8be2b959d27091007602ee
Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel;master.tryserver.chromium.linux:linux_layout_tests_slimming_paint_v2
Reviewed-on: https://chromium-review.googlesource.com/998079Reviewed-by: default avatarBill Orr <billorr@chromium.org>
Reviewed-by: default avatarMartin Barbella <mbarbella@chromium.org>
Reviewed-by: default avatarIan Vollick <vollick@chromium.org>
Commit-Queue: Klaus Weidner <klausw@chromium.org>
Cr-Commit-Position: refs/heads/master@{#548770}
parent a903c6f4
......@@ -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*);
......
......@@ -392,9 +392,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
......@@ -402,15 +400,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 OculusRenderLoop::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