blob: 802d72047d94f2a7ea77fe00800bcddfde38f0a5 [file] [log] [blame]
// Copyright 2017 The Chromium 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 "third_party/blink/renderer/modules/xr/xr_webgl_layer.h"
#include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
#include "third_party/blink/renderer/modules/webgl/webgl2_rendering_context.h"
#include "third_party/blink/renderer/modules/webgl/webgl_framebuffer.h"
#include "third_party/blink/renderer/modules/webgl/webgl_rendering_context.h"
#include "third_party/blink/renderer/modules/xr/xr.h"
#include "third_party/blink/renderer/modules/xr/xr_frame_provider.h"
#include "third_party/blink/renderer/modules/xr/xr_presentation_context.h"
#include "third_party/blink/renderer/modules/xr/xr_session.h"
#include "third_party/blink/renderer/modules/xr/xr_view.h"
#include "third_party/blink/renderer/modules/xr/xr_viewport.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/geometry/double_size.h"
#include "third_party/blink/renderer/platform/geometry/float_point.h"
#include "third_party/blink/renderer/platform/geometry/int_size.h"
namespace blink {
namespace {
const double kFramebufferMinScale = 0.2;
const double kViewportMinScale = 0.2;
const double kViewportMaxScale = 1.0;
// Because including base::ClampToRange would be a dependency violation
double ClampToRange(const double value, const double min, const double max) {
return std::min(std::max(value, min), max);
}
} // namespace
XRWebGLLayer* XRWebGLLayer::Create(
XRSession* session,
const WebGLRenderingContextOrWebGL2RenderingContext& context,
const XRWebGLLayerInit* initializer,
ExceptionState& exception_state) {
if (session->ended()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"Cannot create an XRWebGLLayer for an "
"XRSession which has already ended.");
return nullptr;
}
WebGLRenderingContextBase* webgl_context;
if (context.IsWebGL2RenderingContext()) {
webgl_context = context.GetAsWebGL2RenderingContext();
} else {
webgl_context = context.GetAsWebGLRenderingContext();
}
if (webgl_context->isContextLost()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"Cannot create an XRWebGLLayer with a "
"lost WebGL context.");
return nullptr;
}
if (!webgl_context->IsXRCompatible()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"This context is not marked as XR compatible.");
return nullptr;
}
bool want_antialiasing = initializer->antialias();
bool want_depth_buffer = initializer->depth();
bool want_stencil_buffer = initializer->stencil();
bool want_alpha_channel = initializer->alpha();
double framebuffer_scale = 1.0;
if (initializer->hasFramebufferScaleFactor()) {
// The max size will be either the native resolution or the default
// if that happens to be larger than the native res. (That can happen on
// desktop systems.)
double max_scale = std::max(session->NativeFramebufferScale(), 1.0);
// Clamp the developer-requested framebuffer size to ensure it's not too
// small to see or unreasonably large.
// TODO: Would be best to have the max value communicated from the service
// rather than limited to the native res.
framebuffer_scale = ClampToRange(initializer->framebufferScaleFactor(),
kFramebufferMinScale, max_scale);
}
DoubleSize framebuffers_size = session->DefaultFramebufferSize();
IntSize desired_size(framebuffers_size.Width() * framebuffer_scale,
framebuffers_size.Height() * framebuffer_scale);
// Create an opaque WebGL Framebuffer
WebGLFramebuffer* framebuffer = WebGLFramebuffer::CreateOpaque(webgl_context);
scoped_refptr<XRWebGLDrawingBuffer> drawing_buffer =
XRWebGLDrawingBuffer::Create(webgl_context->GetDrawingBuffer(),
framebuffer->Object(), desired_size,
want_alpha_channel, want_depth_buffer,
want_stencil_buffer, want_antialiasing);
if (!drawing_buffer) {
exception_state.ThrowDOMException(DOMExceptionCode::kOperationError,
"Unable to create a framebuffer.");
return nullptr;
}
return MakeGarbageCollected<XRWebGLLayer>(session, webgl_context,
std::move(drawing_buffer),
framebuffer, framebuffer_scale);
}
XRWebGLLayer::XRWebGLLayer(XRSession* session,
WebGLRenderingContextBase* webgl_context,
scoped_refptr<XRWebGLDrawingBuffer> drawing_buffer,
WebGLFramebuffer* framebuffer,
double framebuffer_scale)
: XRLayer(session, kXRWebGLLayerType),
webgl_context_(webgl_context),
drawing_buffer_(std::move(drawing_buffer)),
framebuffer_(framebuffer),
framebuffer_scale_(framebuffer_scale) {
DCHECK(drawing_buffer_);
// If the contents need mirroring, indicate that to the drawing buffer.
if (session->immersive() && session->outputContext() && session->External()) {
mirroring_ = true;
mirror_client_ = base::AdoptRef(new XRWebGLDrawingBuffer::MirrorClient());
drawing_buffer_->SetMirrorClient(mirror_client_);
}
UpdateViewports();
}
XRWebGLLayer::~XRWebGLLayer() {
if (mirroring_) {
drawing_buffer_->SetMirrorClient(nullptr);
mirror_client_->BeginDestruction();
}
mirror_client_ = nullptr;
drawing_buffer_->BeginDestruction();
}
void XRWebGLLayer::getXRWebGLRenderingContext(
WebGLRenderingContextOrWebGL2RenderingContext& result) const {
if (webgl_context_->ContextType() == Platform::kWebGL2ContextType) {
result.SetWebGL2RenderingContext(
static_cast<WebGL2RenderingContext*>(webgl_context_.Get()));
} else {
result.SetWebGLRenderingContext(
static_cast<WebGLRenderingContext*>(webgl_context_.Get()));
}
}
XRViewport* XRWebGLLayer::getViewport(XRView* view) {
if (!view || view->session() != session())
return nullptr;
return GetViewportForEye(view->EyeValue());
}
XRViewport* XRWebGLLayer::GetViewportForEye(XRView::XREye eye) {
if (viewports_dirty_)
UpdateViewports();
if (eye == XRView::kEyeLeft)
return left_viewport_;
return right_viewport_;
}
void XRWebGLLayer::requestViewportScaling(double scale_factor) {
if (!session()->immersive()) {
// TODO(bajones): For the moment we're just going to ignore viewport changes
// in non-immersive mode. This is legal, but probably not what developers
// would like to see. Look into making viewport scale apply properly.
scale_factor = 1.0;
} else {
// Clamp the developer-requested viewport size to ensure it's not too
// small to see or larger than the framebuffer.
scale_factor =
ClampToRange(scale_factor, kViewportMinScale, kViewportMaxScale);
}
// Don't set this as the viewport_scale_ directly, since that would allow the
// viewports to change mid-frame.
requested_viewport_scale_ = scale_factor;
}
double XRWebGLLayer::getNativeFramebufferScaleFactor(XRSession* session) {
return session->NativeFramebufferScale();
}
void XRWebGLLayer::UpdateViewports() {
long framebuffer_width = framebufferWidth();
long framebuffer_height = framebufferHeight();
viewports_dirty_ = false;
if (session()->immersive()) {
left_viewport_ = MakeGarbageCollected<XRViewport>(
0, 0, framebuffer_width * 0.5 * viewport_scale_,
framebuffer_height * viewport_scale_);
right_viewport_ = MakeGarbageCollected<XRViewport>(
framebuffer_width * 0.5 * viewport_scale_, 0,
framebuffer_width * 0.5 * viewport_scale_,
framebuffer_height * viewport_scale_);
session()->xr()->frameProvider()->UpdateWebGLLayerViewports(this);
// When mirroring make sure to also update the mirrored canvas UVs so it
// only shows a single eye's data, cropped to display proportionally.
if (session()->outputContext()) {
float left = 0;
float top = 0;
float right = static_cast<float>(left_viewport_->width()) /
static_cast<float>(framebuffer_width);
float bottom = static_cast<float>(left_viewport_->height()) /
static_cast<float>(framebuffer_height);
// Adjust the UVs so that the mirrored content always fills the canvas
// and is centered while staying proportional.
DoubleSize output_size = session()->OutputCanvasSize();
double output_aspect = output_size.Width() / output_size.Height();
double viewport_aspect = static_cast<float>(left_viewport_->width()) /
static_cast<float>(left_viewport_->height());
if (output_aspect > viewport_aspect) {
float viewport_scale = bottom;
output_aspect = viewport_aspect / output_aspect;
top = 0.5 - (output_aspect * 0.5);
bottom = top + output_aspect;
top *= viewport_scale;
bottom *= viewport_scale;
} else {
float viewport_scale = right;
output_aspect = output_aspect / viewport_aspect;
left = 0.5 - (output_aspect * 0.5);
right = left + output_aspect;
left *= viewport_scale;
right *= viewport_scale;
}
session()->outputContext()->SetUV(FloatPoint(left, top),
FloatPoint(right, bottom));
}
} else {
left_viewport_ = MakeGarbageCollected<XRViewport>(
0, 0, framebuffer_width * viewport_scale_,
framebuffer_height * viewport_scale_);
}
}
void XRWebGLLayer::OverwriteColorBufferFromMailboxTexture(
const gpu::MailboxHolder& mailbox_holder,
const IntSize& size) {
drawing_buffer_->OverwriteColorBufferFromMailboxTexture(mailbox_holder, size);
framebuffer_->SetContentsChanged(true);
}
void XRWebGLLayer::OnFrameStart(
const base::Optional<gpu::MailboxHolder>& buffer_mailbox_holder) {
// If the requested scale has changed since the last from, update it now.
if (viewport_scale_ != requested_viewport_scale_) {
viewport_scale_ = requested_viewport_scale_;
viewports_dirty_ = true;
}
framebuffer_->MarkOpaqueBufferComplete(true);
framebuffer_->SetContentsChanged(false);
if (buffer_mailbox_holder) {
drawing_buffer_->UseSharedBuffer(buffer_mailbox_holder.value());
is_direct_draw_frame = true;
} else {
is_direct_draw_frame = false;
}
}
void XRWebGLLayer::OnFrameEnd() {
framebuffer_->MarkOpaqueBufferComplete(false);
if (is_direct_draw_frame) {
drawing_buffer_->DoneWithSharedBuffer();
is_direct_draw_frame = false;
}
// Submit the frame to the XR compositor.
if (session()->immersive()) {
// Always call submit, but notify if the contents were changed or not.
session()->xr()->frameProvider()->SubmitWebGLLayer(
this, framebuffer_->HaveContentsChanged());
} else if (session()->outputContext()) {
// 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);
}
}
}
void XRWebGLLayer::OnResize() {
if (!session()->immersive()) {
// For non-immersive sessions a resize indicates we should adjust the
// drawing buffer size to match the canvas.
DoubleSize framebuffers_size = session()->DefaultFramebufferSize();
IntSize desired_size(framebuffers_size.Width() * framebuffer_scale_,
framebuffers_size.Height() * framebuffer_scale_);
drawing_buffer_->Resize(desired_size);
}
// With both immersive and non-immersive session the viewports should be
// recomputed when the output canvas resizes.
viewports_dirty_ = true;
}
void XRWebGLLayer::HandleBackgroundImage(
const gpu::MailboxHolder& mailbox_holder,
const IntSize& size) {
OverwriteColorBufferFromMailboxTexture(mailbox_holder, size);
}
scoped_refptr<StaticBitmapImage> XRWebGLLayer::TransferToStaticBitmapImage(
std::unique_ptr<viz::SingleReleaseCallback>* out_release_callback) {
return drawing_buffer_->TransferToStaticBitmapImage(out_release_callback);
}
void XRWebGLLayer::UpdateWebXRMirror() {
if (mirroring_) {
scoped_refptr<StaticBitmapImage> image = mirror_client_->GetLastImage();
if (image) {
ImageBitmap* image_bitmap = ImageBitmap::Create(std::move(image));
session()->outputContext()->SetImage(image_bitmap);
mirror_client_->CallLastReleaseCallback();
}
}
}
void XRWebGLLayer::Trace(blink::Visitor* visitor) {
visitor->Trace(left_viewport_);
visitor->Trace(right_viewport_);
visitor->Trace(webgl_context_);
visitor->Trace(framebuffer_);
XRLayer::Trace(visitor);
}
} // namespace blink