Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(782)

Side by Side Diff: chrome/browser/android/vr_shell/vr_shell.cc

Issue 2742083002: Revert of Re-land WebVR compositor bypass via BrowserMain context + mailbox (Closed)
Patch Set: Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/android/vr_shell/vr_shell.h" 5 #include "chrome/browser/android/vr_shell/vr_shell.h"
6 6
7 #include <android/native_window_jni.h> 7 #include <android/native_window_jni.h>
8 8
9 #include <string> 9 #include <string>
10 #include <utility> 10 #include <utility>
(...skipping 18 matching lines...) Expand all
29 #include "chrome/browser/android/vr_shell/vr_web_contents_observer.h" 29 #include "chrome/browser/android/vr_shell/vr_web_contents_observer.h"
30 #include "content/public/browser/navigation_controller.h" 30 #include "content/public/browser/navigation_controller.h"
31 #include "content/public/browser/render_view_host.h" 31 #include "content/public/browser/render_view_host.h"
32 #include "content/public/browser/render_widget_host.h" 32 #include "content/public/browser/render_widget_host.h"
33 #include "content/public/browser/render_widget_host_view.h" 33 #include "content/public/browser/render_widget_host_view.h"
34 #include "content/public/browser/web_contents.h" 34 #include "content/public/browser/web_contents.h"
35 #include "content/public/common/content_features.h" 35 #include "content/public/common/content_features.h"
36 #include "content/public/common/referrer.h" 36 #include "content/public/common/referrer.h"
37 #include "device/vr/android/gvr/gvr_device.h" 37 #include "device/vr/android/gvr/gvr_device.h"
38 #include "device/vr/android/gvr/gvr_device_provider.h" 38 #include "device/vr/android/gvr/gvr_device_provider.h"
39 #include "gpu/command_buffer/common/mailbox.h"
40 #include "jni/VrShellImpl_jni.h" 39 #include "jni/VrShellImpl_jni.h"
41 #include "third_party/WebKit/public/platform/WebInputEvent.h" 40 #include "third_party/WebKit/public/platform/WebInputEvent.h"
42 #include "ui/android/view_android.h" 41 #include "ui/android/view_android.h"
43 #include "ui/android/window_android.h" 42 #include "ui/android/window_android.h"
44 #include "ui/base/page_transition_types.h" 43 #include "ui/base/page_transition_types.h"
45 #include "ui/display/display.h" 44 #include "ui/display/display.h"
46 #include "ui/display/screen.h" 45 #include "ui/display/screen.h"
47 #include "ui/gfx/transform.h" 46 #include "ui/gfx/transform.h"
48 #include "ui/gfx/transform_util.h" 47 #include "ui/gfx/transform_util.h"
49 48
50 using base::android::JavaParamRef; 49 using base::android::JavaParamRef;
51 using base::android::JavaRef; 50 using base::android::JavaRef;
52 51
53 namespace vr_shell { 52 namespace vr_shell {
54 53
55 namespace { 54 namespace {
56 vr_shell::VrShell* g_instance; 55 vr_shell::VrShell* g_instance;
57 56
58 static const char kVrShellUIURL[] = "chrome://vr-shell-ui"; 57 static const char kVrShellUIURL[] = "chrome://vr-shell-ui";
59 58
60 // Default downscale factor for computing the recommended WebVR
61 // renderWidth/Height from the 1:1 pixel mapped size. Using a rather
62 // aggressive downscale due to the high overhead of copying pixels
63 // twice before handing off to GVR. For comparison, the polyfill
64 // uses approximately 0.55 on a Pixel XL.
65 static constexpr float kWebVrRecommendedResolutionScale = 0.5;
66
67 void SetIsInVR(content::WebContents* contents, bool is_in_vr) { 59 void SetIsInVR(content::WebContents* contents, bool is_in_vr) {
68 if (contents && contents->GetRenderWidgetHostView()) 60 if (contents && contents->GetRenderWidgetHostView())
69 contents->GetRenderWidgetHostView()->SetIsInVR(is_in_vr); 61 contents->GetRenderWidgetHostView()->SetIsInVR(is_in_vr);
70 } 62 }
71 63
72 } // namespace 64 } // namespace
73 65
74 VrShell::VrShell(JNIEnv* env, 66 VrShell::VrShell(JNIEnv* env,
75 jobject obj, 67 jobject obj,
76 ui::WindowAndroid* content_window, 68 ui::WindowAndroid* content_window,
(...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after
171 ui_contents_->GetController().LoadURL( 163 ui_contents_->GetController().LoadURL(
172 url, content::Referrer(), 164 url, content::Referrer(),
173 ui::PageTransition::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string("")); 165 ui::PageTransition::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string(""));
174 } 166 }
175 167
176 bool RegisterVrShell(JNIEnv* env) { 168 bool RegisterVrShell(JNIEnv* env) {
177 return RegisterNativesImpl(env); 169 return RegisterNativesImpl(env);
178 } 170 }
179 171
180 VrShell::~VrShell() { 172 VrShell::~VrShell() {
181 delegate_provider_->RemoveDelegate();
182 { 173 {
183 // The GvrLayout is, and must always be, used only on the UI thread, and the 174 // The GvrLayout is, and must always be, used only on the UI thread, and the
184 // GvrApi used for rendering should only be used from the GL thread as it's 175 // GvrApi used for rendering should only be used from the GL thread as it's
185 // not thread safe. However, the GvrLayout owns the GvrApi instance, and 176 // not thread safe. However, the GvrLayout owns the GvrApi instance, and
186 // when it gets shut down it deletes the GvrApi instance with it. Therefore, 177 // when it gets shut down it deletes the GvrApi instance with it. Therefore,
187 // we need to block shutting down the GvrLayout on stopping our GL thread 178 // we need to block shutting down the GvrLayout on stopping our GL thread
188 // from using the GvrApi instance. 179 // from using the GvrApi instance.
189 // base::Thread::Stop, which is called when destroying the thread, asserts 180 // base::Thread::Stop, which is called when destroying the thread, asserts
190 // that IO is allowed to prevent jank, but there shouldn't be any concerns 181 // that IO is allowed to prevent jank, but there shouldn't be any concerns
191 // regarding jank in this case, because we're switching from 3D to 2D, 182 // regarding jank in this case, because we're switching from 3D to 2D,
192 // adding/removing a bunch of Java views, and probably changing device 183 // adding/removing a bunch of Java views, and probably changing device
193 // orientation here. 184 // orientation here.
194 base::ThreadRestrictions::ScopedAllowIO allow_io; 185 base::ThreadRestrictions::ScopedAllowIO allow_io;
195 gl_thread_.reset(); 186 gl_thread_.reset();
196 } 187 }
188 delegate_provider_->RemoveDelegate();
197 g_instance = nullptr; 189 g_instance = nullptr;
198 } 190 }
199 191
200 void VrShell::PostToGlThreadWhenReady(const base::Closure& task) { 192 void VrShell::PostToGlThreadWhenReady(const base::Closure& task) {
201 // TODO(mthiesse): Remove this blocking wait. Queue up events if thread isn't 193 // TODO(mthiesse): Remove this blocking wait. Queue up events if thread isn't
202 // finished starting? 194 // finished starting?
203 gl_thread_->WaitUntilThreadStarted(); 195 gl_thread_->WaitUntilThreadStarted();
204 gl_thread_->task_runner()->PostTask(FROM_HERE, task); 196 gl_thread_->task_runner()->PostTask(FROM_HERE, task);
205 } 197 }
206 198
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
272 } 264 }
273 265
274 void VrShell::SetWebVrMode(JNIEnv* env, 266 void VrShell::SetWebVrMode(JNIEnv* env,
275 const base::android::JavaParamRef<jobject>& obj, 267 const base::android::JavaParamRef<jobject>& obj,
276 bool enabled) { 268 bool enabled) {
277 webvr_mode_ = enabled; 269 webvr_mode_ = enabled;
278 if (metrics_helper_) 270 if (metrics_helper_)
279 metrics_helper_->SetWebVREnabled(enabled); 271 metrics_helper_->SetWebVREnabled(enabled);
280 PostToGlThreadWhenReady(base::Bind(&VrShellGl::SetWebVrMode, 272 PostToGlThreadWhenReady(base::Bind(&VrShellGl::SetWebVrMode,
281 gl_thread_->GetVrShellGl(), enabled)); 273 gl_thread_->GetVrShellGl(), enabled));
282
283 html_interface_->SetMode(enabled ? UiInterface::Mode::WEB_VR 274 html_interface_->SetMode(enabled ? UiInterface::Mode::WEB_VR
284 : UiInterface::Mode::STANDARD); 275 : UiInterface::Mode::STANDARD);
285 } 276 }
286 277
287 void VrShell::OnLoadProgressChanged(JNIEnv* env, 278 void VrShell::OnLoadProgressChanged(JNIEnv* env,
288 const JavaParamRef<jobject>& obj, 279 const JavaParamRef<jobject>& obj,
289 double progress) { 280 double progress) {
290 html_interface_->SetLoadProgress(progress); 281 html_interface_->SetLoadProgress(progress);
291 } 282 }
292 283
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
326 jboolean incognito, 317 jboolean incognito,
327 jint id) { 318 jint id) {
328 html_interface_->RemoveTab(incognito, id); 319 html_interface_->RemoveTab(incognito, id);
329 } 320 }
330 321
331 void VrShell::SetWebVRSecureOrigin(bool secure_origin) { 322 void VrShell::SetWebVRSecureOrigin(bool secure_origin) {
332 // TODO(cjgrant): Align this state with the logic that drives the omnibox. 323 // TODO(cjgrant): Align this state with the logic that drives the omnibox.
333 html_interface_->SetWebVRSecureOrigin(secure_origin); 324 html_interface_->SetWebVRSecureOrigin(secure_origin);
334 } 325 }
335 326
336 void VrShell::SubmitWebVRFrame(int16_t frame_index, 327 void VrShell::SubmitWebVRFrame() {}
337 const gpu::MailboxHolder& mailbox) {
338 TRACE_EVENT1("gpu", "SubmitWebVRFrame", "frame", frame_index);
339
340 PostToGlThreadWhenReady(base::Bind(&VrShellGl::SubmitWebVRFrame,
341 gl_thread_->GetVrShellGl(), frame_index,
342 mailbox));
343 }
344 328
345 void VrShell::UpdateWebVRTextureBounds(int16_t frame_index, 329 void VrShell::UpdateWebVRTextureBounds(int16_t frame_index,
346 const gvr::Rectf& left_bounds, 330 const gvr::Rectf& left_bounds,
347 const gvr::Rectf& right_bounds, 331 const gvr::Rectf& right_bounds) {
348 const gvr::Sizei& source_size) {
349 PostToGlThreadWhenReady(base::Bind(&VrShellGl::UpdateWebVRTextureBounds, 332 PostToGlThreadWhenReady(base::Bind(&VrShellGl::UpdateWebVRTextureBounds,
350 gl_thread_->GetVrShellGl(), frame_index, 333 gl_thread_->GetVrShellGl(), frame_index,
351 left_bounds, right_bounds, source_size)); 334 left_bounds, right_bounds));
352 } 335 }
353 336
354 bool VrShell::SupportsPresentation() { 337 bool VrShell::SupportsPresentation() {
355 return true; 338 return true;
356 } 339 }
357 340
358 void VrShell::ResetPose() { 341 void VrShell::ResetPose() {
359 gl_thread_->task_runner()->PostTask( 342 gl_thread_->task_runner()->PostTask(
360 FROM_HERE, base::Bind(&VrShellGl::ResetPose, gl_thread_->GetVrShellGl())); 343 FROM_HERE, base::Bind(&VrShellGl::ResetPose, gl_thread_->GetVrShellGl()));
361 } 344 }
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
395 } 378 }
396 379
397 void VrShell::ContentSurfaceChanged(jobject surface) { 380 void VrShell::ContentSurfaceChanged(jobject surface) {
398 content_surface_ = surface; 381 content_surface_ = surface;
399 content_compositor_->SurfaceChanged(surface); 382 content_compositor_->SurfaceChanged(surface);
400 JNIEnv* env = base::android::AttachCurrentThread(); 383 JNIEnv* env = base::android::AttachCurrentThread();
401 Java_VrShellImpl_contentSurfaceChanged(env, j_vr_shell_.obj()); 384 Java_VrShellImpl_contentSurfaceChanged(env, j_vr_shell_.obj());
402 } 385 }
403 386
404 void VrShell::GvrDelegateReady() { 387 void VrShell::GvrDelegateReady() {
405 PostToGlThreadWhenReady(base::Bind(
406 &VrShellGl::SetSubmitClient, gl_thread_->GetVrShellGl(),
407 base::Passed(
408 delegate_provider_->TakeSubmitFrameClient().PassInterface())));
409 delegate_provider_->SetDelegate(this, gvr_api_); 388 delegate_provider_->SetDelegate(this, gvr_api_);
410 } 389 }
411 390
412 void VrShell::AppButtonPressed() { 391 void VrShell::AppButtonPressed() {
413 if (vr_shell_enabled_) 392 if (vr_shell_enabled_)
414 html_interface_->HandleAppButtonClicked(); 393 html_interface_->HandleAppButtonClicked();
415 } 394 }
416 395
417 void VrShell::ContentPhysicalBoundsChanged(JNIEnv* env, 396 void VrShell::ContentPhysicalBoundsChanged(JNIEnv* env,
418 const JavaParamRef<jobject>& object, 397 const JavaParamRef<jobject>& object,
(...skipping 194 matching lines...) Expand 10 before | Expand all | Expand 10 after
613 592
614 void VrShell::ProcessContentGesture( 593 void VrShell::ProcessContentGesture(
615 std::unique_ptr<blink::WebInputEvent> event) { 594 std::unique_ptr<blink::WebInputEvent> event) {
616 if (content_input_manager_) { 595 if (content_input_manager_) {
617 content_input_manager_->ProcessUpdatedGesture(std::move(event)); 596 content_input_manager_->ProcessUpdatedGesture(std::move(event));
618 } else if (android_ui_gesture_target_) { 597 } else if (android_ui_gesture_target_) {
619 android_ui_gesture_target_->DispatchWebInputEvent(std::move(event)); 598 android_ui_gesture_target_->DispatchWebInputEvent(std::move(event));
620 } 599 }
621 } 600 }
622 601
623 /* static */
624 device::mojom::VRPosePtr VrShell::VRPosePtrFromGvrPose(gvr::Mat4f head_mat) { 602 device::mojom::VRPosePtr VrShell::VRPosePtrFromGvrPose(gvr::Mat4f head_mat) {
625 device::mojom::VRPosePtr pose = device::mojom::VRPose::New(); 603 device::mojom::VRPosePtr pose = device::mojom::VRPose::New();
626 604
627 pose->orientation.emplace(4); 605 pose->orientation.emplace(4);
628 606
629 gfx::Transform inv_transform( 607 gfx::Transform inv_transform(
630 head_mat.m[0][0], head_mat.m[0][1], head_mat.m[0][2], head_mat.m[0][3], 608 head_mat.m[0][0], head_mat.m[0][1], head_mat.m[0][2], head_mat.m[0][3],
631 head_mat.m[1][0], head_mat.m[1][1], head_mat.m[1][2], head_mat.m[1][3], 609 head_mat.m[1][0], head_mat.m[1][1], head_mat.m[1][2], head_mat.m[1][3],
632 head_mat.m[2][0], head_mat.m[2][1], head_mat.m[2][2], head_mat.m[2][3], 610 head_mat.m[2][0], head_mat.m[2][1], head_mat.m[2][2], head_mat.m[2][3],
633 head_mat.m[3][0], head_mat.m[3][1], head_mat.m[3][2], head_mat.m[3][3]); 611 head_mat.m[3][0], head_mat.m[3][1], head_mat.m[3][2], head_mat.m[3][3]);
(...skipping 10 matching lines...) Expand all
644 622
645 pose->position.emplace(3); 623 pose->position.emplace(3);
646 pose->position.value()[0] = decomposed_transform.translate[0]; 624 pose->position.value()[0] = decomposed_transform.translate[0];
647 pose->position.value()[1] = decomposed_transform.translate[1]; 625 pose->position.value()[1] = decomposed_transform.translate[1];
648 pose->position.value()[2] = decomposed_transform.translate[2]; 626 pose->position.value()[2] = decomposed_transform.translate[2];
649 } 627 }
650 628
651 return pose; 629 return pose;
652 } 630 }
653 631
654 /* static */
655 gvr::Sizei VrShell::GetRecommendedWebVrSize(gvr::GvrApi* gvr_api) {
656 // Pick a reasonable default size for the WebVR transfer surface
657 // based on a downscaled 1:1 render resolution. This size will also
658 // be reported to the client via CreateVRDisplayInfo as the
659 // client-recommended renderWidth/renderHeight and for the GVR
660 // framebuffer. If the client chooses a different size or resizes it
661 // while presenting, we'll resize the transfer surface and GVR
662 // framebuffer to match.
663 gvr::Sizei render_target_size =
664 gvr_api->GetMaximumEffectiveRenderTargetSize();
665 gvr::Sizei webvr_size = {static_cast<int>(render_target_size.width *
666 kWebVrRecommendedResolutionScale),
667 static_cast<int>(render_target_size.height *
668 kWebVrRecommendedResolutionScale)};
669 // Ensure that the width is an even number so that the eyes each
670 // get the same size, the recommended renderWidth is per eye
671 // and the client will use the sum of the left and right width.
672 //
673 // TODO(klausw,crbug.com/699350): should we round the recommended
674 // size to a multiple of 2^N pixels to be friendlier to the GPU? The
675 // exact size doesn't matter, and it might be more efficient.
676 webvr_size.width &= ~1;
677
678 return webvr_size;
679 }
680
681 /* static */
682 device::mojom::VRDisplayInfoPtr VrShell::CreateVRDisplayInfo( 632 device::mojom::VRDisplayInfoPtr VrShell::CreateVRDisplayInfo(
683 gvr::GvrApi* gvr_api, 633 gvr::GvrApi* gvr_api,
684 gvr::Sizei recommended_size, 634 gvr::Sizei compositor_size,
685 uint32_t device_id) { 635 uint32_t device_id) {
686 TRACE_EVENT0("input", "GvrDevice::GetVRDevice"); 636 TRACE_EVENT0("input", "GvrDevice::GetVRDevice");
687 637
688 device::mojom::VRDisplayInfoPtr device = device::mojom::VRDisplayInfo::New(); 638 device::mojom::VRDisplayInfoPtr device = device::mojom::VRDisplayInfo::New();
689 639
690 device->index = device_id; 640 device->index = device_id;
691 641
692 device->capabilities = device::mojom::VRDisplayCapabilities::New(); 642 device->capabilities = device::mojom::VRDisplayCapabilities::New();
693 device->capabilities->hasOrientation = true; 643 device->capabilities->hasOrientation = true;
694 device->capabilities->hasPosition = false; 644 device->capabilities->hasPosition = false;
695 device->capabilities->hasExternalDisplay = false; 645 device->capabilities->hasExternalDisplay = false;
696 device->capabilities->canPresent = true; 646 device->capabilities->canPresent = true;
697 647
698 std::string vendor = gvr_api->GetViewerVendor(); 648 std::string vendor = gvr_api->GetViewerVendor();
699 std::string model = gvr_api->GetViewerModel(); 649 std::string model = gvr_api->GetViewerModel();
700 device->displayName = vendor + " " + model; 650 device->displayName = vendor + " " + model;
701 651
702 gvr::BufferViewportList gvr_buffer_viewports = 652 gvr::BufferViewportList gvr_buffer_viewports =
703 gvr_api->CreateEmptyBufferViewportList(); 653 gvr_api->CreateEmptyBufferViewportList();
704 gvr_buffer_viewports.SetToRecommendedBufferViewports(); 654 gvr_buffer_viewports.SetToRecommendedBufferViewports();
705 655
706 device->leftEye = device::mojom::VREyeParameters::New(); 656 device->leftEye = device::mojom::VREyeParameters::New();
707 device->rightEye = device::mojom::VREyeParameters::New(); 657 device->rightEye = device::mojom::VREyeParameters::New();
708 for (auto eye : {GVR_LEFT_EYE, GVR_RIGHT_EYE}) { 658 for (auto eye : {GVR_LEFT_EYE, GVR_RIGHT_EYE}) {
709 device::mojom::VREyeParametersPtr& eye_params = 659 device::mojom::VREyeParametersPtr& eye_params =
710 (eye == GVR_LEFT_EYE) ? device->leftEye : device->rightEye; 660 (eye == GVR_LEFT_EYE) ? device->leftEye : device->rightEye;
711 eye_params->fieldOfView = device::mojom::VRFieldOfView::New(); 661 eye_params->fieldOfView = device::mojom::VRFieldOfView::New();
712 eye_params->offset.resize(3); 662 eye_params->offset.resize(3);
713 eye_params->renderWidth = recommended_size.width / 2; 663 eye_params->renderWidth = compositor_size.width / 2;
714 eye_params->renderHeight = recommended_size.height; 664 eye_params->renderHeight = compositor_size.height;
715 665
716 gvr::BufferViewport eye_viewport = gvr_api->CreateBufferViewport(); 666 gvr::BufferViewport eye_viewport = gvr_api->CreateBufferViewport();
717 gvr_buffer_viewports.GetBufferViewport(eye, &eye_viewport); 667 gvr_buffer_viewports.GetBufferViewport(eye, &eye_viewport);
718 gvr::Rectf eye_fov = eye_viewport.GetSourceFov(); 668 gvr::Rectf eye_fov = eye_viewport.GetSourceFov();
719 eye_params->fieldOfView->upDegrees = eye_fov.top; 669 eye_params->fieldOfView->upDegrees = eye_fov.top;
720 eye_params->fieldOfView->downDegrees = eye_fov.bottom; 670 eye_params->fieldOfView->downDegrees = eye_fov.bottom;
721 eye_params->fieldOfView->leftDegrees = eye_fov.left; 671 eye_params->fieldOfView->leftDegrees = eye_fov.left;
722 eye_params->fieldOfView->rightDegrees = eye_fov.right; 672 eye_params->fieldOfView->rightDegrees = eye_fov.right;
723 673
724 gvr::Mat4f eye_mat = gvr_api->GetEyeFromHeadMatrix(eye); 674 gvr::Mat4f eye_mat = gvr_api->GetEyeFromHeadMatrix(eye);
(...skipping 20 matching lines...) Expand all
745 jboolean reprojected_rendering) { 695 jboolean reprojected_rendering) {
746 return reinterpret_cast<intptr_t>(new VrShell( 696 return reinterpret_cast<intptr_t>(new VrShell(
747 env, obj, reinterpret_cast<ui::WindowAndroid*>(content_window_android), 697 env, obj, reinterpret_cast<ui::WindowAndroid*>(content_window_android),
748 content::WebContents::FromJavaWebContents(ui_web_contents), 698 content::WebContents::FromJavaWebContents(ui_web_contents),
749 reinterpret_cast<ui::WindowAndroid*>(ui_window_android), for_web_vr, 699 reinterpret_cast<ui::WindowAndroid*>(ui_window_android), for_web_vr,
750 VrShellDelegate::GetNativeVrShellDelegate(env, delegate), 700 VrShellDelegate::GetNativeVrShellDelegate(env, delegate),
751 reinterpret_cast<gvr_context*>(gvr_api), reprojected_rendering)); 701 reinterpret_cast<gvr_context*>(gvr_api), reprojected_rendering));
752 } 702 }
753 703
754 } // namespace vr_shell 704 } // namespace vr_shell
OLDNEW
« no previous file with comments | « chrome/browser/android/vr_shell/vr_shell.h ('k') | chrome/browser/android/vr_shell/vr_shell_delegate.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698