diff --git a/garnet/bin/display_capture_test/BUILD.gn b/garnet/bin/display_capture_test/BUILD.gn
index 6526f7bb24381959d6bbf029a2da10eaa33b3587..aa25a07ba7c3bf1edad4f156cba23ff946fae702 100644
--- a/garnet/bin/display_capture_test/BUILD.gn
+++ b/garnet/bin/display_capture_test/BUILD.gn
@@ -29,6 +29,7 @@ executable("bin") {
     "//garnet/public/lib/fsl",
     "//zircon/public/fidl/fuchsia-hardware-camera:fuchsia-hardware-camera_c",
     "//zircon/public/fidl/fuchsia-hardware-display",
+    "//zircon/public/fidl/fuchsia-hardware-display:fuchsia-hardware-display_c",
     "//zircon/public/lib/async-loop-cpp",
     "//zircon/public/lib/fzl",
   ]
diff --git a/garnet/bin/display_capture_test/runner.cc b/garnet/bin/display_capture_test/runner.cc
index 7a30e3326eae7c90e1516b8866871906f0e6067e..80bcfc7821f363b0422585e848e4f7a54289343e 100644
--- a/garnet/bin/display_capture_test/runner.cc
+++ b/garnet/bin/display_capture_test/runner.cc
@@ -8,8 +8,10 @@
 #include <fbl/unique_fd.h>
 #include <fcntl.h>
 #include <fuchsia/hardware/camera/c/fidl.h>
+#include <fuchsia/hardware/display/c/fidl.h>
 #include <lib/fzl/fdio.h>
 #include <zircon/device/display-controller.h>
