[camera] add controller stream to camera_demo

This change add camera controller as an additional streaming source to
the camera manual test / demo. It also adds an extremely simple scenic
image node, populated with the text of the friendly name of the stream
source.

This change also fixes a bug in the legacy ISP driver that manifested in
the new camera_demo_test.

Test: ran the camera_demo_test
Change-Id: I082b1c47da8dc22182d49578452d30c8610160fd
diff --git a/src/camera/drivers/isp/mali-009/arm-isp.cc b/src/camera/drivers/isp/mali-009/arm-isp.cc
index 0a2473a..3c7f4ea 100644
--- a/src/camera/drivers/isp/mali-009/arm-isp.cc
+++ b/src/camera/drivers/isp/mali-009/arm-isp.cc
@@ -761,6 +761,7 @@
 }
 
 int ArmIspDevice::FrameProcessingThread() {
+  bool fr_frame_written = false;
   while (running_frame_processing_.load()) {
     sync_completion_wait(&frame_processing_signal_, ZX_TIME_INFINITE);
     // Currently this is called only on the new frame signal, so we maintain
@@ -770,11 +771,13 @@
     } else {
       // Each of these calls has it's own interrupt, that it could be
       // attached to:
-      full_resolution_dma_->OnFrameWritten();
+      if (fr_frame_written) {
+        full_resolution_dma_->OnFrameWritten();
+      }
       downscaled_dma_->OnFrameWritten();
     }
     // Now for the actions we should take on new frame:
-    full_resolution_dma_->OnNewFrame();
+    fr_frame_written = full_resolution_dma_->OnNewFrame();
     downscaled_dma_->OnNewFrame();
 
     // Reset the signal
diff --git a/src/camera/drivers/isp/modules/dma-mgr.cc b/src/camera/drivers/isp/modules/dma-mgr.cc
index c542190..3e7ad6d5 100644
--- a/src/camera/drivers/isp/modules/dma-mgr.cc
+++ b/src/camera/drivers/isp/modules/dma-mgr.cc
@@ -195,10 +195,10 @@
 }
 
 // Called as one of the later steps when a new frame arrives.
-void DmaManager::OnNewFrame() {
+bool DmaManager::OnNewFrame() {
   // If we have not initialized yet with a format, just skip.
   if (!enabled_) {
-    return;
+    return false;
   }
   // 1) Get another buffer
   auto buffer = buffers_.LockBufferForWrite();
@@ -223,7 +223,7 @@
     event.frame_status = fuchsia_camera_common_FrameStatus_ERROR_BUFFER_FULL;
     event.metadata.timestamp = 0;
     frame_available_callback_(event);
-    return;
+    return false;
   }
   // 2) Optional?  Set the DMA settings again... seems unnecessary
   // 3) Set the DMA address
@@ -251,6 +251,8 @@
   WriteFormat();
   // Add buffer to queue of buffers we are writing:
   write_locked_buffers_.push_front(std::move(*buffer));
+
+  return true;
 }
 
 zx_status_t DmaManager::ReleaseFrame(uint32_t buffer_index) {
diff --git a/src/camera/drivers/isp/modules/dma-mgr.h b/src/camera/drivers/isp/modules/dma-mgr.h
index 9283be5..a9552cf 100644
--- a/src/camera/drivers/isp/modules/dma-mgr.h
+++ b/src/camera/drivers/isp/modules/dma-mgr.h
@@ -51,7 +51,8 @@
                             Stream stream_type, std::unique_ptr<DmaManager>* out);
 
   // Updates the dma writer with the address of a free buffer from the pool.
-  void OnNewFrame();
+  // Returns true iff a new frame was actually written.
+  bool OnNewFrame();
 
   // Signal that all consumers are done with this frame.
   zx_status_t ReleaseFrame(uint32_t buffer_index);
diff --git a/src/camera/examples/demo/BUILD.gn b/src/camera/examples/demo/BUILD.gn
index eeee427..78fb6fd 100644
--- a/src/camera/examples/demo/BUILD.gn
+++ b/src/camera/examples/demo/BUILD.gn
@@ -15,6 +15,7 @@
 
   deps = [
     "./stream_provider",
+    "./text_node",
     "//garnet/public/lib/fsl",
     "//garnet/public/lib/ui/base_view/cpp",
     "//garnet/public/lib/ui/scenic/cpp",
@@ -25,6 +26,7 @@
     "//src/lib/ui/scenic/cpp",
     "//src/ui/lib/glm_workaround",
     "//zircon/public/lib/async-loop-cpp",
+    "//zircon/public/lib/fzl",
   ]
 }
 
