diff --git a/DEPS b/DEPS
index 720f9edc..7bd4eca 100644
--- a/DEPS
+++ b/DEPS
@@ -63,7 +63,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': '51e240033771b0e8f504d3eb401c781d5908fe39',
+  'v8_revision': 'ea01f412c74e35d547b39bedebf5d0338fb060e4',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling swarming_client
   # and whatever else without interference from each other.
@@ -83,7 +83,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
-  'pdfium_revision': '994f20cfb76f4902491a94c4ef61f55705fc124d',
+  'pdfium_revision': '3fff90a670d860a7b0319aa0edf8628917d0a122',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling openmax_dl
   # and whatever else without interference from each other.
diff --git a/chrome/browser/android/vr_shell/vr_controller.cc b/chrome/browser/android/vr_shell/vr_controller.cc
index 3a6fc86..ac340626 100644
--- a/chrome/browser/android/vr_shell/vr_controller.cc
+++ b/chrome/browser/android/vr_shell/vr_controller.cc
@@ -196,16 +196,6 @@
          gfx::ScaleVector3d(pointer_direction, kLaserStartDisplacement);
 }
 
-vr::VrControllerModel::State VrController::GetModelState() const {
-  if (ButtonState(gvr::ControllerButton::GVR_CONTROLLER_BUTTON_CLICK))
-    return vr::VrControllerModel::TOUCHPAD;
-  if (ButtonState(gvr::ControllerButton::GVR_CONTROLLER_BUTTON_APP))
-    return vr::VrControllerModel::APP;
-  if (ButtonState(gvr::ControllerButton::GVR_CONTROLLER_BUTTON_HOME))
-    return vr::VrControllerModel::SYSTEM;
-  return vr::VrControllerModel::IDLE;
-}
-
 bool VrController::TouchDownHappened() {
   return controller_state_->GetTouchDown();
 }
diff --git a/chrome/browser/android/vr_shell/vr_controller.h b/chrome/browser/android/vr_shell/vr_controller.h
index a499ad6..fb833f3 100644
--- a/chrome/browser/android/vr_shell/vr_controller.h
+++ b/chrome/browser/android/vr_shell/vr_controller.h
@@ -10,7 +10,7 @@
 
 #include "base/macros.h"
 #include "base/time/time.h"
-#include "chrome/browser/vr/vr_controller_model.h"
+#include "chrome/browser/vr/controller_mesh.h"
 #include "device/vr/android/gvr/gvr_gamepad_data_provider.h"
 #include "third_party/gvr-android-sdk/src/libraries/headers/vr/gvr/capi/include/gvr_types.h"
 #include "ui/gfx/geometry/point3_f.h"
@@ -68,8 +68,6 @@
   float GetOpacity() const;
   gfx::Point3F GetPointerStart() const;
 
-  vr::VrControllerModel::State GetModelState() const;
-
   bool TouchDownHappened();
 
   bool TouchUpHappened();
diff --git a/chrome/browser/android/vr_shell/vr_shell_gl.cc b/chrome/browser/android/vr_shell/vr_shell_gl.cc
index 2813fefcf..b26d054 100644
--- a/chrome/browser/android/vr_shell/vr_shell_gl.cc
+++ b/chrome/browser/android/vr_shell/vr_shell_gl.cc
@@ -25,6 +25,7 @@
 #include "chrome/browser/android/vr_shell/vr_usage_monitor.h"
 #include "chrome/browser/vr/elements/ui_element.h"
 #include "chrome/browser/vr/fps_meter.h"
+#include "chrome/browser/vr/model/model.h"
 #include "chrome/browser/vr/ui.h"
 #include "chrome/browser/vr/ui_interface.h"
 #include "chrome/browser/vr/ui_scene.h"
@@ -169,14 +170,14 @@
                     rect.top - rect.bottom);
 }
 
-void LoadControllerModelTask(
+void LoadControllerMeshTask(
     base::WeakPtr<VrShellGl> weak_vr_shell_gl,
     scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
-  auto controller_model = vr::VrControllerModel::LoadFromResources();
-  if (controller_model) {
+  auto controller_mesh = vr::ControllerMesh::LoadFromResources();
+  if (controller_mesh) {
     task_runner->PostTask(
-        FROM_HERE, base::Bind(&VrShellGl::SetControllerModel, weak_vr_shell_gl,
-                              base::Passed(&controller_model)));
+        FROM_HERE, base::Bind(&VrShellGl::SetControllerMesh, weak_vr_shell_gl,
+                              base::Passed(&controller_mesh)));
   }
 }
 
@@ -279,7 +280,7 @@
   if (daydream_support_ && !reinitializing) {
     base::PostTaskWithTraits(
         FROM_HERE, {base::TaskPriority::BACKGROUND},
-        base::Bind(LoadControllerModelTask, weak_ptr_factory_.GetWeakPtr(),
+        base::Bind(LoadControllerMeshTask, weak_ptr_factory_.GetWeakPtr(),
                    task_runner_));
   }
 
@@ -544,17 +545,18 @@
   gvr::Mat4f gvr_head_pose;
   TransformToGvrMat(head_pose, &gvr_head_pose);
   controller_->UpdateState(gvr_head_pose);
-  controller_info_.laser_origin = controller_->GetPointerStart();
+  gfx::Point3F laser_origin = controller_->GetPointerStart();
 
   device::GvrGamepadData controller_data = controller_->GetGamepadData();
   if (!ShouldDrawWebVr())
     controller_data.connected = false;
   browser_->UpdateGamepadData(controller_data);
 
-  HandleControllerInput(GetForwardVector(head_pose));
+  HandleControllerInput(laser_origin, GetForwardVector(head_pose));
 }
 
-void VrShellGl::HandleControllerInput(const gfx::Vector3dF& head_direction) {
+void VrShellGl::HandleControllerInput(const gfx::Point3F& laser_origin,
+                                      const gfx::Vector3dF& head_direction) {
   if (is_exiting_) {
     // When we're exiting, we don't show the reticle and the only input
     // processing we do is to handle immediate exits.
@@ -583,30 +585,34 @@
   if (ShouldDrawWebVr())
     return;
 
-  controller_->GetTransform(&controller_info_.transform);
+  vr::ControllerModel controller_model;
+  controller_->GetTransform(&controller_model.transform);
   std::unique_ptr<GestureList> gesture_list_ptr = controller_->DetectGestures();
   GestureList& gesture_list = *gesture_list_ptr;
-  controller_info_.touchpad_button_state = vr::UiInputManager::ButtonState::UP;
+  controller_model.touchpad_button_state = vr::UiInputManager::ButtonState::UP;
   DCHECK(!(controller_->ButtonUpHappened(gvr::kControllerButtonClick) &&
            controller_->ButtonDownHappened(gvr::kControllerButtonClick)))
       << "Cannot handle a button down and up event within one frame.";
   if (controller_->ButtonState(gvr::kControllerButtonClick)) {
-    controller_info_.touchpad_button_state =
+    controller_model.touchpad_button_state =
         vr::UiInputManager::ButtonState::DOWN;
   }
-  controller_info_.app_button_state =
+  controller_model.app_button_state =
       controller_->ButtonState(gvr::kControllerButtonApp)
           ? vr::UiInputManager::ButtonState::DOWN
           : vr::UiInputManager::ButtonState::UP;
-  controller_info_.home_button_state =
+  controller_model.home_button_state =
       controller_->ButtonState(gvr::kControllerButtonHome)
           ? vr::UiInputManager::ButtonState::DOWN
           : vr::UiInputManager::ButtonState::UP;
-  controller_info_.opacity = controller_->GetOpacity();
-  ui_->input_manager()->HandleInput(
-      controller_direction, controller_info_.laser_origin,
-      controller_info_.touchpad_button_state, &gesture_list,
-      &controller_info_.target_point, &controller_info_.reticle_render_target);
+  controller_model.opacity = controller_->GetOpacity();
+  controller_model.laser_direction = controller_direction;
+  controller_model.laser_origin = laser_origin;
+
+  vr::ReticleModel reticle_model;
+  ui_->input_manager()->HandleInput(controller_model, &reticle_model,
+                                    &gesture_list);
+  ui_->OnControllerUpdated(controller_model, reticle_model);
 }
 
 std::unique_ptr<blink::WebMouseEvent> VrShellGl::MakeMouseEvent(
@@ -1006,7 +1012,7 @@
   // At this point, we draw non-WebVR content that could, potentially, fill the
   // viewport.  NB: this is not just 2d browsing stuff, we may have a splash
   // screen showing in WebVR mode that must also fill the screen.
-  ui_->ui_renderer()->Draw(render_info_primary_, controller_info_);
+  ui_->ui_renderer()->Draw(render_info_primary_);
 
   content_frame_available_ = false;
   acquired_frame_.Unbind();
@@ -1064,8 +1070,8 @@
                    kViewportListWebVrBrowserUiOffset, render_size_webvr_ui_,
                    &render_info_webvr_browser_ui);
 
-    ui_->ui_renderer()->DrawWebVrOverlayForeground(render_info_webvr_browser_ui,
-                                                   controller_info_);
+    ui_->ui_renderer()->DrawWebVrOverlayForeground(
+        render_info_webvr_browser_ui);
 
     acquired_frame_.Unbind();
   }
@@ -1231,9 +1237,8 @@
   return ui_->GetBrowserUiWeakPtr();
 }
 
-void VrShellGl::SetControllerModel(
-    std::unique_ptr<vr::VrControllerModel> model) {
-  ui_->vr_shell_renderer()->GetControllerRenderer()->SetUp(std::move(model));
+void VrShellGl::SetControllerMesh(std::unique_ptr<vr::ControllerMesh> mesh) {
+  ui_->vr_shell_renderer()->GetControllerRenderer()->SetUp(std::move(mesh));
 }
 
 void VrShellGl::OnVSync(base::TimeTicks frame_time) {
diff --git a/chrome/browser/android/vr_shell/vr_shell_gl.h b/chrome/browser/android/vr_shell/vr_shell_gl.h
index 2f4a7ba..d6c601c 100644
--- a/chrome/browser/android/vr_shell/vr_shell_gl.h
+++ b/chrome/browser/android/vr_shell/vr_shell_gl.h
@@ -18,9 +18,9 @@
 #include "chrome/browser/android/vr_shell/android_vsync_helper.h"
 #include "chrome/browser/android/vr_shell/vr_controller.h"
 #include "chrome/browser/vr/content_input_delegate.h"
+#include "chrome/browser/vr/controller_mesh.h"
 #include "chrome/browser/vr/ui_input_manager.h"
 #include "chrome/browser/vr/ui_renderer.h"
-#include "chrome/browser/vr/vr_controller_model.h"
 #include "device/vr/vr_service.mojom.h"
 #include "mojo/public/cpp/bindings/binding.h"
 #include "third_party/WebKit/public/platform/WebInputEvent.h"
@@ -108,7 +108,7 @@
   void UIPhysicalBoundsChanged(int width, int height);
   base::WeakPtr<VrShellGl> GetWeakPtr();
 
-  void SetControllerModel(std::unique_ptr<vr::VrControllerModel> model);
+  void SetControllerMesh(std::unique_ptr<vr::ControllerMesh> mesh);
 
   void ConnectPresentingService(
       device::mojom::VRSubmitFrameClientPtrInfo submit_client_info,
@@ -165,7 +165,8 @@
                           const gfx::PointF& normalized_hit_point) override;
 
   void SendImmediateExitRequestIfNecessary();
-  void HandleControllerInput(const gfx::Vector3dF& head_direction);
+  void HandleControllerInput(const gfx::Point3F& laser_origin,
+                             const gfx::Vector3dF& head_direction);
   void HandleControllerAppButtonActivity(
       const gfx::Vector3dF& controller_direction);
   void SendGestureToContent(std::unique_ptr<blink::WebInputEvent> event);
@@ -280,7 +281,6 @@
 
   gfx::Point3F pointer_start_;
 
-  vr::ControllerInfo controller_info_;
   vr::RenderInfo render_info_primary_;
 
   AndroidVSyncHelper vsync_helper_;
diff --git a/chrome/browser/browser_encoding_browsertest.cc b/chrome/browser/browser_encoding_browsertest.cc
index 9f39368..5a9d9511 100644
--- a/chrome/browser/browser_encoding_browsertest.cc
+++ b/chrome/browser/browser_encoding_browsertest.cc
@@ -4,21 +4,21 @@
 
 #include <stddef.h>
 
-#include "base/bind.h"
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/macros.h"
+#include "base/path_service.h"
+#include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
-#include "chrome/browser/net/url_request_mock_util.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_commands.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/common/chrome_paths.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/prefs/pref_service.h"
-#include "content/public/browser/browser_thread.h"
 #include "content/public/browser/download_manager.h"
 #include "content/public/browser/navigation_controller.h"
 #include "content/public/browser/notification_service.h"
@@ -27,7 +27,7 @@
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/download_test_observer.h"
 #include "content/public/test/test_navigation_observer.h"
-#include "net/test/url_request/url_request_mock_http_job.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
 
 namespace {
 
@@ -71,8 +71,6 @@
 
 }  // namespace
 
-using content::BrowserThread;
-
 static const base::FilePath::CharType* kTestDir =
     FILE_PATH_LITERAL("encoding_tests");
 
@@ -85,7 +83,8 @@
   // Saves the current page and verifies that the output matches the expected
   // result.
   void SaveAndCompare(const char* filename_to_write,
-                      const base::FilePath& expected) {
+                      const base::FilePath& expected,
+                      const GURL& url) {
     // Dump the page, the content of dump page should be identical to the
     // expected result file.
     base::FilePath full_file_name = save_dir_.AppendASCII(filename_to_write);
@@ -105,19 +104,32 @@
     base::FilePath expected_file_name = ui_test_utils::GetTestFilePath(
         base::FilePath(kTestDir), expected);
 
-    EXPECT_TRUE(base::ContentsEqual(full_file_name, expected_file_name)) <<
-        "generated_file = " << full_file_name.AsUTF8Unsafe() <<
-        ", expected_file = " << expected_file_name.AsUTF8Unsafe();
+    std::string actual_contents;
+    std::string expected_contents;
+
+    {
+      base::ScopedAllowBlockingForTesting allow_blocking;
+      ASSERT_TRUE(base::ReadFileToString(full_file_name, &actual_contents));
+      ASSERT_TRUE(
+          base::ReadFileToString(expected_file_name, &expected_contents));
+    }
+
+    // Add "Mark of the Web" path with source URL.
+    expected_contents = base::StringPrintf(
+        expected_contents.c_str(), url.spec().length(), url.spec().c_str());
+
+    EXPECT_EQ(expected_contents, actual_contents);
   }
 
   void SetUpOnMainThread() override {
+    base::FilePath test_data_dir;
+    ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir));
+    embedded_test_server()->ServeFilesFromDirectory(test_data_dir);
+    ASSERT_TRUE(embedded_test_server()->Start());
+
     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
     save_dir_ = temp_dir_.GetPath();
     temp_sub_resource_dir_ = save_dir_.AppendASCII("sub_resource_files");
-
-    BrowserThread::PostTask(
-        BrowserThread::IO, FROM_HERE,
-        base::BindOnce(&chrome_browser_net::SetUrlRequestMocksEnabled, true));
   }
 
   base::ScopedTempDir temp_dir_;
@@ -140,7 +152,7 @@
       GetParam().file_name);
 
   GURL url =
-      net::URLRequestMockHTTPJob::GetMockUrl(test_file_path.MaybeAsASCII());
+      embedded_test_server()->GetURL("/" + test_file_path.MaybeAsASCII());
   ui_test_utils::NavigateToURL(browser(), url);
   EXPECT_EQ(GetParam().encoding_name,
             browser()->tab_strip_model()->GetActiveWebContents()->
@@ -232,7 +244,7 @@
     base::FilePath test_file_path(test_dir_path);
     test_file_path = test_file_path.AppendASCII(kTestDatas[i].test_file_name);
     GURL url =
-        net::URLRequestMockHTTPJob::GetMockUrl(test_file_path.MaybeAsASCII());
+        embedded_test_server()->GetURL("/" + test_file_path.MaybeAsASCII());
     ui_test_utils::NavigateToURL(browser(), url);
 
     // Get the encoding of page. It should return the real encoding now.
@@ -244,6 +256,7 @@
         base::FilePath().AppendASCII(kAutoDetectDir).
         AppendASCII(kExpectedResultDir).
         AppendASCII(kTestDatas[i].expected_result);
-    SaveAndCompare(kTestDatas[i].test_file_name, expected_result_file_name);
+    SaveAndCompare(kTestDatas[i].test_file_name, expected_result_file_name,
+                   url);
   }
 }
