// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/vr/android/cardboard/cardboard_render_loop.h"
#include <time.h>
#include <memory>
#include "base/task/bind_post_task.h"
#include "device/vr/android/cardboard/cardboard_image_transport.h"
#include "device/vr/android/cardboard/cardboard_sdk.h"
#include "device/vr/public/mojom/isolated_xr_service.mojom.h"
#include "device/vr/public/mojom/vr_service.mojom-shared.h"
#include "device/vr/util/transform_utils.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_bindings_autogen_gl.h"
#include "ui/gl/gl_context.h"
#include "ui/gl/gl_fence_android_native_fence_sync.h"
#include "ui/gl/gl_surface.h"
#include "ui/gl/gl_utils.h"
#include "ui/gl/init/gl_factory.h"
namespace device {
namespace {
// TODO(crbug.com/40900871): It's not clear if the display rotation
// should factor into Cardboard's viewport orientation. Initial attempts to
// map them together frequently gave wrong results, whereas statically using
// kLandscapeLeft has the expected effect.
constexpr CardboardViewportOrientation kViewportOrientation = kLandscapeLeft;
// Default downscale factor for computing the recommended WebXR
// render_width/render_height from the 1:1 pixel mapped size.
static constexpr float kRecommendedResolutionScale = 0.7;
constexpr uint64_t kNanosInMs = 1000000;
constexpr uint64_t kNanosInSeconds = 1000 * kNanosInMs;
// Static prediction value used in the hello_cardboard sample.
constexpr uint64_t kPredictionTimeWithoutVsyncNanos = 50 * kNanosInMs;
int64_t GetBootTimeNano() {
struct timespec res;
clock_gettime(CLOCK_BOOTTIME, &res);
return (res.tv_sec * kNanosInSeconds) + res.tv_nsec;
}
} // namespace
CardboardRenderLoop::CardboardRenderLoop(
std::unique_ptr<CardboardImageTransportFactory>
cardboard_image_transport_factory,
std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge)
: base::android::JavaHandlerThread("CardboardRenderLoop"),
cardboard_image_transport_factory_(
std::move(cardboard_image_transport_factory)),
mailbox_bridge_(std::move(mailbox_bridge)),
webxr_(std::make_unique<WebXrPresentationState>()) {}
CardboardRenderLoop::~CardboardRenderLoop() {
Stop();
}
void CardboardRenderLoop::GetEnvironmentIntegrationProvider(
mojo::PendingAssociatedReceiver<
device::mojom::XREnvironmentIntegrationProvider> environment_provider) {
// Environment integration is not supported. This call should not
// be made on this device.
frame_data_receiver_.ReportBadMessage(
"Environment integration is not supported.");
}
void CardboardRenderLoop::CreateSession(
CardboardRequestSessionCallback session_request_callback,
base::OnceClosure session_shutdown_callback,
CardboardSdk* cardboard_sdk,
gfx::AcceleratedWidget drawing_widget,
const gfx::Size& frame_size,
display::Display::Rotation display_rotation,
mojom::XRRuntimeSessionOptionsPtr options) {
DCHECK(task_runner()->BelongsToCurrentThread());
CHECK(!session_request_callback_);
CHECK(!frame_size.IsEmpty());
DVLOG(1) << __func__;
cardboard_sdk_ = cardboard_sdk;
// The initial frame size given here should correspond with the display size.
cardboard_image_transport_ = cardboard_image_transport_factory_->Create(
std::move(mailbox_bridge_), frame_size);
session_request_callback_ = std::move(session_request_callback);
session_shutdown_callback_ = std::move(session_shutdown_callback);
texture_size_ = frame_size;
// Filtering should be done at the higher level, so just enable all requested
// features.
enabled_features_.insert(options->required_features.begin(),
options->required_features.end());
enabled_features_.insert(options->optional_features.begin(),
options->optional_features.end());
if (!InitializeGl(drawing_widget)) {
std::move(session_request_callback_).Run(nullptr);
return;
}
cardboard_image_transport_->Initialize(
webxr_.get(),
base::BindOnce(&CardboardRenderLoop::OnCardboardImageTransportReady,
weak_ptr_factory_.GetWeakPtr()));
left_eye_ = mojom::XRView::New();
left_eye_->eye = mojom::XREye::kLeft;
left_eye_->viewport =
gfx::Rect(0, 0, texture_size_.width() / 2, texture_size_.height());
right_eye_ = mojom::XRView::New();
right_eye_->eye = mojom::XREye::kRight;
right_eye_->viewport =
gfx::Rect(texture_size_.width() / 2, 0, texture_size_.width() / 2,
texture_size_.height());
left_eye_->mojo_from_view = gfx::Transform();
left_eye_->field_of_view =
cardboard_image_transport_->GetFOV(CardboardEye::kLeft);
right_eye_->mojo_from_view = gfx::Transform();
right_eye_->field_of_view =
cardboard_image_transport_->GetFOV(CardboardEye::kRight);
head_tracker_ = internal::ScopedCardboardObject<CardboardHeadTracker*>(
CardboardHeadTracker_create());
// If the head tracker isn't explicitly resumed after creation it doesn't
// deliver any poses. Not clear if this is intended, as it's not mentioned in
// the documentation.
CardboardHeadTracker_resume(head_tracker_.get());
CardboardHeadTracker_recenter(head_tracker_.get());
}
bool CardboardRenderLoop::InitializeGl(gfx::AcceleratedWidget drawing_widget) {
DVLOG(1) << __func__;
DCHECK(task_runner()->BelongsToCurrentThread());
CHECK(drawing_widget);
// TODO(crbug.com/40744597): While we actually *can* launch Cardboard
// with ANGLE support; if we do so, once we try to launch ARCore (which
// disables it), we end up hitting a crash. We should investigate if this can
// be resolved to use ANGLE with Cardboard.
gl::DisableANGLE();
gl::GLDisplay* display = nullptr;
if (gl::GetGLImplementation() == gl::kGLImplementationNone) {
display = gl::init::InitializeGLOneOff(
/*gpu_preference=*/gl::GpuPreference::kDefault);
if (!display) {
DLOG(ERROR) << "gl::init::InitializeGLOneOff failed";
return false;
}
} else {
display = gl::GetDefaultDisplayEGL();
}
scoped_refptr<gl::GLSurface> surface =
gl::init::CreateViewGLSurface(display, drawing_widget);
DVLOG(3) << "surface=" << surface.get();
if (!surface.get()) {
DLOG(ERROR) << "gl::init::CreateViewGLSurface failed";
return false;
}
scoped_refptr<gl::GLContext> context =
gl::init::CreateGLContext(nullptr, surface.get(), gl::GLContextAttribs());
if (!context.get()) {
DLOG(ERROR) << "gl::init::CreateGLContext failed";
return false;
}
if (!context->MakeCurrent(surface.get())) {
DLOG(ERROR) << "gl::GLContext::MakeCurrent() failed";
return false;
}
// Swap the surface once so that it will show an empty texture rather than
// just being transparent.
surface->SwapBuffers(base::DoNothing(), gfx::FrameData());
// Assign the surface and context members now that initialization has
// succeeded.
surface_ = std::move(surface);
context_ = std::move(context);
return true;
}
void CardboardRenderLoop::OnBindingDisconnect() {
DVLOG(1) << __func__;
CloseBindingsIfOpen();
pending_shutdown_ = true;
// Even if we're currently pending shutdown, it doesn't hurt to ensure that
// the bindings have been closed; but if we've already called the session
// shutdown callback and get a binding disconnect before we're destroyed, then
// we may not have a session_shutdown_callback to call.
if (session_shutdown_callback_) {
std::move(session_shutdown_callback_).Run();
}
}
void CardboardRenderLoop::CloseBindingsIfOpen() {
DVLOG(1) << __func__;
frame_data_receiver_.reset();
session_controller_receiver_.reset();
presentation_receiver_.reset();
submit_client_.reset();
}
void CardboardRenderLoop::OnCardboardImageTransportReady(bool success) {
DCHECK(task_runner()->BelongsToCurrentThread());
DVLOG(1) << __func__ << ": success=" << success;
if (!success) {
std::move(session_request_callback_).Run(nullptr);
return;
}
webxr_->NotifyMailboxBridgeReady();
// Reset all of our bindings before we assign them.
CloseBindingsIfOpen();
device::mojom::XRPresentationTransportOptionsPtr transport_options =
device::mojom::XRPresentationTransportOptions::New();
transport_options->wait_for_gpu_fence = true;
if (CardboardImageTransport::UseSharedBuffer()) {
DVLOG(2) << __func__
<< ": UseSharedBuffer()=true, DRAW_INTO_TEXTURE_MAILBOX";
transport_options->transport_method =
device::mojom::XRPresentationTransportMethod::DRAW_INTO_TEXTURE_MAILBOX;
} else {
DVLOG(2) << __func__
<< ": UseSharedBuffer()=false, SUBMIT_AS_MAILBOX_HOLDER";
transport_options->transport_method =
device::mojom::XRPresentationTransportMethod::SUBMIT_AS_MAILBOX_HOLDER;
transport_options->wait_for_transfer_notification = true;
cardboard_image_transport_->SetFrameAvailableCallback(base::BindRepeating(
&CardboardRenderLoop::RenderFrame, weak_ptr_factory_.GetWeakPtr()));
}
mojom::XRRuntimeSessionResultPtr result =
device::mojom::XRRuntimeSessionResult::New();
result->controller = session_controller_receiver_.BindNewPipeAndPassRemote();
result->session = mojom::XRSession::New();
auto* session = result->session.get();
session->data_provider = frame_data_receiver_.BindNewPipeAndPassRemote();
session->submit_frame_sink = device::mojom::XRPresentationConnection::New();
auto* submit_frame_sink = session->submit_frame_sink.get();
submit_frame_sink->client_receiver =
submit_client_.BindNewPipeAndPassReceiver();
submit_frame_sink->provider =
presentation_receiver_.BindNewPipeAndPassRemote();
submit_frame_sink->transport_options = std::move(transport_options);
session->enabled_features.assign(enabled_features_.begin(),
enabled_features_.end());
session->device_config = device::mojom::XRSessionDeviceConfig::New();
auto* config = session->device_config.get();
// TODO(crbug.com/40900872): Determine if we should support this.
config->supports_viewport_scaling = false;
config->default_framebuffer_scale = kRecommendedResolutionScale;
config->views.push_back(left_eye_.Clone());
config->views.push_back(right_eye_.Clone());
session->enviroment_blend_mode =
device::mojom::XREnvironmentBlendMode::kOpaque;
session->interaction_mode = device::mojom::XRInteractionMode::kWorldSpace;
std::move(session_request_callback_).Run(std::move(result));
frame_data_receiver_.set_disconnect_handler(
base::BindOnce(&CardboardRenderLoop::OnBindingDisconnect,
weak_ptr_factory_.GetWeakPtr()));
session_controller_receiver_.set_disconnect_handler(
base::BindOnce(&CardboardRenderLoop::OnBindingDisconnect,
weak_ptr_factory_.GetWeakPtr()));
presentation_receiver_.set_disconnect_handler(
base::BindOnce(&CardboardRenderLoop::OnBindingDisconnect,
weak_ptr_factory_.GetWeakPtr()));
submit_client_.set_disconnect_handler(
base::BindOnce(&CardboardRenderLoop::OnBindingDisconnect,
weak_ptr_factory_.GetWeakPtr()));
}
void CardboardRenderLoop::CleanUp() {
// This happens on the GL Thread, but we cannot assert that we are on it
// because the thread is stopping; but this should only be called by our
// parent.
weak_ptr_factory_.InvalidateWeakPtrs();
cardboard_image_transport_->DestroySharedBuffers(webxr_.get());
cardboard_image_transport_.reset();
OnBindingDisconnect();
context_.reset();
surface_.reset();
}
void CardboardRenderLoop::GetFrameData(
mojom::XRFrameDataRequestOptionsPtr options,
mojom::XRFrameDataProvider::GetFrameDataCallback callback) {
TRACE_EVENT1("gpu", __func__, "frame", webxr_->PeekNextFrameIndex());
DCHECK(task_runner()->BelongsToCurrentThread());
CHECK(!texture_size_.IsEmpty());
if (!CanStartNewAnimatingFrame()) {
// We bind this as a post task so that whatever processing is run when we
// attempt to get new frame data can complete before the pending
// GetFrameData call actually happens.
pending_getframedata_ = base::BindPostTask(
task_runner(), base::BindOnce(&CardboardRenderLoop::GetFrameData,
weak_ptr_factory_.GetWeakPtr(),
std::move(options), std::move(callback)));
return;
}
if (restrict_frame_data_) {
DVLOG(2) << __func__ << ": frame data restricted, returning nullptr.";
std::move(callback).Run(nullptr);
return;
}
if (is_paused_) {
DVLOG(2) << __func__ << ": paused but frame data not restricted. Resuming.";
Resume();
}
base::TimeTicks now = base::TimeTicks::Now();
mojom::XRFrameDataPtr frame_data = mojom::XRFrameData::New();
frame_data->frame_id = webxr_->StartFrameAnimating();
WebXrFrame* xr_frame = webxr_->GetAnimatingFrame();
xr_frame->time_pose = now;
xr_frame->bounds_left = left_bounds_;
xr_frame->bounds_right = right_bounds_;
if (CardboardImageTransport::UseSharedBuffer()) {
// We aren't modifying the texture that we give to the page, so we just pass
// in identity for the uv_transform.
WebXrSharedBuffer* shared_buffer =
cardboard_image_transport_->TransferFrame(webxr_.get(), texture_size_,
gfx::Transform());
CHECK(shared_buffer);
frame_data->buffer_shared_image = shared_buffer->shared_image->Export();
frame_data->buffer_sync_token = shared_buffer->sync_token;
}
// Get the head pose
int64_t timestamp_ns = GetBootTimeNano() + kPredictionTimeWithoutVsyncNanos;
float position[3];
float orientation[4];
CardboardHeadTracker_getPose(head_tracker_.get(), timestamp_ns,
kViewportOrientation, position, orientation);
// Translate the head pose into the viewer pose pointer
// This needs to be inverted because the Cardboard SDK appears to be giving
// back values that are the inverse of what WebXR expects.
mojom::VRPosePtr pose = mojom::VRPose::New();
pose->position = gfx::Point3F(-position[0], -position[1], -position[2]);
pose->orientation = gfx::Quaternion(-orientation[0], -orientation[1],
-orientation[2], orientation[3]);
pose->emulated_position = true;
gfx::Transform mojo_from_viewer = vr_utils::VrPoseToTransform(pose.get());
frame_data->mojo_from_viewer = std::move(pose);
// Get the view transform for each eye
left_eye_->mojo_from_view =
cardboard_image_transport_->GetMojoFromView(kLeft, mojo_from_viewer);
right_eye_->mojo_from_view =
cardboard_image_transport_->GetMojoFromView(kRight, mojo_from_viewer);
frame_data->views.push_back(left_eye_.Clone());
frame_data->views.push_back(right_eye_.Clone());
std::vector<mojom::XRInputSourceStatePtr> input_state;
input_state.push_back(GetInputSourceState());
frame_data->input_state = std::move(input_state);
frame_data->time_delta = now - base::TimeTicks();
// TODO(crbug.com/40900872): Calculating
// frame_data->rendering_time_ratio may be necessary for viewport scaling.
std::move(callback).Run(std::move(frame_data));
}
bool CardboardRenderLoop::IsSubmitFrameExpected(int16_t frame_index) {
DVLOG(3) << __func__ << ": Frame Index=" << frame_index
<< " submit_client_=" << !!submit_client_.get()
<< " HaveAnimatingFrame()=" << webxr_->HaveAnimatingFrame()
<< " pending_shutdown_=" << pending_shutdown_;
// submit_client_ could be null when we exit presentation, if there were
// pending SubmitFrame messages queued. XRSessionClient::OnExitPresent
// will clean up state in blink, so it doesn't wait for
// OnSubmitFrameTransferred or OnSubmitFrameRendered. Similarly,
// the animating frame state is cleared when exiting presentation,
// and we should ignore a leftover queued SubmitFrame.
if (!submit_client_.get() || !webxr_->HaveAnimatingFrame()) {
return false;
}
if (pending_shutdown_) {
return false;
}
WebXrFrame* animating_frame = webxr_->GetAnimatingFrame();
animating_frame->time_js_submit = base::TimeTicks::Now();
if (animating_frame->index != frame_index) {
DVLOG(1) << __func__ << ": wrong frame index, got " << frame_index
<< ", expected " << animating_frame->index;
presentation_receiver_.ReportBadMessage(
"SubmitFrame called with wrong frame index");
OnBindingDisconnect();
return false;
}
// Frame looks valid.
return true;
}
void CardboardRenderLoop::SubmitFrameMissing(int16_t frame_index,
const gpu::SyncToken& sync_token) {
TRACE_EVENT1("gpu", __func__, "frame", frame_index);
DVLOG(2) << __func__ << ": frame=" << frame_index;
if (!IsSubmitFrameExpected(frame_index)) {
return;
}
webxr_->RecycleUnusedAnimatingFrame();
cardboard_image_transport_->WaitSyncToken(sync_token);
FinishFrame(frame_index);
if (pending_getframedata_) {
std::move(pending_getframedata_).Run();
}
}
void CardboardRenderLoop::SubmitFrame(int16_t frame_index,
const gpu::MailboxHolder& mailbox,
base::TimeDelta time_waited) {
TRACE_EVENT1("gpu", __func__, "frame", frame_index);
DVLOG(2) << __func__ << ": frame=" << frame_index;
CHECK(!CardboardImageTransport::UseSharedBuffer());
if (!IsSubmitFrameExpected(frame_index)) {
return;
}
webxr_->ProcessOrDefer(
base::BindOnce(&CardboardRenderLoop::ProcessFrameFromMailbox,
weak_ptr_factory_.GetWeakPtr(), frame_index, mailbox));
}
void CardboardRenderLoop::ProcessFrameFromMailbox(
int16_t frame_index,
const gpu::MailboxHolder& mailbox) {
TRACE_EVENT1("gpu", __func__, "frame", frame_index);
DVLOG(2) << __func__ << ": frame=" << frame_index;
CHECK(webxr_->HaveProcessingFrame());
CHECK(!CardboardImageTransport::UseSharedBuffer());
// We aren't modifying the texture that we've received from the page, so we
// just pass in identity.
cardboard_image_transport_->CopyMailboxToSurfaceAndSwap(
texture_size_, mailbox, gfx::Transform());
// Notify the client that we're done with the mailbox so that the underlying
// image is eligible for destruction.
submit_client_->OnSubmitFrameTransferred(true);
// Now wait for cardboard_image_transport_ to call RenderFrame indicating that
// the image drawn onto the Surface is ready for consumption from the
// SurfaceTexture.
}
void CardboardRenderLoop::SubmitFrameDrawnIntoTexture(
int16_t frame_index,
const gpu::SyncToken& sync_token,
base::TimeDelta time_waited) {
TRACE_EVENT1("gpu", __func__, "frame", frame_index);
DVLOG(2) << __func__ << ": frame=" << frame_index;
CHECK(CardboardImageTransport::UseSharedBuffer());
if (!IsSubmitFrameExpected(frame_index)) {
return;
}
// Start processing the frame now if possible. If there's already a current
// processing frame, defer it until that frame calls TryDeferredProcessing.
webxr_->ProcessOrDefer(
base::BindOnce(&CardboardRenderLoop::ProcessFrameDrawnIntoTexture,
weak_ptr_factory_.GetWeakPtr(), sync_token));
}
void CardboardRenderLoop::ProcessFrameDrawnIntoTexture(
const gpu::SyncToken& sync_token) {
cardboard_image_transport_->CreateGpuFenceForSyncToken(
sync_token,
base::BindOnce(&CardboardRenderLoop::OnWebXrTokenSignaled, GetWeakPtr()));
if (pending_getframedata_) {
std::move(pending_getframedata_).Run();
}
}
void CardboardRenderLoop::OnWebXrTokenSignaled(
std::unique_ptr<gfx::GpuFence> gpu_fence) {
cardboard_image_transport_->ServerWaitForGpuFence(std::move(gpu_fence));
RenderFrame(gfx::Transform());
}
void CardboardRenderLoop::TransitionProcessingFrameToRendering() {
if (webxr_->HaveRenderingFrame()) {
// It's possible, though unlikely, that the previous rendering frame hasn't
// finished yet, for example if an unusually slow frame is followed by an
// unusually quick one. In that case, wait for that frame to finish
// rendering first before proceeding with this one. The state machine
// doesn't permit two frames to be in rendering state at once. (Also, adding
// even more GPU work in that condition would be counterproductive.)
DVLOG(3) << __func__ << ": wait for previous rendering frame to complete";
FinishRenderingFrame();
}
CHECK(!webxr_->HaveRenderingFrame());
CHECK(webxr_->HaveProcessingFrame());
auto* frame = webxr_->GetProcessingFrame();
frame->time_copied = base::TimeTicks::Now();
frame->render_completion_fence = nullptr;
webxr_->TransitionFrameProcessingToRendering();
// We finished processing a frame, unblock a potentially waiting next frame.
webxr_->TryDeferredProcessing();
}
void CardboardRenderLoop::RenderFrame(const gfx::Transform& uv_transform) {
DVLOG(2) << __func__;
CHECK(webxr_->HaveProcessingFrame());
int16_t frame_index = webxr_->GetProcessingFrame()->index;
TRACE_EVENT1("gpu", __func__, "frame", frame_index);
TransitionProcessingFrameToRendering();
cardboard_image_transport_->Render(webxr_.get(), /*framebuffer=*/0);
FinishFrame(frame_index);
if (submit_client_) {
// Create a local GpuFence and pass it to the Renderer via IPC.
std::unique_ptr<gl::GLFence> gl_fence = gl::GLFence::CreateForGpuFence();
std::unique_ptr<gfx::GpuFence> gpu_fence2 = gl_fence->GetGpuFence();
submit_client_->OnSubmitFrameGpuFence(
gpu_fence2->GetGpuFenceHandle().Clone());
}
if (pending_getframedata_) {
std::move(pending_getframedata_).Run();
}
}
void CardboardRenderLoop::FinishRenderingFrame(WebXrFrame* frame) {
CHECK(frame || webxr_->HaveRenderingFrame());
if (!frame) {
frame = webxr_->GetRenderingFrame();
}
if (!frame->render_completion_fence) {
frame->render_completion_fence = gl::GLFence::CreateForGpuFence();
}
ClearRenderingFrame(frame);
}
void CardboardRenderLoop::ClearRenderingFrame(WebXrFrame* frame) {
TRACE_EVENT1("gpu", __func__, "frame", frame->index);
DVLOG(3) << __func__ << ": frame=" << frame->index;
// Ensure that we're totally finished with the rendering frame, then collect
// stats and move the frame out of the rendering path.
DVLOG(3) << __func__ << ": client wait start";
frame->render_completion_fence->ClientWait();
DVLOG(3) << __func__ << ": client wait done";
webxr_->EndFrameRendering(frame);
}
void CardboardRenderLoop::FinishFrame(int16_t frame_index) {
TRACE_EVENT1("gpu", __func__, "frame", frame_index);
DVLOG(3) << __func__;
surface_->SwapBuffers(base::DoNothing(), gfx::FrameData());
// If we have a rendering frame we need to create a GLFence
if (!webxr_->HaveRenderingFrame()) {
return;
}
WebXrFrame* frame = webxr_->GetRenderingFrame();
frame->render_completion_fence = gl::GLFence::CreateForGpuFence();
}
bool CardboardRenderLoop::CanStartNewAnimatingFrame() {
if (pending_shutdown_) {
return false;
}
if (webxr_->HaveAnimatingFrame()) {
DVLOG(3) << __func__ << ": deferring, HaveAnimatingFrame";
return false;
}
if (!webxr_->CanStartFrameAnimating()) {
DVLOG(3) << __func__ << ": deferring, no available frames in swapchain";
return false;
}
// If there are already two frames in flight, ensure that the rendering frame
// completes first before starting a new animating frame. It may be complete
// already, in that case just collect its statistics. (Don't wait if there's a
// rendering frame but no processing frame.)
if (webxr_->HaveProcessingFrame() && webxr_->HaveRenderingFrame()) {
DVLOG(2) << __func__ << ": wait, have processing&rendering frames";
FinishRenderingFrame();
}
// If there is still a rendering frame (we didn't wait for it), check
// if it's complete. If yes, collect its statistics now so that the GPU
// time estimate for the upcoming frame is up to date.
if (webxr_->HaveRenderingFrame()) {
auto* frame = webxr_->GetRenderingFrame();
if (frame->render_completion_fence &&
frame->render_completion_fence->HasCompleted()) {
FinishRenderingFrame();
}
}
return true;
}
void CardboardRenderLoop::UpdateLayerBounds(int16_t frame_index,
const gfx::RectF& left_bounds,
const gfx::RectF& right_bounds,
const gfx::Size& source_size) {
DCHECK(task_runner()->BelongsToCurrentThread());
DVLOG(2) << __func__ << " source_size=" << source_size.ToString()
<< " left_bounds=" << left_bounds.ToString()
<< " right_bounds=" << right_bounds.ToString();
// The first UpdateLayerBounds may arrive early, when there's
// no animating frame yet. In that case, just save it in
// `left_bounds_`/`right_bounds_` so that it's applied to the next animating
// frame.
if (webxr_->HaveAnimatingFrame()) {
webxr_->GetAnimatingFrame()->bounds_left = left_bounds;
webxr_->GetAnimatingFrame()->bounds_right = right_bounds;
}
left_bounds_ = left_bounds;
right_bounds_ = right_bounds;
// TODO(crbug.com/40900879): This was lifted from ArCoreGl which does
// a very similar thing, but both cases actually use this texture_size_ to
// render with and there isn't a corresponding item on the
// WebXrPresentationState. Replacing the assignment below with a CHECK did not
// trigger in my limited testing.
// Early setting of `texture_size_` is OK since that's only used by the
// animating frame. Processing/rendering frames use the bounds from
// WebXRPresentationState.
texture_size_ = source_size;
}
void CardboardRenderLoop::SetFrameDataRestricted(bool frame_data_restricted) {
DCHECK(task_runner()->BelongsToCurrentThread());
DVLOG(3) << __func__ << ": frame_data_restricted=" << frame_data_restricted;
restrict_frame_data_ = frame_data_restricted;
if (restrict_frame_data_) {
Pause();
} else {
Resume();
}
}
void CardboardRenderLoop::OnTriggerEvent(bool pressed) {
DVLOG(2) << __func__ << ": pressed=" << pressed;
if (pressed) {
trigger_pressed_ = true;
} else if (trigger_pressed_) {
trigger_pressed_ = false;
trigger_clicked_ = true;
}
}
device::mojom::XRInputSourceStatePtr
CardboardRenderLoop::GetInputSourceState() {
device::mojom::XRInputSourceStatePtr state =
device::mojom::XRInputSourceState::New();
// Only one gaze input source to worry about, so it can have a static id.
state->source_id = 1;
// Report any trigger state changes made since the last call and reset the
// state here.
state->primary_input_pressed = trigger_pressed_;
state->primary_input_clicked = trigger_clicked_;
trigger_clicked_ = false;
state->description = device::mojom::XRInputSourceDescription::New();
// It's a gaze-cursor-based device.
state->description->target_ray_mode = device::mojom::XRTargetRayMode::GAZING;
state->emulated_position = true;
// No implicit handedness
state->description->handedness = device::mojom::XRHandedness::NONE;
// Pointer and grip transforms are omitted since this is a gaze-based source.
return state;
}
void CardboardRenderLoop::Pause() {
DCHECK(task_runner()->BelongsToCurrentThread());
DVLOG(1) << __func__;
CardboardHeadTracker_pause(head_tracker_.get());
is_paused_ = true;
}
void CardboardRenderLoop::Resume() {
DCHECK(task_runner()->BelongsToCurrentThread());
DVLOG(1) << __func__;
CardboardHeadTracker_resume(head_tracker_.get());
is_paused_ = false;
}
} // namespace device