diff --git a/src/camera/examples/demo/main.cc b/src/camera/examples/demo/main.cc
index 82c5443..137bd9c 100644
--- a/src/camera/examples/demo/main.cc
+++ b/src/camera/examples/demo/main.cc
@@ -17,6 +17,7 @@
 #include <lib/zx/time.h>
 #include <stream_provider.h>
 #include <sys/types.h>
+#include <text_node.h>
 
 #include <queue>
 #include <random>
@@ -54,8 +55,8 @@
 }
 
 // Draws a scenic scene containing a single rectangle with an image pipe material,
-// constructed with buffers populated by a stream provider (in this case, the ArmIspDevice).
-class DemoView : public scenic::BaseView, public fuchsia::camera2::Stream_EventSender {
+// constructed with buffers populated by a stream provider.
+class DemoView : public scenic::BaseView, public fuchsia::camera2::Stream::EventSender_ {
  public:
   explicit DemoView(scenic::ViewContext context, async::Loop* loop, bool chaos)
       : BaseView(std::move(context), "Camera Demo"),
@@ -63,7 +64,8 @@
         chaos_(chaos),
         chaos_dist_(kChaosMaxSleepMsec,
                     static_cast<float>(kChaosMeanSleepMsec) / kChaosMaxSleepMsec),
-        node_(session()) {}
+        node_(session()),
+        text_node_(session()) {}
 
   ~DemoView() override {
     // Manually delete Wait instances before their corresponding events to avoid a failed assert.
@@ -78,16 +80,20 @@
                                           bool chaos) {
     auto view = std::make_unique<DemoView>(std::move(context), loop, chaos);
 
-    auto stream_provider = StreamProvider::Create(StreamProvider::Source::ISP);
-    if (!stream_provider) {
-      FXL_LOG(ERROR) << "Failed to get ISP stream provider";
+    view->stream_provider_ = StreamProvider::Create(StreamProvider::Source::CONTROLLER);
+    if (!view->stream_provider_) {
+      FXL_LOG(ERROR) << "Failed to get CONTROLLER stream provider";
       return nullptr;
     }
 
     fuchsia::sysmem::ImageFormat_2 format;
     fuchsia::sysmem::BufferCollectionInfo_2 buffers;
-    view->stream_ =
-        stream_provider->ConnectToStream(view.get(), &format, &buffers, &view->should_rotate_);
+    view->stream_ = view->stream_provider_->ConnectToStream(view.get(), &format, &buffers,
+                                                            &view->should_rotate_);
+    if (!view->stream_) {
+      FXL_LOG(ERROR) << "Failed to connect to stream";
+      return nullptr;
+    }
 
     uint32_t image_pipe_id = view->session()->AllocResourceId();
     view->session()->Enqueue(
@@ -95,16 +101,17 @@
     scenic::Material material(view->session());
     material.SetTexture(image_pipe_id);
     view->session()->ReleaseResource(image_pipe_id);
-    scenic::Rectangle shape(view->session(), format.display_width, format.display_height);
-    view->shape_width_ = format.display_width;
-    view->shape_height_ = format.display_height;
+    scenic::Rectangle shape(view->session(), format.coded_width, format.coded_height);
+    view->shape_width_ = format.coded_width;
+    view->shape_height_ = format.coded_height;
     view->node_.SetShape(shape);
     view->node_.SetMaterial(material);
     view->root_node().AddChild(view->node_);
+    view->root_node().AddChild(view->text_node_);
 
     fuchsia::images::ImageInfo image_info{};
-    image_info.width = format.display_width;
-    image_info.height = format.display_height;
+    image_info.width = format.coded_width;
+    image_info.height = format.coded_height;
     image_info.stride = format.bytes_per_row;
     image_info.pixel_format = convertFormat(format.pixel_format.type);
     for (uint32_t i = 0; i < buffers.buffer_count; ++i) {
@@ -125,16 +132,22 @@
  private:
   // |scenic::BaseView|
   void OnSceneInvalidated(fuchsia::images::PresentationInfo presentation_info) override {
-    if (!has_logical_size())
+    if (!has_logical_size() || !has_metrics())
       return;
     if (should_rotate_) {
       auto rotation = glm::angleAxis(glm::half_pi<float>(), glm::vec3(0, 0, 1));
       node_.SetRotation(rotation.x, rotation.y, rotation.z, rotation.w);
     }
-    node_.SetTranslation(logical_size().x * 0.5f, logical_size().y * 0.5f, -5.0f);
+    node_.SetTranslation(logical_size().x * 0.5f, logical_size().y * 0.5f, -1.0f);
     const float shape_vertical_size = should_rotate_ ? shape_width_ : shape_height_;
     const float scale = logical_size().y / shape_vertical_size;  // Fit vertically.
     node_.SetScale(scale, scale, 1.0f);
+    text_node_.SetText(stream_provider_->GetName() +
+                       (should_rotate_ ? " (Rotated by Scenic)" : ""));
+    text_node_.SetTranslation(logical_size().x * 0.5f + metrics().scale_x * 0.5f,
+                              logical_size().y * 0.02f, -1.1f);
+    text_node_.SetScale(1.0f / metrics().scale_x, 1.0f / metrics().scale_y,
+                        1.0f / metrics().scale_z);
   }
 
   void OnInputEvent(fuchsia::ui::input::InputEvent event) override {
@@ -222,12 +235,14 @@
   std::binomial_distribution<uint32_t> chaos_dist_;
   std::unique_ptr<fuchsia::camera2::Stream> stream_;
   scenic::ShapeNode node_;
+  TextNode text_node_;
   fuchsia::images::ImagePipePtr image_pipe_;
   std::map<uint32_t, uint32_t> image_ids_;
   float shape_width_;
   float shape_height_;
   bool should_rotate_;
   std::queue<std::pair<std::unique_ptr<async::Wait>, zx::event>> waiters_;
+  std::unique_ptr<StreamProvider> stream_provider_;
 };
 
 int main(int argc, const char** argv) {
diff --git a/src/camera/examples/demo/meta/demo.cmx b/src/camera/examples/demo/meta/demo.cmx
index 057da52..566e1cc 100644
--- a/src/camera/examples/demo/meta/demo.cmx
+++ b/src/camera/examples/demo/meta/demo.cmx
@@ -4,11 +4,13 @@
   },
   "sandbox": {
     "dev": [
-      "class/isp-device-test"
+      "class/isp-device-test",
+      "camera-controller"
     ],
     "services": [
       "fuchsia.sys.Environment",
       "fuchsia.sys.Launcher",
+      "fuchsia.sysmem.Allocator",
       "fuchsia.tracing.provider.Registry",
       "fuchsia.ui.policy.Presenter",
       "fuchsia.ui.scenic.Scenic"
diff --git a/src/camera/examples/demo/stream_provider/BUILD.gn b/src/camera/examples/demo/stream_provider/BUILD.gn
index ea58016..8e8ba29 100644
--- a/src/camera/examples/demo/stream_provider/BUILD.gn
+++ b/src/camera/examples/demo/stream_provider/BUILD.gn
@@ -15,8 +15,9 @@
   visibility = [ ":*" ]
 
   sources = [
-    "stream_provider.cc",
+    "controller_stream_provider.cc",
     "isp_stream_provider.cc",
+    "stream_provider.cc",
   ]
 
   deps = [
@@ -26,12 +27,14 @@
     "//sdk/fidl/fuchsia.camera2.hal",
     "//sdk/fidl/fuchsia.ui.gfx",
     "//sdk/lib/fidl/cpp",
+    "//sdk/lib/sys/cpp",
     "//src/lib/fxl",
     "//src/lib/ui/scenic/cpp",
     "//src/ui/lib/glm_workaround",
     "//zircon/public/lib/async-loop-cpp",
     "//zircon/system/fidl/fuchsia-camera-common",
     "//zircon/system/fidl/fuchsia-camera-test",
+    "//zircon/system/fidl/fuchsia-hardware-camera",
   ]
 
   configs += [
diff --git a/src/camera/examples/demo/stream_provider/controller_stream_provider.cc b/src/camera/examples/demo/stream_provider/controller_stream_provider.cc
new file mode 100644
index 0000000..deef0af
--- /dev/null
+++ b/src/camera/examples/demo/stream_provider/controller_stream_provider.cc
@@ -0,0 +1,197 @@
+// Copyright 2019 The Fuchsia 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 "controller_stream_provider.h"
+
+#include <fcntl.h>
+#include <fuchsia/hardware/camera/cpp/fidl.h>
+#include <lib/fdio/fdio.h>
+#include <lib/fzl/vmo-mapper.h>
+
+#include <fbl/unique_fd.h>
+#include <src/lib/fxl/logging.h>
+
+#include "streamptr_wrapper.h"
+
+static constexpr const char* kDevicePath = "/dev/camera-controller/camera-controller-device";
+
+ControllerStreamProvider::~ControllerStreamProvider() {
+  if (controller_ && streaming_) {
+    zx_status_t status = controller_->DisableStreaming();
+    if (status != ZX_OK) {
+      FXL_PLOG(WARNING, status) << "Failed to stop streaming via the controller";
+    }
+  }
+  if (buffer_collection_) {
+    zx_status_t status = buffer_collection_->Close();
+    if (status != ZX_OK) {
+      FXL_PLOG(ERROR, status);
+    }
+  }
+}
+
+std::unique_ptr<StreamProvider> ControllerStreamProvider::Create() {
+  auto provider = std::make_unique<ControllerStreamProvider>();
+
+  // Connect to sysmem.
+  zx_status_t status =
+      sys::ComponentContext::Create()->svc()->Connect(provider->allocator_.NewRequest());
+  if (status != ZX_OK) {
+    FXL_PLOG(ERROR, status) << "Failed to connect to sysmem allocator service";
+    return nullptr;
+  }
+  if (!provider->allocator_) {
+    FXL_LOG(ERROR) << "Failed to connect to sysmem allocator service";
+    return nullptr;
+  }
+
+  // Connect to the controller device.
+  int result = open(kDevicePath, O_RDONLY);
+  if (result < 0) {
+    FXL_LOG(ERROR) << "Error opening " << kDevicePath;
+    return nullptr;
+  }
+  fbl::unique_fd controller_fd(result);
+  zx::channel channel;
+  status = fdio_get_service_handle(controller_fd.get(), channel.reset_and_get_address());
+  if (status != ZX_OK) {
+    FXL_PLOG(ERROR, status) << "Failed to get service handle";
+    return nullptr;
+  }
+  fuchsia::hardware::camera::DevicePtr device;
+  device.Bind(std::move(channel));
+
+  // Connect to the controller interface.
+  device->GetChannel2(provider->controller_.NewRequest().TakeChannel());
+  if (!provider->controller_) {
+    FXL_LOG(ERROR) << "Failed to get controller interface from device";
+    return nullptr;
+  }
+
+  // Immediately enable streaming.
+  status = provider->controller_->EnableStreaming();
+  if (status != ZX_OK) {
+    FXL_LOG(WARNING) << "Failed to start streaming via the controller";
+  }
+  provider->streaming_ = true;
+
+  return std::move(provider);
+}
+
+// Offer a stream as served through the tester interface.
+std::unique_ptr<fuchsia::camera2::Stream> ControllerStreamProvider::ConnectToStream(
+    fuchsia::camera2::Stream::EventSender_* event_handler,
+    fuchsia::sysmem::ImageFormat_2* format_out,
+    fuchsia::sysmem::BufferCollectionInfo_2* buffers_out, bool* should_rotate_out) {
+  if (!format_out || !buffers_out || !should_rotate_out) {
+    return nullptr;
+  }
+
+  static constexpr const uint32_t kConfigIndex = 0;
+  static constexpr const uint32_t kStreamConfigIndex = 0;
+  static constexpr const uint32_t kImageFormatIndex = 0;
+
+  if (buffer_collection_.is_bound()) {
+    FXL_PLOG(ERROR, ZX_ERR_ALREADY_BOUND) << "Stream already bound by caller.";
+    return nullptr;
+  }
+
+  // Get the list of valid configs as reported by the controller.
+  fidl::VectorPtr<fuchsia::camera2::hal::Config> configs;
+  zx_status_t status_return = ZX_OK;
+  zx_status_t status = controller_->GetConfigs(&configs, &status_return);
+  if (status != ZX_OK) {
+    FXL_PLOG(ERROR, status) << "Failed to call GetConfigs";
+    return nullptr;
+  }
+  if (status_return != ZX_OK) {
+    FXL_PLOG(ERROR, status_return) << "Failed to get configs";
+    return nullptr;
+  }
+  if (configs->size() <= kConfigIndex) {
+    FXL_LOG(ERROR) << "Invalid config index " << kConfigIndex;
+    return nullptr;
+  }
+  auto& config = configs->at(kConfigIndex);
+  if (config.stream_configs.size() <= kStreamConfigIndex) {
+    FXL_LOG(ERROR) << "Invalid stream config index " << kStreamConfigIndex;
+    return nullptr;
+  }
+  auto& stream_config = config.stream_configs[kStreamConfigIndex];
+  if (stream_config.image_formats.size() <= kImageFormatIndex) {
+    FXL_LOG(ERROR) << "Invalid image format index " << kImageFormatIndex;
+    return nullptr;
+  }
+  auto& image_format = stream_config.image_formats[kImageFormatIndex];
+
+  // Attempt to create a buffer collection using controller-provided constraints.
+  if (!allocator_) {
+    FXL_LOG(ERROR) << "Allocator is dead!";
+  }
+  status = allocator_->AllocateNonSharedCollection(buffer_collection_.NewRequest());
+  if (status != ZX_OK) {
+    FXL_PLOG(ERROR, status) << "Failed to allocate new collection";
+    return nullptr;
+  }
+  status = buffer_collection_->SetConstraints(true, stream_config.constraints);
+  if (status != ZX_OK) {
+    FXL_PLOG(ERROR, status) << "Failed to set constraints to those reported by the controller";
+    return nullptr;
+  }
+  status_return = ZX_OK;
+  fuchsia::sysmem::BufferCollectionInfo_2 buffers;
+  status = buffer_collection_->WaitForBuffersAllocated(&status_return, &buffers);
+  if (status != ZX_OK) {
+    FXL_PLOG(ERROR, status) << "Failed to call WaitForBuffersAllocated";
+    return nullptr;
+  }
+  if (status_return != ZX_OK) {
+    FXL_PLOG(ERROR, status_return) << "Failed to allocate buffers";
+    return nullptr;
+  }
+
+  // TODO(fxb/37296): remove ISP workarounds
+  // The ISP does not currently write the chroma layer, so initialize all VMOs to 128 (grayscale).
+  // This avoids the resulting image from appearing as 100% saturated green.
+  for (uint32_t i = 0; i < buffers.buffer_count; ++i) {
+    fzl::VmoMapper mapper;
+    status = mapper.Map(buffers.buffers[i].vmo, 0, buffers.settings.buffer_settings.size_bytes,
+                        ZX_VM_PERM_READ | ZX_VM_PERM_WRITE);
+    if (status != ZX_OK) {
+      FXL_PLOG(ERROR, status) << "Error mapping vmo";
+      return nullptr;
+    }
+    memset(mapper.start(), 128, mapper.size());
+    mapper.Unmap();
+  }
+
+  // Duplicate the collection info so it can be returned to the caller.
+  fuchsia::sysmem::BufferCollectionInfo_2 buffers_for_caller;
+  status = buffers.Clone(&buffers_for_caller);
+  if (status != ZX_OK) {
+    FXL_PLOG(ERROR, status) << "Failed to clone buffer collection";
+    return nullptr;
+  }
+
+  // Create the stream using the created buffer collection.
+  fuchsia::camera2::StreamPtr stream;
+  stream.set_error_handler(
+      [](zx_status_t status) { FXL_PLOG(ERROR, status) << "Server disconnected"; });
+  stream.events().OnFrameAvailable =
+      fit::bind_member(event_handler, &fuchsia::camera2::Stream::EventSender_::OnFrameAvailable);
+  status = controller_->CreateStream(kConfigIndex, kStreamConfigIndex, kImageFormatIndex,
+                                     std::move(buffers), stream.NewRequest());
+  if (status != ZX_OK) {
+    FXL_PLOG(ERROR, status) << "Failed to create stream";
+    return nullptr;
+  }
+
+  // The stream from controller is currently unrotated.
+  // TODO: once GDC is hooked up to do the rotation within the controller, set this to 'false'
+  *should_rotate_out = true;
+
+  *format_out = std::move(image_format);
+  *buffers_out = std::move(buffers_for_caller);
+  return std::make_unique<StreamPtrWrapper>(std::move(stream));
+}
diff --git a/src/camera/examples/demo/stream_provider/controller_stream_provider.h b/src/camera/examples/demo/stream_provider/controller_stream_provider.h
new file mode 100644
index 0000000..48a8237
--- /dev/null
+++ b/src/camera/examples/demo/stream_provider/controller_stream_provider.h
@@ -0,0 +1,30 @@
+// Copyright 2019 The Fuchsia 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 SRC_CAMERA_EXAMPLES_DEMO_CONTROLLER_STREAM_PROVIDER_H_
+#define SRC_CAMERA_EXAMPLES_DEMO_CONTROLLER_STREAM_PROVIDER_H_
+
+#include <fuchsia/camera2/hal/cpp/fidl.h>
+#include <lib/sys/cpp/component_context.h>
+
+#include "stream_provider.h"
+
+class ControllerStreamProvider : public StreamProvider {
+ public:
+  ~ControllerStreamProvider();
+  static std::unique_ptr<StreamProvider> Create();
+  virtual std::unique_ptr<fuchsia::camera2::Stream> ConnectToStream(
+      fuchsia::camera2::Stream::EventSender_* event_handler,
+      fuchsia::sysmem::ImageFormat_2* format_out,
+      fuchsia::sysmem::BufferCollectionInfo_2* buffers_out, bool* should_rotate_out) override;
+  virtual std::string GetName() override { return "fuchsia.camera2.Controller service"; }
+
+ private:
+  bool streaming_ = false;
+  fuchsia::camera2::hal::ControllerSyncPtr controller_;
+  fuchsia::sysmem::AllocatorSyncPtr allocator_;
+  fuchsia::sysmem::BufferCollectionSyncPtr buffer_collection_;
+};
+
+#endif  // SRC_CAMERA_EXAMPLES_DEMO_CONTROLLER_STREAM_PROVIDER_H_
diff --git a/src/camera/examples/demo/stream_provider/include/stream_provider.h b/src/camera/examples/demo/stream_provider/include/stream_provider.h
index e584c22..899826b 100644
--- a/src/camera/examples/demo/stream_provider/include/stream_provider.h
+++ b/src/camera/examples/demo/stream_provider/include/stream_provider.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 SRC_CAMERA_EXAMPLES_DEMO_STREAM_PROVIDER_STREAM_PROVIDER_H_
-#define SRC_CAMERA_EXAMPLES_DEMO_STREAM_PROVIDER_STREAM_PROVIDER_H_
+#ifndef SRC_CAMERA_EXAMPLES_DEMO_STREAM_PROVIDER_INCLUDE_STREAM_PROVIDER_H_
+#define SRC_CAMERA_EXAMPLES_DEMO_STREAM_PROVIDER_INCLUDE_STREAM_PROVIDER_H_
 
 #include <fuchsia/camera2/cpp/fidl.h>
 #include <fuchsia/sysmem/cpp/fidl.h>
@@ -18,6 +18,7 @@
  public:
   enum class Source {
     ISP,
+    CONTROLLER,
     NUM_SOURCES,
   };
   virtual ~StreamProvider() = default;
@@ -52,4 +53,4 @@
       fuchsia::sysmem::BufferCollectionInfo_2* buffers_out, bool* should_rotate_out) = 0;
 };
 
-#endif  // SRC_CAMERA_EXAMPLES_DEMO_STREAM_PROVIDER_STREAM_PROVIDER_H_
+#endif  // SRC_CAMERA_EXAMPLES_DEMO_STREAM_PROVIDER_INCLUDE_STREAM_PROVIDER_H_
diff --git a/src/camera/examples/demo/stream_provider/isp_stream_provider.cc b/src/camera/examples/demo/stream_provider/isp_stream_provider.cc
index f1caf59..ca760873 100644
--- a/src/camera/examples/demo/stream_provider/isp_stream_provider.cc
+++ b/src/camera/examples/demo/stream_provider/isp_stream_provider.cc
@@ -7,7 +7,6 @@
 #include <fcntl.h>
 #include <fuchsia/camera/common/cpp/fidl.h>
 #include <fuchsia/camera/test/cpp/fidl.h>
-#include <lib/async-loop/cpp/loop.h>
 #include <lib/fdio/fdio.h>
 
 #include <src/lib/fxl/logging.h>
diff --git a/src/camera/examples/demo/stream_provider/stream_provider.cc b/src/camera/examples/demo/stream_provider/stream_provider.cc
index c89940a..e195b39 100644
--- a/src/camera/examples/demo/stream_provider/stream_provider.cc
+++ b/src/camera/examples/demo/stream_provider/stream_provider.cc
@@ -4,12 +4,15 @@
 
 #include "stream_provider.h"
 
+#include "controller_stream_provider.h"
 #include "isp_stream_provider.h"
 
 std::unique_ptr<StreamProvider> StreamProvider::Create(Source source) {
   switch (source) {
     case Source::ISP:
       return IspStreamProvider::Create();
+    case Source::CONTROLLER:
+      return ControllerStreamProvider::Create();
     default:
       return nullptr;
   }
diff --git a/src/camera/examples/demo/stream_provider/streamptr_wrapper.h b/src/camera/examples/demo/stream_provider/streamptr_wrapper.h
new file mode 100644
index 0000000..8d2a498
--- /dev/null
+++ b/src/camera/examples/demo/stream_provider/streamptr_wrapper.h
@@ -0,0 +1,36 @@
+// Copyright 2019 The Fuchsia 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 SRC_CAMERA_EXAMPLES_DEMO_STREAMPTR_WRAPPER_H_
+#define SRC_CAMERA_EXAMPLES_DEMO_STREAMPTR_WRAPPER_H_
+
+#include <fuchsia/camera2/cpp/fidl.h>
+
+// This is a simple wrapper that forwards all Stream methods to an owned StreamPtr instance. It can
+// be used to serve the Stream interface as a std::unique_ptr.
+class StreamPtrWrapper : public fuchsia::camera2::Stream {
+ public:
+  StreamPtrWrapper(fuchsia::camera2::StreamPtr stream) : stream_(std::move(stream)) {}
+  fuchsia::camera2::StreamPtr& operator->() { return stream_; }
+  virtual void Start() override { stream_->Start(); }
+  virtual void Stop() override { stream_->Stop(); }
+  virtual void ReleaseFrame(uint32_t buffer_id) override { stream_->ReleaseFrame(buffer_id); }
+  virtual void AcknowledgeFrameError() override { stream_->AcknowledgeFrameError(); }
+  virtual void SetRegionOfInterest(float x_min, float y_min, float x_max, float y_max,
+                                   SetRegionOfInterestCallback callback) override {
+    stream_->SetRegionOfInterest(x_min, y_min, x_max, y_max, std::move(callback));
+  }
+  virtual void SetImageFormat(uint32_t image_format_index,
+                              SetImageFormatCallback callback) override {
+    stream_->SetImageFormat(image_format_index, std::move(callback));
+  }
+  virtual void GetImageFormats(GetImageFormatsCallback callback) override {
+    stream_->GetImageFormats(std::move(callback));
+  }
+
+ private:
+  fuchsia::camera2::StreamPtr stream_;
+};
+
+#endif  // SRC_CAMERA_EXAMPLES_DEMO_STREAMPTR_WRAPPER_H_
diff --git a/src/camera/examples/demo/test/meta/camera_demo_test.cmx b/src/camera/examples/demo/test/meta/camera_demo_test.cmx
index bb2b04f..ee48dc4 100644
--- a/src/camera/examples/demo/test/meta/camera_demo_test.cmx
+++ b/src/camera/examples/demo/test/meta/camera_demo_test.cmx
@@ -3,7 +3,8 @@
     "fuchsia.test": {
       "system-services": [
         "fuchsia.ui.policy.Presenter",
-        "fuchsia.ui.scenic.Scenic"
+        "fuchsia.ui.scenic.Scenic",
+        "fuchsia.sysmem.Allocator"
       ]
     }
   },
diff --git a/src/camera/examples/demo/text_node/BUILD.gn b/src/camera/examples/demo/text_node/BUILD.gn
new file mode 100644
index 0000000..5fd98cd
--- /dev/null
+++ b/src/camera/examples/demo/text_node/BUILD.gn
@@ -0,0 +1,49 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+group("text_node") {
+  public_deps = [
+    ":text_node_lib",
+  ]
+  public_configs = [
+    ":text_node_config",
+  ]
+}
+
+source_set("text_node_lib") {
+  visibility = [ ":*" ]
+
+  sources = [
+    "text_node.cc",
+    "//zircon/system/ulib/gfx-font-data/9x16.c",
+  ]
+
+  include_dirs = [
+    "//zircon/system/ulib/gfx/include",
+    "//zircon/system/ulib/gfx-font-data/include",
+  ]
+
+  deps = [
+    "//src/lib/fxl",
+    "//src/lib/ui/scenic/cpp",
+    "//zircon/public/lib/fzl",
+  ]
+
+  configs += [
+    ":text_node_config",
+  ]
+
+  public_deps = [
+    "//sdk/fidl/fuchsia.camera2",
+    "//zircon/system/fidl/fuchsia-sysmem",
+  ]
+}
+
+config("text_node_config") {
+  visibility = [ ":*" ]
+
+  include_dirs = [
+    "//src/camera/examples/demo/text_node/include",
+  ]
+}
diff --git a/src/camera/examples/demo/text_node/include/text_node.h b/src/camera/examples/demo/text_node/include/text_node.h
new file mode 100644
index 0000000..40105d8
--- /dev/null
+++ b/src/camera/examples/demo/text_node/include/text_node.h
@@ -0,0 +1,21 @@
+// Copyright 2019 The Fuchsia 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 SRC_CAMERA_EXAMPLES_DEMO_TEXT_NODE_INCLUDE_TEXT_NODE_H_
+#define SRC_CAMERA_EXAMPLES_DEMO_TEXT_NODE_INCLUDE_TEXT_NODE_H_
+
+#include <lib/ui/scenic/cpp/commands.h>
+#include <lib/ui/scenic/cpp/resources.h>
+
+class TextNode : public scenic::Node {
+ public:
+  explicit TextNode(scenic::Session* session);
+  TextNode(TextNode&& moved);
+  ~TextNode();
+
+  // Sets the text contents (lower 128 ASCII set) of the node.
+  zx_status_t SetText(const std::string s);
+};
+
+#endif  // SRC_CAMERA_EXAMPLES_DEMO_TEXT_NODE_INCLUDE_TEXT_NODE_H_
diff --git a/src/camera/examples/demo/text_node/text_node.cc b/src/camera/examples/demo/text_node/text_node.cc
new file mode 100644
index 0000000..4989f66
--- /dev/null
+++ b/src/camera/examples/demo/text_node/text_node.cc
@@ -0,0 +1,98 @@
+// Copyright 2019 The Fuchsia 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 <lib/fzl/vmo-mapper.h>
+#include <lib/gfx-font-data/gfx-font-data.h>
+#include <text_node.h>
+
+#include <algorithm>
+
+#include <src/lib/fxl/logging.h>
+
+TextNode::TextNode(scenic::Session* session) : scenic::Node(session) {
+  session->Enqueue(scenic::NewCreateShapeNodeCmd(id()));
+}
+
+TextNode::TextNode(TextNode&& moved) : scenic::Node(std::move(moved)) {}
+
+TextNode::~TextNode() = default;
+
+zx_status_t TextNode::SetText(const std::string s) {
+  static constexpr const uint32_t kBitmapScale = 1;  // Per-dimension pixel scatter ratio.
+  static constexpr const uint32_t kTextForeground = 0xFF000000;  // Opaque black.
+  static constexpr const uint32_t kTextBackground = 0x7FFFFFFF;  // Semi-transparent white.
+  static constexpr const struct {
+    uint8_t r = 0xFF;
+    uint8_t g = 0xFF;
+    uint8_t b = 0xFF;
+    uint8_t a = 0xFE;
+  } kShapeColor;  // Nearly opaque white.
+
+  const auto& font = gfx_font_9x16;
+  static constexpr const uint32_t kFontDataBits = 8;
+
+  fuchsia::images::ImageInfo image_info;
+  uint32_t left_pad = font.width - kFontDataBits;
+  image_info.width = (font.width * s.size() + left_pad) * kBitmapScale;
+  image_info.height = font.height * kBitmapScale;
+  image_info.stride = image_info.width * sizeof(uint32_t);
+  image_info.alpha_format = fuchsia::images::AlphaFormat::NON_PREMULTIPLIED;
+  size_t image_size = image_info.width * image_info.height * sizeof(uint32_t);
+
+  zx::vmo vmo;
+  fzl::VmoMapper mapper;
+  zx_status_t status =
+      mapper.CreateAndMap(image_size, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, nullptr, &vmo);
+  if (status != ZX_OK) {
+    FXL_PLOG(ERROR, status);
+    return status;
+  }
+
+  auto bitmap_data = reinterpret_cast<uint32_t*>(mapper.start());
+
+  size_t x_offset = left_pad * kBitmapScale;
+  for (uint32_t v = 0; v < image_info.height; ++v) {
+    for (uint32_t u = 0; u < x_offset; ++u) {
+      bitmap_data[image_info.width * v + u] = kTextBackground;
+    }
+  }
+  for (auto c : s) {
+    if (c & 0x80) {  // Font only defines lower ASCII bitmaps.
+      c = '?';
+    }
+    const auto* char_data = &font.data[c * font.height];
+    for (uint32_t v = 0; v < font.height; ++v) {
+      for (uint32_t u = 0; u < font.width; ++u) {
+        uint32_t pixel = kTextBackground;
+        if (u < kFontDataBits && (char_data[v] & (1u << u))) {
+          pixel = kTextForeground;
+        }
+        for (uint32_t vv = 0; vv < kBitmapScale; ++vv) {
+          for (uint32_t uu = 0; uu < kBitmapScale; ++uu) {
+            bitmap_data[image_info.width * (v * kBitmapScale + vv) + u * kBitmapScale + uu +
+                        x_offset] = pixel;
+          }
+        }
+      }
+    }
+    x_offset += font.width * kBitmapScale;
+  }
+
+  mapper.Unmap();
+
+  scenic::Memory memory(session(), std::move(vmo), image_size,
+                        fuchsia::images::MemoryType::HOST_MEMORY);
+  scenic::Image image(memory, 0, fidl::Clone(image_info));
+  scenic::Material material(session());
+  material.SetTexture(image);
+  // TODO(fxb/38373): support is_alpha
+  material.SetColor(kShapeColor.r, kShapeColor.g, kShapeColor.b, kShapeColor.a);
+
+  scenic::Rectangle shape(session(), image_info.width * 1.0f / kBitmapScale,
+                          image_info.height * 1.0f / kBitmapScale);
+  session()->Enqueue(scenic::NewSetShapeCmd(id(), shape.id()));
+  session()->Enqueue(scenic::NewSetMaterialCmd(id(), material.id()));
+
+  return ZX_OK;
+}