+
 #include <cmath>
 
 namespace display_test {
@@ -204,19 +206,38 @@ void Runner::GetFormatCallback(::std::vector<fuchsia::camera::VideoFormat> fmts,
 
 void Runner::InitDisplay() {
   zx_status_t status;
-  fxl::UniqueFD fd(open(kDisplayController, O_RDWR));
+  fbl::unique_fd fd(open(kDisplayController, O_RDWR));
   if (!fd.is_valid()) {
     ZX_ASSERT_MSG(false, "Failed to open display controller");
   }
 
-  zx::channel dc_handle;
-  if (ioctl_display_controller_get_handle(
-          fd.get(), dc_handle.reset_and_get_address()) != sizeof(zx_handle_t)) {
-    ZX_ASSERT_MSG(false, "Failed to obtain display controller handle");
+  zx::channel device_server, device_client;
+  status = zx::channel::create(0, &device_server, &device_client);
+  if (status != ZX_OK) {
+    ZX_ASSERT_MSG(false, "Failed to create device channel %d\n", status);
+  }
+
+  zx::channel dc_server, dc_client;
+  status = zx::channel::create(0, &dc_server, &dc_client);
+  if (status != ZX_OK) {
+    ZX_ASSERT_MSG(false, "Failed to get create controller channel %d\n",
+                  status);
+  }
+
+  fzl::FdioCaller dev(std::move(fd));
+  zx_status_t fidl_status = fuchsia_hardware_display_ProviderOpenController(
+      dev.borrow_channel(), device_server.release(), dc_server.release(),
+      &status);
+  if (fidl_status != ZX_OK) {
+    ZX_ASSERT_MSG(false, "Failed to call service handle %d\n", status);
   }
+  if (status != ZX_OK) {
+    ZX_ASSERT_MSG(false, "Failed to open controller %d\n", status);
+  }
+
+  display_controller_conn_ = std::move(device_client);
 
-  dc_fd_ = std::move(fd);
-  if ((status = display_controller_.Bind(std::move(dc_handle),
+  if ((status = display_controller_.Bind(std::move(dc_client),
                                          loop_->dispatcher())) != ZX_OK) {
     ZX_ASSERT_MSG(false, "Failed to bind to display controller %d\n", status);
   }
diff --git a/garnet/bin/display_capture_test/runner.h b/garnet/bin/display_capture_test/runner.h
index 282dc693feb5007d05b46697461c93efea575e39..7493622ba1756ca76067852f0e2c69f70b8bd205 100644
--- a/garnet/bin/display_capture_test/runner.h
+++ b/garnet/bin/display_capture_test/runner.h
@@ -11,14 +11,15 @@
 #include <lib/async-loop/cpp/loop.h>
 #include <lib/fidl/cpp/synchronous_interface_ptr.h>
 #include <lib/fsl/io/device_watcher.h>
-#include "src/lib/files/unique_fd.h"
 #include <zircon/pixelformat.h>
 #include <zircon/types.h>
+
 #include <deque>
 
 #include "context.h"
 #include "image.h"
 #include "layer.h"
+#include "src/lib/files/unique_fd.h"
 
 namespace display_test {
 
@@ -61,9 +62,8 @@ class Runner {
   void FrameNotifyCallback(const fuchsia::camera::FrameAvailableEvent& resp);
 
   void InitDisplay();
-  void OnDisplaysChanged(
-      ::std::vector<fuchsia::hardware::display::Info> added,
-      ::std::vector<uint64_t> removed);
+  void OnDisplaysChanged(::std::vector<fuchsia::hardware::display::Info> added,
+                         ::std::vector<uint64_t> removed);
   void OnClientOwnershipChange(bool is_owner);
   void OnVsync(uint64_t display_id, uint64_t timestamp,
                ::std::vector<uint64_t> image_ids);
@@ -82,7 +82,7 @@ class Runner {
   PrimaryLayer* calibration_layer_;
 
   const char* display_name_;
-  fxl::UniqueFD dc_fd_;
+  zx::channel display_controller_conn_;
   fuchsia::hardware::display::ControllerPtr display_controller_;
   uint64_t display_id_ = 0;
 
diff --git a/garnet/lib/ui/gfx/BUILD.gn b/garnet/lib/ui/gfx/BUILD.gn
index 8395d6a1163aa893476795441760d2336f7e2edb..6e44e040dcf617ea02c959b241ba24362c017dbc 100644
--- a/garnet/lib/ui/gfx/BUILD.gn
+++ b/garnet/lib/ui/gfx/BUILD.gn
@@ -220,6 +220,7 @@ source_set("gfx") {
     "//sdk/lib/sys/cpp",
     "//src/lib/fxl",
     "//zircon/public/fidl/fuchsia-hardware-display",
+    "//zircon/public/fidl/fuchsia-hardware-display:fuchsia-hardware-display_c",
     "//zircon/public/lib/fit",
   ]
 
@@ -231,6 +232,8 @@ source_set("gfx") {
     "//garnet/public/lib/ui/scenic/cpp",
     "//zircon/public/fidl/fuchsia-scheduler",
     "//zircon/public/fidl/fuchsia-sysmem",
+    "//zircon/public/lib/fbl",
+    "//zircon/public/lib/fzl",
     "//zircon/public/lib/trace",
   ]
 }
diff --git a/garnet/lib/ui/gfx/displays/display_manager.cc b/garnet/lib/ui/gfx/displays/display_manager.cc
index b1d618aa1014a2eeb91bdc93a797adaebcc1a055..88ee6b8704e223765e72be02d53cec37ac7079b7 100644
--- a/garnet/lib/ui/gfx/displays/display_manager.cc
+++ b/garnet/lib/ui/gfx/displays/display_manager.cc
@@ -8,6 +8,7 @@
 #include <lib/fdio/directory.h>
 #include <trace/event.h>
 #include <zircon/syscalls.h>
+
 #include "fuchsia/ui/scenic/cpp/fidl.h"
 
 namespace scenic_impl {
@@ -34,8 +35,8 @@ void DisplayManager::WaitForDefaultDisplay(fit::closure callback) {
 
   display_available_cb_ = std::move(callback);
   display_watcher_.WaitForDisplay(
-      [this](fxl::UniqueFD fd, zx::channel dc_handle) {
-        dc_fd_ = std::move(fd);
+      [this](zx::channel device, zx::channel dc_handle) {
+        dc_device_ = std::move(device);
         dc_channel_ = dc_handle.get();
         display_controller_.Bind(std::move(dc_handle));
 
diff --git a/garnet/lib/ui/gfx/displays/display_manager.h b/garnet/lib/ui/gfx/displays/display_manager.h
index 30327dcbdca9c661fa7c8504ba6d1fa385dd95a4..bd4a0856154f0cbde84c2483b9dd2ad2e34a1923 100644
--- a/garnet/lib/ui/gfx/displays/display_manager.h
+++ b/garnet/lib/ui/gfx/displays/display_manager.h
@@ -5,6 +5,10 @@
 #ifndef GARNET_LIB_UI_GFX_DISPLAYS_DISPLAY_MANAGER_H_
 #define GARNET_LIB_UI_GFX_DISPLAYS_DISPLAY_MANAGER_H_
 
+#include <lib/fit/function.h>
+#include <lib/zx/event.h>
+#include <zircon/pixelformat.h>
+
 #include <cstdint>
 
 #include "fuchsia/hardware/display/cpp/fidl.h"
@@ -14,10 +18,6 @@
 #include "lib/async/cpp/wait.h"
 #include "src/lib/fxl/macros.h"
 
-#include <lib/fit/function.h>
-#include <zircon/pixelformat.h>
-#include <lib/zx/event.h>
-
 namespace scenic_impl {
 namespace gfx {
 
@@ -91,7 +91,7 @@ class DisplayManager {
                        ::std::vector<uint64_t> removed);
   void ClientOwnershipChange(bool has_ownership);
 
-  fxl::UniqueFD dc_fd_;
+  zx::channel dc_device_;
   fuchsia::hardware::display::ControllerSyncPtr display_controller_;
   fidl::InterfacePtr<fuchsia::hardware::display::Controller> event_dispatcher_;
   zx_handle_t dc_channel_;  // display_controller_ owns the zx::channel
diff --git a/garnet/lib/ui/gfx/displays/display_watcher.cc b/garnet/lib/ui/gfx/displays/display_watcher.cc
index 8176becd6d844c87948b4a3b2f6a2ca8eb51191b..7f5b33dd359cd25fd768f2cc15d58c9b1fd3e830 100644
--- a/garnet/lib/ui/gfx/displays/display_watcher.cc
+++ b/garnet/lib/ui/gfx/displays/display_watcher.cc
@@ -4,13 +4,13 @@
 
 #include "garnet/lib/ui/gfx/displays/display_watcher.h"
 
+#include <fbl/unique_fd.h>
 #include <fcntl.h>
-
+#include <fuchsia/hardware/display/c/fidl.h>
 #include <lib/fidl/cpp/message.h>
-#include <zircon/device/display-controller.h>
-
-#include "src/lib/fxl/logging.h"
-#include "src/lib/files/unique_fd.h"
+#include <lib/fzl/fdio.h>
+#include <src/lib/fxl/logging.h>
+#include <zircon/status.h>
 
 namespace scenic_impl {
 namespace gfx {
@@ -39,22 +39,49 @@ void DisplayWatcher::HandleDevice(DisplayReadyCallback callback, int dir_fd,
 
   FXL_LOG(INFO) << "Scenic: Acquired display controller " << path << ".("
                 << filename << ")";
-  fxl::UniqueFD fd(open(path.c_str(), O_RDWR));
+  fbl::unique_fd fd(open(path.c_str(), O_RDWR));
   if (!fd.is_valid()) {
     FXL_DLOG(ERROR) << "Failed to open " << path << ": errno=" << errno;
-    callback(fxl::UniqueFD(), zx::channel());
+    callback(zx::channel(), zx::channel());
+    return;
+  }
+
+  zx::channel device_server, device_client;
+  zx_status_t status = zx::channel::create(0, &device_server, &device_client);
+  if (status != ZX_OK) {
+    FXL_DLOG(ERROR) << "Failed to create device channel: "
+                    << zx_status_get_string(status);
+    callback(zx::channel(), zx::channel());
     return;
   }
 
-  zx::channel dc_handle;
-  if (ioctl_display_controller_get_handle(
-          fd.get(), dc_handle.reset_and_get_address()) != sizeof(zx_handle_t)) {
-    FXL_DLOG(ERROR) << "Failed to get device channel";
-    callback(fxl::UniqueFD(), zx::channel());
+  zx::channel dc_server, dc_client;
+  status = zx::channel::create(0, &dc_server, &dc_client);
+  if (status != ZX_OK) {
+    FXL_DLOG(ERROR) << "Failed to create controller channel: "
+                    << zx_status_get_string(status);
+    callback(zx::channel(), zx::channel());
+    return;
+  }
+
+  fzl::FdioCaller caller(std::move(fd));
+  zx_status_t fidl_status = fuchsia_hardware_display_ProviderOpenController(
+      caller.borrow_channel(), device_server.release(), dc_server.release(),
+      &status);
+  if (fidl_status != ZX_OK) {
+    FXL_DLOG(ERROR) << "Failed to call service handle: "
+                    << zx_status_get_string(fidl_status);
+    callback(zx::channel(), zx::channel());
+    return;
+  }
+  if (status != ZX_OK) {
+    FXL_DLOG(ERROR) << "Failed to open controller : "
+                    << zx_status_get_string(status);
+    callback(zx::channel(), zx::channel());
     return;
   }
 
-  callback(std::move(fd), std::move(dc_handle));
+  callback(std::move(device_client), std::move(dc_client));
 }
 
 }  // namespace gfx
diff --git a/garnet/lib/ui/gfx/displays/display_watcher.h b/garnet/lib/ui/gfx/displays/display_watcher.h
index 777a32b34d82080ca271084a12c72f841fc51d14..b647ea0a68e3f1ea72c126ca1442a5f2f6900712 100644
--- a/garnet/lib/ui/gfx/displays/display_watcher.h
+++ b/garnet/lib/ui/gfx/displays/display_watcher.h
@@ -5,13 +5,10 @@
 #ifndef GARNET_LIB_UI_GFX_DISPLAYS_DISPLAY_WATCHER_H_
 #define GARNET_LIB_UI_GFX_DISPLAYS_DISPLAY_WATCHER_H_
 
-#include <memory>
-
 #include <lib/fit/function.h>
-
-#include "lib/fsl/io/device_watcher.h"
-#include "src/lib/fxl/macros.h"
-#include "lib/zx/event.h"
+#include <lib/fsl/io/device_watcher.h>
+#include <memory>
+#include <src/lib/fxl/macros.h>
 
 namespace scenic_impl {
 namespace gfx {
@@ -20,10 +17,10 @@ namespace gfx {
 // attributes through a callback.
 class DisplayWatcher {
  public:
-  // Callback that accepts display metrics.
-  // |metrics| may be null if the display was not successfully acquired.
+  // Callback provides channels to the display controller device and FIDL
+  // interface.
   using DisplayReadyCallback =
-      fit::function<void(fxl::UniqueFD fd, zx::channel dc_handle)>;
+      fit::function<void(zx::channel device, zx::channel controller)>;
 
   DisplayWatcher();
   ~DisplayWatcher();
diff --git a/garnet/lib/ui/gfx/tests/meta/mock_pose_buffer_provider.cmx b/garnet/lib/ui/gfx/tests/meta/mock_pose_buffer_provider.cmx
index b548c5ddb84983e3f51243a0bec59801b5734b79..3540a15e47be387f23029ffeabdf467fa6438f1e 100644
--- a/garnet/lib/ui/gfx/tests/meta/mock_pose_buffer_provider.cmx
+++ b/garnet/lib/ui/gfx/tests/meta/mock_pose_buffer_provider.cmx
@@ -3,6 +3,9 @@
         "binary": "bin/app"
     },
     "sandbox": {
+        "dev": [
+            "class/display-controller"
+        ],
         "services": [
             "fuchsia.tracelink.Registry",
             "fuchsia.ui.scenic.Scenic"
diff --git a/garnet/lib/vulkan/src/swapchain/BUILD.gn b/garnet/lib/vulkan/src/swapchain/BUILD.gn
index cb48a303c74b2e16a01fb572606f90909da9ba7c..04136c3a22a81cae161519684a648a9461de7271 100644
--- a/garnet/lib/vulkan/src/swapchain/BUILD.gn
+++ b/garnet/lib/vulkan/src/swapchain/BUILD.gn
@@ -55,10 +55,13 @@ loadable_module("fb") {
     "$loader_build_root:extra_vulkan_headers",
     "$loader_build_root/layers:micro_layer_common",
     "//zircon/public/fidl/fuchsia-hardware-display",
+    "//zircon/public/fidl/fuchsia-hardware-display:fuchsia-hardware-display_c",
     "//zircon/public/fidl/fuchsia-sysmem",
     "//zircon/public/lib/async-cpp",
     "//zircon/public/lib/async-loop-cpp",
+    "//zircon/public/lib/fbl",
     "//zircon/public/lib/fdio",
+    "//zircon/public/lib/fzl",
   ]
   ldflags = [ "-static-libstdc++" ]
 }
diff --git a/garnet/lib/vulkan/src/swapchain/image_pipe_surface_display.cc b/garnet/lib/vulkan/src/swapchain/image_pipe_surface_display.cc
index 2f550f4b565dcff20fe9d092be87745fd2fabceb..30d0a1cb24f86fbcabb68d91e29a05eac7206186 100644
--- a/garnet/lib/vulkan/src/swapchain/image_pipe_surface_display.cc
+++ b/garnet/lib/vulkan/src/swapchain/image_pipe_surface_display.cc
@@ -3,17 +3,22 @@
 // found in the LICENSE file.
 
 #include "image_pipe_surface_display.h"
+
+#include <fbl/unique_fd.h>
 #include <fcntl.h>
+#include <fuchsia/hardware/display/c/fidl.h>
 #include <lib/async/cpp/task.h>
 #include <lib/fdio/directory.h>
-#include <zircon/device/display-controller.h>
+#include <lib/fzl/fdio.h>
+#include <limits.h>
 #include <zircon/pixelformat.h>
+#include <zircon/status.h>
+
 #include <cstdio>
+
 #include "vk_dispatch_table_helper.h"
 #include "vulkan/vk_layer.h"
 
-#include <limits.h>
-
 namespace image_pipe_swapchain {
 
 ImagePipeSurfaceDisplay::ImagePipeSurfaceDisplay()
@@ -28,24 +33,47 @@ bool ImagePipeSurfaceDisplay::Init() {
     fprintf(stderr, "Couldn't connect to sysmem service\n");
     return false;
   }
-  int dc_fd = open("/dev/class/display-controller/000", O_RDWR);
-  if (dc_fd < 0) {
+
+  fbl::unique_fd fd(open("/dev/class/display-controller/000", O_RDWR));
+  if (!fd) {
     fprintf(stderr, "No display controller\n");
     return false;
   }
 
-  zx_handle_t dc_handle;
+  zx::channel device_server, device_client;
+  status = zx::channel::create(0, &device_server, &device_client);
+  if (status != ZX_OK) {
+    fprintf(stderr, "Failed to create device channel %d (%s)\n", status,
+            zx_status_get_string(status));
+    return false;
+  }
 
-  if (ioctl_display_controller_get_handle(dc_fd, &dc_handle) !=
-      sizeof(zx_handle_t)) {
-    close(dc_fd);
-    fprintf(stderr, "No display controller 2\n");
+  zx::channel dc_server, dc_client;
+  status = zx::channel::create(0, &dc_server, &dc_client);
+  if (status != ZX_OK) {
+    fprintf(stderr, "Failed to create controller channel %d (%s)\n", status,
+            zx_status_get_string(status));
     return false;
   }
 
-  dc_fd_ = dc_fd;
+  fzl::FdioCaller caller(std::move(fd));
+  zx_status_t fidl_status = fuchsia_hardware_display_ProviderOpenController(
+      caller.borrow_channel(), device_server.release(), dc_server.release(),
+      &status);
+  if (fidl_status != ZX_OK) {
+    fprintf(stderr, "Failed to call service handle %d (%s)\n", fidl_status,
+            zx_status_get_string(fidl_status));
+    return false;
+  }
+  if (status != ZX_OK) {
+    fprintf(stderr, "Failed to open controller %d (%s)\n", status,
+            zx_status_get_string(status));
+    return false;
+  }
+
+  dc_device_ = std::move(device_client);
 
-  display_controller_.Bind(zx::channel(dc_handle), loop_.dispatcher());
+  display_controller_.Bind(std::move(dc_client), loop_.dispatcher());
 
   display_controller_.set_error_handler(
       fit::bind_member(this, &ImagePipeSurfaceDisplay::ControllerError));
@@ -60,11 +88,6 @@ bool ImagePipeSurfaceDisplay::Init() {
   return true;
 }
 
-ImagePipeSurfaceDisplay::~ImagePipeSurfaceDisplay() {
-  if (dc_fd_ >= 0)
-    close(dc_fd_);
-}
-
 void ImagePipeSurfaceDisplay::ControllerError(zx_status_t status) {
   display_connection_exited_ = true;
 }
diff --git a/garnet/lib/vulkan/src/swapchain/image_pipe_surface_display.h b/garnet/lib/vulkan/src/swapchain/image_pipe_surface_display.h
index 63cc1324c946ecb5febbf6fbfd0c3ce5fd3825f3..bde3f2da686a4388070c7e8ffacf32f64aaee41c 100644
--- a/garnet/lib/vulkan/src/swapchain/image_pipe_surface_display.h
+++ b/garnet/lib/vulkan/src/swapchain/image_pipe_surface_display.h
@@ -5,12 +5,13 @@
 #ifndef GARNET_LIB_VULKAN_SRC_SWAPCHAIN_IMAGE_PIPE_SURFACE_DISPLAY_H_
 #define GARNET_LIB_VULKAN_SRC_SWAPCHAIN_IMAGE_PIPE_SURFACE_DISPLAY_H_
 
+#include <lib/async-loop/cpp/loop.h>
+
 #include <map>
-#include "image_pipe_surface.h"
 
-#include <lib/async-loop/cpp/loop.h>
-#include "fuchsia/hardware/display/cpp/fidl.h"
-#include "fuchsia/sysmem/cpp/fidl.h"
+#include <fuchsia/hardware/display/cpp/fidl.h>
+#include <fuchsia/sysmem/cpp/fidl.h>
+#include "image_pipe_surface.h"
 
 namespace image_pipe_swapchain {
 
@@ -18,7 +19,6 @@ namespace image_pipe_swapchain {
 class ImagePipeSurfaceDisplay : public ImagePipeSurface {
  public:
   ImagePipeSurfaceDisplay();
-  ~ImagePipeSurfaceDisplay() override;
 
   bool Init() override;
 
@@ -50,7 +50,7 @@ class ImagePipeSurfaceDisplay : public ImagePipeSurface {
   async::Loop loop_;
   std::map<uint64_t, uint64_t> image_id_map;
 
-  int dc_fd_ = -1;
+  zx::channel dc_device_;
   bool display_connection_exited_ = false;
   bool got_message_response_ = false;
   bool have_display_ = false;
diff --git a/garnet/public/rust/fuchsia-framebuffer/BUILD.gn b/garnet/public/rust/fuchsia-framebuffer/BUILD.gn
index fb9cb82abc3258c197bbf207144cf6f7340e8205..ac44d9a1878f317f152b267aac1d35ec846574e8 100644
--- a/garnet/public/rust/fuchsia-framebuffer/BUILD.gn
+++ b/garnet/public/rust/fuchsia-framebuffer/BUILD.gn
@@ -9,6 +9,7 @@ rustc_library("fuchsia-framebuffer") {
   version = "0.1.0"
   edition = "2018"
   deps = [
+    "//garnet/public/lib/fidl/rust/fidl",
     "//garnet/public/rust/fdio",
     "//garnet/public/rust/fuchsia-async",
     "//garnet/public/rust/fuchsia-runtime",
diff --git a/garnet/public/rust/fuchsia-framebuffer/src/lib.rs b/garnet/public/rust/fuchsia-framebuffer/src/lib.rs
index effa586a401d769f569f4bdbe69925a7675db3b5..b660246a6890663dc88903df6a428489c961eb6a 100644
--- a/garnet/public/rust/fuchsia-framebuffer/src/lib.rs
+++ b/garnet/public/rust/fuchsia-framebuffer/src/lib.rs
@@ -5,27 +5,26 @@
 #![allow(dead_code)]
 
 use failure::{format_err, Error, ResultExt};
-use fdio::fdio_sys::{fdio_ioctl, IOCTL_FAMILY_DISPLAY_CONTROLLER, IOCTL_KIND_GET_HANDLE};
-use fdio::make_ioctl;
 use fdio::watch_directory;
-use fidl_fuchsia_hardware_display::{ControllerEvent, ControllerProxy, ImageConfig, ImagePlane};
+use fidl::endpoints;
+use fidl_fuchsia_hardware_display::{
+    ControllerEvent, ControllerMarker, ControllerProxy, ImageConfig, ImagePlane,
+    ProviderSynchronousProxy,
+};
 use fuchsia_async as fasync;
 use fuchsia_runtime::vmar_root_self;
 use fuchsia_zircon::{
     self as zx,
     sys::{
-        zx_cache_flush, zx_cache_policy_t::ZX_CACHE_POLICY_WRITE_COMBINING, zx_handle_t,
+        zx_cache_flush, zx_cache_policy_t::ZX_CACHE_POLICY_WRITE_COMBINING,
         ZX_CACHE_FLUSH_DATA, ZX_TIME_INFINITE,
     },
-    Handle, VmarFlags, Vmo,
+    VmarFlags, Vmo,
 };
 use futures::{future, StreamExt, TryFutureExt, TryStreamExt};
 use shared_buffer::SharedBuffer;
 use std::cell::RefCell;
-use std::fs::{File, OpenOptions};
-use std::mem;
-use std::os::unix::io::AsRawFd;
-use std::ptr;
+use std::fs::OpenOptions;
 use std::rc::Rc;
 use std::{thread, time};
 
@@ -295,38 +294,13 @@ impl Drop for Frame {
 }
 
 pub struct FrameBuffer {
-    display_controller: File,
+    display_controller: zx::Channel,
     controller: ControllerProxy,
     config: Config,
     layer_id: u64,
 }
 
 impl FrameBuffer {
-    fn get_display_handle(file: &File) -> Result<Handle, Error> {
-        let fd = file.as_raw_fd() as i32;
-        let ioctl_display_controller_get_handle =
-            make_ioctl(IOCTL_KIND_GET_HANDLE, IOCTL_FAMILY_DISPLAY_CONTROLLER, 1);
-        let mut display_handle: zx_handle_t = 0;
-        let display_handle_ptr: *mut std::os::raw::c_void =
-            &mut display_handle as *mut _ as *mut std::os::raw::c_void;
-        let result_size = unsafe {
-            fdio_ioctl(
-                fd,
-                ioctl_display_controller_get_handle,
-                ptr::null(),
-                0,
-                display_handle_ptr,
-                mem::size_of::<zx_handle_t>(),
-            )
-        };
-
-        if result_size != mem::size_of::<zx_handle_t>() as isize {
-            return Err(format_err!("ioctl_display_controller_get_handle failed: {}", result_size));
-        }
-
-        Ok(unsafe { Handle::from_raw(display_handle) })
-    }
-
     fn create_config_from_event_stream(
         proxy: &ControllerProxy,
         executor: &mut fasync::Executor,
@@ -445,14 +419,23 @@ impl FrameBuffer {
             first_path.unwrap()
         };
         let file = OpenOptions::new().read(true).write(true).open(device_path)?;
-        let zx_handle = Self::get_display_handle(&file)?;
-        let channel = fasync::Channel::from_channel(zx_handle.into())?;
-        let proxy = ControllerProxy::new(channel);
+
+        let channel = fdio::clone_channel(&file)?;
+        let mut provider = ProviderSynchronousProxy::new(channel);
+
+        let (device_client, device_server) = zx::Channel::create()?;
+        let (dc_client, dc_server) = endpoints::create_endpoints::<ControllerMarker>()?;
+        let status = provider.open_controller(device_server, dc_server, zx::Time::INFINITE)?;
+        if status != zx::sys::ZX_OK {
+            return Err(format_err!("Failed to open display controller"));
+        }
+
+        let proxy = dc_client.into_proxy()?;
         let config = Self::create_config_from_event_stream(&proxy, executor)?;
         let layer = Self::configure_layer(config, &proxy, executor)?;
 
         Ok(FrameBuffer {
-            display_controller: file,
+            display_controller: device_client,
             controller: proxy,
             config: config,
             layer_id: layer,
diff --git a/garnet/tests/gfxlatency/main.cpp b/garnet/tests/gfxlatency/main.cpp
index 8f364a35d0748aff08050574b761789265c57bef..d78aa0878415c8cd08ea04316ef3a7481b813c7c 100644
--- a/garnet/tests/gfxlatency/main.cpp
+++ b/garnet/tests/gfxlatency/main.cpp
@@ -34,6 +34,7 @@
 #include <zircon/process.h>
 #include <zircon/syscalls.h>
 #include <zircon/types.h>
+#include <zircon/status.h>
 
 #include <utility>
 
@@ -814,19 +815,38 @@ int main(int argc, char* argv[]) {
     }
   }
 
-  int32_t dc_fd = open("/dev/class/display-controller/000", O_RDWR);
-  if (dc_fd < 0) {
+  fbl::unique_fd fd(open("/dev/class/display-controller/000", O_RDWR));
+  if (!fd) {
     fprintf(stderr, "failed to open display controller\n");
     return -1;
   }
 
-  if (ioctl_display_controller_get_handle(dc_fd, &dc_handle) !=
-      sizeof(zx_handle_t)) {
-    fprintf(stderr, "failed to get display controller handle\n");
+  zx::channel device_server, device_client;
+  zx_status_t status = zx::channel::create(0, &device_server, &device_client);
+  if (status != ZX_OK) {
+    fprintf(stderr, "failed to create device channel %d (%s)\n", status, zx_status_get_string(status));
+    return -1;
+  }
+  zx::channel dc_server, dc_client;
+  status = zx::channel::create(0, &dc_server, &dc_client);
+  if (status != ZX_OK) {
+    fprintf(stderr, "failed to create controller channel %d (%s)\n", status, zx_status_get_string(status));
     return -1;
   }
 
-  zx_status_t status;
+  fzl::FdioCaller caller(std::move(fd));
+  zx_status_t fidl_status = fuchsia_hardware_display_ProviderOpenController(
+      caller.borrow_channel(), device_server.release(), dc_server.release(), &status);
+  if (fidl_status != ZX_OK) {
+    fprintf(stderr, "failed to call service handle %d (%s)\n", fidl_status, zx_status_get_string(fidl_status));
+    return -1;
+  }
+  if (status != ZX_OK) {
+    fprintf(stderr, "failed to open controller %d (%s)\n", status, zx_status_get_string(status));
+    return -1;
+  }
+  dc_handle = dc_client.release();
+
   uint8_t bytes[ZX_CHANNEL_MAX_MSG_BYTES];
   uint32_t actual_bytes, actual_handles;
   bool has_display = false;
@@ -1998,6 +2018,5 @@ int main(int argc, char* argv[]) {
   if (touchpadfd >= 0)
     close(touchpadfd);
   zx_handle_close(dc_handle);
-  close(dc_fd);
   return 0;
 }
diff --git a/zircon/system/core/virtcon/vc-display.cpp b/zircon/system/core/virtcon/vc-display.cpp
index 0212e9c72ee2310713ed992ddaab4c30541e3363..71986ad51de9ef7f881b198befb5501357bd30c1 100644
--- a/zircon/system/core/virtcon/vc-display.cpp
+++ b/zircon/system/core/virtcon/vc-display.cpp
@@ -5,6 +5,7 @@
 #include <fbl/unique_fd.h>
 #include <fcntl.h>
 #include <fuchsia/io/c/fidl.h>
+#include <lib/fdio/fd.h>
 #include <lib/fdio/io.h>
 #include <lib/fidl/coding.h>
 #include <lib/fzl/fdio.h>
@@ -25,7 +26,7 @@
 static constexpr const char* kDisplayControllerDir = "/dev/class/display-controller";
 
 static int dc_dir_fd;
-static int dc_fd;
+static zx_handle_t dc_device;
 
 // At any point, |dc_ph| will either be waiting on the display controller device directory
 // for a display controller instance or it will be waiting on a display controller interface
@@ -568,7 +569,7 @@ static zx_status_t dc_callback_handler(port_handler_t* ph, zx_signals_t signals,
             handle_display_removed(list_peek_head_type(&display_list, display_info_t, node)->id);
         }
 
-        close(dc_fd);
+        zx_handle_close(dc_device);
         zx_handle_close(dc_ph.handle);
 
         vc_find_display_controller();
@@ -618,30 +619,45 @@ static zx_status_t vc_dc_event(uint32_t evt, const char* name) {
         return ZX_OK;
     }
 
-    printf("vc: new display device %s/%s/virtcon\n", kDisplayControllerDir, name);
+    printf("vc: new display device %s/%s\n", kDisplayControllerDir, name);
 
     char buf[64];
-    snprintf(buf, 64, "%s/%s/virtcon", kDisplayControllerDir, name);
+    snprintf(buf, 64, "%s/%s", kDisplayControllerDir, name);
     fbl::unique_fd fd(open(buf, O_RDWR));
     if (!fd) {
         printf("vc: failed to open display controller device\n");
         return ZX_OK;
     }
 
-    zx::channel dc_channel;
-    if (ioctl_display_controller_get_handle(fd.get(), dc_channel.reset_and_get_address())
-            != sizeof(zx_handle_t)) {
-        printf("vc: failed to get display controller handle\n");
-        return ZX_OK;
+    zx::channel device_server, device_client;
+    zx_status_t status = zx::channel::create(0, &device_server, &device_client);
+    if (status != ZX_OK) {
+        return status;
+    }
+
+    zx::channel dc_server, dc_client;
+    status = zx::channel::create(0, &dc_server, &dc_client);
+    if (status != ZX_OK) {
+        return status;
+    }
+
+    fzl::FdioCaller caller(std::move(fd));
+    zx_status_t fidl_status = fuchsia_hardware_display_ProviderOpenVirtconController(
+        caller.borrow_channel(), device_server.release(), dc_server.release(), &status);
+    if (fidl_status != ZX_OK) {
+        return fidl_status;
+    }
+    if (status != ZX_OK) {
+        return status;
     }
 
+    dc_device = device_client.release();
     zx_handle_close(dc_ph.handle);
-    dc_fd = fd.release();
-    dc_ph.handle = dc_channel.release();
+    dc_ph.handle = dc_client.release();
 
-    zx_status_t status = vc_set_mode(getenv("virtcon.hide-on-boot") == nullptr
-                                         ? fuchsia_hardware_display_VirtconMode_FALLBACK
-                                         : fuchsia_hardware_display_VirtconMode_INACTIVE);
+    status = vc_set_mode(getenv("virtcon.hide-on-boot") == nullptr
+                             ? fuchsia_hardware_display_VirtconMode_FALLBACK
+                             : fuchsia_hardware_display_VirtconMode_INACTIVE);
     if (status != ZX_OK) {
         printf("vc: Failed to set initial ownership %d\n", status);
         vc_find_display_controller();
diff --git a/zircon/system/dev/display/display/BUILD.gn b/zircon/system/dev/display/display/BUILD.gn
index 962fa9ecd469c302fb3e930ac267aff7b578a78d..5417b8a7b02b0e4e80b87bb76d2235c4c6e8d5eb 100644
--- a/zircon/system/dev/display/display/BUILD.gn
+++ b/zircon/system/dev/display/display/BUILD.gn
@@ -23,6 +23,7 @@ driver("display") {
     "$zx/system/ulib/edid",
     "$zx/system/ulib/fbl",
     "$zx/system/ulib/fidl",
+    "$zx/system/ulib/fidl-utils",
     "$zx/system/ulib/hwreg",
     "$zx/system/ulib/image-format",
     "$zx/system/ulib/trace:trace-driver",
diff --git a/zircon/system/dev/display/display/client.cpp b/zircon/system/dev/display/display/client.cpp
index 19fef600bce89f3c84bec6b797a25b5f188092a5..75759f7f29b93cb6e77f44ac770468d4c094795e 100644
--- a/zircon/system/dev/display/display/client.cpp
+++ b/zircon/system/dev/display/display/client.cpp
@@ -1864,7 +1864,7 @@ void ClientProxy::OnDisplayVsync(uint64_t display_id, zx_time_t timestamp,
 
     memcpy(msg + 1, image_ids, sizeof(uint64_t) * count);
 
-    zx_status_t status = server_handle_.write(0, data, size, nullptr, 0);
+    zx_status_t status = server_channel_.write(0, data, size, nullptr, 0);
     if (status != ZX_OK) {
         zxlogf(WARN, "Failed to send vsync event %d\n", status);
     }
@@ -1874,7 +1874,7 @@ void ClientProxy::OnClientDead() {
     controller_->OnClientDead(this);
     // After OnClientDead, there won't be any more vsync calls. Since that is the only use of
     // the channel off of the loop thread, there's no need to worry about synchronization.
-    server_handle_.reset();
+    server_channel_.reset();
 }
 
 void ClientProxy::Close() {
@@ -1916,28 +1916,8 @@ void ClientProxy::Close() {
     }
 }
 
-zx_status_t ClientProxy::DdkIoctl(uint32_t op, const void* in_buf, size_t in_len, void* out_buf,
-                             size_t out_len, size_t* actual) {
-    switch (op) {
-    case IOCTL_DISPLAY_CONTROLLER_GET_HANDLE: {
-        if (out_len != sizeof(zx_handle_t)) {
-            return ZX_ERR_INVALID_ARGS;
-        }
-
-        if (client_handle_.get() == ZX_HANDLE_INVALID) {
-            return ZX_ERR_ALREADY_BOUND;
-        }
-
-        *reinterpret_cast<zx_handle_t*>(out_buf) = client_handle_.release();
-        *actual = sizeof(zx_handle_t);
-        return ZX_OK;
-    }
-    default:
-        return ZX_ERR_NOT_SUPPORTED;
-    }
-}
-
 zx_status_t ClientProxy::DdkClose(uint32_t flags) {
+    printf("DdkClose\n");
     Close();
     return ZX_OK;
 }
@@ -1946,15 +1926,9 @@ void ClientProxy::DdkRelease() {
     delete this;
 }
 
-zx_status_t ClientProxy::Init() {
-    zx_status_t status;
-    if ((status = zx_channel_create(0, server_handle_.reset_and_get_address(),
-                                    client_handle_.reset_and_get_address())) != ZX_OK) {
-        zxlogf(ERROR, "Failed to create channels %d\n", status);
-        return status;
-    }
-
-    return handler_.Init(server_handle_.get());
+zx_status_t ClientProxy::Init(zx::channel server_channel) {
+    server_channel_ = std::move(server_channel);
+    return handler_.Init(server_channel_.get());
 }
 
 ClientProxy::ClientProxy(Controller* controller, bool is_vc)
diff --git a/zircon/system/dev/display/display/client.h b/zircon/system/dev/display/display/client.h
index 27da3e2e0a776504b48e58b9549820ea3ee41825..68fae4de38faeb6307997ec90e8b4f55819f5940 100644
--- a/zircon/system/dev/display/display/client.h
+++ b/zircon/system/dev/display/display/client.h
@@ -268,16 +268,14 @@ private:
 
 // ClientProxy manages interactions between its Client instance and the ddk and the
 // controller. Methods on this class are thread safe.
-using ClientParent = ddk::Device<ClientProxy, ddk::Ioctlable, ddk::Closable>;
+using ClientParent = ddk::Device<ClientProxy, ddk::Closable>;
 class ClientProxy : public ClientParent {
 public:
     ClientProxy(Controller* controller, bool is_vc);
     ~ClientProxy();
-    zx_status_t Init();
+    zx_status_t Init(zx::channel server_channel);
 
     zx_status_t DdkClose(uint32_t flags);
-    zx_status_t DdkIoctl(uint32_t op, const void* in_buf, size_t in_len,
-                         void* out_buf, size_t out_len, size_t* actual);
     void DdkRelease();
 
     // Requires holding controller_->mtx() lock
@@ -302,8 +300,7 @@ private:
     Client handler_;
     bool enable_vsync_ = false;
 
-    zx::channel server_handle_;
-    zx::channel client_handle_;
+    zx::channel server_channel_;
 };
 
 } // namespace display
diff --git a/zircon/system/dev/display/display/controller.cpp b/zircon/system/dev/display/display/controller.cpp
index 26764517daafe7fbc7bce085f4ab47c8725cbf97..257b20dfb0d739a74dcce7e46a1f72140125296c 100644
--- a/zircon/system/dev/display/display/controller.cpp
+++ b/zircon/system/dev/display/display/controller.cpp
@@ -752,10 +752,11 @@ bool Controller::GetDisplayIdentifiers(uint64_t display_id, const char** manufac
 }
 
 zx_status_t Controller::DdkOpen(zx_device_t** dev_out, uint32_t flags) {
-    return DdkOpenAt(dev_out, "", flags);
+    return ZX_OK;
 }
 
-zx_status_t Controller::DdkOpenAt(zx_device_t** dev_out, const char* path, uint32_t flags) {
+zx_status_t Controller::CreateClient(bool is_vc, zx::channel device_channel,
+                                     zx::channel client_channel) {
     fbl::AllocChecker ac;
     fbl::unique_ptr<async::Task> task = fbl::make_unique_checked<async::Task>(&ac);
     if (!ac.check()) {
@@ -765,7 +766,6 @@ zx_status_t Controller::DdkOpenAt(zx_device_t** dev_out, const char* path, uint3
 
     fbl::AutoLock lock(&mtx_);
 
-    bool is_vc = strcmp("virtcon", path) == 0;
     if ((is_vc && vc_client_) || (!is_vc && primary_client_)) {
         zxlogf(TRACE, "Already bound\n");
         return ZX_ERR_ALREADY_BOUND;
@@ -777,21 +777,27 @@ zx_status_t Controller::DdkOpenAt(zx_device_t** dev_out, const char* path, uint3
         return ZX_ERR_NO_MEMORY;
     }
 
-    zx_status_t status = client->Init();
+    zx_status_t status = client->Init(std::move(client_channel));
     if (status != ZX_OK) {
         zxlogf(TRACE, "Failed to init client %d\n", status);
         return status;
     }
 
-    if ((status = client->DdkAdd(is_vc ? "dc-vc" : "dc", DEVICE_ADD_INSTANCE)) != ZX_OK) {
+    status = client->DdkAdd(is_vc ? "dc-vc" : "dc",
+                            DEVICE_ADD_INSTANCE,
+                            nullptr /* props */,
+                            0 /* prop_count */,
+                            0 /* proto_id */,
+                            nullptr /* proxy_args */,
+                            device_channel.release());
+    if (status != ZX_OK) {
         zxlogf(TRACE, "Failed to add client %d\n", status);
         return status;
     }
 
     ClientProxy* client_ptr = client.release();
-    *dev_out = client_ptr->zxdev();
 
-    zxlogf(TRACE, "New client connected at \"%s\"\n", path);
+    zxlogf(TRACE, "New client connected.\n");
 
     if (is_vc) {
         vc_client_ = client_ptr;
@@ -831,6 +837,28 @@ zx_status_t Controller::DdkOpenAt(zx_device_t** dev_out, const char* path, uint3
     return task.release()->Post(loop_.dispatcher());
 }
 
+zx_status_t Controller::OpenVirtconController(zx_handle_t device, zx_handle_t controller,
+                                              fidl_txn_t* txn) {
+    zx::channel device_channel(device);
+    zx::channel controller_channel(controller);
+    zx_status_t status = CreateClient(true /* is_vc */, std::move(device_channel),
+                                      std::move(controller_channel));
+    return fuchsia_hardware_display_ProviderOpenVirtconController_reply(txn, status);
+}
+
+zx_status_t Controller::OpenController(zx_handle_t device, zx_handle_t controller,
+                                       fidl_txn_t* txn) {
+    zx::channel device_channel(device);
+    zx::channel controller_channel(controller);
+    zx_status_t status = CreateClient(false /* is_vc */, std::move(device_channel),
+                                      std::move(controller_channel));
+    return fuchsia_hardware_display_ProviderOpenController_reply(txn, status);
+}
+
+zx_status_t Controller::DdkMessage(fidl_msg_t* msg, fidl_txn_t* txn) {
+    return fuchsia_hardware_display_Provider_dispatch(this, txn, msg, &fidl_ops_);
+}
+
 zx_status_t Controller::Bind(fbl::unique_ptr<display::Controller>* device_ptr) {
     zx_status_t status;
     dc_ = ddk::DisplayControllerImplProtocolClient(parent_);
diff --git a/zircon/system/dev/display/display/controller.h b/zircon/system/dev/display/display/controller.h
index cd560d8dff81e7113b2e79e3b53691189e9f8065..95be81927e8a2263219edf06dfb91698d5b1303b 100644
--- a/zircon/system/dev/display/display/controller.h
+++ b/zircon/system/dev/display/display/controller.h
@@ -11,6 +11,7 @@
 #include <ddktl/protocol/empty-protocol.h>
 #include <ddktl/protocol/i2cimpl.h>
 #include <fbl/array.h>
+#include <lib/fidl-utils/bind.h>
 #include <fbl/intrusive_double_list.h>
 #include <fbl/intrusive_hash_table.h>
 #include <fbl/unique_ptr.h>
@@ -65,7 +66,7 @@ public:
     bool switching_client = false;
 };
 
-using ControllerParent = ddk::Device<Controller, ddk::Unbindable, ddk::Openable, ddk::OpenAtable>;
+using ControllerParent = ddk::Device<Controller, ddk::Unbindable, ddk::Openable, ddk::Messageable>;
 class Controller : public ControllerParent,
                    public ddk::DisplayControllerInterfaceProtocol<Controller>,
                    public ddk::EmptyProtocol<ZX_PROTOCOL_DISPLAY_CONTROLLER> {
@@ -75,7 +76,7 @@ public:
     static void PopulateDisplayMode(const edid::timing_params_t& params, display_mode_t* mode);
 
     zx_status_t DdkOpen(zx_device_t** dev_out, uint32_t flags);
-    zx_status_t DdkOpenAt(zx_device_t** dev_out, const char* path, uint32_t flags);
+    zx_status_t DdkMessage(fidl_msg_t* msg, fidl_txn_t* txn);
     void DdkUnbind();
     void DdkRelease();
     zx_status_t Bind(fbl::unique_ptr<display::Controller>* device_ptr);
@@ -124,6 +125,16 @@ private:
     void HandleClientOwnershipChanges() __TA_REQUIRES(mtx_);
     void PopulateDisplayTimings(const fbl::RefPtr<DisplayInfo>& info) __TA_EXCLUDES(mtx_);
     void PopulateDisplayAudio(const fbl::RefPtr<DisplayInfo>& info);
+    zx_status_t CreateClient(bool is_vc, zx::channel device, zx::channel client);
+
+    zx_status_t OpenVirtconController(zx_handle_t device, zx_handle_t controller, fidl_txn_t* txn);
+    zx_status_t OpenController(zx_handle_t device, zx_handle_t controller, fidl_txn_t* txn);
+
+    static constexpr fuchsia_hardware_display_Provider_ops_t fidl_ops_ = {
+        .OpenVirtconController =
+                fidl::Binder<Controller>::BindMember<&Controller::OpenVirtconController>,
+        .OpenController = fidl::Binder<Controller>::BindMember<&Controller::OpenController>,
+    };
 
     // mtx_ is a global lock on state shared among clients.
     mtx_t mtx_;
diff --git a/zircon/system/fidl/fuchsia-hardware-display/display-controller.fidl b/zircon/system/fidl/fuchsia-hardware-display/display-controller.fidl
index bb146a71eff959f74319cc5fbbee542deaab1340..003ed330e51099e62fe0da52ef1bbb8fd6b18c51 100644
--- a/zircon/system/fidl/fuchsia-hardware-display/display-controller.fidl
+++ b/zircon/system/fidl/fuchsia-hardware-display/display-controller.fidl
@@ -171,26 +171,49 @@ struct ClientCompositionOp {
     ClientCompositionOpcode opcode;
 };
 
-// Interface for accessing the display hardware.
-//
-// The driver supports two simultaneous clients - a primary client and a virtcon
-// client. The primary client is obtained by directly opening the devfs device,
-// while the virtcon client is obtained by opening a 'virtcon' child of the
-// device.
-//
-// A display configuration can be separated into two parts: the layer layout and
-// the layer contents. The layout includes all parts of a configuration other
-// than the image handles. The active configuration is composed of the most
-// recently applied layout and an active image from each layer - see
-// SetLayerImage for details on how the active image is defined. Note the
-// requirement that each layer has an active image. Whenever a new active
-// configuration is available, it is immediately given to the hardware. This
-// allows the layout and each layer's contents to advance independently when
-// possible.
-//
-
-// Performing illegal actions on the interface will result in the interface
-// being closed.
+/// Provider for display controllers.
+///
+/// The driver supports two simultaneous clients - a primary client and a virtcon
+/// client.
+[Layout = "Simple"]
+protocol Provider {
+    /// Open a virtcon client. |device| should be a handle to one endpoint of a
+    /// channel that (on success) will become an open connection to a new
+    /// instance of a display client device. An protocol request |controller|
+    /// provides an interface to the Controller for the new device. Closing the
+    /// connection to |device| will also close the |controller| interface. If
+    /// the display device already has a virtcon controller then this method
+    /// will return ZX_ERR_ALREADY_BOUND.
+    // TODO(ZX-3889): Once llcpp is supported in Zircon, unify |device| and
+    // |controller|.
+    OpenVirtconController(handle<channel> device, request<Controller> controller) -> (zx.status s);
+
+    /// Open a primary client. |device| should be a handle to one endpoint of a
+    /// channel that (on success) will become an open connection to a new
+    /// instance of a display client device. An protocol request |controller|
+    /// provides an interface to the Controller for the new device. Closing the
+    /// connection to |device| will also close the |controller| interface. If
+    /// the display device already has a primary controller then this method
+    /// will return ZX_ERR_ALREADY_BOUND.
+    // TODO(ZX-3889): Once llcpp is supported in Zircon, unify |device| and
+    // |controller|.
+    OpenController(handle<channel> device, request<Controller> controller) -> (zx.status s);
+};
+
+/// Interface for accessing the display hardware.
+///
+/// A display configuration can be separated into two parts: the layer layout and
+/// the layer contents. The layout includes all parts of a configuration other
+/// than the image handles. The active configuration is composed of the most
+/// recently applied layout and an active image from each layer - see
+/// SetLayerImage for details on how the active image is defined. Note the
+/// requirement that each layer has an active image. Whenever a new active
+/// configuration is available, it is immediately given to the hardware. This
+/// allows the layout and each layer's contents to advance independently when
+/// possible.
+///
+/// Performing illegal actions on the interface will result in the interface
+/// being closed.
 protocol Controller {
     // Event fired when displays are added or removed. This event will be fired
     // when the callback is registered if there are any connected displays.
diff --git a/zircon/system/uapp/display-test/BUILD.gn b/zircon/system/uapp/display-test/BUILD.gn
index 56791cde207f309cbd653754e9c9fd547d8cfb45..48e0294a5e6298953006e8824962a94169d2f9ea 100644
--- a/zircon/system/uapp/display-test/BUILD.gn
+++ b/zircon/system/uapp/display-test/BUILD.gn
@@ -15,6 +15,7 @@ executable("display-test") {
     "$zx/system/ulib/fbl",
     "$zx/system/ulib/fdio",
     "$zx/system/ulib/fidl",
+    "$zx/system/ulib/fzl",
     "$zx/system/ulib/zircon",
     "$zx/system/ulib/zx",
     "$zx/system/ulib/zxcpp",
diff --git a/zircon/system/uapp/display-test/main.cpp b/zircon/system/uapp/display-test/main.cpp
index 8b5c3e4c7d94281418595630765a4ee37c3e812a..d53ddcc4e782a615466af1248bb1e03fa9b4aa21 100644
--- a/zircon/system/uapp/display-test/main.cpp
+++ b/zircon/system/uapp/display-test/main.cpp
@@ -14,11 +14,13 @@
 #include <zircon/syscalls.h>
 #include <zircon/types.h>
 
-#include <fbl/vector.h>
 #include <fbl/algorithm.h>
+#include <fbl/vector.h>
+#include <fbl/unique_fd.h>
 #include <lib/fidl/cpp/message.h>
 #include <lib/fidl/cpp/string_view.h>
 #include <lib/fidl/cpp/vector_view.h>
+#include <lib/fzl/fdio.h>
 
 #include <zircon/pixelformat.h>
 #include <zircon/status.h>
@@ -28,6 +30,7 @@
 #include "fuchsia/hardware/display/c/fidl.h"
 #include "virtual-layer.h"
 
+static zx_handle_t device_handle;
 static zx_handle_t dc_handle;
 static bool has_ownership;
 
@@ -47,17 +50,43 @@ static bool wait_for_driver_event(zx_time_t deadline) {
 
 static bool bind_display(fbl::Vector<Display>* displays) {
     printf("Opening controller\n");
-    int vfd = open("/dev/class/display-controller/000", O_RDWR);
-    if (vfd < 0) {
+    fbl::unique_fd fd(open("/dev/class/display-controller/000", O_RDWR));
+    if (!fd) {
         printf("Failed to open display controller (%d)\n", errno);
         return false;
     }
 
-    if (ioctl_display_controller_get_handle(vfd, &dc_handle) != sizeof(zx_handle_t)) {
-        printf("Failed to get display controller handle\n");
+    zx::channel device_server, device_client;
+    zx_status_t status = zx::channel::create(0, &device_server, &device_client);
+    if (status != ZX_OK) {
+        printf("Failed to create device channel %d (%s)\n", status, zx_status_get_string(status));
+        return false;
+    }
+
+    zx::channel dc_server, dc_client;
+    status = zx::channel::create(0, &dc_server, &dc_client);
+    if (status != ZX_OK) {
+        printf("Failed to create controller channel %d (%s)\n", status,
+               zx_status_get_string(status));
+        return false;
+    }
+
+    fzl::FdioCaller caller(std::move(fd));
+    zx_status_t fidl_status = fuchsia_hardware_display_ProviderOpenController(
+        caller.borrow_channel(), device_server.release(), dc_server.release(), &status);
+    if (fidl_status != ZX_OK) {
+        printf("Failed to call service handle %d (%s)\n", fidl_status,
+               zx_status_get_string(fidl_status));
+        return false;
+    }
+    if (status != ZX_OK) {
+        printf("Failed to open controller %d (%s)\n", status, zx_status_get_string(status));
         return false;
     }
 
+    dc_handle = dc_client.release();
+    device_handle = device_client.release();
+
     uint8_t byte_buffer[ZX_CHANNEL_MAX_MSG_BYTES];
     fidl::Message msg(fidl::BytePart(byte_buffer, ZX_CHANNEL_MAX_MSG_BYTES), fidl::HandlePart());
     while (displays->is_empty()) {
@@ -512,5 +541,8 @@ int main(int argc, const char* argv[]) {
     printf("Done rendering\n");
     zx_nanosleep(zx_deadline_after(ZX_MSEC(500)));
 
+    zx_handle_close(dc_handle);
+    zx_handle_close(device_handle);
+
     return 0;
 }
diff --git a/zircon/system/ulib/framebuffer/BUILD.gn b/zircon/system/ulib/framebuffer/BUILD.gn
index 8c7884636c81f30f2d140d2831406ed498c45050..923430c74b902235dbd0f2d3fd11fe7f756586b8 100644
--- a/zircon/system/ulib/framebuffer/BUILD.gn
+++ b/zircon/system/ulib/framebuffer/BUILD.gn
@@ -14,6 +14,7 @@ library("framebuffer") {
     "$zx/system/ulib/fbl",
     "$zx/system/ulib/fdio",
     "$zx/system/ulib/fidl",
+    "$zx/system/ulib/fzl",
     "$zx/system/ulib/zircon",
     "$zx/system/ulib/zx",
   ]
diff --git a/zircon/system/ulib/framebuffer/framebuffer.cpp b/zircon/system/ulib/framebuffer/framebuffer.cpp
index 5d557a7b476cbcfb363dcf7bcfc67807227d978b..486f3028186ca99a851f0d3c52205697f5b9127b 100644
--- a/zircon/system/ulib/framebuffer/framebuffer.cpp
+++ b/zircon/system/ulib/framebuffer/framebuffer.cpp
@@ -10,7 +10,9 @@
 #include <unistd.h>
 
 #include <fbl/auto_call.h>
+#include <fbl/unique_fd.h>
 #include <lib/fidl/coding.h>
+#include <lib/fzl/fdio.h>
 #include <lib/zx/vmo.h>
 #include <zircon/assert.h>
 #include <zircon/device/display-controller.h>
@@ -22,7 +24,7 @@
 #include "fuchsia/hardware/display/c/fidl.h"
 #include "lib/framebuffer/framebuffer.h"
 
-static int32_t dc_fd = -1;
+static zx_handle_t device_handle = ZX_HANDLE_INVALID;
 static zx_handle_t dc_handle = ZX_HANDLE_INVALID;
 
 static int32_t txid;
@@ -84,23 +86,44 @@ zx_status_t fb_bind(bool single_buffer, const char** err_msg_out) {
     }
 
     // TODO(stevensd): Don't hardcode display controller 0
-    zx_status_t status;
-    dc_fd = open("/dev/class/display-controller/000", O_RDWR);
-    if (dc_fd < 0) {
+    fbl::unique_fd dc_fd(open("/dev/class/display-controller/000", O_RDWR));
+    if (!dc_fd) {
         *err_msg_out = "Failed to open display controller";
         return ZX_ERR_NO_RESOURCES;
     }
-    fbl::AutoCall close_dc_fd([]() {
-        close(dc_fd);
-        dc_fd = -1;
-    });
 
-    if (ioctl_display_controller_get_handle(dc_fd, &dc_handle) != sizeof(zx_handle_t)) {
-        *err_msg_out = "Failed to get display controller handle";
-        return ZX_ERR_INTERNAL;
+    zx::channel device_server, device_client;
+    zx_status_t status = zx::channel::create(0, &device_server, &device_client);
+    if (status != ZX_OK) {
+        *err_msg_out = "Failed to create device channel";
+        return status;
+    }
+
+    zx::channel dc_server, dc_client;
+    status = zx::channel::create(0, &dc_server, &dc_client);
+    if (status != ZX_OK) {
+        *err_msg_out = "Failed to create controller channel";
+        return status;
     }
+
+    fzl::FdioCaller caller(std::move(dc_fd));
+    zx_status_t fidl_status = fuchsia_hardware_display_ProviderOpenController(
+        caller.borrow_channel(), device_server.release(), dc_server.release(), &status);
+    if (fidl_status != ZX_OK) {
+        *err_msg_out = "Failed to call service handle";
+        return fidl_status;
+    }
+    if (status != ZX_OK) {
+        *err_msg_out = "Failed to open controller";
+        return status;
+    }
+
+    device_handle = device_client.release();
+    dc_handle = dc_client.release();
     fbl::AutoCall close_dc_handle([]() {
+        zx_handle_close(device_handle);
         zx_handle_close(dc_handle);
+        device_handle = ZX_HANDLE_INVALID;
         dc_handle = ZX_HANDLE_INVALID;
     });
 
@@ -283,7 +306,6 @@ zx_status_t fb_bind(bool single_buffer, const char** err_msg_out) {
 
     clear_inited.cancel();
     vmo = local_vmo.release();
-    close_dc_fd.cancel();
     close_dc_handle.cancel();
 
     return ZX_OK;
@@ -294,12 +316,11 @@ void fb_release() {
         return;
     }
 
+    zx_handle_close(device_handle);
     zx_handle_close(dc_handle);
+    device_handle = ZX_HANDLE_INVALID;
     dc_handle = ZX_HANDLE_INVALID;
 
-    close(dc_fd);
-    dc_fd = -1;
-
     if (in_single_buffer_mode) {
         zx_handle_close(vmo);
         vmo = ZX_HANDLE_INVALID;