chromium/device/vr/openxr/android/openxr_graphics_binding_open_gles.cc

// 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/openxr/android/openxr_graphics_binding_open_gles.h"

#include <vector>

#include "base/containers/contains.h"
#include "components/viz/common/resources/shared_image_format.h"
#include "device/vr/android/xr_image_transport_base.h"
#include "device/vr/openxr/openxr_api_wrapper.h"
#include "device/vr/openxr/openxr_platform.h"
#include "device/vr/openxr/openxr_util.h"
#include "gpu/command_buffer/client/client_shared_image.h"
#include "gpu/command_buffer/client/shared_image_interface.h"
#include "gpu/command_buffer/common/shared_image_usage.h"
#include "gpu/command_buffer/service/ahardwarebuffer_utils.h"
#include "gpu/ipc/common/android/android_hardware_buffer_utils.h"
#include "third_party/openxr/src/include/openxr/openxr.h"
#include "ui/gfx/buffer_types.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/gpu_fence.h"
#include "ui/gl/gl_context.h"
#include "ui/gl/gl_context_egl.h"
#include "ui/gl/gl_fence.h"
#include "ui/gl/gl_surface.h"
#include "ui/gl/gl_utils.h"
#include "ui/gl/init/gl_factory.h"
#include "ui/gl/scoped_egl_image.h"

namespace device {
namespace {
// Per the WebXR spec, the OS should treat the content generated by the page
// as sRGB, so we request/only support the sRGB format from the runtime.
constexpr int64_t kSwapchainFormat = GL_SRGB8_ALPHA8;
}  // namespace

// static
void OpenXrGraphicsBinding::GetRequiredExtensions(
    std::vector<const char*>& extensions) {
  extensions.push_back(XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME);
}

OpenXrGraphicsBindingOpenGLES::OpenXrGraphicsBindingOpenGLES() = default;
OpenXrGraphicsBindingOpenGLES::~OpenXrGraphicsBindingOpenGLES() {
  if (back_buffer_fbo_) {
    glDeleteFramebuffersEXT(1, &back_buffer_fbo_);
  }

  if (overlay_texture_.id) {
    glDeleteTextures(1, &overlay_texture_.id);
  }
}

bool OpenXrGraphicsBindingOpenGLES::Initialize(XrInstance instance,
                                               XrSystemId system) {
  if (initialized_) {
    return true;
  }

  PFN_xrGetOpenGLESGraphicsRequirementsKHR get_graphics_requirements_fn{
      nullptr};
  if (XR_FAILED(xrGetInstanceProcAddr(
          instance, "xrGetOpenGLESGraphicsRequirementsKHR",
          (PFN_xrVoidFunction*)(&get_graphics_requirements_fn)))) {
    return false;
  }

  // TODO(alcooper): Validate/set version based on the output here.
  XrGraphicsRequirementsOpenGLESKHR graphics_requirements = {
      XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR};
  if (XR_FAILED(get_graphics_requirements_fn(instance, system,
                                             &graphics_requirements))) {
    return false;
  }

  // None of the other runtimes support ANGLE, so we disable it too for now.
  // TODO(alcooper): Investigate if we can support ANGLE or if we'll run into
  // similar problems as cardboard.
  gl::DisableANGLE();

  // Everything below is a hacky first pass at making a session and likely needs
  // to be re-written with proper context/surfaces/types.
  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;
    }
  }
  display = gl::GetDefaultDisplayEGL();

  DCHECK(gl::GetGLImplementation() != gl::kGLImplementationEGLANGLE);

  scoped_refptr<gl::GLSurface> surface;
  surface = gl::init::CreateOffscreenGLSurface(display, {0, 0});

  DVLOG(3) << "surface=" << surface.get();
  if (!surface.get()) {
    DLOG(ERROR) << "gl::init::CreateViewGLSurface failed";
    return false;
  }

  gl::GLContextAttribs context_attribs;
  // OpenXr's shared EGL context needs to be compatible with ours.
  // Any mismatches result in a EGL_BAD_MATCH error, including different reset
  // notification behavior according to
  // https://www.khronos.org/registry/EGL/specs/eglspec.1.5.pdf page 56.
  // Chromium defaults to lose context on reset when the robustness extension is
  // present, even if robustness features are not requested specifically.
  context_attribs.lose_context_on_reset = false;

  scoped_refptr<gl::GLContextEGL> egl_context = new gl::GLContextEGL(nullptr);
  scoped_refptr<gl::GLContext> context = gl::InitializeGLContext(
      egl_context.get(), surface.get(), context_attribs);
  if (!context.get()) {
    DLOG(ERROR) << "gl::init::CreateGLContext failed";
    return false;
  }
  if (!context->MakeCurrent(surface.get())) {
    DLOG(ERROR) << "gl::GLContext::MakeCurrent() failed";
    return false;
  }

  // Assign the surface and context members now that initialization has
  // succeeded.
  surface_ = std::move(surface);
  context_ = std::move(context);
  egl_context_ = std::move(egl_context);

  binding_.display = display->GetDisplay();
  binding_.config = (EGLConfig)0;
  binding_.context = egl_context_->GetHandle();

  renderer_ = std::make_unique<XrRenderer>();

  initialized_ = true;
  return true;
}

