blob: 18259e82ada9bf7a96005e81439100f63dc14567 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/xr/service/xr_runtime_manager_impl.h"
#include <string>
#include <utility>
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/lazy_instance.h"
#include "base/memory/singleton.h"
#include "base/no_destructor.h"
#include "base/observer_list.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/typed_macros.h"
#include "build/build_config.h"
#include "content/browser/xr/service/xr_frame_sink_client_impl.h"
#include "content/browser/xr/webxr_internals/mojom/webxr_internals.mojom.h"
#include "content/browser/xr/webxr_internals/webxr_internals_handler_impl.h"
#include "content/browser/xr/xr_utils.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/device_service.h"
#include "content/public/browser/gpu_data_manager.h"
#include "content/public/browser/gpu_utils.h"
#include "content/public/browser/xr_runtime_manager.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "device/vr/buildflags/buildflags.h"
#include "device/vr/orientation/orientation_device_provider.h"
#include "device/vr/public/cpp/features.h"
#include "device/vr/public/cpp/vr_device_provider.h"
#include "gpu/config/gpu_info.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "services/device/public/mojom/sensor_provider.mojom.h"
#include "ui/gl/gl_switches.h"
#if !BUILDFLAG(IS_ANDROID)
#include "content/browser/xr/service/isolated_device_provider.h"
#endif // !BUILDFLAG(IS_ANDROID)
namespace content {
using XrRuntimeManagerObservers =
base::ObserverList<XRRuntimeManager::Observer>;
namespace {
XRRuntimeManagerImpl* g_xr_runtime_manager = nullptr;
XrRuntimeManagerObservers& GetXrRuntimeManagerObservers() {
static base::NoDestructor<XrRuntimeManagerObservers>
xr_runtime_manager_observers;
return *xr_runtime_manager_observers;
}
bool IsForcedRuntime(const base::CommandLine* command_line,
const std::string& name) {
return (base::CompareCaseInsensitiveASCII(
command_line->GetSwitchValueASCII(switches::kWebXrForceRuntime),
name) == 0);
}
std::optional<device::mojom::XRDeviceId> GetForcedRuntime(
device::mojom::XRSessionMode mode) {
auto* cmd_line = base::CommandLine::ForCurrentProcess();
if (!cmd_line->HasSwitch(switches::kWebXrForceRuntime)) {
return std::nullopt;
}
switch (mode) {
case device::mojom::XRSessionMode::kImmersiveAr:
#if BUILDFLAG(ENABLE_ARCORE)
if (IsForcedRuntime(cmd_line, switches::kWebXrRuntimeArCore)) {
return device::mojom::XRDeviceId::ARCORE_DEVICE_ID;
}
#endif
#if BUILDFLAG(ENABLE_OPENXR)
if (IsForcedRuntime(cmd_line, switches::kWebXrRuntimeOpenXr)) {
return device::mojom::XRDeviceId::OPENXR_DEVICE_ID;
}
#endif
break;
case device::mojom::XRSessionMode::kImmersiveVr:
#if BUILDFLAG(ENABLE_OPENXR)
if (IsForcedRuntime(cmd_line, switches::kWebXrRuntimeOpenXr)) {
return device::mojom::XRDeviceId::OPENXR_DEVICE_ID;
}
#endif
#if BUILDFLAG(ENABLE_CARDBOARD)
if (IsForcedRuntime(cmd_line, switches::kWebXrRuntimeCardboard)) {
return device::mojom::XRDeviceId::CARDBOARD_DEVICE_ID;
}
#endif
break;
case device::mojom::XRSessionMode::kInline:
if (IsForcedRuntime(cmd_line,
switches::kWebXrRuntimeOrientationSensors)) {
return device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID;
}
break;
}
return device::mojom::XRDeviceId::FAKE_DEVICE_ID;
}
std::unique_ptr<device::XrFrameSinkClient> FrameSinkClientFactory(
int32_t render_process_id,
int32_t render_frame_id) {
// The XrFrameSinkClientImpl needs to be constructed (and destructed) on the
// main thread. Currently, the only runtime that uses this is ArCore, which
// runs on the browser main thread (which per comments in
// content/public/browser/browser_thread.h is also the UI thread).
DCHECK(GetUIThreadTaskRunner({})->BelongsToCurrentThread())
<< "Must construct XrFrameSinkClient from UI thread";
return std::make_unique<XrFrameSinkClientImpl>(render_process_id,
render_frame_id);
}
} // namespace
// XRRuntimeManager statics
XRRuntimeManager* XRRuntimeManager::GetInstanceIfCreated() {
return g_xr_runtime_manager;
}
void XRRuntimeManager::AddObserver(XRRuntimeManager::Observer* observer) {
GetXrRuntimeManagerObservers().AddObserver(observer);
}
void XRRuntimeManager::RemoveObserver(XRRuntimeManager::Observer* observer) {
GetXrRuntimeManagerObservers().RemoveObserver(observer);
}
void XRRuntimeManager::ExitImmersivePresentation() {
if (!g_xr_runtime_manager) {
return;
}
auto* browser_xr_runtime =
g_xr_runtime_manager->GetCurrentlyPresentingImmersiveRuntime();
if (!browser_xr_runtime) {
return;
}
browser_xr_runtime->ExitActiveImmersiveSession();
}
// Static
scoped_refptr<XRRuntimeManagerImpl>
XRRuntimeManagerImpl::GetOrCreateRuntimeManagerInternal(
WebContents* web_contents) {
if (g_xr_runtime_manager) {
return base::WrapRefCounted(g_xr_runtime_manager);
}
// Start by getting any providers specified by the XrIntegrationClient
XRProviderList providers;
auto* integration_client = GetXrIntegrationClient();
if (integration_client) {
auto additional_providers = integration_client->GetAdditionalProviders();
providers.insert(providers.end(),
make_move_iterator(additional_providers.begin()),
make_move_iterator(additional_providers.end()));
}
// Then add any other "built-in" providers
#if !BUILDFLAG(IS_ANDROID)
providers.push_back(std::make_unique<IsolatedVRDeviceProvider>());
#endif // !BUILDFLAG(IS_ANDROID)
const bool is_orientation_provider_forced =
IsForcedRuntime(base::CommandLine::ForCurrentProcess(),
switches::kWebXrRuntimeOrientationSensors);
// We can use the orientation provider if it's forced, or if the feature is
// enabled and 2D chrome is not being rendered in a head-mounted display.
// On such displays inline sessions can cause "swimmy" behavior, because the
// content would move in response to the user's head motion, but in unexpected
// ways.
bool orientation_provider_enabled =
is_orientation_provider_forced ||
(base::FeatureList::IsEnabled(
device::features::kWebXrOrientationSensorDevice) &&
!device::features::IsXrDevice());
if (orientation_provider_enabled) {
mojo::PendingRemote<device::mojom::SensorProvider> sensor_provider;
GetDeviceService().BindSensorProvider(
sensor_provider.InitWithNewPipeAndPassReceiver());
providers.emplace_back(
std::make_unique<device::VROrientationDeviceProvider>(
std::move(sensor_provider)));
}
return CreateInstance(std::move(providers), web_contents);
}
scoped_refptr<XRRuntimeManagerImpl> XRRuntimeManagerImpl::GetOrCreateInstance(
WebContents& web_contents) {
return GetOrCreateRuntimeManagerInternal(&web_contents);
}
scoped_refptr<XRRuntimeManagerImpl>
XRRuntimeManagerImpl::GetOrCreateInstanceForTesting() {
return GetOrCreateRuntimeManagerInternal(nullptr);
}
// static
content::WebContents* XRRuntimeManagerImpl::GetImmersiveSessionWebContents() {
if (!g_xr_runtime_manager)
return nullptr;
BrowserXRRuntimeImpl* browser_xr_runtime =
g_xr_runtime_manager->GetCurrentlyPresentingImmersiveRuntime();
if (!browser_xr_runtime)
return nullptr;
VRServiceImpl* vr_service =
browser_xr_runtime->GetServiceWithActiveImmersiveSession();
return vr_service ? vr_service->GetWebContents() : nullptr;
}
void XRRuntimeManagerImpl::AddService(VRServiceImpl* service) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DVLOG(2) << __func__;
if (AreAllProvidersInitialized())
service->InitializationComplete();
services_.insert(service);
for (const auto& runtime : runtimes_) {
runtime.second->OnServiceAdded(service);
}
}
void XRRuntimeManagerImpl::RemoveService(VRServiceImpl* service) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DVLOG(2) << __func__;
services_.erase(service);
for (const auto& runtime : runtimes_) {
runtime.second->OnServiceRemoved(service);
}
}
BrowserXRRuntimeImpl* XRRuntimeManagerImpl::GetRuntime(
device::mojom::XRDeviceId id) {
auto it = runtimes_.find(id);
if (it == runtimes_.end())
return nullptr;
return it->second.get();
}
content::WebXrLoggerManager& XRRuntimeManagerImpl::GetLoggerManager() {
return logger_manager_;
}
BrowserXRRuntimeImpl* XRRuntimeManagerImpl::GetRuntimeForOptions(
device::mojom::XRSessionOptions* options) {
BrowserXRRuntimeImpl* runtime = nullptr;
switch (options->mode) {
case device::mojom::XRSessionMode::kImmersiveAr:
runtime = GetImmersiveArRuntime();
break;
case device::mojom::XRSessionMode::kImmersiveVr:
runtime = GetImmersiveVrRuntime();
break;
case device::mojom::XRSessionMode::kInline:
runtime = GetInlineRuntime();
break;
}
// Return the runtime from above if we got one and it supports all required
// features.
return runtime && runtime->SupportsAllFeatures(options->required_features)
? runtime
: nullptr;
}
BrowserXRRuntimeImpl* XRRuntimeManagerImpl::GetImmersiveVrRuntime() {
std::optional<device::mojom::XRDeviceId> maybe_runtime =
GetForcedRuntime(device::mojom::XRSessionMode::kImmersiveVr);
if (maybe_runtime.has_value()) {
return GetRuntime(maybe_runtime.value());
}
// OpenXR is the highest priority if it's available.
#if BUILDFLAG(ENABLE_OPENXR)
auto* openxr = GetRuntime(device::mojom::XRDeviceId::OPENXR_DEVICE_ID);
if (openxr) {
return openxr;
}
#endif
#if BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(ENABLE_CARDBOARD)
auto* cardboard = GetRuntime(device::mojom::XRDeviceId::CARDBOARD_DEVICE_ID);
if (cardboard) {
return cardboard;
}
#endif
#endif
return nullptr;
}
BrowserXRRuntimeImpl* XRRuntimeManagerImpl::GetImmersiveArRuntime() {
std::optional<device::mojom::XRDeviceId> maybe_runtime =
GetForcedRuntime(device::mojom::XRSessionMode::kImmersiveAr);
if (maybe_runtime.has_value()) {
auto* runtime = GetRuntime(maybe_runtime.value());
return runtime && runtime->SupportsArBlendMode() ? runtime : nullptr;
}
#if BUILDFLAG(ENABLE_OPENXR)
// If OpenXR is available and the runtime supports an AR blend mode, prefer
// it over ARCore to unify VR/AR rendering paths.
if (device::features::IsOpenXrArEnabled()) {
auto* openxr = GetRuntime(device::mojom::XRDeviceId::OPENXR_DEVICE_ID);
if (openxr && openxr->SupportsArBlendMode())
return openxr;
}
#endif
#if BUILDFLAG(ENABLE_ARCORE)
auto* arcore_runtime =
GetRuntime(device::mojom::XRDeviceId::ARCORE_DEVICE_ID);
if (arcore_runtime && arcore_runtime->SupportsArBlendMode()) {
return arcore_runtime;
}
#endif
return nullptr;
}
BrowserXRRuntimeImpl* XRRuntimeManagerImpl::GetInlineRuntime() {
std::optional<device::mojom::XRDeviceId> maybe_runtime =
GetForcedRuntime(device::mojom::XRSessionMode::kInline);
if (maybe_runtime.has_value()) {
return GetRuntime(maybe_runtime.value());
}
// Try the orientation provider if it exists.
// If we don't have an orientation provider, then we don't have an
// explicit runtime to back a non-immersive session.
return GetRuntime(device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID);
}
BrowserXRRuntimeImpl*
XRRuntimeManagerImpl::GetCurrentlyPresentingImmersiveRuntime() {
auto it = std::ranges::find_if(
runtimes_, [](const DeviceRuntimeMap::value_type& val) {
return val.second->GetServiceWithActiveImmersiveSession() != nullptr;
});
if (it != runtimes_.end()) {
return it->second.get();
}
return nullptr;
}
bool XRRuntimeManagerImpl::HasPendingImmersiveRequest() {
return std::ranges::any_of(
runtimes_, [](const DeviceRuntimeMap::value_type& val) {
return val.second->HasPendingImmersiveSessionRequest();
});
}
bool XRRuntimeManagerImpl::IsOtherClientPresenting(VRServiceImpl* service) {
DCHECK(service);
auto* runtime = GetCurrentlyPresentingImmersiveRuntime();
if (!runtime)
return false; // No immersive runtime to be presenting.
auto* presenting_service = runtime->GetServiceWithActiveImmersiveSession();
// True if some other VRServiceImpl is presenting.
return (presenting_service != service);
}
void XRRuntimeManagerImpl::SupportsSession(
device::mojom::XRSessionOptionsPtr options,
device::mojom::VRService::SupportsSessionCallback callback) {
auto* runtime = GetRuntimeForOptions(options.get());
if (!runtime) {
TRACE_EVENT("xr",
"XRRuntimeManagerImpl::SupportsSession: runtime not found",
perfetto::Flow::Global(options->trace_id));
std::move(callback).Run(false);
return;
}
// TODO(http://crbug.com/842025): Pass supports session on to the runtimes.
std::move(callback).Run(true);
}
void XRRuntimeManagerImpl::MakeXrCompatible() {
auto* runtime = GetImmersiveVrRuntime();
if (!runtime)
runtime = GetImmersiveArRuntime();
if (!runtime) {
for (VRServiceImpl* service : services_)
service->OnMakeXrCompatibleComplete(
device::mojom::XrCompatibleResult::kNoDeviceAvailable);
return;
}
if (!IsInitializedOnCompatibleAdapter(runtime)) {
#if BUILDFLAG(IS_WIN)
std::optional<CHROME_LUID> luid = runtime->GetLuid();
// IsInitializedOnCompatibleAdapter should have returned true if the
// runtime doesn't specify a LUID.
DCHECK(luid && (luid->HighPart != 0 || luid->LowPart != 0));
// Add the XR compatible adapter LUID to the browser command line.
// GpuProcessHost::LaunchGpuProcess passes this to the GPU process.
std::string luid_string = base::NumberToString(luid->HighPart) + "," +
base::NumberToString(luid->LowPart);
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
switches::kUseAdapterLuid, luid_string);
// Store the current GPU so we can revert back once XR is no longer needed.
// If default_gpu_ is nonzero, we have already previously stored the
// default GPU and should not overwrite it.
if (default_gpu_.LowPart == 0 && default_gpu_.HighPart == 0) {
default_gpu_ = content::GpuDataManager::GetInstance()
->GetGPUInfo()
.active_gpu()
.luid;
}
xr_compatible_restarted_gpu_ = true;
// Get notified when the new GPU process sends back its GPUInfo. This
// indicates that the GPU process has finished initializing and the GPUInfo
// contains the LUID of the active adapter.
content::GpuDataManager::GetInstance()->AddObserver(this);
content::KillGpuProcess();
return;
#else
// MakeXrCompatible is not yet supported on other platforms so
// IsInitializedOnCompatibleAdapter should have returned true.
NOTREACHED();
#endif
}
for (VRServiceImpl* service : services_)
service->OnMakeXrCompatibleComplete(
device::mojom::XrCompatibleResult::kAlreadyCompatible);
}
bool XRRuntimeManagerImpl::IsInitializedOnCompatibleAdapter(
BrowserXRRuntimeImpl* runtime) {
#if BUILDFLAG(IS_WIN)
std::optional<CHROME_LUID> luid = runtime->GetLuid();
if (luid && (luid->HighPart != 0 || luid->LowPart != 0)) {
CHROME_LUID active_luid =
content::GpuDataManager::GetInstance()->GetGPUInfo().active_gpu().luid;
return active_luid.HighPart == luid->HighPart &&
active_luid.LowPart == luid->LowPart;
}
#endif
return true;
}
void XRRuntimeManagerImpl::OnGpuInfoUpdate() {
content::GpuDataManager::GetInstance()->RemoveObserver(this);
device::mojom::XrCompatibleResult xr_compatible_result;
auto* runtime = GetImmersiveVrRuntime();
if (runtime && IsInitializedOnCompatibleAdapter(runtime)) {
xr_compatible_result =
device::mojom::XrCompatibleResult::kCompatibleAfterRestart;
} else {
// We can still be incompatible after restarting if either:
// 1. The runtime has been removed (usually means the VR headset was
// unplugged) since the GPU process restart was triggered. Per the WebXR
// spec, if there is no device, xr compatible is false.
// 2. The GPU process is still not using the correct GPU after restarting.
xr_compatible_result =
device::mojom::XrCompatibleResult::kNotCompatibleAfterRestart;
}
for (VRServiceImpl* service : services_)
service->OnMakeXrCompatibleComplete(xr_compatible_result);
}
XRRuntimeManagerImpl::XRRuntimeManagerImpl(XRProviderList providers,
WebContents* web_contents)
: providers_(std::move(providers)) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
CHECK(!g_xr_runtime_manager);
g_xr_runtime_manager = this;
InitializeProviders(web_contents);
}
XRRuntimeManagerImpl::~XRRuntimeManagerImpl() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
CHECK_EQ(g_xr_runtime_manager, this);
g_xr_runtime_manager = nullptr;
// If a GPU adapter LUID was added to the command line to pass to the GPU
// process, remove the switch so subsequent GPU processes initialize on the
// default GPU.
if (xr_compatible_restarted_gpu_) {
base::CommandLine::ForCurrentProcess()->RemoveSwitch(
switches::kUseAdapterLuid);
#if BUILDFLAG(IS_WIN)
// If we changed the GPU, revert it back to the default GPU. This is
// separate from xr_compatible_restarted_gpu_ because the GPU process may
// not have been successfully initialized using the specified GPU and is
// still on the default adapter.
CHROME_LUID active_gpu =
content::GpuDataManager::GetInstance()->GetGPUInfo().active_gpu().luid;
if (active_gpu.LowPart != default_gpu_.LowPart ||
active_gpu.HighPart != default_gpu_.HighPart) {
content::KillGpuProcess();
}
#endif
}
}
scoped_refptr<XRRuntimeManagerImpl> XRRuntimeManagerImpl::CreateInstance(
XRProviderList providers,
WebContents* contents) {
auto* ptr = new XRRuntimeManagerImpl(std::move(providers), contents);
CHECK_EQ(ptr, g_xr_runtime_manager);
return base::AdoptRef(ptr);
}
device::mojom::XRRuntime* XRRuntimeManagerImpl::GetRuntimeForTest(
device::mojom::XRDeviceId id) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DeviceRuntimeMap::iterator iter =
runtimes_.find(static_cast<device::mojom::XRDeviceId>(id));
if (iter == runtimes_.end())
return nullptr;
return iter->second->GetRuntime();
}
size_t XRRuntimeManagerImpl::NumberOfConnectedServices() {
return services_.size();
}
// The initializing service is available so that providers that need access to
// some aspect of the service, such as the WebContents, to perform
// initialization can do so, but the providers should be initialized in such a
// way that they are not explicitly tied to this service.
void XRRuntimeManagerImpl::InitializeProviders(WebContents* web_contents) {
if (providers_initialized_)
return;
for (const auto& provider : providers_) {
if (!provider) {
// TODO(crbug.com/40673158): Remove this logging after investigation.
LOG(ERROR) << __func__ << " got null XR provider";
continue;
}
// It is acceptable for the providers to potentially take/keep a reference
// to ourselves here, since we own the providers and can guarantee that they
// will not outlive us. Providers should not take a long-term reference to
// the WebContents.
provider->Initialize(this, web_contents);
}
providers_initialized_ = true;
}
void XRRuntimeManagerImpl::OnProviderInitialized() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
++num_initialized_providers_;
if (AreAllProvidersInitialized()) {
for (VRServiceImpl* service : services_)
service->InitializationComplete();
}
}
bool XRRuntimeManagerImpl::AreAllProvidersInitialized() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
return num_initialized_providers_ == providers_.size();
}
void XRRuntimeManagerImpl::AddRuntime(
device::mojom::XRDeviceId id,
device::mojom::XRDeviceDataPtr device_data,
mojo::PendingRemote<device::mojom::XRRuntime> runtime) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(runtimes_.find(id) == runtimes_.end());
TRACE_EVENT_INSTANT1("xr", "AddRuntime", TRACE_EVENT_SCOPE_THREAD, "id", id);
webxr::mojom::RuntimeInfoPtr runtime_added_record =
webxr::mojom::RuntimeInfo::New();
runtime_added_record->device_id = id;
runtime_added_record->supported_features = device_data->supported_features;
runtime_added_record->is_ar_blend_mode_supported =
device_data->is_ar_blend_mode_supported;
GetLoggerManager().RecordRuntimeAdded(std::move(runtime_added_record));
runtimes_[id] = std::make_unique<BrowserXRRuntimeImpl>(
id, std::move(device_data), std::move(runtime));
for (Observer& obs : GetXrRuntimeManagerObservers()) {
obs.OnRuntimeAdded(runtimes_[id].get());
}
for (VRServiceImpl* service : services_) {
// TODO(sumankancherla): Consider combining with XRRuntimeManager::Observer.
service->RuntimesChanged();
runtimes_[id]->OnServiceAdded(service);
}
}
void XRRuntimeManagerImpl::RemoveRuntime(device::mojom::XRDeviceId id) {
DVLOG(1) << __func__ << " id: " << id;
TRACE_EVENT_INSTANT1("xr", "RemoveRuntime", TRACE_EVENT_SCOPE_THREAD, "id",
id);
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
auto it = runtimes_.find(id);
CHECK(it != runtimes_.end());
GetLoggerManager().RecordRuntimeRemoved(id);
// Give the runtime a chance to clean itself up before notifying services
// that it was removed.
it->second->BeforeRuntimeRemoved();
// Remove the runtime from runtimes_ before notifying services that it was
// removed, since they will query for runtimes in RuntimesChanged.
std::unique_ptr<BrowserXRRuntimeImpl> removed_runtime = std::move(it->second);
runtimes_.erase(it);
for (VRServiceImpl* service : services_)
service->RuntimesChanged();
}
device::XrFrameSinkClientFactory
XRRuntimeManagerImpl::GetXrFrameSinkClientFactory() {
return base::BindRepeating(&FrameSinkClientFactory);
}
std::vector<webxr::mojom::RuntimeInfoPtr>
XRRuntimeManagerImpl::GetActiveRuntimes() {
std::vector<webxr::mojom::RuntimeInfoPtr> active_runtimes;
for (auto& runtime : runtimes_) {
webxr::mojom::RuntimeInfoPtr runtime_info =
webxr::mojom::RuntimeInfo::New();
runtime_info->device_id = runtime.first;
runtime_info->supported_features = runtime.second->GetSupportedFeatures();
runtime_info->is_ar_blend_mode_supported =
runtime.second->SupportsArBlendMode();
active_runtimes.push_back(std::move(runtime_info));
}
return active_runtimes;
}
} // namespace content