diff --git a/chrome/browser/vr/BUILD.gn b/chrome/browser/vr/BUILD.gn
index e51b53e..c29c525 100644
--- a/chrome/browser/vr/BUILD.gn
+++ b/chrome/browser/vr/BUILD.gn
@@ -20,6 +20,8 @@
     "browser_ui_interface.h",
     "color_scheme.cc",
     "color_scheme.h",
+    "controller_mesh.cc",
+    "controller_mesh.h",
     "databinding/binding.h",
     "databinding/binding_base.cc",
     "databinding/binding_base.h",
@@ -31,6 +33,8 @@
     "elements/button_texture.h",
     "elements/content_element.cc",
     "elements/content_element.h",
+    "elements/controller.cc",
+    "elements/controller.h",
     "elements/draw_phase.h",
     "elements/exclusive_screen_toast.cc",
     "elements/exclusive_screen_toast.h",
@@ -50,12 +54,16 @@
     "elements/grid.h",
     "elements/invisible_hit_target.cc",
     "elements/invisible_hit_target.h",
+    "elements/laser.cc",
+    "elements/laser.h",
     "elements/linear_layout.cc",
     "elements/linear_layout.h",
     "elements/rect.cc",
     "elements/rect.h",
     "elements/render_text_wrapper.cc",
     "elements/render_text_wrapper.h",
+    "elements/reticle.cc",
+    "elements/reticle.h",
     "elements/simple_textured_element.h",
     "elements/spinner.cc",
     "elements/spinner.h",
@@ -74,6 +82,7 @@
     "elements/ui_element.cc",
     "elements/ui_element.h",
     "elements/ui_element_iterator.h",
+    "elements/ui_element_name.cc",
     "elements/ui_element_name.h",
     "elements/ui_element_transform_operations.cc",
     "elements/ui_element_transform_operations.h",
@@ -102,10 +111,13 @@
     "gltf_asset.h",
     "gltf_parser.cc",
     "gltf_parser.h",
+    "model/controller_model.h",
     "model/model.cc",
     "model/model.h",
     "model/omnibox_suggestions.cc",
     "model/omnibox_suggestions.h",
+    "model/reticle_model.h",
+    "model/web_vr_timeout_state.h",
     "service/vr_device_manager.cc",
     "service/vr_device_manager.h",
     "service/vr_display_host.cc",
@@ -136,8 +148,6 @@
     "ui_scene_manager.cc",
     "ui_scene_manager.h",
     "ui_unsupported_mode.h",
-    "vr_controller_model.cc",
-    "vr_controller_model.h",
     "vr_gl_util.cc",
     "vr_gl_util.h",
     "vr_shell_renderer.cc",
@@ -146,12 +156,15 @@
     "web_contents_event_forwarder.h",
   ]
 
+  public_deps = [
+    "//chrome/browser/resources:vr_shell_resources",
+  ]
+
   deps = [
     "//base",
     "//cc/animation",
     "//cc/paint",
     "//chrome/app:generated_resources",
-    "//chrome/browser/resources:vr_shell_resources",
     "//chrome/browser/vr/vector_icons",
     "//chrome/common:constants",
     "//components/omnibox/browser",
@@ -179,6 +192,7 @@
     "databinding/vector_binding_unittest.cc",
     "elements/exit_prompt_unittest.cc",
     "elements/linear_layout_unittest.cc",
+    "elements/rect_unittest.cc",
     "elements/spinner_unittest.cc",
     "elements/throbber_unittest.cc",
     "elements/transient_element_unittest.cc",
diff --git a/chrome/browser/vr/vr_controller_model.cc b/chrome/browser/vr/controller_mesh.cc
similarity index 76%
rename from chrome/browser/vr/vr_controller_model.cc
rename to chrome/browser/vr/controller_mesh.cc
index cd3845f..e0c4e75d 100644
--- a/chrome/browser/vr/vr_controller_model.cc
+++ b/chrome/browser/vr/controller_mesh.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/vr/vr_controller_model.h"
+#include "chrome/browser/vr/controller_mesh.h"
 
 #include "base/memory/ptr_util.h"
 #include "base/trace_event/trace_event.h"
@@ -11,6 +11,7 @@
 #include "third_party/skia/include/core/SkCanvas.h"
 #include "third_party/skia/include/core/SkRect.h"
 #include "third_party/skia/include/core/SkSurface.h"
+#include "third_party/skia/include/core/SkSwizzle.h"
 #include "ui/base/resource/resource_bundle.h"
 #include "ui/gfx/codec/png_codec.h"
 
@@ -35,25 +36,31 @@
 sk_sp<SkImage> LoadPng(int resource_id) {
   base::StringPiece data =
       ui::ResourceBundle::GetSharedInstance().GetRawDataResource(resource_id);
+
   SkBitmap bitmap;
   bool decoded =
       gfx::PNGCodec::Decode(reinterpret_cast<const unsigned char*>(data.data()),
                             data.size(), &bitmap);
   DCHECK(decoded);
-  DCHECK(bitmap.colorType() == kRGBA_8888_SkColorType);
+  if (bitmap.colorType() == kBGRA_8888_SkColorType) {
+    SkSwapRB(bitmap.getAddr32(0, 0), bitmap.getAddr32(0, 0),
+             bitmap.width() * bitmap.height());
+  } else {
+    DCHECK(bitmap.colorType() == kRGBA_8888_SkColorType);
+  }
   return SkImage::MakeFromBitmap(bitmap);
 }
 
 }  // namespace
 