const void* OpenXrGraphicsBindingOpenGLES::GetSessionCreateInfo() const {
  CHECK(initialized_);
  return &binding_;
}

int64_t OpenXrGraphicsBindingOpenGLES::GetSwapchainFormat(
    XrSession session) const {
  uint32_t format_length = 0;
  RETURN_IF_XR_FAILED(
      xrEnumerateSwapchainFormats(session, 0, &format_length, nullptr));
  std::vector<int64_t> swapchain_formats(format_length);
  RETURN_IF_XR_FAILED(
      xrEnumerateSwapchainFormats(session, (uint32_t)swapchain_formats.size(),
                                  &format_length, swapchain_formats.data()));
  DCHECK(!swapchain_formats.empty());

  // If the runtime doesn't support the swapchain format we need, log it. We'll
  // still return it anyway as that will cause an error with creating the
  // swapchain, which is better than arbitrarily returning a type that the
  // runtime supports, but we don't.
  if (!base::Contains(swapchain_formats, kSwapchainFormat)) {
    LOG(ERROR) << "No matching supported swapchain formats with OpenXr Runtime";
  }

  return kSwapchainFormat;
}

XrResult OpenXrGraphicsBindingOpenGLES::EnumerateSwapchainImages(
    const XrSwapchain& color_swapchain) {
  CHECK(color_swapchain != XR_NULL_HANDLE);
  CHECK(color_swapchain_images_.empty());

  uint32_t chain_length;
  RETURN_IF_XR_FAILED(
      xrEnumerateSwapchainImages(color_swapchain, 0, &chain_length, nullptr));
  std::vector<XrSwapchainImageOpenGLESKHR> xr_color_swapchain_images(
      chain_length, {XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR});

  RETURN_IF_XR_FAILED(xrEnumerateSwapchainImages(
      color_swapchain, xr_color_swapchain_images.size(), &chain_length,
      reinterpret_cast<XrSwapchainImageBaseHeader*>(
          xr_color_swapchain_images.data())));

  color_swapchain_images_.reserve(xr_color_swapchain_images.size());
  for (const auto& swapchain_image : xr_color_swapchain_images) {
    color_swapchain_images_.emplace_back(swapchain_image.image);
  }

  return XR_SUCCESS;
}

void OpenXrGraphicsBindingOpenGLES::ClearSwapchainImages() {
  color_swapchain_images_.clear();
}

base::span<SwapChainInfo> OpenXrGraphicsBindingOpenGLES::GetSwapChainImages() {
  return color_swapchain_images_;
}

bool OpenXrGraphicsBindingOpenGLES::CanUseSharedImages() const {
  return XrImageTransportBase::UseSharedBuffer();
}

// This is more or less copied from XrImageTransportBase::ResizeSharedBuffer,
// with just the types changed as needed, and logic extracted out of the
// mailbox_to_surface_bridge.
void OpenXrGraphicsBindingOpenGLES::ResizeSharedBuffer(
    SwapChainInfo& swap_chain_info,
    gpu::SharedImageInterface* sii) {
  CHECK(sii);
  auto transfer_size = GetTransferSize();
  if (!using_shared_images_ ||
      swap_chain_info.shared_buffer_size == transfer_size) {
    return;
  }

  // Unbind previous image (if any).
  if (swap_chain_info.shared_image) {
    DVLOG(2) << ": DestroySharedImage, mailbox="
             << swap_chain_info.shared_image->mailbox().ToDebugString();
    // Note: the sync token in mailbox_holder may not be accurate. See comment
    // in TransferFrame below.
    sii->DestroySharedImage(swap_chain_info.sync_token,
                            std::move(swap_chain_info.shared_image));
  }

  // Remove reference to previous image (if any).
  swap_chain_info.local_eglimage.reset();

  static constexpr gfx::BufferFormat format = gfx::BufferFormat::RGBA_8888;
  static constexpr gfx::BufferUsage usage = gfx::BufferUsage::SCANOUT;

  // The SharedImages created here will eventually be transferred to other
  // processes to have their contents written by WebGL and read via GL by
  // OpenXR.
  gpu::SharedImageUsageSet shared_image_usage =
      gpu::SHARED_IMAGE_USAGE_SCANOUT | gpu::SHARED_IMAGE_USAGE_DISPLAY_READ |
      gpu::SHARED_IMAGE_USAGE_GLES2_READ | gpu::SHARED_IMAGE_USAGE_GLES2_WRITE;

  swap_chain_info.scoped_ahb_handle =
      gpu::CreateScopedHardwareBufferHandle(transfer_size, format, usage);
  swap_chain_info.shared_buffer_size = transfer_size;

  // Create a GMB Handle from scoped_ahb_handle.
  gfx::GpuMemoryBufferHandle gmb_handle;
  gmb_handle.type = gfx::ANDROID_HARDWARE_BUFFER;
  // GpuMemoryBufferId is not used in this case and hence hardcoding it to 1.
  gmb_handle.id = gfx::GpuMemoryBufferId(1);
  gmb_handle.android_hardware_buffer =
      swap_chain_info.scoped_ahb_handle.Clone();

  // Though we've created the OpenXR swapchain to have an SRGB format (see
  // `kSwapchainFormat` above), all of the compositing that the page does is
  // actually done in the linear space. So set up our shared image to be linear.
  // Note that this matches the behavior in the D3D11 graphics binding as well.
  swap_chain_info.shared_image = sii->CreateSharedImage(
      {viz::SinglePlaneFormat::kRGBA_8888, transfer_size,
       gfx::ColorSpace(gfx::ColorSpace::PrimaryID::BT709,
                       gfx::ColorSpace::TransferID::LINEAR),
       shared_image_usage, "OpenXrGraphicsBinding"},
      std::move(gmb_handle));
  CHECK(swap_chain_info.shared_image);
  swap_chain_info.sync_token = sii->GenVerifiedSyncToken();
  DCHECK_EQ(swap_chain_info.shared_image->GetTextureTarget(),
            static_cast<uint32_t>(GL_TEXTURE_2D));

  DVLOG(2) << ": CreateSharedImage, mailbox="
           << swap_chain_info.shared_image->mailbox().ToDebugString()
           << ", SyncToken=" << swap_chain_info.sync_token.ToDebugString();

  // Create an EGLImage for the buffer.
  auto egl_image = gpu::CreateEGLImageFromAHardwareBuffer(
      swap_chain_info.scoped_ahb_handle.get());
  if (!egl_image.is_valid()) {
    DLOG(WARNING) << __func__ << ": ERROR: failed to initialize image!";
    return;
  }

  swap_chain_info.shared_buffer_texture.target =
      XrImageTransportBase::SharedBufferTextureTarget();
  glGenTextures(1, &swap_chain_info.shared_buffer_texture.id);
  glBindTexture(swap_chain_info.shared_buffer_texture.target,
                swap_chain_info.shared_buffer_texture.id);
  glEGLImageTargetTexture2DOES(swap_chain_info.shared_buffer_texture.target,
                               egl_image.get());
  swap_chain_info.local_eglimage = std::move(egl_image);
}

void OpenXrGraphicsBindingOpenGLES::CreateSharedImages(
    gpu::SharedImageInterface* sii) {
  CHECK(sii);
  using_shared_images_ = true;

  for (auto& swap_chain_info : color_swapchain_images_) {
    // ResizeSharedBuffer will also cause the shared buffer to be recreated.
    ResizeSharedBuffer(swap_chain_info, sii);
  }
}