-VrControllerModel::VrControllerModel(
+ControllerMesh::ControllerMesh(
     std::unique_ptr<vr::gltf::Asset> gltf_asset,
     std::vector<std::unique_ptr<vr::gltf::Buffer>> buffers)
     : gltf_asset_(std::move(gltf_asset)), buffers_(std::move(buffers)) {}
 
-VrControllerModel::~VrControllerModel() = default;
+ControllerMesh::~ControllerMesh() = default;
 
-const GLvoid* VrControllerModel::ElementsBuffer() const {
+const GLvoid* ControllerMesh::ElementsBuffer() const {
   const vr::gltf::BufferView* buffer_view =
       gltf_asset_->GetBufferView(ELEMENTS_BUFFER_ID);
   DCHECK(buffer_view && buffer_view->target == GL_ARRAY_BUFFER);
@@ -61,14 +68,14 @@
   return buffer ? buffer + buffer_view->byte_offset : nullptr;
 }
 
-GLsizeiptr VrControllerModel::ElementsBufferSize() const {
+GLsizeiptr ControllerMesh::ElementsBufferSize() const {
   const vr::gltf::BufferView* buffer_view =
       gltf_asset_->GetBufferView(ELEMENTS_BUFFER_ID);
   DCHECK(buffer_view && buffer_view->target == GL_ARRAY_BUFFER);
   return buffer_view->byte_length;
 }
 
-const GLvoid* VrControllerModel::IndicesBuffer() const {
+const GLvoid* ControllerMesh::IndicesBuffer() const {
   const vr::gltf::BufferView* buffer_view =
       gltf_asset_->GetBufferView(INDICES_BUFFER_ID);
   DCHECK(buffer_view && buffer_view->target == GL_ELEMENT_ARRAY_BUFFER);
@@ -76,38 +83,38 @@
   return buffer ? buffer + buffer_view->byte_offset : nullptr;
 }
 
-GLsizeiptr VrControllerModel::IndicesBufferSize() const {
+GLsizeiptr ControllerMesh::IndicesBufferSize() const {
   const vr::gltf::BufferView* buffer_view =
       gltf_asset_->GetBufferView(INDICES_BUFFER_ID);
   DCHECK(buffer_view && buffer_view->target == GL_ELEMENT_ARRAY_BUFFER);
   return buffer_view->byte_length;
 }
 
-GLenum VrControllerModel::DrawMode() const {
+GLenum ControllerMesh::DrawMode() const {
   const vr::gltf::Mesh* mesh = gltf_asset_->GetMesh(0);
   DCHECK(mesh && mesh->primitives.size());
   return mesh->primitives[0]->mode;
 }
 
-const vr::gltf::Accessor* VrControllerModel::IndicesAccessor() const {
+const vr::gltf::Accessor* ControllerMesh::IndicesAccessor() const {
   const vr::gltf::Mesh* mesh = gltf_asset_->GetMesh(0);
   DCHECK(mesh && mesh->primitives.size());
   return mesh->primitives[0]->indices;
 }
 
-const vr::gltf::Accessor* VrControllerModel::PositionAccessor() const {
+const vr::gltf::Accessor* ControllerMesh::PositionAccessor() const {
   return Accessor(kPosition);
 }
 
-const vr::gltf::Accessor* VrControllerModel::TextureCoordinateAccessor() const {
+const vr::gltf::Accessor* ControllerMesh::TextureCoordinateAccessor() const {
   return Accessor(kTexCoord);
 }
 
-void VrControllerModel::SetBaseTexture(sk_sp<SkImage> image) {
+void ControllerMesh::SetBaseTexture(sk_sp<SkImage> image) {
   base_texture_ = image;
 }
 
-void VrControllerModel::SetTexture(int state, sk_sp<SkImage> patch) {
+void ControllerMesh::SetTexture(int state, sk_sp<SkImage> patch) {
   DCHECK(state >= 0 && state < STATE_COUNT);
   if (!patch) {
     textures_[state] = base_texture_;
@@ -124,18 +131,18 @@
   textures_[state] = sk_sp<SkImage>(surface->makeImageSnapshot());
 }
 
-sk_sp<SkImage> VrControllerModel::GetTexture(int state) const {
+sk_sp<SkImage> ControllerMesh::GetTexture(int state) const {
   DCHECK(state >= 0 && state < STATE_COUNT);
   return textures_[state];
 }
 
-const char* VrControllerModel::Buffer() const {
+const char* ControllerMesh::Buffer() const {
   if (buffers_.empty())
     return nullptr;
   return buffers_[0]->data();
 }
 
-const vr::gltf::Accessor* VrControllerModel::Accessor(
+const vr::gltf::Accessor* ControllerMesh::Accessor(
     const std::string& key) const {
   const vr::gltf::Mesh* mesh = gltf_asset_->GetMesh(0);
   DCHECK(mesh && mesh->primitives.size());
@@ -144,8 +151,8 @@
   return it->second;
 }
 
-std::unique_ptr<VrControllerModel> VrControllerModel::LoadFromResources() {
-  TRACE_EVENT0("gpu", "VrControllerModel::LoadFromResources");
+std::unique_ptr<ControllerMesh> ControllerMesh::LoadFromResources() {
+  TRACE_EVENT0("gpu", "ControllerMesh::LoadFromResources");
   std::vector<std::unique_ptr<vr::gltf::Buffer>> buffers;
   auto model_data = ui::ResourceBundle::GetSharedInstance().GetRawDataResource(
       IDR_VR_SHELL_DDCONTROLLER_MODEL);
@@ -154,11 +161,11 @@
   DCHECK(asset);
 
   auto controller_model =
-      base::MakeUnique<VrControllerModel>(std::move(asset), std::move(buffers));
+      base::MakeUnique<ControllerMesh>(std::move(asset), std::move(buffers));
   sk_sp<SkImage> base_texture = LoadPng(IDR_VR_SHELL_DDCONTROLLER_IDLE_TEXTURE);
   controller_model->SetBaseTexture(std::move(base_texture));
 
-  for (int i = 0; i < VrControllerModel::STATE_COUNT; i++) {
+  for (int i = 0; i < ControllerMesh::STATE_COUNT; i++) {
     if (kTexturePatchesResources[i] == -1) {
       controller_model->SetTexture(i, nullptr);
     } else {
diff --git a/chrome/browser/vr/vr_controller_model.h b/chrome/browser/vr/controller_mesh.h
similarity index 82%
rename from chrome/browser/vr/vr_controller_model.h
rename to chrome/browser/vr/controller_mesh.h
index 04bedef..149c5a64 100644
--- a/chrome/browser/vr/vr_controller_model.h
+++ b/chrome/browser/vr/controller_mesh.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef CHROME_BROWSER_VR_VR_CONTROLLER_MODEL_H_
-#define CHROME_BROWSER_VR_VR_CONTROLLER_MODEL_H_
+#ifndef CHROME_BROWSER_VR_CONTROLLER_MESH_H_
+#define CHROME_BROWSER_VR_CONTROLLER_MESH_H_
 
 #include <memory>
 
@@ -14,7 +14,7 @@
 
 namespace vr {
 
-class VrControllerModel {
+class ControllerMesh {
  public:
   enum State {
     IDLE = 0,
@@ -25,10 +25,10 @@
     STATE_COUNT,
   };
 
-  explicit VrControllerModel(
+  explicit ControllerMesh(
       std::unique_ptr<vr::gltf::Asset> gltf_asset,
       std::vector<std::unique_ptr<vr::gltf::Buffer>> buffers);
-  ~VrControllerModel();
+  ~ControllerMesh();
 
   const GLvoid* ElementsBuffer() const;
   GLsizeiptr ElementsBufferSize() const;
@@ -42,7 +42,7 @@
   void SetTexture(int state, sk_sp<SkImage> patch);
   sk_sp<SkImage> GetTexture(int state) const;
 
-  static std::unique_ptr<VrControllerModel> LoadFromResources();
+  static std::unique_ptr<ControllerMesh> LoadFromResources();
 
  private:
   std::unique_ptr<vr::gltf::Asset> gltf_asset_;
@@ -56,4 +56,4 @@
 
 }  // namespace vr
 
-#endif  // CHROME_BROWSER_VR_VR_CONTROLLER_MODEL_H_
+#endif  // CHROME_BROWSER_VR_CONTROLLER_MESH_H_
diff --git a/chrome/browser/vr/elements/content_element.h b/chrome/browser/vr/elements/content_element.h
index 38bdbb87..a027b5c 100644
--- a/chrome/browser/vr/elements/content_element.h
+++ b/chrome/browser/vr/elements/content_element.h
@@ -46,6 +46,8 @@
   unsigned int texture_id_ = 0;
   UiElementRenderer::TextureLocation texture_location_ =
       UiElementRenderer::kTextureLocationExternal;
+
+  DISALLOW_COPY_AND_ASSIGN(ContentElement);
 };
 
 }  // namespace vr
diff --git a/chrome/browser/vr/elements/controller.cc b/chrome/browser/vr/elements/controller.cc
new file mode 100644
index 0000000..e18bbd0d
--- /dev/null
+++ b/chrome/browser/vr/elements/controller.cc
@@ -0,0 +1,40 @@
+// 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 "chrome/browser/vr/elements/controller.h"
+
+#include "chrome/browser/vr/controller_mesh.h"
+#include "chrome/browser/vr/ui_element_renderer.h"
+
+namespace vr {
+
+Controller::Controller() {
+  set_name(kController);
+  set_hit_testable(false);
+  SetVisible(true);
+}
+
+Controller::~Controller() = default;
+
+void Controller::Render(UiElementRenderer* renderer,
+                        const gfx::Transform& model_view_proj_matrix) const {
+  ControllerMesh::State state;
+  if (touchpad_button_pressed_) {
+    state = ControllerMesh::TOUCHPAD;
+  } else if (app_button_pressed_) {
+    state = ControllerMesh::APP;
+  } else if (home_button_pressed_) {
+    state = ControllerMesh::SYSTEM;
+  } else {
+    state = ControllerMesh::IDLE;
+  }
+
+  renderer->DrawController(state, computed_opacity(), model_view_proj_matrix);
+}
+
+gfx::Transform Controller::LocalTransform() const {
+  return local_transform_;
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/elements/controller.h b/chrome/browser/vr/elements/controller.h
new file mode 100644
index 0000000..d867d81
--- /dev/null
+++ b/chrome/browser/vr/elements/controller.h
@@ -0,0 +1,47 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_ELEMENTS_CONTROLLER_H_
+#define CHROME_BROWSER_VR_ELEMENTS_CONTROLLER_H_
+
+#include "base/macros.h"
+#include "chrome/browser/vr/elements/ui_element.h"
+
+namespace vr {
+
+// This represents the controller.
+class Controller : public UiElement {
+ public:
+  Controller();
+  ~Controller() override;
+
+  void set_touchpad_button_pressed(bool pressed) {
+    touchpad_button_pressed_ = pressed;
+  }
+
+  void set_app_button_pressed(bool pressed) { app_button_pressed_ = pressed; }
+
+  void set_home_button_pressed(bool pressed) { home_button_pressed_ = pressed; }
+
+  void set_local_transform(const gfx::Transform& transform) {
+    local_transform_ = transform;
+  }
+
+ private:
+  void Render(UiElementRenderer* renderer,
+              const gfx::Transform& model_view_proj_matrix) const final;
+
+  gfx::Transform LocalTransform() const override;
+
+  bool touchpad_button_pressed_ = false;
+  bool app_button_pressed_ = false;
+  bool home_button_pressed_ = false;
+  gfx::Transform local_transform_;
+
+  DISALLOW_COPY_AND_ASSIGN(Controller);
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_ELEMENTS_CONTROLLER_H_
diff --git a/chrome/browser/vr/elements/laser.cc b/chrome/browser/vr/elements/laser.cc
new file mode 100644
index 0000000..650bc7c
--- /dev/null
+++ b/chrome/browser/vr/elements/laser.cc
@@ -0,0 +1,67 @@
+// 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 "chrome/browser/vr/elements/laser.h"
+
+#include "base/numerics/math_constants.h"
+#include "chrome/browser/vr/model/model.h"
+#include "chrome/browser/vr/ui_element_renderer.h"
+#include "chrome/browser/vr/ui_scene_constants.h"
+
+namespace vr {
+
+Laser::Laser(Model* model) : model_(model) {
+  set_name(kLaser);
+  set_hit_testable(false);
+  SetVisible(true);
+}
+
+Laser::~Laser() = default;
+
+void Laser::Render(UiElementRenderer* renderer,
+                   const gfx::Transform& model_view_proj_matrix) const {
+  // Find the length of the beam (from hand to target).
+  const float laser_length =
+      std::sqrt(model_->controller.laser_origin.SquaredDistanceTo(
+          ScalePoint(model_->reticle.target_point, kReticleOffset)));
+
+  // Build a beam, originating from the origin.
+  gfx::Transform mat;
+
+  // Move the beam half its height so that its end sits on the origin.
+  mat.matrix().postTranslate(0.0f, 0.5f, 0.0f);
+  mat.matrix().postScale(kLaserWidth, laser_length, 1);
+
+  // Tip back 90 degrees to flat, pointing at the scene.
+  const gfx::Quaternion quat(gfx::Vector3dF(1.0f, 0.0f, 0.0f),
+                             -base::kPiDouble / 2);
+  gfx::Transform rotation_mat(quat);
+  mat = rotation_mat * mat;
+
+  const gfx::Vector3dF beam_direction =
+      model_->reticle.target_point - model_->controller.laser_origin;
+
+  gfx::Transform beam_direction_mat(
+      gfx::Quaternion(gfx::Vector3dF(0.0f, 0.0f, -1.0f), beam_direction));
+
+  // Render multiple faces to make the laser appear cylindrical.
+  const int faces = 4;
+  gfx::Transform face_transform;
+  gfx::Transform transform;
+  for (int i = 0; i < faces; i++) {
+    // Rotate around Z.
+    const float angle = base::kPiFloat * 2 * i / faces;
+    const gfx::Quaternion rot({0.0f, 0.0f, 1.0f}, angle);
+    face_transform = beam_direction_mat * gfx::Transform(rot) * mat;
+
+    // Move the beam origin to the hand.
+    face_transform.matrix().postTranslate(model_->controller.laser_origin.x(),
+                                          model_->controller.laser_origin.y(),
+                                          model_->controller.laser_origin.z());
+    transform = model_view_proj_matrix * face_transform;
+    renderer->DrawLaser(computed_opacity(), transform);
+  }
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/elements/laser.h b/chrome/browser/vr/elements/laser.h
new file mode 100644
index 0000000..fbfbdea
--- /dev/null
+++ b/chrome/browser/vr/elements/laser.h
@@ -0,0 +1,36 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_ELEMENTS_LASER_H_
+#define CHROME_BROWSER_VR_ELEMENTS_LASER_H_
+
+#include "chrome/browser/vr/elements/ui_element.h"
+#include "ui/gfx/geometry/point3_f.h"
+
+namespace vr {
+
+struct Model;
+
+class Laser : public UiElement {
+ public:
+  explicit Laser(Model* model);
+  ~Laser() override;
+
+ private:
+  void Render(UiElementRenderer* renderer,
+              const gfx::Transform& model_view_proj_matrix) const final;
+
+  // Since the laser needs to render in response to model changes that occur
+  // after the scene update (i.e., after input), we cannot rely on the usual
+  // data binding flow since that would result in a frame of latency. Opacity
+  // changes, however, are not latency sensitive and are bound in the usual way
+  // (they also do not update due to input).
+  Model* model_;
+
+  DISALLOW_COPY_AND_ASSIGN(Laser);
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_ELEMENTS_LASER_H_
diff --git a/chrome/browser/vr/elements/rect.cc b/chrome/browser/vr/elements/rect.cc
index 3199c254..1c4ad40c 100644
--- a/chrome/browser/vr/elements/rect.cc
+++ b/chrome/browser/vr/elements/rect.cc
@@ -25,7 +25,7 @@
 
 void Rect::SetEdgeColor(SkColor color) {
   animation_player().TransitionColorTo(last_frame_time(), FOREGROUND_COLOR,
-                                       center_color_, color);
+                                       edge_color_, color);
 }
 
 void Rect::NotifyClientColorAnimated(SkColor color,
diff --git a/chrome/browser/vr/elements/rect_unittest.cc b/chrome/browser/vr/elements/rect_unittest.cc
new file mode 100644
index 0000000..6eb5969
--- /dev/null
+++ b/chrome/browser/vr/elements/rect_unittest.cc
@@ -0,0 +1,55 @@
+// 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 "chrome/browser/vr/elements/rect.h"
+
+#include "base/memory/ptr_util.h"
+#include "chrome/browser/vr/target_property.h"
+#include "chrome/browser/vr/test/animation_utils.h"
+#include "chrome/browser/vr/test/constants.h"
+#include "chrome/browser/vr/ui_scene.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/skia/include/core/SkColor.h"
+
+namespace vr {
+
+TEST(Rect, SetColorCorrectly) {
+  auto rect = base::MakeUnique<Rect>();
+
+  EXPECT_NE(SK_ColorCYAN, rect->edge_color());
+  EXPECT_NE(SK_ColorCYAN, rect->center_color());
+
+  rect->SetColor(SK_ColorCYAN);
+  EXPECT_EQ(SK_ColorCYAN, rect->edge_color());
+  EXPECT_EQ(SK_ColorCYAN, rect->center_color());
+
+  rect->SetEdgeColor(SK_ColorRED);
+  rect->SetCenterColor(SK_ColorBLUE);
+
+  EXPECT_EQ(SK_ColorRED, rect->edge_color());
+  EXPECT_EQ(SK_ColorBLUE, rect->center_color());
+}
+
+TEST(Rect, AnimateColorCorrectly) {
+  UiScene scene;
+  auto element = base::MakeUnique<Rect>();
+  Rect* rect = element.get();
+  scene.AddUiElement(kRoot, std::move(element));
+
+  rect->SetEdgeColor(SK_ColorRED);
+  rect->SetCenterColor(SK_ColorBLUE);
+
+  rect->SetTransitionedProperties({BACKGROUND_COLOR, FOREGROUND_COLOR});
+  rect->SetColor(SK_ColorBLACK);
+
+  scene.OnBeginFrame(MsToTicks(1), kForwardVector);
+  EXPECT_EQ(SK_ColorRED, rect->edge_color());
+  EXPECT_EQ(SK_ColorBLUE, rect->center_color());
+
+  scene.OnBeginFrame(MsToTicks(5000), kForwardVector);
+  EXPECT_EQ(SK_ColorBLACK, rect->edge_color());
+  EXPECT_EQ(SK_ColorBLACK, rect->center_color());
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/elements/reticle.cc b/chrome/browser/vr/elements/reticle.cc
new file mode 100644
index 0000000..f682d6c
--- /dev/null
+++ b/chrome/browser/vr/elements/reticle.cc
@@ -0,0 +1,59 @@
+// 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 "chrome/browser/vr/elements/reticle.h"
+
+#include "base/numerics/math_constants.h"
+#include "chrome/browser/vr/model/model.h"
+#include "chrome/browser/vr/ui_element_renderer.h"
+#include "chrome/browser/vr/ui_scene.h"
+#include "chrome/browser/vr/ui_scene_constants.h"
+
+namespace vr {
+
+Reticle::Reticle(UiScene* scene, Model* model) : scene_(scene), model_(model) {
+  set_name(kReticle);
+  set_hit_testable(false);
+  SetVisible(true);
+}
+
+Reticle::~Reticle() = default;
+
+void Reticle::Render(UiElementRenderer* renderer,
+                     const gfx::Transform& model_view_proj_matrix) const {
+  // Scale the reticle to have a fixed FOV size at any distance.
+  const float eye_to_target =
+      std::sqrt(model_->reticle.target_point.SquaredDistanceTo(kOrigin));
+
+  gfx::Transform mat;
+  mat.Scale3d(kReticleWidth * eye_to_target, kReticleHeight * eye_to_target, 1);
+
+  gfx::Quaternion rotation;
+
+  UiElement* target =
+      scene_->GetUiElementById(model_->reticle.target_element_id);
+
+  if (target) {
+    // Make the reticle planar to the element it's hitting.
+    rotation = gfx::Quaternion(gfx::Vector3dF(0.0f, 0.0f, -1.0f),
+                               -target->GetNormal());
+  } else {
+    // Rotate the reticle to directly face the eyes.
+    rotation = gfx::Quaternion(gfx::Vector3dF(0.0f, 0.0f, -1.0f),
+                               model_->reticle.target_point - kOrigin);
+  }
+  gfx::Transform rotation_mat(rotation);
+  mat = rotation_mat * mat;
+
+  gfx::Point3F target_point =
+      ScalePoint(model_->reticle.target_point, kReticleOffset);
+  // Place the pointer slightly in front of the plane intersection point.
+  mat.matrix().postTranslate(target_point.x(), target_point.y(),
+                             target_point.z());
+
+  gfx::Transform transform = model_view_proj_matrix * mat;
+  renderer->DrawReticle(computed_opacity(), transform);
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/elements/reticle.h b/chrome/browser/vr/elements/reticle.h
new file mode 100644
index 0000000..144c506
--- /dev/null
+++ b/chrome/browser/vr/elements/reticle.h
@@ -0,0 +1,45 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_ELEMENTS_RETICLE_H_
+#define CHROME_BROWSER_VR_ELEMENTS_RETICLE_H_
+
+#include "chrome/browser/vr/elements/ui_element.h"
+#include "ui/gfx/geometry/point3_f.h"
+
+namespace vr {
+
+class UiScene;
+struct Model;
+
+class Reticle : public UiElement {
+ public:
+  Reticle(UiScene* scene, Model* model);
+  ~Reticle() override;
+
+ private:
+  void Render(UiElementRenderer* renderer,
+              const gfx::Transform& model_view_proj_matrix) const final;
+
+  gfx::Point3F origin_;
+  gfx::Point3F target_;
+
+  // This is used to convert the target id into a UiElement instance. We are not
+  // permitted to retain pointers to UiElements since they may be destructed,
+  // but the scene itself is constant, so we will look up our elements on the
+  // fly.
+  UiScene* scene_;
+
+  // Unlike other UiElements which bind their values form the model, the reticle
+  // must derive values from the model late in the pipeline after the scene has
+  // fully updated its geometry. We therefore retain a pointer to the model and
+  // make use of it in |Render|.
+  Model* model_;
+
+  DISALLOW_COPY_AND_ASSIGN(Reticle);
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_ELEMENTS_RETICLE_H_
diff --git a/chrome/browser/vr/elements/ui_element_name.cc b/chrome/browser/vr/elements/ui_element_name.cc
new file mode 100644
index 0000000..2ee8b44a
--- /dev/null
+++ b/chrome/browser/vr/elements/ui_element_name.cc
@@ -0,0 +1,92 @@
+// 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 "chrome/browser/vr/elements/ui_element_name.h"
+
+#include "base/macros.h"
+
+namespace vr {
+
+namespace {
+
+static const char* g_ui_element_name_strings[] = {
+    "kNone",
+    "kRoot",
+    "k2dBrowsingRoot",
+    "k2dBrowsingBackground",
+    "k2dBrowsingForeground",
+    "k2dBrowsingContentGroup",
+    "k2dBrowsingViewportAwareRoot",
+    "kWebVrRoot",
+    "kWebVrViewportAwareRoot",
+    "kContentQuad",
+    "kControllerGroup",
+    "kLaser",
+    "kController",
+    "kReticle",
+    "kBackplane",
+    "kCeiling",
+    "kFloor",
+    "kUrlBar",
+    "kIndicatorLayout",
+    "kAudioCaptureIndicator",
+    "kVideoCaptureIndicator",
+    "kScreenCaptureIndicator",
+    "kLocationAccessIndicator",
+    "kBluetoothConnectedIndicator",
+    "kLoadingIndicator",
+    "kLoadingIndicatorForeground",
+    "kCloseButton",
+    "kVoiceSearchButton",
+    "kScreenDimmer",
+    "kExitWarning",
+    "kExitPrompt",
+    "kExitPromptBackplane",
+    "kWebVrUrlToastTransientParent",
+    "kWebVrUrlToast",
+    "kExclusiveScreenToastTransientParent",
+    "kExclusiveScreenToast",
+    "kExclusiveScreenToastViewportAwareTransientParent",
+    "kExclusiveScreenToastViewportAware",
+    "kSplashScreenRoot",
+    "kSplashScreenTransientParent",
+    "kSplashScreenViewportAwareRoot",
+    "kSplashScreenText",
+    "kSplashScreenBackground",
+    "kBackgroundFront",
+    "kBackgroundLeft",
+    "kBackgroundBack",
+    "kBackgroundRight",
+    "kBackgroundTop",
+    "kBackgroundBottom",
+    "kUnderDevelopmentNotice",
+    "kWebVrTimeoutSpinner",
+    "kWebVrTimeoutSpinnerBackground",
+    "kWebVrTimeoutMessage",
+    "kWebVrTimeoutMessageLayout",
+    "kWebVrTimeoutMessageIcon",
+    "kWebVrTimeoutMessageText",
+    "kWebVrTimeoutMessageButton",
+    "kWebVrTimeoutMessageButtonText",
+    "kSpeechRecognitionPrompt",
+    "kSpeechRecognitionPromptGrowingCircle",
+    "kSpeechRecognitionPromptInnerCircle",
+    "kSpeechRecognitionPromptMicrophoneIcon",
+    "kSpeechRecognitionPromptBackplane",
+    "kSuggestionLayout",
+    "kNumUiElementNames",
+};
+
+static_assert(
+    kNumUiElementNames + 1 == arraysize(g_ui_element_name_strings),
+    "Mismatch between the kUiElementName enum and the corresponding array "
+    "of strings.");
+
+}  // namespace
+
+std::string UiElementNameToString(UiElementName name) {
+  return g_ui_element_name_strings[name];
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/elements/ui_element_name.h b/chrome/browser/vr/elements/ui_element_name.h
index 70488676..729a7fa 100644
--- a/chrome/browser/vr/elements/ui_element_name.h
+++ b/chrome/browser/vr/elements/ui_element_name.h
@@ -5,6 +5,8 @@
 #ifndef CHROME_BROWSER_VR_ELEMENTS_UI_ELEMENT_NAME_H_
 #define CHROME_BROWSER_VR_ELEMENTS_UI_ELEMENT_NAME_H_
 
+#include <string>
+
 namespace vr {
 
 // These identifiers serve as stable, semantic identifiers for UI elements.
@@ -19,6 +21,10 @@
   kWebVrRoot,
   kWebVrViewportAwareRoot,
   kContentQuad,
+  kControllerGroup,
+  kLaser,
+  kController,
+  kReticle,
   kBackplane,
   kCeiling,
   kFloor,
@@ -69,8 +75,13 @@
   kSpeechRecognitionPromptMicrophoneIcon,
   kSpeechRecognitionPromptBackplane,
   kSuggestionLayout,
+
+  // This must be last.
+  kNumUiElementNames,
 };
 
+std::string UiElementNameToString(UiElementName name);
+
 }  // namespace vr
 
 #endif  // CHROME_BROWSER_VR_ELEMENTS_UI_ELEMENT_NAME_H_
diff --git a/chrome/browser/vr/model/controller_model.h b/chrome/browser/vr/model/controller_model.h
new file mode 100644
index 0000000..0d8dd11c
--- /dev/null
+++ b/chrome/browser/vr/model/controller_model.h
@@ -0,0 +1,30 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_MODEL_CONTROLLER_MODEL_H_
+#define CHROME_BROWSER_VR_MODEL_CONTROLLER_MODEL_H_
+
+#include "chrome/browser/vr/ui_input_manager.h"
+#include "ui/gfx/geometry/point3_f.h"
+#include "ui/gfx/transform.h"
+
+namespace vr {
+
+// The ControllerModel encapsulates generic controller information read from the
+// platform-specific VR subsystem (e.g., GVR). It is used by both the
+// UiInputManager (for generating gestures), and by the UI for rendering the
+// controller.
+struct ControllerModel {
+  gfx::Transform transform;
+  gfx::Vector3dF laser_direction;
+  gfx::Point3F laser_origin;
+  UiInputManager::ButtonState touchpad_button_state = UiInputManager::UP;
+  UiInputManager::ButtonState app_button_state = UiInputManager::UP;
+  UiInputManager::ButtonState home_button_state = UiInputManager::UP;
+  float opacity = 1.0f;
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_MODEL_CONTROLLER_MODEL_H_
diff --git a/chrome/browser/vr/model/model.h b/chrome/browser/vr/model/model.h
index 5260d17..07df52e6de 100644
--- a/chrome/browser/vr/model/model.h
+++ b/chrome/browser/vr/model/model.h
@@ -5,26 +5,13 @@
 #ifndef CHROME_BROWSER_VR_MODEL_MODEL_H_
 #define CHROME_BROWSER_VR_MODEL_MODEL_H_
 
+#include "chrome/browser/vr/model/controller_model.h"
 #include "chrome/browser/vr/model/omnibox_suggestions.h"
+#include "chrome/browser/vr/model/reticle_model.h"
+#include "chrome/browser/vr/model/web_vr_timeout_state.h"
 
 namespace vr {
 
-// As we wait for WebVR frames, we may pass through the following states.
-enum WebVrTimeoutState {
-  // We are not awaiting a WebVR frame.
-  kWebVrNoTimeoutPending,
-  kWebVrAwaitingFirstFrame,
-  // We are awaiting a WebVR frame, and we will soon exceed the amount of time
-  // that we're willing to wait. In this state, it could be appropriate to show
-  // an affordance to the user to let them know that WebVR is delayed (eg, this
-  // would be when we might show a spinner or progress bar).
-  kWebVrTimeoutImminent,
-  // In this case the time allotted for waiting for the first WebVR frame has
-  // been entirely exceeded. This would, for example, be an appropriate time to
-  // show "sad tab" UI to allow the user to bail on the WebVR content.
-  kWebVrTimedOut,
-};
-
 struct Model {
   Model();
   ~Model();
@@ -38,6 +25,9 @@
   bool recognizing_speech = false;
   int speech_recognition_state = 0;
 
+  ControllerModel controller;
+  ReticleModel reticle;
+
   std::vector<OmniboxSuggestion> omnibox_suggestions;
 };
 
diff --git a/chrome/browser/vr/model/reticle_model.h b/chrome/browser/vr/model/reticle_model.h
new file mode 100644
index 0000000..768ae3e
--- /dev/null
+++ b/chrome/browser/vr/model/reticle_model.h
@@ -0,0 +1,24 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_MODEL_RETICLE_MODEL_H_
+#define CHROME_BROWSER_VR_MODEL_RETICLE_MODEL_H_
+
+#include "ui/gfx/geometry/point3_f.h"
+
+namespace vr {
+
+// The ReticleModel contains information related to the target of the
+// controller's laser. It is computed by the UiInputManager and is used by the
+// input manager in the production of gestures as well as by the Reticle element
+// in the scene.
+struct ReticleModel {
+  gfx::Point3F target_point;
+  gfx::PointF target_local_point;
+  int target_element_id = 0;
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_MODEL_RETICLE_MODEL_H_
diff --git a/chrome/browser/vr/model/web_vr_timeout_state.h b/chrome/browser/vr/model/web_vr_timeout_state.h
new file mode 100644
index 0000000..84f756a3
--- /dev/null
+++ b/chrome/browser/vr/model/web_vr_timeout_state.h
@@ -0,0 +1,28 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_MODEL_WEB_VR_TIMEOUT_STATE_H_
+#define CHROME_BROWSER_VR_MODEL_WEB_VR_TIMEOUT_STATE_H_
+
+namespace vr {
+
+// As we wait for WebVR frames, we may pass through the following states.
+enum WebVrTimeoutState {
+  // We are not awaiting a WebVR frame.
+  kWebVrNoTimeoutPending,
+  kWebVrAwaitingFirstFrame,
+  // We are awaiting a WebVR frame, and we will soon exceed the amount of time
+  // that we're willing to wait. In this state, it could be appropriate to show
+  // an affordance to the user to let them know that WebVR is delayed (eg, this
+  // would be when we might show a spinner or progress bar).
+  kWebVrTimeoutImminent,
+  // In this case the time allotted for waiting for the first WebVR frame has
+  // been entirely exceeded. This would, for example, be an appropriate time to
+  // show "sad tab" UI to allow the user to bail on the WebVR content.
+  kWebVrTimedOut,
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_MODEL_WEB_VR_TIMEOUT_STATE_H_
diff --git a/chrome/browser/vr/test/fake_ui_element_renderer.cc b/chrome/browser/vr/test/fake_ui_element_renderer.cc
index e5b46878..1c84c40 100644
--- a/chrome/browser/vr/test/fake_ui_element_renderer.cc
+++ b/chrome/browser/vr/test/fake_ui_element_renderer.cc
@@ -45,4 +45,25 @@
   called_ = true;
 }
 
+void FakeUiElementRenderer::DrawController(
+    ControllerMesh::State state,
+    float opacity,
+    const gfx::Transform& view_proj_matrix) {
+  opacity_ = opacity;
+  called_ = true;
+}
+
+void FakeUiElementRenderer::DrawLaser(float opacity,
+                                      const gfx::Transform& view_proj_matrix) {
+  opacity_ = opacity;
+  called_ = true;
+}
+
+void FakeUiElementRenderer::DrawReticle(
+    float opacity,
+    const gfx::Transform& view_proj_matrix) {
+  opacity_ = opacity;
+  called_ = true;
+}
+
 }  // namespace vr
diff --git a/chrome/browser/vr/test/fake_ui_element_renderer.h b/chrome/browser/vr/test/fake_ui_element_renderer.h
index cc0a5ae..279cb110 100644
--- a/chrome/browser/vr/test/fake_ui_element_renderer.h
+++ b/chrome/browser/vr/test/fake_ui_element_renderer.h
@@ -40,6 +40,16 @@
                             int gridline_count,
                             float opacity) override;
 
+  void DrawController(ControllerMesh::State state,
+                      float opacity,
+                      const gfx::Transform& view_proj_matrix) override;
+
+  void DrawLaser(float opacity,
+                 const gfx::Transform& view_proj_matrix) override;
+
+  void DrawReticle(float opacity,
+                   const gfx::Transform& view_proj_matrix) override;
+
  private:
   float opacity_ = -1.f;
   float called_ = false;
diff --git a/chrome/browser/vr/test/ui_pixel_test.cc b/chrome/browser/vr/test/ui_pixel_test.cc
index 7771feb..366b1a8 100644
--- a/chrome/browser/vr/test/ui_pixel_test.cc
+++ b/chrome/browser/vr/test/ui_pixel_test.cc
@@ -7,6 +7,7 @@
 #include "base/memory/ptr_util.h"
 #include "build/build_config.h"
 #include "chrome/browser/vr/browser_ui_interface.h"
+#include "chrome/browser/vr/model/model.h"
 #include "chrome/browser/vr/test/constants.h"
 #include "chrome/browser/vr/ui_browser_interface.h"
 #include "chrome/browser/vr/ui_input_manager.h"
@@ -69,13 +70,14 @@
                          const gfx::Transform& controller_transform,
                          const gfx::Transform& view_matrix,
                          const gfx::Transform& proj_matrix) {
-  ControllerInfo controller_info;
-  controller_info.transform = controller_transform;
-  controller_info.opacity = controller_opacity;
-  controller_info.laser_origin = laser_origin;
-  controller_info.touchpad_button_state = button_state;
-  controller_info.app_button_state = UiInputManager::ButtonState::UP;
-  controller_info.home_button_state = UiInputManager::ButtonState::UP;
+  ControllerModel controller_model;
+  controller_model.laser_direction = kForwardVector;
+  controller_model.transform = controller_transform;
+  controller_model.opacity = controller_opacity;
+  controller_model.laser_origin = laser_origin;
+  controller_model.touchpad_button_state = button_state;
+  controller_model.app_button_state = UiInputManager::ButtonState::UP;
+  controller_model.home_button_state = UiInputManager::ButtonState::UP;
   RenderInfo render_info;
   render_info.head_pose = view_matrix;
   render_info.left_eye_info.view_matrix = view_matrix;
@@ -88,17 +90,16 @@
   render_info.right_eye_info.viewport = {0, 0, 0, 0};
 
   GestureList gesture_list;
+  ReticleModel reticle_model;
   EXPECT_TRUE(ui_->scene()->OnBeginFrame(
       base::TimeTicks(),
       gfx::Vector3dF(-render_info.head_pose.matrix().get(2, 0),
                      -render_info.head_pose.matrix().get(2, 1),
                      -render_info.head_pose.matrix().get(2, 2))));
-  ui_->input_manager()->HandleInput(
-      gfx::Vector3dF(0.0f, 0.0f, -1.0f), controller_info.laser_origin,
-      controller_info.touchpad_button_state, &gesture_list,
-      &controller_info.target_point, &controller_info.reticle_render_target);
-
-  ui_->ui_renderer()->Draw(render_info, controller_info);
+  ui_->input_manager()->HandleInput(controller_model, &reticle_model,
+                                    &gesture_list);
+  ui_->OnControllerUpdated(controller_model, reticle_model);
+  ui_->ui_renderer()->Draw(render_info);
 
   // We produce GL errors while rendering. Clear them all so that we can check
   // for errors of subsequent calls.
diff --git a/chrome/browser/vr/testapp/vr_test_context.cc b/chrome/browser/vr/testapp/vr_test_context.cc
index 3cd5fdc..e9913a4 100644
--- a/chrome/browser/vr/testapp/vr_test_context.cc
+++ b/chrome/browser/vr/testapp/vr_test_context.cc
@@ -10,6 +10,7 @@
 #include "base/path_service.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/vr/controller_mesh.h"
 #include "chrome/browser/vr/model/model.h"
 #include "chrome/browser/vr/model/omnibox_suggestions.h"
 #include "chrome/browser/vr/test/constants.h"
@@ -45,15 +46,12 @@
 constexpr float kViewScaleAdjustmentFactor = 0.2f;
 
 constexpr gfx::Point3F kLaserOrigin = {0.5f, -0.5f, 0.f};
+constexpr gfx::Vector3dF kLaserLocalOffset = {0.f, -0.0075f, -0.05f};
+constexpr float kControllerScaleFactor = 1.5f;
 
 }  // namespace
 
-VrTestContext::VrTestContext()
-    : controller_info_(base::MakeUnique<ControllerInfo>()),
-      view_scale_factor_(kDefaultViewScaleFactor) {
-  controller_info_->reticle_render_target = nullptr;
-  controller_info_->laser_origin = kLaserOrigin;
-
+VrTestContext::VrTestContext() : view_scale_factor_(kDefaultViewScaleFactor) {
   base::FilePath pak_path;
   PathService::Get(base::DIR_MODULE, &pak_path);
   ui::ResourceBundle::InitSharedInstanceWithPakPath(
@@ -97,7 +95,7 @@
   // Update the render position of all UI elements (including desktop).
   ui_->scene()->OnBeginFrame(current_time, kForwardVector);
   ui_->OnProjMatrixChanged(render_info.left_eye_info.proj_matrix);
-  ui_->ui_renderer()->Draw(render_info, *controller_info_);
+  ui_->ui_renderer()->Draw(render_info);
 
   // TODO(cjgrant): Render viewport-aware elements.
 }
@@ -182,12 +180,27 @@
   gfx::Vector3dF controller_direction = {0, 0, -1.f};
   beam_transform.TransformVector(&controller_direction);
 
+  gfx::Vector3dF local_offset = kLaserLocalOffset;
+  beam_transform.TransformVector(&local_offset);
+  local_offset.Scale(kControllerScaleFactor);
+
+  ControllerModel controller_model;
+  controller_model.laser_direction = controller_direction;
+  controller_model.laser_origin = kLaserOrigin + local_offset;
+
+  controller_model.transform.Translate3d(kLaserOrigin.x(), kLaserOrigin.y(),
+                                         kLaserOrigin.z());
+
+  controller_model.transform.Scale3d(
+      kControllerScaleFactor, kControllerScaleFactor, kControllerScaleFactor);
+  controller_model.transform.RotateAboutYAxis(head_angle_y_degrees_ + delta_y);
+  controller_model.transform.RotateAboutXAxis(head_angle_x_degrees_ + delta_x);
+
   GestureList gesture_list;
-  ui_->input_manager()->HandleInput(
-      controller_direction, controller_info_->laser_origin,
-      controller_info_->touchpad_button_state, &gesture_list,
-      &controller_info_->target_point,
-      &controller_info_->reticle_render_target);
+  ReticleModel reticle_model;
+  ui_->input_manager()->HandleInput(controller_model, &reticle_model,
+                                    &gesture_list);
+  ui_->OnControllerUpdated(controller_model, reticle_model);
 }
 
 void VrTestContext::OnGlInitialized(const gfx::Size& window_size) {
@@ -196,6 +209,9 @@
   window_size_ = window_size;
   ui_->OnGlInitialized(content_texture_id,
                        UiElementRenderer::kTextureLocationLocal);
+
+  ui_->vr_shell_renderer()->GetControllerRenderer()->SetUp(
+      ControllerMesh::LoadFromResources());
 }
 
 unsigned int VrTestContext::CreateFakeContentTexture() {
diff --git a/chrome/browser/vr/testapp/vr_test_context.h b/chrome/browser/vr/testapp/vr_test_context.h
index 5b311b4..428f5367 100644
--- a/chrome/browser/vr/testapp/vr_test_context.h
+++ b/chrome/browser/vr/testapp/vr_test_context.h
@@ -19,7 +19,6 @@
 
 namespace vr {
 
-struct ControllerInfo;
 class Ui;
 
 // This class provides a home for the VR UI in a testapp context, and
@@ -69,8 +68,6 @@
   std::unique_ptr<Ui> ui_;
   gfx::Size window_size_;
 
-  std::unique_ptr<ControllerInfo> controller_info_;
-
   gfx::Transform head_pose_;
   float head_angle_x_degrees_ = 0;
   float head_angle_y_degrees_ = 0;
diff --git a/chrome/browser/vr/ui.cc b/chrome/browser/vr/ui.cc
index c930f86e..b8e0340 100644
--- a/chrome/browser/vr/ui.cc
+++ b/chrome/browser/vr/ui.cc
@@ -132,6 +132,12 @@
   scene_manager_->OnAppButtonGesturePerformed(direction);
 }
 
+void Ui::OnControllerUpdated(const ControllerModel& controller_model,
+                             const ReticleModel& reticle_model) {
+  model_->controller = controller_model;
+  model_->reticle = reticle_model;
+}
+
 void Ui::OnProjMatrixChanged(const gfx::Transform& proj_matrix) {
   scene_manager_->OnProjMatrixChanged(proj_matrix);
 }
diff --git a/chrome/browser/vr/ui.h b/chrome/browser/vr/ui.h
index e38d9f4..4524a4f 100644
--- a/chrome/browser/vr/ui.h
+++ b/chrome/browser/vr/ui.h
@@ -77,6 +77,8 @@
       UiElementRenderer::TextureLocation content_location) override;
   void OnAppButtonClicked() override;
   void OnAppButtonGesturePerformed(UiInterface::Direction direction) override;
+  void OnControllerUpdated(const ControllerModel& controller_model,
+                           const ReticleModel& reticle_model) override;
   void OnProjMatrixChanged(const gfx::Transform& proj_matrix) override;
   void OnWebVrFrameAvailable() override;
   void OnWebVrTimedOut() override;
diff --git a/chrome/browser/vr/ui_element_renderer.h b/chrome/browser/vr/ui_element_renderer.h
index 08f10ee..f110bc9 100644
--- a/chrome/browser/vr/ui_element_renderer.h
+++ b/chrome/browser/vr/ui_element_renderer.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_VR_UI_ELEMENT_RENDERER_H_
 #define CHROME_BROWSER_VR_UI_ELEMENT_RENDERER_H_
 
+#include "chrome/browser/vr/controller_mesh.h"
 #include "third_party/skia/include/core/SkColor.h"
 
 namespace gfx {
@@ -46,6 +47,17 @@
                                     const SkColor grid_color,
                                     int gridline_count,
                                     float opacity) = 0;
+
+  // TODO(crbug/779108) This presumes a Daydream controller.
+  virtual void DrawController(ControllerMesh::State state,
+                              float opacity,
+                              const gfx::Transform& view_proj_matrix) = 0;
+
+  virtual void DrawLaser(float opacity,
+                         const gfx::Transform& view_proj_matrix) = 0;
+
+  virtual void DrawReticle(float opacity,
+                           const gfx::Transform& view_proj_matrix) = 0;
 };
 
 }  // namespace vr
diff --git a/chrome/browser/vr/ui_input_manager.cc b/chrome/browser/vr/ui_input_manager.cc
index 6e89523..9fd474882 100644
--- a/chrome/browser/vr/ui_input_manager.cc
+++ b/chrome/browser/vr/ui_input_manager.cc
@@ -8,6 +8,8 @@
 
 #include "base/macros.h"
 #include "chrome/browser/vr/elements/ui_element.h"
+#include "chrome/browser/vr/model/controller_model.h"
+#include "chrome/browser/vr/model/reticle_model.h"
 #include "chrome/browser/vr/ui_scene.h"
 // TODO(tiborg): Remove include once we use a generic type to pass scroll/fling
 // gestures.
@@ -70,15 +72,12 @@
 }
 
 void HitTestElements(UiElement* element,
+                     ReticleModel* reticle_model,
                      gfx::Vector3dF* out_eye_to_target,
-                     float* out_closest_element_distance,
-                     gfx::Point3F* out_target_point,
-                     UiElement** out_target_element,
-                     gfx::PointF* out_target_local_point) {
+                     float* out_closest_element_distance) {
   for (auto& child : element->children()) {
-    HitTestElements(child.get(), out_eye_to_target,
-                    out_closest_element_distance, out_target_point,
-                    out_target_element, out_target_local_point);
+    HitTestElements(child.get(), reticle_model, out_eye_to_target,
+                    out_closest_element_distance);
   }
 
   if (!element->IsHitTestable()) {
@@ -98,9 +97,9 @@
   }
 
   *out_closest_element_distance = distance_to_plane;
-  *out_target_point = plane_intersection_point;
-  *out_target_element = element;
-  *out_target_local_point = local_point;
+  reticle_model->target_point = plane_intersection_point;
+  reticle_model->target_element_id = element->id();
+  reticle_model->target_local_point = local_point;
 }
 
 }  // namespace
@@ -109,18 +108,13 @@
 
 UiInputManager::~UiInputManager() {}
 
-void UiInputManager::HandleInput(const gfx::Vector3dF& laser_direction,
-                                 const gfx::Point3F& laser_origin,
-                                 ButtonState button_state,
-                                 GestureList* gesture_list,
-                                 gfx::Point3F* out_target_point,
-                                 UiElement** out_reticle_render_target) {
-  gfx::PointF target_local_point(kInvalidTargetPoint);
+void UiInputManager::HandleInput(const ControllerModel& controller_model,
+                                 ReticleModel* reticle_model,
+                                 GestureList* gesture_list) {
   gfx::Vector3dF eye_to_target;
-  *out_reticle_render_target = nullptr;
-  GetVisualTargetElement(laser_direction, laser_origin, &eye_to_target,
-                         out_target_point, out_reticle_render_target,
-                         &target_local_point);
+  reticle_model->target_element_id = 0;
+  reticle_model->target_local_point = kInvalidTargetPoint;
+  GetVisualTargetElement(controller_model, reticle_model, &eye_to_target);
 
   UiElement* target_element = nullptr;
   // TODO(vollick): this should be replaced with a formal notion of input
@@ -132,16 +126,16 @@
       float distance_to_plane;
       if (!GetTargetLocalPoint(eye_to_target, *target_element,
                                2 * scene_->background_distance(),
-                               &target_local_point, &plane_intersection_point,
-                               &distance_to_plane)) {
-        target_local_point = kInvalidTargetPoint;
+                               &reticle_model->target_local_point,
+                               &plane_intersection_point, &distance_to_plane)) {
+        reticle_model->target_local_point = kInvalidTargetPoint;
       }
     }
   } else if (!in_scroll_ && !in_click_) {
     // TODO(vollick): support multiple dispatch. We may want to, for example,
     // dispatch raw events to several elements we hit (imagine nested horizontal
     // and vertical scrollers). Currently, we only dispatch to one "winner".
-    target_element = *out_reticle_render_target;
+    target_element = scene_->GetUiElementById(reticle_model->target_element_id);
     if (target_element && IsScrollEvent(*gesture_list)) {
       UiElement* ancestor = target_element;
       while (!ancestor->scrollable() && ancestor->parent()) {
@@ -153,12 +147,14 @@
     }
   }
 
-  SendFlingCancel(gesture_list, target_local_point);
+  SendFlingCancel(gesture_list, reticle_model->target_local_point);
   // For simplicity, don't allow scrolling while clicking until we need to.
   if (!in_click_) {
-    SendScrollEnd(gesture_list, target_local_point, button_state);
-    if (!SendScrollBegin(target_element, gesture_list, target_local_point)) {
-      SendScrollUpdate(gesture_list, target_local_point);
+    SendScrollEnd(gesture_list, reticle_model->target_local_point,
+                  controller_model.touchpad_button_state);
+    if (!SendScrollBegin(target_element, gesture_list,
+                         reticle_model->target_local_point)) {
+      SendScrollUpdate(gesture_list, reticle_model->target_local_point);
     }
   }
 
@@ -168,18 +164,22 @@
     return;
   }
   SendHoverLeave(target_element);
-  if (!SendHoverEnter(target_element, target_local_point)) {
-    SendHoverMove(target_local_point);
+  if (!SendHoverEnter(target_element, reticle_model->target_local_point)) {
+    SendHoverMove(reticle_model->target_local_point);
   }
-  SendButtonDown(target_element, target_local_point, button_state);
-  if (SendButtonUp(target_element, target_local_point, button_state)) {
-    target_element = *out_reticle_render_target;
+  SendButtonDown(target_element, reticle_model->target_local_point,
+                 controller_model.touchpad_button_state);
+  if (SendButtonUp(target_element, reticle_model->target_local_point,
+                   controller_model.touchpad_button_state)) {
+    target_element = scene_->GetUiElementById(reticle_model->target_element_id);
     SendHoverLeave(target_element);
-    SendHoverEnter(target_element, target_local_point);
+    SendHoverEnter(target_element, reticle_model->target_local_point);
   }
 
   previous_button_state_ =
-      (button_state == ButtonState::CLICKED) ? ButtonState::UP : button_state;
+      (controller_model.touchpad_button_state == ButtonState::CLICKED)
+          ? ButtonState::UP
+          : controller_model.touchpad_button_state;
 }
 
 void UiInputManager::SendFlingCancel(GestureList* gesture_list,
@@ -374,12 +374,9 @@
 }
 
 void UiInputManager::GetVisualTargetElement(
-    const gfx::Vector3dF& laser_direction,
-    const gfx::Point3F& laser_origin,
-    gfx::Vector3dF* out_eye_to_target,
-    gfx::Point3F* out_target_point,
-    UiElement** out_target_element,
-    gfx::PointF* out_target_local_point) const {
+    const ControllerModel& controller_model,
+    ReticleModel* reticle_model,
+    gfx::Vector3dF* out_eye_to_target) const {
   // If we place the reticle based on elements intersecting the controller beam,
   // we can end up with the reticle hiding behind elements, or jumping laterally
   // in the field of view. This is physically correct, but hard to use. For
@@ -395,17 +392,19 @@
   // that the sphere is centered at the controller, rather than the eye, for
   // simplicity.
   float distance = scene_->background_distance();
-  *out_target_point = GetRayPoint(laser_origin, laser_direction, distance);
-  *out_eye_to_target = *out_target_point - kOrigin;
+  reticle_model->target_point =
+      GetRayPoint(controller_model.laser_origin,
+                  controller_model.laser_direction, distance);
+  *out_eye_to_target = reticle_model->target_point - kOrigin;
   out_eye_to_target->GetNormalized(out_eye_to_target);
 
   // Determine which UI element (if any) intersects the line between the eyes
   // and the controller target position.
-  float closest_element_distance = (*out_target_point - kOrigin).Length();
+  float closest_element_distance =
+      (reticle_model->target_point - kOrigin).Length();
 
-  HitTestElements(&scene_->root_element(), out_eye_to_target,
-                  &closest_element_distance, out_target_point,
-                  out_target_element, out_target_local_point);
+  HitTestElements(&scene_->root_element(), reticle_model, out_eye_to_target,
+                  &closest_element_distance);
 }
 
 }  // namespace vr
diff --git a/chrome/browser/vr/ui_input_manager.h b/chrome/browser/vr/ui_input_manager.h
index 2920b95d..55ea0729 100644
--- a/chrome/browser/vr/ui_input_manager.h
+++ b/chrome/browser/vr/ui_input_manager.h
@@ -20,6 +20,8 @@
 
 class UiScene;
 class UiElement;
+struct ControllerModel;
+struct ReticleModel;
 
 using GestureList = std::vector<std::unique_ptr<blink::WebGestureEvent>>;
 
@@ -36,12 +38,9 @@
   explicit UiInputManager(UiScene* scene);
   ~UiInputManager();
   // TODO(tiborg): Use generic gesture type instead of blink::WebGestureEvent.
-  void HandleInput(const gfx::Vector3dF& laser_direction,
-                   const gfx::Point3F& laser_origin,
-                   ButtonState button_state,
-                   GestureList* gesture_list,
-                   gfx::Point3F* out_target_point,
-                   UiElement** out_reticle_render_target);
+  void HandleInput(const ControllerModel& controller_model,
+                   ReticleModel* reticle_model,
+                   GestureList* gesture_list);
 
  private:
   void SendFlingCancel(GestureList* gesture_list,
@@ -63,12 +62,9 @@
   bool SendButtonUp(UiElement* target,
                     const gfx::PointF& target_point,
                     ButtonState button_state);
-  void GetVisualTargetElement(const gfx::Vector3dF& laser_direction,
-                              const gfx::Point3F& laser_origin,
-                              gfx::Vector3dF* out_eye_to_target,
-                              gfx::Point3F* out_target_point,
-                              UiElement** out_target_element,
-                              gfx::PointF* out_target_local_point) const;
+  void GetVisualTargetElement(const ControllerModel& controller_model,
+                              ReticleModel* reticle_model,
+                              gfx::Vector3dF* out_eye_to_target) const;
 
   UiScene* scene_;
   int hover_target_id_ = 0;
diff --git a/chrome/browser/vr/ui_input_manager_unittest.cc b/chrome/browser/vr/ui_input_manager_unittest.cc
index c9786a1b..fb8f2ba 100644
--- a/chrome/browser/vr/ui_input_manager_unittest.cc
+++ b/chrome/browser/vr/ui_input_manager_unittest.cc
@@ -11,6 +11,7 @@
 #include "chrome/browser/vr/content_input_delegate.h"
 #include "chrome/browser/vr/elements/rect.h"
 #include "chrome/browser/vr/elements/ui_element.h"
+#include "chrome/browser/vr/model/model.h"
 #include "chrome/browser/vr/test/animation_utils.h"
 #include "chrome/browser/vr/test/constants.h"
 #include "chrome/browser/vr/test/mock_content_input_delegate.h"
@@ -65,11 +66,14 @@
 
   void HandleInput(const gfx::Vector3dF& laser_direction,
                    UiInputManager::ButtonState button_state) {
+    ControllerModel controller_model;
+    controller_model.laser_direction = laser_direction;
+    controller_model.laser_origin = {0, 0, 0};
+    controller_model.touchpad_button_state = button_state;
+    ReticleModel reticle_model;
     GestureList gesture_list;
-    gfx::Point3F target_point;
-    UiElement* target_element;
-    input_manager_->HandleInput(laser_direction, {0, 0, 0}, button_state,
-                                &gesture_list, &target_point, &target_element);
+    input_manager_->HandleInput(controller_model, &reticle_model,
+                                &gesture_list);
   }
 
  protected:
@@ -99,18 +103,20 @@
   scene_->AddUiElement(kRoot, std::move(element));
   scene_->OnBeginFrame(base::TimeTicks(), kForwardVector);
 
+  ControllerModel controller_model;
+  controller_model.laser_direction = kBackwardVector;
+  controller_model.laser_origin = {0, 0, 0};
+  controller_model.touchpad_button_state = kUp;
+  ReticleModel reticle_model;
   GestureList gesture_list;
-  gfx::Point3F target_point;
-  UiElement* target;
 
-  input_manager_->HandleInput(kBackwardVector, {0, 0, 0}, kUp, &gesture_list,
-                              &target_point, &target);
-  EXPECT_EQ(target, nullptr);
+  input_manager_->HandleInput(controller_model, &reticle_model, &gesture_list);
+  EXPECT_EQ(0, reticle_model.target_element_id);
 
-  input_manager_->HandleInput(kForwardVector, {0, 0, 0}, kUp, &gesture_list,
-                              &target_point, &target);
-  EXPECT_EQ(target, p_element);
-  EXPECT_NEAR(target_point.z(), -1.0, kEpsilon);
+  controller_model.laser_direction = kForwardVector;
+  input_manager_->HandleInput(controller_model, &reticle_model, &gesture_list);
+  EXPECT_EQ(p_element->id(), reticle_model.target_element_id);
+  EXPECT_NEAR(-1.0, reticle_model.target_point.z(), kEpsilon);
 }
 
 // Test hover and click by toggling button state, and directing the controller
@@ -230,25 +236,25 @@
   gfx::Point3F content_quad_center;
   content_quad->world_space_transform().TransformPoint(&content_quad_center);
   gfx::Point3F origin;
+
+  ControllerModel controller_model;
+  controller_model.laser_direction = content_quad_center - origin;
+  controller_model.laser_origin = origin;
+  controller_model.touchpad_button_state = UiInputManager::ButtonState::DOWN;
+  ReticleModel reticle_model;
   GestureList gesture_list;
-  gfx::Point3F out_target_point;
-  UiElement* out_reticle_render_target;
-  input_manager_->HandleInput(content_quad_center - origin, origin,
-                              UiInputManager::ButtonState::DOWN, &gesture_list,
-                              &out_target_point, &out_reticle_render_target);
+  input_manager_->HandleInput(controller_model, &reticle_model, &gesture_list);
 
   // We should have hit the content quad if our math was correct.
-  ASSERT_NE(nullptr, out_reticle_render_target);
-  EXPECT_EQ(UiElementName::kContentQuad, out_reticle_render_target->name());
+  ASSERT_NE(0, reticle_model.target_element_id);
+  EXPECT_EQ(content_quad->id(), reticle_model.target_element_id);
 
   // Unless we suppress content move events during clicks, this will cause us to
   // call OnContentMove on the delegate. We should do this suppression, so we
   // set the expected number of calls to zero.
   EXPECT_CALL(*content_input_delegate_, OnContentMove(testing::_)).Times(0);
 
-  input_manager_->HandleInput(content_quad_center - origin, origin,
-                              UiInputManager::ButtonState::DOWN, &gesture_list,
-                              &out_target_point, &out_reticle_render_target);
+  input_manager_->HandleInput(controller_model, &reticle_model, &gesture_list);
 }
 
 }  // namespace vr
diff --git a/chrome/browser/vr/ui_interface.h b/chrome/browser/vr/ui_interface.h
index 3f3c0b1..d487c706 100644
--- a/chrome/browser/vr/ui_interface.h
+++ b/chrome/browser/vr/ui_interface.h
@@ -13,6 +13,9 @@
 
 namespace vr {
 
+struct ControllerModel;
+struct ReticleModel;
+
 // This is the platform-specific interface to the VR UI.
 class UiInterface {
  public:
@@ -34,6 +37,8 @@
   virtual void OnAppButtonClicked() = 0;
   virtual void OnAppButtonGesturePerformed(
       UiInterface::Direction direction) = 0;
+  virtual void OnControllerUpdated(const ControllerModel& controller_model,
+                                   const ReticleModel& reticle_model) = 0;
   virtual void OnProjMatrixChanged(const gfx::Transform& proj_matrix) = 0;
   virtual void OnWebVrFrameAvailable() = 0;
   virtual void OnWebVrTimedOut() = 0;
diff --git a/chrome/browser/vr/ui_renderer.cc b/chrome/browser/vr/ui_renderer.cc
index 920ca5b..59c01cb 100644
--- a/chrome/browser/vr/ui_renderer.cc
+++ b/chrome/browser/vr/ui_renderer.cc
@@ -4,61 +4,34 @@
 
 #include "chrome/browser/vr/ui_renderer.h"
 
-#include "base/numerics/math_constants.h"
 #include "base/trace_event/trace_event.h"
 #include "chrome/browser/vr/elements/ui_element.h"
 #include "chrome/browser/vr/ui_scene.h"
-#include "chrome/browser/vr/vr_controller_model.h"
 #include "chrome/browser/vr/vr_shell_renderer.h"
 #include "ui/gl/gl_bindings.h"
 
 namespace vr {
 
-namespace {
-
-static constexpr gfx::Point3F kOrigin = {0.0f, 0.0f, 0.0f};
-
-// Fraction of the distance to the object the reticle is drawn at to avoid
-// rounding errors drawing the reticle behind the object.
-// TODO(mthiesse): Find a better approach for drawing the reticle on an object.
-// Right now we have to wedge it very precisely between the content window and
-// backplane to avoid rendering artifacts. We should stop using the depth buffer
-// since the back-to-front order of our elements is well defined. This would,
-// among other things, prevent z-fighting when we draw content in the same
-// plane.
-static constexpr float kReticleOffset = 0.999f;
-
-static constexpr float kLaserWidth = 0.01f;
-
-static constexpr float kReticleWidth = 0.025f;
-static constexpr float kReticleHeight = 0.025f;
-
-}  // namespace
-
 UiRenderer::UiRenderer(UiScene* scene, VrShellRenderer* vr_shell_renderer)
     : scene_(scene), vr_shell_renderer_(vr_shell_renderer) {}
 
 UiRenderer::~UiRenderer() = default;
 
-// TODO(crbug.com/767515): UiRenderer must not care about the elements its
+// TODO(crbug.com/767515): UiRenderer must not care about the elements it's
 // rendering and be platform agnostic, each element should know how to render
 // itself correctly.
-void UiRenderer::Draw(const RenderInfo& render_info,
-                      const ControllerInfo& controller_info) {
-  Draw2dBrowsing(render_info, controller_info);
-  DrawSplashScreen(render_info, controller_info);
+void UiRenderer::Draw(const RenderInfo& render_info) {
+  Draw2dBrowsing(render_info);
+  DrawSplashScreen(render_info);
 }
 
-void UiRenderer::Draw2dBrowsing(const RenderInfo& render_info,
-                                const ControllerInfo& controller_info) {
+void UiRenderer::Draw2dBrowsing(const RenderInfo& render_info) {
   const auto& elements = scene_->GetVisible2dBrowsingElements();
   const auto& elements_overlay = scene_->GetVisible2dBrowsingOverlayElements();
+  const auto& controller_elements = scene_->GetVisibleControllerElements();
   if (elements.empty() && elements_overlay.empty())
     return;
 
-  // TODO(crbug.com/767583): The controller is drawn as part of a call to
-  // DrawUiView for 2d browsing elements. This means that the controller is not
-  // supported with overlay elements.
   if (!elements.empty()) {
     // Enable depth testing. Note that we do not clear the color buffer. The
     // scene's background elements are responsible for drawing a complete
@@ -67,24 +40,24 @@
     glEnable(GL_DEPTH_TEST);
     glDepthMask(GL_TRUE);
     glClear(GL_DEPTH_BUFFER_BIT);
-    DrawUiView(render_info, controller_info, elements,
-               scene_->ControllerWouldBeVisibleInTheSceneGraph()
-                   ? kReticleVisible
-                   : kReticleHidden);
+    DrawUiView(render_info, elements);
   }
 
-  if (elements_overlay.empty())
+  if (elements_overlay.empty() && controller_elements.empty())
     return;
 
   // The overlays do not make use of depth testing.
   glDisable(GL_CULL_FACE);
   glDisable(GL_DEPTH_TEST);
   glDepthMask(GL_FALSE);
-  DrawUiView(render_info, controller_info, elements_overlay, kReticleHidden);
+  DrawUiView(render_info, elements_overlay);
+
+  // We do want to cull backfaces on the controller, however.
+  glEnable(GL_CULL_FACE);
+  DrawUiView(render_info, controller_elements);
 }
 
-void UiRenderer::DrawSplashScreen(const RenderInfo& render_info,
-                                  const ControllerInfo& controller_info) {
+void UiRenderer::DrawSplashScreen(const RenderInfo& render_info) {
   const auto& elements = scene_->GetVisibleSplashScreenElements();
   if (elements.empty())
     return;
@@ -100,18 +73,13 @@
   glDisable(GL_DEPTH_TEST);
   glDepthMask(GL_FALSE);
 
-  DrawUiView(render_info, controller_info, elements,
-             scene_->ControllerWouldBeVisibleInTheSceneGraph()
-                 ? kReticleVisible
-                 : kReticleHidden);
+  DrawUiView(render_info, elements);
 
   // NB: we do not draw the viewport aware objects here. They get put into
   // another buffer that is size optimized.
 }
 
-void UiRenderer::DrawWebVrOverlayForeground(
-    const RenderInfo& render_info,
-    const ControllerInfo& controller_info) {
+void UiRenderer::DrawWebVrOverlayForeground(const RenderInfo& render_info) {
   // The WebVR overlay foreground is drawn as a separate pass, so we need to set
   // up our gl state before drawing.
   glEnable(GL_CULL_FACE);
@@ -120,15 +88,11 @@
 
   glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-  DrawUiView(render_info, controller_info,
-             scene_->GetVisibleWebVrOverlayForegroundElements(),
-             kReticleHidden);
+  DrawUiView(render_info, scene_->GetVisibleWebVrOverlayForegroundElements());
 }
 
 void UiRenderer::DrawUiView(const RenderInfo& render_info,
-                            const ControllerInfo& controller_info,
-                            const std::vector<const UiElement*>& elements,
-                            UiRenderer::ReticleMode reticle_mode) {
+                            const std::vector<const UiElement*>& elements) {
   TRACE_EVENT0("gpu", "VrShellGl::DrawUiView");
 
   auto sorted_elements = GetElementsInDrawOrder(elements);
@@ -138,40 +102,18 @@
     glViewport(eye_info.viewport.x(), eye_info.viewport.y(),
                eye_info.viewport.width(), eye_info.viewport.height());
 
-    DrawElements(eye_info.view_proj_matrix, sorted_elements, render_info,
-                 controller_info, reticle_mode);
-    if (reticle_mode == kReticleVisible) {
-      DrawController(eye_info.view_proj_matrix, render_info, controller_info);
-      DrawLaser(eye_info.view_proj_matrix, render_info, controller_info);
-    }
+    DrawElements(eye_info.view_proj_matrix, sorted_elements, render_info);
   }
 }
 
 void UiRenderer::DrawElements(const gfx::Transform& view_proj_matrix,
                               const std::vector<const UiElement*>& elements,
-                              const RenderInfo& render_info,
-                              const ControllerInfo& controller_info,
-                              UiRenderer::ReticleMode reticle_mode) {
+                              const RenderInfo& render_info) {
   if (elements.empty()) {
     return;
   }
-  bool drawn_reticle = false;
   for (const auto* element : elements) {
-    // If we have no element to draw the reticle on, draw it after the
-    // background (the initial draw phase).
-    if (!controller_info.reticle_render_target &&
-        reticle_mode == kReticleVisible && !drawn_reticle &&
-        element->draw_phase() >= scene_->first_foreground_draw_phase()) {
-      DrawReticle(view_proj_matrix, render_info, controller_info);
-      drawn_reticle = true;
-    }
-
     DrawElement(view_proj_matrix, *element);
-
-    if (reticle_mode == kReticleVisible &&
-        (controller_info.reticle_render_target == element)) {
-      DrawReticle(view_proj_matrix, render_info, controller_info);
-    }
   }
   vr_shell_renderer_->Flush();
 }
@@ -190,6 +132,8 @@
   // Sort elements primarily based on their draw phase (lower draw phase first)
   // and secondarily based on their tree order (as specified by the sorted
   // |elements| vector).
+  // TODO(vollick): update the predicate to take into account some notion of "3d
+  // rendering contexts" and the ordering of the reticle wrt to other elements.
   std::stable_sort(sorted_elements.begin(), sorted_elements.end(),
                    [](const UiElement* first, const UiElement* second) {
                      return first->draw_phase() < second->draw_phase();
@@ -198,112 +142,4 @@
   return sorted_elements;
 }
 
-void UiRenderer::DrawReticle(const gfx::Transform& render_matrix,
-                             const RenderInfo& render_info,
-                             const ControllerInfo& controller_info) {
-  // Scale the reticle to have a fixed FOV size at any distance.
-  const float eye_to_target =
-      std::sqrt(controller_info.target_point.SquaredDistanceTo(kOrigin));
-
-  gfx::Transform mat;
-  mat.Scale3d(kReticleWidth * eye_to_target, kReticleHeight * eye_to_target, 1);
-
-  gfx::Quaternion rotation;
-
-  if (controller_info.reticle_render_target != nullptr) {
-    // Make the reticle planar to the element it's hitting.
-    rotation =
-        gfx::Quaternion(gfx::Vector3dF(0.0f, 0.0f, -1.0f),
-                        -controller_info.reticle_render_target->GetNormal());
-  } else {
-    // Rotate the reticle to directly face the eyes.
-    rotation = gfx::Quaternion(gfx::Vector3dF(0.0f, 0.0f, -1.0f),
-                               controller_info.target_point - kOrigin);
-  }
-  gfx::Transform rotation_mat(rotation);
-  mat = rotation_mat * mat;
-
-  gfx::Point3F target_point =
-      ScalePoint(controller_info.target_point, kReticleOffset);
-  // Place the pointer slightly in front of the plane intersection point.
-  mat.matrix().postTranslate(target_point.x(), target_point.y(),
-                             target_point.z());
-
-  gfx::Transform transform = render_matrix * mat;
-  vr_shell_renderer_->GetReticleRenderer()->Draw(transform);
-}
-
-void UiRenderer::DrawLaser(const gfx::Transform& render_matrix,
-                           const RenderInfo& render_info,
-                           const ControllerInfo& controller_info) {
-  // Find the length of the beam (from hand to target).
-  const float laser_length =
-      std::sqrt(controller_info.laser_origin.SquaredDistanceTo(
-          ScalePoint(controller_info.target_point, kReticleOffset)));
-
-  // Build a beam, originating from the origin.
-  gfx::Transform mat;
-
-  // Move the beam half its height so that its end sits on the origin.
-  mat.matrix().postTranslate(0.0f, 0.5f, 0.0f);
-  mat.matrix().postScale(kLaserWidth, laser_length, 1);
-
-  // Tip back 90 degrees to flat, pointing at the scene.
-  const gfx::Quaternion quat(gfx::Vector3dF(1.0f, 0.0f, 0.0f),
-                             -base::kPiDouble / 2);
-  gfx::Transform rotation_mat(quat);
-  mat = rotation_mat * mat;
-
-  const gfx::Vector3dF beam_direction =
-      controller_info.target_point - controller_info.laser_origin;
-
-  gfx::Transform beam_direction_mat(
-      gfx::Quaternion(gfx::Vector3dF(0.0f, 0.0f, -1.0f), beam_direction));
-
-  // Render multiple faces to make the laser appear cylindrical.
-  const int faces = 4;
-  gfx::Transform face_transform;
-  gfx::Transform transform;
-  for (int i = 0; i < faces; i++) {
-    // Rotate around Z.
-    const float angle = base::kPiFloat * 2 * i / faces;
-    const gfx::Quaternion rot({0.0f, 0.0f, 1.0f}, angle);
-    face_transform = beam_direction_mat * gfx::Transform(rot) * mat;
-
-    // Move the beam origin to the hand.
-    face_transform.matrix().postTranslate(controller_info.laser_origin.x(),
-                                          controller_info.laser_origin.y(),
-                                          controller_info.laser_origin.z());
-    transform = render_matrix * face_transform;
-    vr_shell_renderer_->GetLaserRenderer()->Draw(controller_info.opacity,
-                                                 transform);
-  }
-}
-
-void UiRenderer::DrawController(const gfx::Transform& view_proj_matrix,
-                                const RenderInfo& render_info,
-                                const ControllerInfo& controller_info) {
-  if (!vr_shell_renderer_->GetControllerRenderer()->IsSetUp()) {
-    return;
-  }
-
-  VrControllerModel::State state;
-  if (controller_info.touchpad_button_state ==
-      UiInputManager::ButtonState::DOWN) {
-    state = VrControllerModel::TOUCHPAD;
-  } else if (controller_info.app_button_state ==
-             UiInputManager::ButtonState::DOWN) {
-    state = VrControllerModel::APP;
-  } else if (controller_info.home_button_state ==
-             UiInputManager::ButtonState::DOWN) {
-    state = VrControllerModel::SYSTEM;
-  } else {
-    state = VrControllerModel::IDLE;
-  }
-
-  gfx::Transform transform = view_proj_matrix * controller_info.transform;
-  vr_shell_renderer_->GetControllerRenderer()->Draw(
-      state, controller_info.opacity, transform);
-}
-
 }  // namespace vr
diff --git a/chrome/browser/vr/ui_renderer.h b/chrome/browser/vr/ui_renderer.h
index e02e119..b769387 100644
--- a/chrome/browser/vr/ui_renderer.h
+++ b/chrome/browser/vr/ui_renderer.h
@@ -16,18 +16,6 @@
 class UiElement;
 class VrShellRenderer;
 
-// Provides information needed to render the controller.
-struct ControllerInfo {
-  gfx::Point3F target_point;
-  gfx::Point3F laser_origin;
-  UiInputManager::ButtonState touchpad_button_state = UiInputManager::UP;
-  UiInputManager::ButtonState app_button_state = UiInputManager::UP;
-  UiInputManager::ButtonState home_button_state = UiInputManager::UP;
-  gfx::Transform transform;
-  float opacity = 1.0f;
-  UiElement* reticle_render_target = nullptr;
-};
-
 // Provides information for rendering such as the viewport and view/projection
 // matrix.
 struct RenderInfo {
@@ -52,45 +40,26 @@
              VrShellRenderer* vr_shell_renderer);
   ~UiRenderer();
 
-  void Draw(const RenderInfo& render_info,
-            const ControllerInfo& controller_info);
+  void Draw(const RenderInfo& render_info);
 
   // This is exposed separately because we do a separate pass to render this
   // content into an optimized viewport.
-  void DrawWebVrOverlayForeground(const RenderInfo& render_info,
-                                  const ControllerInfo& controller_info);
+  void DrawWebVrOverlayForeground(const RenderInfo& render_info);
 
   static std::vector<const UiElement*> GetElementsInDrawOrder(
       const std::vector<const UiElement*>& elements);
 
  private:
-  enum ReticleMode { kReticleVisible, kReticleHidden };
-
-  void Draw2dBrowsing(const RenderInfo& render_info,
-                      const ControllerInfo& controller_info);
-  void DrawSplashScreen(const RenderInfo& render_info,
-                        const ControllerInfo& controller_info);
+  void Draw2dBrowsing(const RenderInfo& render_info);
+  void DrawSplashScreen(const RenderInfo& render_info);
 
   void DrawUiView(const RenderInfo& render_info,
-                  const ControllerInfo& controller_info,
-                  const std::vector<const UiElement*>& elements,
-                  ReticleMode reticle_mode);
+                  const std::vector<const UiElement*>& elements);
   void DrawElements(const gfx::Transform& view_proj_matrix,
                     const std::vector<const UiElement*>& elements,
-                    const RenderInfo& render_info,
-                    const ControllerInfo& controller_info,
-                    ReticleMode reticle_mode);
+                    const RenderInfo& render_info);
   void DrawElement(const gfx::Transform& view_proj_matrix,
                    const UiElement& element);
-  void DrawReticle(const gfx::Transform& render_matrix,
-                   const RenderInfo& render_info,
-                   const ControllerInfo& controller_info);
-  void DrawLaser(const gfx::Transform& render_matrix,
-                 const RenderInfo& render_info,
-                 const ControllerInfo& controller_info);
-  void DrawController(const gfx::Transform& view_proj_matrix,
-                      const RenderInfo& render_info,
-                      const ControllerInfo& controller_info);
 
   UiScene* scene_ = nullptr;
   VrShellRenderer* vr_shell_renderer_ = nullptr;
diff --git a/chrome/browser/vr/ui_renderer_unittest.cc b/chrome/browser/vr/ui_renderer_unittest.cc
index 4f33a34..0841991 100644
--- a/chrome/browser/vr/ui_renderer_unittest.cc
+++ b/chrome/browser/vr/ui_renderer_unittest.cc
@@ -94,6 +94,10 @@
      {
          kWebVrUrlToast, kExclusiveScreenToastViewportAware,
      }},
+    {&UiScene::GetVisibleControllerElements,
+     {
+         kController, kLaser, kReticle,
+     }},
 };
 
 INSTANTIATE_TEST_CASE_P(SortingTests,
diff --git a/chrome/browser/vr/ui_scene.cc b/chrome/browser/vr/ui_scene.cc
index 9baa2db5..7cbc253 100644
--- a/chrome/browser/vr/ui_scene.cc
+++ b/chrome/browser/vr/ui_scene.cc
@@ -212,6 +212,13 @@
       });
 }
 
+UiScene::Elements UiScene::GetVisibleControllerElements() const {
+  return GetVisibleElements(GetUiElementByName(kControllerGroup),
+                            [](UiElement* element) {
+                              return element->draw_phase() == kPhaseForeground;
+                            });
+}
+
 UiScene::UiScene() {
   root_element_ = base::MakeUnique<UiElement>();
   root_element_->set_name(kRoot);
@@ -230,18 +237,4 @@
     element.Initialize();
 }
 
-bool UiScene::ControllerWouldBeVisibleInTheSceneGraph() const {
-  UiElement* button = GetUiElementByName(kWebVrTimeoutMessageButton);
-  if (button && button->IsVisible())
-    return true;
-
-  UiElement* browsing_root = GetUiElementByName(k2dBrowsingRoot);
-  bool browsing_mode = browsing_root && browsing_root->IsVisible();
-
-  UiElement* spinner_bg = GetUiElementByName(kWebVrTimeoutSpinnerBackground);
-  bool transitioning_to_webvr = spinner_bg && spinner_bg->IsVisible();
-
-  return browsing_mode && !transitioning_to_webvr;
-}
-
 }  // namespace vr
diff --git a/chrome/browser/vr/ui_scene.h b/chrome/browser/vr/ui_scene.h
index 5403093..5b1b18bd 100644
--- a/chrome/browser/vr/ui_scene.h
+++ b/chrome/browser/vr/ui_scene.h
@@ -58,6 +58,7 @@
   Elements GetVisible2dBrowsingOverlayElements() const;
   Elements GetVisibleSplashScreenElements() const;
   Elements GetVisibleWebVrOverlayForegroundElements() const;
+  Elements GetVisibleControllerElements() const;
 
   float background_distance() const { return background_distance_; }
   void set_background_distance(float d) { background_distance_ = d; }
@@ -80,10 +81,6 @@
 
   void OnGlInitialized();
 
-  // TODO(vollick): this should be removed once the controller is in the scene
-  // graph. crbug.com/774501
-  bool ControllerWouldBeVisibleInTheSceneGraph() const;
-
  private:
   std::unique_ptr<UiElement> root_element_;
 
diff --git a/chrome/browser/vr/ui_scene_constants.h b/chrome/browser/vr/ui_scene_constants.h
index f2398897..1582caa 100644
--- a/chrome/browser/vr/ui_scene_constants.h
+++ b/chrome/browser/vr/ui_scene_constants.h
@@ -165,6 +165,23 @@
 
 static constexpr float kScreenDimmerOpacity = 0.9f;
 
+static constexpr gfx::Point3F kOrigin = {0.0f, 0.0f, 0.0f};
+
+// Fraction of the distance to the object the reticle is drawn at to avoid
+// rounding errors drawing the reticle behind the object.
+// TODO(mthiesse): Find a better approach for drawing the reticle on an object.
+// Right now we have to wedge it very precisely between the content window and
+// backplane to avoid rendering artifacts. We should stop using the depth buffer
+// since the back-to-front order of our elements is well defined. This would,
+// among other things, prevent z-fighting when we draw content in the same
+// plane.
+static constexpr float kReticleOffset = 0.999f;
+
+static constexpr float kLaserWidth = 0.01f;
+
+static constexpr float kReticleWidth = 0.025f;
+static constexpr float kReticleHeight = 0.025f;
+
 static constexpr float kVoiceSearchButtonXOffset = 0.25f;
 
 static constexpr float kSuggestionGap = 0.01f;
diff --git a/chrome/browser/vr/ui_scene_manager.cc b/chrome/browser/vr/ui_scene_manager.cc
index 86303633..804255f 100644
--- a/chrome/browser/vr/ui_scene_manager.cc
+++ b/chrome/browser/vr/ui_scene_manager.cc
@@ -13,6 +13,7 @@
 #include "chrome/browser/vr/databinding/vector_binding.h"
 #include "chrome/browser/vr/elements/button.h"
 #include "chrome/browser/vr/elements/content_element.h"
+#include "chrome/browser/vr/elements/controller.h"
 #include "chrome/browser/vr/elements/draw_phase.h"
 #include "chrome/browser/vr/elements/exclusive_screen_toast.h"
 #include "chrome/browser/vr/elements/exit_prompt.h"
@@ -20,8 +21,10 @@
 #include "chrome/browser/vr/elements/full_screen_rect.h"
 #include "chrome/browser/vr/elements/grid.h"
 #include "chrome/browser/vr/elements/invisible_hit_target.h"
+#include "chrome/browser/vr/elements/laser.h"
 #include "chrome/browser/vr/elements/linear_layout.h"
 #include "chrome/browser/vr/elements/rect.h"
+#include "chrome/browser/vr/elements/reticle.h"
 #include "chrome/browser/vr/elements/spinner.h"
 #include "chrome/browser/vr/elements/system_indicator.h"
 #include "chrome/browser/vr/elements/text.h"
@@ -166,6 +169,7 @@
   CreateSplashScreen(model);
   CreateUnderDevelopmentNotice();
   CreateVoiceSearchUiGroup(model);
+  CreateController(model);
 
   ConfigureScene();
 }
@@ -623,6 +627,54 @@
   scene_->AddUiElement(kSpeechRecognitionPrompt, std::move(element));
 }
 
+void UiSceneManager::CreateController(Model* model) {
+  auto group = base::MakeUnique<UiElement>();
+  group->set_name(kControllerGroup);
+  group->SetVisible(true);
+  group->set_hit_testable(false);
+  group->AddBinding(base::MakeUnique<Binding<bool>>(
+      base::Bind(
+          [](Model* m, UiSceneManager* mgr) {
+            bool browsing_mode =
+                !mgr->web_vr_mode() && !mgr->showing_web_vr_splash_screen();
+            return browsing_mode || m->web_vr_timeout_state == kWebVrTimedOut;
+          },
+          base::Unretained(model), base::Unretained(this)),
+      base::Bind([](UiElement* v, const bool& b) { v->SetVisible(b); },
+                 base::Unretained(group.get()))));
+  scene_->AddUiElement(kRoot, std::move(group));
+
+  auto controller = base::MakeUnique<Controller>();
+  controller->set_draw_phase(kPhaseForeground);
+  controller->AddBinding(VR_BIND_FUNC(gfx::Transform, Model, model,
+                                      controller.transform, Controller,
+                                      controller.get(), set_local_transform));
+  controller->AddBinding(
+      VR_BIND_FUNC(bool, Model, model,
+                   controller.touchpad_button_state == UiInputManager::DOWN,
+                   Controller, controller.get(), set_touchpad_button_pressed));
+  controller->AddBinding(VR_BIND_FUNC(
+      bool, Model, model, controller.app_button_state == UiInputManager::DOWN,
+      Controller, controller.get(), set_app_button_pressed));
+  controller->AddBinding(VR_BIND_FUNC(
+      bool, Model, model, controller.home_button_state == UiInputManager::DOWN,
+      Controller, controller.get(), set_home_button_pressed));
+  controller->AddBinding(VR_BIND_FUNC(float, Model, model, controller.opacity,
+                                      Controller, controller.get(),
+                                      SetOpacity));
+  scene_->AddUiElement(kControllerGroup, std::move(controller));
+
+  auto laser = base::MakeUnique<Laser>(model);
+  laser->set_draw_phase(kPhaseForeground);
+  laser->AddBinding(VR_BIND_FUNC(float, Model, model, controller.opacity, Laser,
+                                 laser.get(), SetOpacity));
+  scene_->AddUiElement(kControllerGroup, std::move(laser));
+
+  auto reticle = base::MakeUnique<Reticle>(scene_, model);
+  reticle->set_draw_phase(kPhaseForeground);
+  scene_->AddUiElement(kControllerGroup, std::move(reticle));
+}
+
 void UiSceneManager::CreateUrlBar(Model* model) {
   auto url_bar = base::MakeUnique<UrlBar>(
       512,
diff --git a/chrome/browser/vr/ui_scene_manager.h b/chrome/browser/vr/ui_scene_manager.h
index fd6ba133..e6f41fd 100644
--- a/chrome/browser/vr/ui_scene_manager.h
+++ b/chrome/browser/vr/ui_scene_manager.h
@@ -95,6 +95,10 @@
 //           kWebVrTimeoutMessageText
 //           kWebVrTimeoutMessageButton
 //             kWebVrTimeoutMessageButtonText
+//   kControllerGroup
+//     kLaser
+//     kController
+//     kReticle
 //
 // TODO(vollick): The above hierarchy is complex, brittle, and would be easier
 // to manage if it were specified in a declarative format.
@@ -135,8 +139,12 @@
   void OnSecurityIconClickedForTesting();
   void OnExitPromptChoiceForTesting(bool chose_exit);
 
-  // TODO(vollick): this should move to the model.
+  // TODO(vollick): these should move to the model.
   const ColorScheme& color_scheme() const;
+  bool web_vr_mode() const { return web_vr_mode_; }
+  bool showing_web_vr_splash_screen() const {
+    return showing_web_vr_splash_screen_;
+  }
 
  private:
   void Create2dBrowsingSubtreeRoots(Model* model);
@@ -156,6 +164,7 @@
   void CreateExitPrompt();
   void CreateToasts(Model* model);
   void CreateVoiceSearchUiGroup(Model* model);
+  void CreateController(Model* model);
 
   void ConfigureScene();
   void ConfigureExclusiveScreenToast();
diff --git a/chrome/browser/vr/vr_shell_renderer.cc b/chrome/browser/vr/vr_shell_renderer.cc
index 8bad7f07..8d63177 100644
--- a/chrome/browser/vr/vr_shell_renderer.cc
+++ b/chrome/browser/vr/vr_shell_renderer.cc
@@ -204,6 +204,7 @@
   uniform mediump float inner_ring_thickness;
   uniform mediump float mid_ring_end;
   uniform mediump float mid_ring_opacity;
+  uniform mediump float opacity;
 
   void main() {
     mediump float r = length(v_TexCoordinate - vec2(0.5, 0.5));
@@ -222,7 +223,7 @@
     mediump float black_alpha_factor =
         mid_ring_opacity * (1.0 - (r - black_radius) * black_feather);
     mediump float alpha = clamp(
-        min(hole_alpha, max(color1, black_alpha_factor)), 0.0, 1.0);
+        min(hole_alpha, max(color1, black_alpha_factor)) * opacity, 0.0, 1.0);
     lowp vec3 color_rgb = color1 * color.xyz;
     gl_FragColor = vec4(color_rgb * color.w * alpha, color.w * alpha);
   }
@@ -317,7 +318,10 @@
     float mask = 1.0 - step(1.0, length(v_CornerPosition));
     // Add some noise to prevent banding artifacts in the gradient.
     float n = (fract(dot(v_Position.xy, vec2(12345.67, 456.7))) - 0.5) / 255.0;
-    gl_FragColor = (color + n) * u_Opacity * mask;
+
+    color = color + n;
+    color = vec4(color.rgb * color.a, color.a);
+    gl_FragColor = color * u_Opacity * mask;
   }
 );
 
@@ -750,9 +754,11 @@
   mid_ring_end_handle_ = glGetUniformLocation(program_handle_, "mid_ring_end");
   mid_ring_opacity_handle_ =
       glGetUniformLocation(program_handle_, "mid_ring_opacity");
+  opacity_handle_ = glGetUniformLocation(program_handle_, "opacity");
 }
 
-void ReticleRenderer::Draw(const gfx::Transform& view_proj_matrix) {
+void ReticleRenderer::Draw(float opacity,
+                           const gfx::Transform& view_proj_matrix) {
   PrepareToDraw(model_view_proj_matrix_handle_, view_proj_matrix);
 
   glUniform4f(color_handle_, kReticleColor[0], kReticleColor[1],
@@ -763,6 +769,7 @@
   glUniform1f(inner_ring_thickness_handle_, kInnerRingThickness);
   glUniform1f(mid_ring_end_handle_, kMidRingEnd);
   glUniform1f(mid_ring_opacity_handle_, kMidRingOpacity);
+  glUniform1f(opacity_handle_, opacity);
 
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_);
   glDrawElements(GL_TRIANGLES, arraysize(kQuadIndices), GL_UNSIGNED_SHORT, 0);
@@ -815,7 +822,7 @@
 
 ControllerRenderer::ControllerRenderer()
     : BaseRenderer(kControllerVertexShader, kControllerFragmentShader),
-      texture_handles_(VrControllerModel::STATE_COUNT) {
+      texture_handles_(ControllerMesh::STATE_COUNT) {
   model_view_proj_matrix_handle_ =
       glGetUniformLocation(program_handle_, "u_ModelViewProjMatrix");
   tex_coord_handle_ = glGetAttribLocation(program_handle_, "a_TexCoordinate");
@@ -825,7 +832,7 @@
 
 ControllerRenderer::~ControllerRenderer() = default;
 
-void ControllerRenderer::SetUp(std::unique_ptr<VrControllerModel> model) {
+void ControllerRenderer::SetUp(std::unique_ptr<ControllerMesh> model) {
   TRACE_EVENT0("gpu", "ControllerRenderer::SetUp");
   glGenBuffersARB(1, &indices_buffer_);
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices_buffer_);
@@ -837,8 +844,8 @@
   glBufferData(GL_ARRAY_BUFFER, model->ElementsBufferSize(),
                model->ElementsBuffer(), GL_STATIC_DRAW);
 
-  glGenTextures(VrControllerModel::STATE_COUNT, texture_handles_.data());
-  for (int i = 0; i < VrControllerModel::STATE_COUNT; i++) {
+  glGenTextures(ControllerMesh::STATE_COUNT, texture_handles_.data());
+  for (int i = 0; i < ControllerMesh::STATE_COUNT; i++) {
     sk_sp<SkImage> texture = model->GetTexture(i);
     SkPixmap pixmap;
     if (!texture->peekPixels(&pixmap)) {
@@ -871,7 +878,7 @@
   setup_ = true;
 }
 
-void ControllerRenderer::Draw(VrControllerModel::State state,
+void ControllerRenderer::Draw(ControllerMesh::State state,
                               float opacity,
                               const gfx::Transform& view_proj_matrix) {
   glUseProgram(program_handle_);
@@ -1098,6 +1105,25 @@
                                   opacity);
 }
 
+void VrShellRenderer::DrawController(ControllerMesh::State state,
+                                     float opacity,
+                                     const gfx::Transform& view_proj_matrix) {
+  if (!GetControllerRenderer()->IsSetUp()) {
+    return;
+  }
+  GetControllerRenderer()->Draw(state, opacity, view_proj_matrix);
+}
+
+void VrShellRenderer::DrawLaser(float opacity,
+                                const gfx::Transform& view_proj_matrix) {
+  GetLaserRenderer()->Draw(opacity, view_proj_matrix);
+}
+
+void VrShellRenderer::DrawReticle(float opacity,
+                                  const gfx::Transform& view_proj_matrix) {
+  GetReticleRenderer()->Draw(opacity, view_proj_matrix);
+}
+
 ExternalTexturedQuadRenderer*
 VrShellRenderer::GetExternalTexturedQuadRenderer() {
   FlushIfNecessary(external_textured_quad_renderer_.get());
diff --git a/chrome/browser/vr/vr_shell_renderer.h b/chrome/browser/vr/vr_shell_renderer.h
index 419df95..15b1e91 100644
--- a/chrome/browser/vr/vr_shell_renderer.h
+++ b/chrome/browser/vr/vr_shell_renderer.h
@@ -10,8 +10,8 @@
 
 #include "base/containers/queue.h"
 #include "base/macros.h"
+#include "chrome/browser/vr/controller_mesh.h"
 #include "chrome/browser/vr/ui_element_renderer.h"
-#include "chrome/browser/vr/vr_controller_model.h"
 #include "ui/gfx/geometry/rect_f.h"
 #include "ui/gfx/geometry/size.h"
 #include "ui/gfx/geometry/size_f.h"
@@ -56,6 +56,15 @@
                             const SkColor grid_color,
                             int gridline_count,
                             float opacity) override;
+  void DrawController(ControllerMesh::State state,
+                      float opacity,
+                      const gfx::Transform& view_proj_matrix) override;
+
+  void DrawLaser(float opacity,
+                 const gfx::Transform& view_proj_matrix) override;
+
+  void DrawReticle(float opacity,
+                   const gfx::Transform& view_proj_matrix) override;
 
   // VrShell's internal GL rendering API.
   ExternalTexturedQuadRenderer* GetExternalTexturedQuadRenderer();
@@ -201,7 +210,7 @@
   ReticleRenderer();
   ~ReticleRenderer() override;
 
-  void Draw(const gfx::Transform& view_proj_matrix);
+  void Draw(float opacity, const gfx::Transform& view_proj_matrix);
 
  private:
   GLuint model_view_proj_matrix_handle_;
@@ -212,6 +221,7 @@
   GLuint inner_ring_thickness_handle_;
   GLuint mid_ring_end_handle_;
   GLuint mid_ring_opacity_handle_;
+  GLuint opacity_handle_;
 
   DISALLOW_COPY_AND_ASSIGN(ReticleRenderer);
 };
@@ -240,8 +250,8 @@
   ControllerRenderer();
   ~ControllerRenderer() override;
 
-  void SetUp(std::unique_ptr<VrControllerModel> model);
-  void Draw(VrControllerModel::State state,
+  void SetUp(std::unique_ptr<ControllerMesh> model);
+  void Draw(ControllerMesh::State state,
             float opacity,
             const gfx::Transform& view_proj_matrix);
   bool IsSetUp() const { return setup_; }
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_Big5_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_Big5_saved_from_no_encoding_specified.html
index 1dea24b..016e4df 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_Big5_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_Big5_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0080)http://mock.http/encoding_tests/auto_detect/Big5_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=Big5">
   <title> Big5 </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_EUC-KR_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_EUC-KR_saved_from_no_encoding_specified.html
index 161fac3..8ac0f96e 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_EUC-KR_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_EUC-KR_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0082)http://mock.http/encoding_tests/auto_detect/EUC-KR_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
   <title> EUC-KR </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_GBK_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_GBK_saved_from_no_encoding_specified.html
index cdabea0..8d78ce5 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_GBK_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_GBK_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD Xhtml 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<!-- saved from url=(0079)http://mock.http/encoding_tests/auto_detect/GBK_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=GBK">
 <title>ÖйúÖÆÔìµÄÁì¾üÕß3Ãû</title>
 </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-5_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-5_saved_from_no_encoding_specified.html
index 4c15e6e..2f0d69f 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-5_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-5_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0086)http://mock.http/encoding_tests/auto_detect/ISO-8859-5_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-5">
   <title> ISO-8859-5 </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-6_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-6_saved_from_no_encoding_specified.html
index 66e4052..d1bdd6fe1 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-6_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-6_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0086)http://mock.http/encoding_tests/auto_detect/ISO-8859-6_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-6">
   <title> ISO-8859-6 </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-7_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-7_saved_from_no_encoding_specified.html
index 12da1fb5..35c3da1 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-7_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-7_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0086)http://mock.http/encoding_tests/auto_detect/ISO-8859-7_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-7">
   <title> ISO-8859-7 </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-8-I_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-8-I_saved_from_no_encoding_specified.html
index de39f5a0..7ad794e 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-8-I_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_ISO-8859-8-I_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0088)http://mock.http/encoding_tests/auto_detect/ISO-8859-8-I_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1255">
   <title> ISO-8859-8-I </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_KOI8-R_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_KOI8-R_saved_from_no_encoding_specified.html
index d1d8f9af..c5ba0b9f 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_KOI8-R_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_KOI8-R_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0082)http://mock.http/encoding_tests/auto_detect/KOI8-R_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=KOI8-R">
   <title> KOI8-R </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_Shift-JIS_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_Shift-JIS_saved_from_no_encoding_specified.html
index 7b25aaa..81247ed2 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_Shift-JIS_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_Shift-JIS_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0085)http://mock.http/encoding_tests/auto_detect/Shift-JIS_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
   <title> Shift_JIS </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_iso-8859-1_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_iso-8859-1_saved_from_no_encoding_specified.html
index c647f3b..955f1d7 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_iso-8859-1_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_iso-8859-1_saved_from_no_encoding_specified.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD html 4.01 Transitional//EN" "http://www.w3c.o rg/TR/1999/REC-html401-19991224/loose.dtd">
-<!-- saved from url=(0086)http://mock.http/encoding_tests/auto_detect/iso-8859-1_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml"><head profile="http://www.w3.org/2000/08/w3c-synd/#"><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
 <title>iso-8859-1</title>
 </head>
 <body>
-<h1 id="logo"><img height="48" alt="The World Wide Web Consortium (W3C)" src="http://mock.http/Icons/w3c_main" width="315"></h1>
+<h1 id="logo"><img height="48" alt="The World Wide Web Consortium (W3C)" src="./sub_resource_files/w3c_main" width="315"></h1>
 <h2 id="slogan">Leading the Web to Its Full Potential...</h2>
 
 </body></html>
\ No newline at end of file
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1251_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1251_saved_from_no_encoding_specified.html
index 159cbc8..ede027a0 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1251_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1251_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0088)http://mock.http/encoding_tests/auto_detect/windows-1251_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
   <title> windows-1251 </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1254_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1254_saved_from_no_encoding_specified.html
index c4ae80d..64bbcbe 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1254_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1254_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0088)http://mock.http/encoding_tests/auto_detect/windows-1254_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1254">
   <title> windows-1254 </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1255_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1255_saved_from_no_encoding_specified.html
index bfe6159..90a92a3 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1255_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1255_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0088)http://mock.http/encoding_tests/auto_detect/windows-1255_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1255">
   <title> windows-1255 </title>
  </head>
diff --git a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1256_saved_from_no_encoding_specified.html b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1256_saved_from_no_encoding_specified.html
index e684de3..98ea829 100644
--- a/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1256_saved_from_no_encoding_specified.html
+++ b/chrome/test/data/encoding_tests/auto_detect/expected_results/expected_windows-1256_saved_from_no_encoding_specified.html
@@ -1,5 +1,5 @@
 
-<!-- saved from url=(0088)http://mock.http/encoding_tests/auto_detect/windows-1256_with_no_encoding_specified.html -->
+<!-- saved from url=(%04d)%s -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1256">
   <title> windows-1256 </title>
  </head>
diff --git a/content/browser/frame_host/render_frame_message_filter.cc b/content/browser/frame_host/render_frame_message_filter.cc
index 9e9a5e7..1f20a07 100644
--- a/content/browser/frame_host/render_frame_message_filter.cc
+++ b/content/browser/frame_host/render_frame_message_filter.cc
@@ -442,7 +442,9 @@
 void RenderFrameMessageFilter::SetCookie(int32_t render_frame_id,
                                          const GURL& url,
                                          const GURL& site_for_cookies,
-                                         const std::string& cookie) {
+                                         const std::string& cookie,
+                                         SetCookieCallback callback) {
+  std::move(callback).Run();
   ChildProcessSecurityPolicyImpl* policy =
       ChildProcessSecurityPolicyImpl::GetInstance();
   if (!policy->CanAccessDataForOrigin(render_process_id_, url)) {
diff --git a/content/browser/frame_host/render_frame_message_filter.h b/content/browser/frame_host/render_frame_message_filter.h
index 95a8c39..9f4ecba 100644
--- a/content/browser/frame_host/render_frame_message_filter.h
+++ b/content/browser/frame_host/render_frame_message_filter.h
@@ -127,7 +127,8 @@
   void SetCookie(int32_t render_frame_id,
                  const GURL& url,
                  const GURL& site_for_cookies,
-                 const std::string& cookie) override;
+                 const std::string& cookie,
+                 SetCookieCallback callback) override;
   void GetCookies(int render_frame_id,
                   const GURL& url,
                   const GURL& site_for_cookies,
diff --git a/content/browser/frame_host/render_frame_message_filter_browsertest.cc b/content/browser/frame_host/render_frame_message_filter_browsertest.cc
index aeda684..f743a84 100644
--- a/content/browser/frame_host/render_frame_message_filter_browsertest.cc
+++ b/content/browser/frame_host/render_frame_message_filter_browsertest.cc
@@ -4,6 +4,8 @@
 
 #include <string>
 
+#include "base/bind.h"
+#include "base/bind_helpers.h"
 #include "base/command_line.h"
 #include "base/files/file_path.h"
 #include "base/test/histogram_tester.h"
@@ -234,10 +236,11 @@
       ->PostTask(FROM_HERE, base::BindOnce(
                                 [](RenderFrameHost* frame) {
                                   GetFilterForProcess(frame->GetProcess())
-                                      ->SetCookie(frame->GetRoutingID(),
-                                                  GURL("https://baz.com/"),
-                                                  GURL("https://baz.com/"),
-                                                  "pwn=ed");
+                                      ->SetCookie(
+                                          frame->GetRoutingID(),
+                                          GURL("https://baz.com/"),
+                                          GURL("https://baz.com/"), "pwn=ed",
+                                          base::BindOnce(&base::DoNothing));
                                 },
                                 main_frame));
 
diff --git a/content/common/render_frame_message_filter.mojom b/content/common/render_frame_message_filter.mojom
index 5bbf05e..0a9f158 100644
--- a/content/common/render_frame_message_filter.mojom
+++ b/content/common/render_frame_message_filter.mojom
@@ -7,10 +7,13 @@
 import "url/mojo/url.mojom";
 
 interface RenderFrameMessageFilter {
-  // Sets a cookie. The cookie is set asynchronously, but will be available to
-  // any subsequent GetCookies() request.
+  // Sets a cookie. Returns after the cookie write request has been scheduled on
+  // the IO thread (the database is modified asynchronously). This ensures that
+  // network requests issued after the cookie setter call are processed after
+  // the cookie change is committed, and therefore see the change.
+  [Sync]
   SetCookie(int32 render_frame_id, url.mojom.Url url,
-      url.mojom.Url first_party_for_cookies, string cookie);
+      url.mojom.Url first_party_for_cookies, string cookie) => ();
 
   // Used to get cookies for the given URL. This may block waiting for a
   // previous SetCookie message to be processed.
diff --git a/content/renderer/renderer_webcookiejar_impl.cc b/content/renderer/renderer_webcookiejar_impl.cc
index fc25e80..4c22634 100644
--- a/content/renderer/renderer_webcookiejar_impl.cc
+++ b/content/renderer/renderer_webcookiejar_impl.cc
@@ -4,6 +4,8 @@
 
 #include "content/renderer/renderer_webcookiejar_impl.h"
 
+#include "base/bind.h"
+#include "base/bind_helpers.h"
 #include "base/strings/utf_string_conversions.h"
 #include "content/common/frame_messages.h"
 #include "content/public/renderer/content_renderer_client.h"
@@ -21,7 +23,8 @@
   std::string value_utf8 =
       value.Utf8(WebString::UTF8ConversionMode::kStrictReplacingErrorsWithFFFD);
   RenderThreadImpl::current()->render_frame_message_filter()->SetCookie(
-      sender_->GetRoutingID(), url, site_for_cookies, value_utf8);
+      sender_->GetRoutingID(), url, site_for_cookies, value_utf8,
+      base::BindOnce(&base::DoNothing));
 }
 
 WebString RendererWebCookieJarImpl::Cookies(const WebURL& url,
diff --git a/testing/buildbot/filters/mojo.fyi.network_browser_tests.filter b/testing/buildbot/filters/mojo.fyi.network_browser_tests.filter
index fd345df3..30cb5cc 100644
--- a/testing/buildbot/filters/mojo.fyi.network_browser_tests.filter
+++ b/testing/buildbot/filters/mojo.fyi.network_browser_tests.filter
@@ -2384,7 +2384,6 @@
 -BrowserCloseManagerWithDownloadsBrowserTest/BrowserCloseManagerWithDownloadsBrowserTest.TestWithOffTheRecordDownloads/1
 -BrowserCloseManagerWithDownloadsBrowserTest/BrowserCloseManagerWithDownloadsBrowserTest.TestWithOffTheRecordWindowAndRegularDownload/0
 -BrowserCloseManagerWithDownloadsBrowserTest/BrowserCloseManagerWithDownloadsBrowserTest.TestWithOffTheRecordWindowAndRegularDownload/1
--BrowserEncodingTest.TestEncodingAutoDetect
 -CaptivePortalBrowserTest.AbortLoad
 -CaptivePortalBrowserTest.CloseLoginTab
 -CaptivePortalBrowserTest.Disabled
@@ -2539,35 +2538,6 @@
 -DownloadTest.UnknownSize
 -DownloadTestWithFakeSafeBrowsing.NoUncommonDownloadReportWithoutUserProceed
 -DownloadTestWithFakeSafeBrowsing.SendUncommonDownloadReportIfUserProceed
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/0
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/1
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/10
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/11
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/12
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/13
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/14
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/15
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/16
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/18
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/19
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/2
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/20
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/21
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/22
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/23
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/24
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/25
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/26
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/27
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/28
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/29
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/3
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/4
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/5
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/6
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/7
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/8
--EncodingAliases/BrowserEncodingTest.TestEncodingAliasMapping/9
 -ErrorPageAutoReloadTest.AutoReload
 -ErrorPageAutoReloadTest.IgnoresSameDocumentNavigation
 -ErrorPageAutoReloadTest.ManualReloadNotSuppressed
diff --git a/third_party/WebKit/Source/core/loader/CookieJar.cpp b/third_party/WebKit/Source/core/loader/CookieJar.cpp
index d8c6708c..33595464 100644
--- a/third_party/WebKit/Source/core/loader/CookieJar.cpp
+++ b/third_party/WebKit/Source/core/loader/CookieJar.cpp
@@ -61,6 +61,7 @@
   WebCookieJar* cookie_jar = ToCookieJar(document);
   if (!cookie_jar)
     return;
+  SCOPED_BLINK_UMA_HISTOGRAM_TIMER("Blink.CookieJar.SyncCookiesSetTime");
   cookie_jar->SetCookie(url, document->SiteForCookies(), cookie_string);
 }
 
diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py
index 1dede1d..8bab466a 100644
--- a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py
+++ b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py
@@ -470,8 +470,15 @@
             description += 'Build: %s\n\n' % build_link
 
         description += (
-            'Note to sheriffs: This is an automatically-generated CL. Please\n'
-            'contact ecosystem-infra@chromium.org in case of problems.\n\n')
+            'Note to sheriffs: This CL imports external tests and adds\n'
+            'expectations for those tests; if this CL is large and causes\n'
+            'a few new failures, please fix the failures by adding new\n'
+            'lines to TestExpectations rather than reverting. See:\n'
+            'https://chromium.googlesource.com'
+            '/chromium/src/+/master/docs/testing/web_platform_tests.md\n\n')
+
+        if directory_owners:
+            description += self._format_directory_owners(directory_owners) + '\n\n'
 
         # Move any No-Export tag to the end of the description.
         description = description.replace('No-Export: true', '')
diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer_unittest.py b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer_unittest.py
index 44d891c..0015fc6 100644
--- a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer_unittest.py
+++ b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer_unittest.py
@@ -4,7 +4,6 @@
 
 import datetime
 import json
-import unittest
 
 from webkitpy.common.checkout.git_mock import MockGit
 from webkitpy.common.host_mock import MockHost
@@ -268,8 +267,12 @@
         self.assertEqual(
             description,
             'Last commit message\n\n'
-            'Note to sheriffs: This is an automatically-generated CL. Please\n'
-            'contact ecosystem-infra@chromium.org in case of problems.\n\n'
+            'Note to sheriffs: This CL imports external tests and adds\n'
+            'expectations for those tests; if this CL is large and causes\n'
+            'a few new failures, please fix the failures by adding new\n'
+            'lines to TestExpectations rather than reverting. See:\n'
+            'https://chromium.googlesource.com'
+            '/chromium/src/+/master/docs/testing/web_platform_tests.md\n\n'
             'No-Export: true')
         self.assertEqual(host.executive.calls, [['git', 'log', '-1', '--format=%B']])
 
@@ -295,7 +298,6 @@
             'No-Export: true',
             description)
 
-    @unittest.skip("http://crbug.com/780055")
     def test_cl_description_with_directory_owners(self):
         host = MockHost()
         host.executive = MockExecutive(output='Last commit message\n\n')
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index 464b6766..4f5e1c8 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -6572,6 +6572,12 @@
   </summary>
 </histogram>
 
+<histogram name="Blink.CookieJar.SyncCookiesSetTime" units="microseconds">
+  <owner>kinuko@chromium.org</owner>
+  <owner>dcheng@chromium.org</owner>
+  <summary>Microseconds per sync IPC call to set cookies.</summary>
+</histogram>
+
 <histogram name="Blink.CookieJar.SyncCookiesTime" units="microseconds">
   <owner>kinuko@chromium.org</owner>
   <owner>dcheng@chromium.org</owner>