const SwapChainInfo& OpenXrGraphicsBindingOpenGLES::GetActiveSwapchainImage() {
  CHECK(has_active_swapchain_image());
  CHECK(active_swapchain_index() < color_swapchain_images_.size());

  // We don't do any index translation on the images returned from the system;
  // so whatever the system says is the active swapchain image, it is in the
  // same spot in our vector.
  return color_swapchain_images_[active_swapchain_index()];
}

bool OpenXrGraphicsBindingOpenGLES::Render(
    const scoped_refptr<viz::ContextProvider>& context_provider) {
  if (!has_active_swapchain_image() ||
      active_swapchain_index() >= color_swapchain_images_.size()) {
    return false;
  }

  // We don't do any index translation on the images returned from the system;
  // so whatever the system says is the active swapchain image, it is in the
  // same spot in our vector.
  auto& swap_chain_info = color_swapchain_images_[active_swapchain_index()];

  gfx::Size swapchain_image_size = GetSwapchainImageSize();

  if (!back_buffer_fbo_) {
    glGenFramebuffersEXT(1, &back_buffer_fbo_);
  }
  glBindFramebufferEXT(GL_FRAMEBUFFER, back_buffer_fbo_);
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                            swap_chain_info.openxr_texture, 0);

  glDisable(GL_CULL_FACE);
  glDisable(GL_SCISSOR_TEST);
  glDisable(GL_POLYGON_OFFSET_FILL);
  glDisable(GL_BLEND);

  // TODO(https://crbug.com/324596270): This shouldn't be necessary, but we
  // can't seem to get the image set up to be treated as linear any other way.
  glDisable(GL_FRAMEBUFFER_SRGB);
  glViewport(0, 0, swapchain_image_size.width(), swapchain_image_size.height());

  gfx::Transform transform;
  float transform_floats[16];
  transform.GetColMajorF(transform_floats);

  if (webxr_visible_) {
    renderer_->Draw(swap_chain_info.shared_buffer_texture, transform_floats);
  }

  if (overlay_visible_) {
    glEnable(GL_FRAMEBUFFER_SRGB);
    if (webxr_visible_) {
      glEnable(GL_BLEND);
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    }

    auto egl_image = gpu::CreateEGLImageFromAHardwareBuffer(
        overlay_handle_.android_hardware_buffer.get());
    if (!egl_image.is_valid()) {
      return false;
    }

    if (!overlay_texture_.id) {
      overlay_texture_.target =
          XrImageTransportBase::SharedBufferTextureTarget();
      glGenTextures(1, &overlay_texture_.id);
    }

    glBindTexture(overlay_texture_.target, overlay_texture_.id);
    glEGLImageTargetTexture2DOES(overlay_texture_.target, egl_image.get());
    renderer_->Draw(overlay_texture_, transform_floats);
  }

  return true;
}

void OpenXrGraphicsBindingOpenGLES::CleanupWithoutSubmit() {
  // Nothing to do, we store all of our necessary data on the active swapchain
  // image, which gets recycled when the frame ends.
}

bool OpenXrGraphicsBindingOpenGLES::WaitOnFence(gfx::GpuFence& gpu_fence) {
  std::unique_ptr<gl::GLFence> local_fence =
      gl::GLFence::CreateFromGpuFence(gpu_fence);
  local_fence->ServerWait();

  return true;
}

bool OpenXrGraphicsBindingOpenGLES::ShouldFlipSubmittedImage() {
  return false;
}

void OpenXrGraphicsBindingOpenGLES::OnSwapchainImageActivated(
    gpu::SharedImageInterface* sii) {
  CHECK(has_active_swapchain_image());
  CHECK(active_swapchain_index() < color_swapchain_images_.size());
  ResizeSharedBuffer(color_swapchain_images_[active_swapchain_index()], sii);
}

void OpenXrGraphicsBindingOpenGLES::SetOverlayAndWebXrVisibility(
    bool overlay_visible,
    bool webxr_visible) {
  overlay_visible_ = overlay_visible;
  webxr_visible_ = webxr_visible;
}

bool OpenXrGraphicsBindingOpenGLES::SetOverlayTexture(
    gfx::GpuMemoryBufferHandle texture,
    const gpu::SyncToken& sync_token,
    const gfx::RectF& left,
    const gfx::RectF& right) {
  if (texture.is_null()) {
    return false;
  }

  CHECK(texture.type == gfx::ANDROID_HARDWARE_BUFFER);
  overlay_handle_ = std::move(texture);
  return true;
}

}  // namespace device