Merge inbound to mozilla-central. a=merge
authorCsoregi Natalia <ncsoregi@mozilla.com>
Fri, 12 Oct 2018 13:14:37 +0300
changeset 496569 7a0840d60252
parent 496538 8bd12e6c3f99 (current diff)
parent 496568 ee4f12cc7136 (diff)
child 496570 0ab221f0a996
child 496585 73c033dd56a2
child 496604 09b16fa61f28
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0a1
first release with
nightly linux32
7a0840d60252 / 64.0a1 / 20181012103207 / files
nightly linux64
7a0840d60252 / 64.0a1 / 20181012103207 / files
nightly mac
7a0840d60252 / 64.0a1 / 20181012103207 / files
nightly win32
7a0840d60252 / 64.0a1 / 20181012103207 / files
nightly win64
7a0840d60252 / 64.0a1 / 20181012103207 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
testing/mochitest/rungeckoview.py
--- a/build/clang-plugin/mozsearch-plugin/MozsearchIndexer.cpp
+++ b/build/clang-plugin/mozsearch-plugin/MozsearchIndexer.cpp
@@ -91,33 +91,36 @@ class IndexConsumer;
 // here.
 struct FileInfo {
   FileInfo(std::string &Rname) : Realname(Rname) {
     if (Rname.compare(0, Objdir.length(), Objdir) == 0) {
       // We're in the objdir, so we are probably a generated header
       // We use the escape character to indicate the objdir nature.
       // Note that output also has the `/' already placed
       Interesting = true;
+      Generated = true;
       Realname.replace(0, Objdir.length(), GENERATED);
       return;
     }
 
     // Empty filenames can get turned into Srcdir when they are resolved as
     // absolute paths, so we should exclude files that are exactly equal to
     // Srcdir or anything outside Srcdir.
     Interesting = (Rname.length() > Srcdir.length()) &&
                   (Rname.compare(0, Srcdir.length(), Srcdir) == 0);
+    Generated = false;
     if (Interesting) {
       // Remove the trailing `/' as well.
       Realname.erase(0, Srcdir.length() + 1);
     }
   }
   std::string Realname;
   std::vector<std::string> Output;
   bool Interesting;
+  bool Generated;
 };
 
 class IndexConsumer;
 
 class PreprocessorHook : public PPCallbacks {
   IndexConsumer *Indexer;
 
 public:
@@ -325,16 +328,24 @@ private:
 
   std::string mangleLocation(SourceLocation Loc,
                              std::string Backup = std::string()) {
     FileInfo *F = getFileInfo(Loc);
     std::string Filename = F->Realname;
     if (Filename.length() == 0 && Backup.length() != 0) {
       return Backup;
     }
+    if (F->Generated) {
+      // Since generated files may be different on different platforms,
+      // we need to include a platform-specific thing in the hash. Otherwise
+      // we can end up with hash collisions where different symbols from
+      // different platforms map to the same thing.
+      char* Platform = getenv("MOZSEARCH_PLATFORM");
+      Filename = std::string(Platform ? Platform : "") + std::string("@") + Filename;
+    }
     return hash(Filename + std::string("@") + locationToString(Loc));
   }
 
   std::string mangleQualifiedName(std::string Name) {
     std::replace(Name.begin(), Name.end(), ' ', '_');
     return Name;
   }
 
--- a/devtools/client/inspector/layout/components/LayoutApp.js
+++ b/devtools/client/inspector/layout/components/LayoutApp.js
@@ -115,19 +115,19 @@ class LayoutApp extends PureComponent {
         }
       });
 
       // If the current selected node is both a flex container and flex item. Render
       // an accordion with another Flexbox component where the flexbox to show is the
       // parent flex container of the current selected node.
       if (this.props.flexbox.flexItemContainer &&
           this.props.flexbox.flexItemContainer.actorID) {
-        // Insert the parent flex container to the second index of the accordion item
+        // Insert the parent flex container to the first index of the accordion item
         // list.
-        items.splice(1, 0, {
+        items.splice(0, 0, {
           component: Flexbox,
           componentProps: {
             ...this.props,
             flexContainer: this.props.flexbox.flexItemContainer,
           },
           header: this.getFlexboxHeader(this.props.flexbox.flexItemContainer),
           opened: Services.prefs.getBoolPref(FLEXBOX_OPENED_PREF),
           onToggled: () => {
--- a/dom/base/nsDOMNavigationTiming.cpp
+++ b/dom/base/nsDOMNavigationTiming.cpp
@@ -14,16 +14,19 @@
 #include "nsIDocShellTreeItem.h"
 #include "nsIScriptSecurityManager.h"
 #include "prtime.h"
 #include "nsIURI.h"
 #include "nsPrintfCString.h"
 #include "mozilla/dom/PerformanceNavigation.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Telemetry.h"
+#ifdef MOZ_GECKO_PROFILER
+#include "ProfilerMarkerPayload.h"
+#endif
 
 using namespace mozilla;
 
 nsDOMNavigationTiming::nsDOMNavigationTiming(nsDocShell* aDocShell)
 {
   Clear();
 
   mDocShell = aDocShell;
@@ -248,16 +251,123 @@ nsDOMNavigationTiming::NotifyDOMContentL
   PROFILER_TRACING("Navigation", "DOMContentLoaded", TRACING_INTERVAL_END);
 
   if (IsTopLevelContentDocumentInContentProcess()) {
     Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_DOM_CONTENT_LOADED_END_MS,
                                    mNavigationStart);
   }
 }
 
+// static
+void
+nsDOMNavigationTiming::TTITimeoutCallback(nsITimer* aTimer, void *aClosure)
+{
+  nsDOMNavigationTiming* self = static_cast<nsDOMNavigationTiming*>(aClosure);
+  self->TTITimeout(aTimer);
+}
+
+// Return the max of aT1 and aT2, or the lower of the two if there's more
+// than Nms (the window size) between them.  In other words, the window
+// starts at the lower of aT1 and aT2, and we only want to respect
+// timestamps within the window (and pick the max of those).
+//
+// This approach handles the edge case of a late wakeup: where there was
+// more than Nms after one (of aT1 or aT2) without the other, but the other
+// happened after Nms and before we woke up.  For example, if aT1 was 10
+// seconds after aT2, but we woke up late (after aT1) we don't want to
+// return aT1 if the window is 5 seconds.
+static const TimeStamp&
+MaxWithinWindowBeginningAtMin(const TimeStamp& aT1, const TimeStamp& aT2,
+                              const TimeDuration& aWindowSize)
+{
+  if (aT2.IsNull()) {
+    return aT1;
+  } else if (aT1.IsNull()) {
+    return aT2;
+  }
+  if (aT1 > aT2) {
+    if ((aT1 - aT2) > aWindowSize) {
+      return aT2;
+    }
+    return aT1;
+  }
+  if ((aT2 - aT1) > aWindowSize) {
+    return aT1;
+  }
+  return aT2;
+}
+
+#define TTI_WINDOW_SIZE_MS (5 * 1000)
+
+void
+nsDOMNavigationTiming::TTITimeout(nsITimer* aTimer)
+{
+  // Check TTI: see if it's been 5 seconds since the last Long Task
+  TimeStamp now = TimeStamp::Now();
+  MOZ_RELEASE_ASSERT(!mNonBlankPaint.IsNull(), "TTI timeout with no non-blank-paint?");
+
+  nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+  TimeStamp lastLongTaskEnded;
+  mainThread->GetLastLongNonIdleTaskEnd(&lastLongTaskEnded);
+  if (!lastLongTaskEnded.IsNull()) {
+    TimeDuration delta = now - lastLongTaskEnded;
+    if (delta.ToMilliseconds() < TTI_WINDOW_SIZE_MS) {
+      // Less than 5 seconds since the last long task.  Schedule another check
+      aTimer->InitWithNamedFuncCallback(TTITimeoutCallback, this, TTI_WINDOW_SIZE_MS,
+                                        nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+                                         "nsDOMNavigationTiming::TTITimeout");
+      return;
+    }
+  }
+  // To correctly implement TTI/TTFI as proposed, we'd need to use
+  // FirstContentfulPaint (FCP, which we have not yet implemented) instead
+  // of FirstNonBlankPaing (FNBP) to start at, and not fire it until there
+  // are no more than 2 network loads.  By the proposed definition, without
+  // that we're closer to TimeToFirstInteractive.
+
+  // XXX check number of network loads, and if > 2 mark to check if loads
+  // decreases to 2 (or record that point and let the normal timer here
+  // handle it)
+
+  // TTI has occurred!  TTI is either FCP (if there are no longtasks and no
+  // DCLEnd in the window that starts at FCP), or at the end of the last
+  // Long Task or DOMContentLoadedEnd (whichever is later).
+
+  if (mTTFI.IsNull()) {
+    mTTFI = MaxWithinWindowBeginningAtMin(lastLongTaskEnded, mDOMContentLoadedEventEnd,
+                                          TimeDuration::FromMilliseconds(TTI_WINDOW_SIZE_MS));
+    if (mTTFI.IsNull()) {
+      mTTFI = mNonBlankPaint;
+    }
+  }
+  // XXX Implement TTI via check number of network loads, and if > 2 mark
+  // to check if loads decreases to 2 (or record that point and let the
+  // normal timer here handle it)
+
+  mTTITimer = nullptr;
+
+#ifdef MOZ_GECKO_PROFILER
+  if (profiler_is_active()) {
+    TimeDuration elapsed = mTTFI - mNavigationStart;
+    TimeDuration elapsedLongTask = lastLongTaskEnded.IsNull() ? 0 : lastLongTaskEnded - mNavigationStart;
+    nsAutoCString spec;
+    if (mLoadedURI) {
+      mLoadedURI->GetSpec(spec);
+    }
+    nsPrintfCString marker("TTFI after %dms (LongTask after %dms) for URL %s",
+                           int(elapsed.ToMilliseconds()),
+                           int(elapsedLongTask.ToMilliseconds()),spec.get());
+
+    profiler_add_marker(
+      "TTI", MakeUnique<UserTimingMarkerPayload>(NS_ConvertASCIItoUTF16(marker), mTTFI));
+  }
+#endif
+  return;
+}
+
 void
 nsDOMNavigationTiming::NotifyNonBlankPaintForRootContentDocument()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mNavigationStart.IsNull());
 
   if (!mNonBlankPaint.IsNull()) {
     return;
@@ -274,16 +384,25 @@ nsDOMNavigationTiming::NotifyNonBlankPai
     }
     nsPrintfCString marker("Non-blank paint after %dms for URL %s, %s",
                            int(elapsed.ToMilliseconds()), spec.get(),
                            mDocShellHasBeenActiveSinceNavigationStart ? "foreground tab" : "this tab was inactive some of the time between navigation start and first non-blank paint");
     profiler_add_marker(marker.get());
   }
 #endif
 
+  if (!mTTITimer) {
+    mTTITimer = NS_NewTimer();
+  }
+
+  // TTI is first checked 5 seconds after the FCP (non-blank-paint is very close to FCP).
+  mTTITimer->InitWithNamedFuncCallback(TTITimeoutCallback, this, TTI_WINDOW_SIZE_MS,
+                                       nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+                                       "nsDOMNavigationTiming::TTITimeout");
+
   if (mDocShellHasBeenActiveSinceNavigationStart) {
     if (net::nsHttp::IsBeforeLastActiveTabLoadOptimization(mNavigationStart)) {
       Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_NON_BLANK_PAINT_NETOPT_MS,
                                      mNavigationStart,
                                      mNonBlankPaint);
     } else {
       Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_NON_BLANK_PAINT_NO_NETOPT_MS,
                                      mNavigationStart,
--- a/dom/base/nsDOMNavigationTiming.h
+++ b/dom/base/nsDOMNavigationTiming.h
@@ -7,16 +7,17 @@
 #ifndef nsDOMNavigationTiming_h___
 #define nsDOMNavigationTiming_h___
 
 #include "nsCOMPtr.h"
 #include "nsCOMArray.h"
 #include "mozilla/WeakPtr.h"
 #include "mozilla/RelativeTimeline.h"
 #include "mozilla/TimeStamp.h"
+#include "nsITimer.h"
 
 class nsDocShell;
 class nsIURI;
 
 typedef unsigned long long DOMTimeMilliSec;
 typedef double DOMHighResTimeStamp;
 
 class nsDOMNavigationTiming final
@@ -91,16 +92,20 @@ public:
   DOMTimeMilliSec GetLoadEventEnd() const
   {
     return TimeStampToDOM(mLoadEventEnd);
   }
   DOMTimeMilliSec GetTimeToNonBlankPaint() const
   {
     return TimeStampToDOM(mNonBlankPaint);
   }
+  DOMTimeMilliSec GetTimeToTTFI() const
+  {
+    return TimeStampToDOM(mTTFI);
+  }
   DOMTimeMilliSec GetTimeToDOMContentFlushed() const
   {
     return TimeStampToDOM(mDOMContentFlushed);
   }
 
   DOMHighResTimeStamp GetUnloadEventStartHighRes()
   {
     mozilla::TimeStamp stamp = GetUnloadEventStartTimeStamp();
@@ -159,16 +164,20 @@ public:
   // Document changes state to 'loading' before connecting to timing
   void SetDOMLoadingTimeStamp(nsIURI* aURI, mozilla::TimeStamp aValue);
   void NotifyDOMLoading(nsIURI* aURI);
   void NotifyDOMInteractive(nsIURI* aURI);
   void NotifyDOMComplete(nsIURI* aURI);
   void NotifyDOMContentLoadedStart(nsIURI* aURI);
   void NotifyDOMContentLoadedEnd(nsIURI* aURI);
 
+  static void TTITimeoutCallback(nsITimer* aTimer, void *aClosure);
+  void TTITimeout(nsITimer* aTimer);
+
+  void NotifyLongTask(mozilla::TimeStamp aWhen);
   void NotifyNonBlankPaintForRootContentDocument();
   void NotifyDOMContentFlushedForRootContentDocument();
   void NotifyDocShellStateChanged(DocShellState aDocShellState);
 
   DOMTimeMilliSec TimeStampToDOM(mozilla::TimeStamp aStamp) const;
 
   inline DOMHighResTimeStamp TimeStampToDOMHighRes(mozilla::TimeStamp aStamp) const
   {
@@ -189,16 +198,17 @@ private:
   mozilla::TimeStamp GetUnloadEventEndTimeStamp() const;
 
   bool IsTopLevelContentDocumentInContentProcess() const;
 
   mozilla::WeakPtr<nsDocShell> mDocShell;
 
   nsCOMPtr<nsIURI> mUnloadedURI;
   nsCOMPtr<nsIURI> mLoadedURI;
+  nsCOMPtr<nsITimer> mTTITimer;
 
   Type mNavigationType;
   DOMHighResTimeStamp mNavigationStartHighRes;
   mozilla::TimeStamp mNavigationStart;
   mozilla::TimeStamp mNonBlankPaint;
   mozilla::TimeStamp mDOMContentFlushed;
 
   mozilla::TimeStamp mBeforeUnloadStart;
@@ -208,12 +218,14 @@ private:
   mozilla::TimeStamp mLoadEventEnd;
 
   mozilla::TimeStamp mDOMLoading;
   mozilla::TimeStamp mDOMInteractive;
   mozilla::TimeStamp mDOMContentLoadedEventStart;
   mozilla::TimeStamp mDOMContentLoadedEventEnd;
   mozilla::TimeStamp mDOMComplete;
 
+  mozilla::TimeStamp mTTFI;
+
   bool mDocShellHasBeenActiveSinceNavigationStart : 1;
 };
 
 #endif /* nsDOMNavigationTiming_h___ */
--- a/dom/cache/Cache.cpp
+++ b/dom/cache/Cache.cpp
@@ -347,17 +347,17 @@ Cache::Add(JSContext* aContext, const Re
     return nullptr;
   }
 
   GlobalObject global(aContext, mGlobal->GetGlobalJSObject());
   MOZ_DIAGNOSTIC_ASSERT(!global.Failed());
 
   nsTArray<RefPtr<Request>> requestList(1);
   RefPtr<Request> request = Request::Constructor(global, aRequest,
-                                                   RequestInit(), aRv);
+                                                 RequestInit(), aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   nsAutoString url;
   request->GetUrl(url);
   if (NS_WARN_IF(!IsValidPutRequestURL(url, aRv))) {
     return nullptr;
@@ -395,17 +395,17 @@ Cache::AddAll(JSContext* aContext,
       }
     } else {
       requestOrString.SetAsUSVString().Rebind(
         aRequestList[i].GetAsUSVString().Data(),
         aRequestList[i].GetAsUSVString().Length());
     }
 
     RefPtr<Request> request = Request::Constructor(global, requestOrString,
-                                                     RequestInit(), aRv);
+                                                   RequestInit(), aRv);
     if (NS_WARN_IF(aRv.Failed())) {
       return nullptr;
     }
 
     nsAutoString url;
     request->GetUrl(url);
     if (NS_WARN_IF(!IsValidPutRequestURL(url, aRv))) {
       return nullptr;
--- a/dom/cache/TypeUtils.cpp
+++ b/dom/cache/TypeUtils.cpp
@@ -207,17 +207,21 @@ TypeUtils::ToCacheResponseWithoutBody(Ca
   aOut.paddingSize() = aIn.GetPaddingSize();
 }
 
 void
 TypeUtils::ToCacheResponse(JSContext* aCx, CacheResponse& aOut, Response& aIn,
                            nsTArray<UniquePtr<AutoIPCStream>>& aStreamCleanupList,
                            ErrorResult& aRv)
 {
-  if (aIn.BodyUsed()) {
+  bool bodyUsed = aIn.GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+  if (bodyUsed) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return;
   }
 
   RefPtr<InternalResponse> ir = aIn.GetInternalResponse();
   ToCacheResponseWithoutBody(aOut, *ir, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
@@ -441,17 +445,21 @@ TypeUtils::CheckAndSetBodyUsed(JSContext
                                BodyAction aBodyAction, ErrorResult& aRv)
 {
   MOZ_DIAGNOSTIC_ASSERT(aRequest);
 
   if (aBodyAction == IgnoreBody) {
     return;
   }
 
-  if (aRequest->BodyUsed()) {
+  bool bodyUsed = aRequest->GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+  if (bodyUsed) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return;
   }
 
   nsCOMPtr<nsIInputStream> stream;
   aRequest->GetBody(getter_AddRefs(stream));
   if (stream) {
     aRequest->SetBodyUsed(aCx, aRv);
--- a/dom/fetch/Fetch.cpp
+++ b/dom/fetch/Fetch.cpp
@@ -51,30 +51,37 @@
 #include "mozilla/dom/WorkerScope.h"
 
 namespace mozilla {
 namespace dom {
 
 namespace {
 
 void
-AbortStream(JSContext* aCx, JS::Handle<JSObject*> aStream)
+AbortStream(JSContext* aCx, JS::Handle<JSObject*> aStream, ErrorResult& aRv)
 {
-  if (!JS::ReadableStreamIsReadable(aStream)) {
+  bool isReadable;
+  if (!JS::ReadableStreamIsReadable(aCx, aStream, &isReadable)) {
+    aRv.StealExceptionFromJSContext(aCx);
+    return;
+  }
+  if (!isReadable) {
     return;
   }
 
   RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR);
 
   JS::Rooted<JS::Value> value(aCx);
   if (!GetOrCreateDOMReflector(aCx, e, &value)) {
     return;
   }
 
-  JS::ReadableStreamError(aCx, aStream, value);
+  if (!JS::ReadableStreamError(aCx, aStream, value)) {
+    aRv.StealExceptionFromJSContext(aCx);
+  }
 }
 
 } // anonymous
 
 class AbortSignalMainThread final : public AbortSignalImpl
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
@@ -1096,49 +1103,68 @@ FetchBody<Derived>::~FetchBody()
 template
 FetchBody<Request>::~FetchBody();
 
 template
 FetchBody<Response>::~FetchBody();
 
 template <class Derived>
 bool
-FetchBody<Derived>::BodyUsed() const
+FetchBody<Derived>::GetBodyUsed(ErrorResult& aRv) const
 {
   if (mBodyUsed) {
     return true;
   }
 
-  // If this object is disturbed or locked, return false.
+  // If this stream is disturbed or locked, return true.
   if (mReadableStreamBody) {
     AutoJSAPI jsapi;
     if (!jsapi.Init(mOwner)) {
+      aRv.Throw(NS_ERROR_FAILURE);
       return true;
     }
 
     JSContext* cx = jsapi.cx();
+    JS::Rooted<JSObject*> body(cx, mReadableStreamBody);
+    bool disturbed;
+    bool locked;
+    bool readable;
+    if (!JS::ReadableStreamIsDisturbed(cx, body, &disturbed) ||
+        !JS::ReadableStreamIsLocked(cx, body, &locked) ||
+        !JS::ReadableStreamIsReadable(cx, body, &readable)) {
+      aRv.StealExceptionFromJSContext(cx);
+      return false;
+    }
 
-    JS::Rooted<JSObject*> body(cx, mReadableStreamBody);
-    if (JS::ReadableStreamIsDisturbed(body) ||
-        JS::ReadableStreamIsLocked(body) ||
-        !JS::ReadableStreamIsReadable(body)) {
-      return true;
-    }
+    return disturbed || locked || !readable;
   }
 
   return false;
 }
 
 template
 bool
-FetchBody<Request>::BodyUsed() const;
+FetchBody<Request>::GetBodyUsed(ErrorResult&) const;
 
 template
 bool
-FetchBody<Response>::BodyUsed() const;
+FetchBody<Response>::GetBodyUsed(ErrorResult&) const;
+
+template <class Derived>
+bool
+FetchBody<Derived>::CheckBodyUsed() const
+{
+  IgnoredErrorResult result;
+  bool bodyUsed = GetBodyUsed(result);
+  if (result.Failed()) {
+    // Ignore the error.
+    return true;
+  }
+  return bodyUsed;
+}
 
 template <class Derived>
 void
 FetchBody<Derived>::SetBodyUsed(JSContext* aCx, ErrorResult& aRv)
 {
   MOZ_ASSERT(aCx);
   MOZ_ASSERT(mOwner->EventTargetFor(TaskCategory::Other)->IsOnCurrentThread());
 
@@ -1147,18 +1173,24 @@ FetchBody<Derived>::SetBodyUsed(JSContex
   }
 
   mBodyUsed = true;
 
   // If we already have a ReadableStreamBody and it has been created by DOM, we
   // have to lock it now because it can have been shared with other objects.
   if (mReadableStreamBody) {
     JS::Rooted<JSObject*> readableStreamObj(aCx, mReadableStreamBody);
-    if (JS::ReadableStreamGetMode(readableStreamObj) ==
-          JS::ReadableStreamMode::ExternalSource) {
+
+    JS::ReadableStreamMode mode;
+    if (!JS::ReadableStreamGetMode(aCx, readableStreamObj, &mode)) {
+      aRv.StealExceptionFromJSContext(aCx);
+      return;
+    }
+
+    if (mode == JS::ReadableStreamMode::ExternalSource) {
       LockStream(aCx, readableStreamObj, aRv);
       if (NS_WARN_IF(aRv.Failed())) {
         return;
       }
     } else {
       // If this is not a native ReadableStream, let's activate the
       // FetchStreamReader.
       MOZ_ASSERT(mFetchStreamReader);
@@ -1187,17 +1219,21 @@ FetchBody<Derived>::ConsumeBody(JSContex
                                 ErrorResult& aRv)
 {
   RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl();
   if (signalImpl && signalImpl->Aborted()) {
     aRv.Throw(NS_ERROR_DOM_ABORT_ERR);
     return nullptr;
   }
 
-  if (BodyUsed()) {
+  bool bodyUsed = GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  if (bodyUsed) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   SetBodyUsed(aCx, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
@@ -1293,17 +1329,21 @@ FetchBody<Derived>::SetReadableStreamBod
   RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl();
   if (!signalImpl) {
     return;
   }
 
   bool aborted = signalImpl->Aborted();
   if (aborted) {
     JS::Rooted<JSObject*> body(aCx, mReadableStreamBody);
-    AbortStream(aCx, body);
+    IgnoredErrorResult result;
+    AbortStream(aCx, body, result);
+    if (NS_WARN_IF(result.Failed())) {
+      return;
+    }
   } else if (!IsFollowing()) {
     Follow(signalImpl);
   }
 }
 
 template
 void
 FetchBody<Request>::SetReadableStreamBody(JSContext* aCx, JSObject* aBody);
@@ -1336,27 +1376,34 @@ FetchBody<Derived>::GetBody(JSContext* a
                       inputStream, &body, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
 
   MOZ_ASSERT(body);
 
   // If the body has been already consumed, we lock the stream.
-  if (BodyUsed()) {
+  bool bodyUsed = GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+  if (bodyUsed) {
     LockStream(aCx, body, aRv);
     if (NS_WARN_IF(aRv.Failed())) {
       return;
     }
   }
 
   RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl();
   if (signalImpl) {
     if (signalImpl->Aborted()) {
-      AbortStream(aCx, body);
+      AbortStream(aCx, body, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
     } else if (!IsFollowing()) {
       Follow(signalImpl);
     }
   }
 
   mReadableStreamBody = body;
   aBodyOut.set(mReadableStreamBody);
 }
@@ -1374,18 +1421,24 @@ FetchBody<Response>::GetBody(JSContext* 
                              ErrorResult& aRv);
 
 template <class Derived>
 void
 FetchBody<Derived>::LockStream(JSContext* aCx,
                                JS::HandleObject aStream,
                                ErrorResult& aRv)
 {
-  MOZ_ASSERT(JS::ReadableStreamGetMode(aStream) ==
-               JS::ReadableStreamMode::ExternalSource);
+#if DEBUG
+  JS::ReadableStreamMode streamMode;
+  if (!JS::ReadableStreamGetMode(aCx, aStream, &streamMode)) {
+    aRv.StealExceptionFromJSContext(aCx);
+    return;
+  }
+  MOZ_ASSERT(streamMode == JS::ReadableStreamMode::ExternalSource);
+#endif // DEBUG
 
   // This is native stream, creating a reader will not execute any JS code.
   JS::Rooted<JSObject*> reader(aCx,
                                JS::ReadableStreamGetReader(aCx, aStream,
                                                            JS::ReadableStreamReaderMode::Default));
   if (!reader) {
     aRv.StealExceptionFromJSContext(aCx);
     return;
@@ -1411,32 +1464,37 @@ void
 FetchBody<Derived>::MaybeTeeReadableStreamBody(JSContext* aCx,
                                                JS::MutableHandle<JSObject*> aBodyOut,
                                                FetchStreamReader** aStreamReader,
                                                nsIInputStream** aInputStream,
                                                ErrorResult& aRv)
 {
   MOZ_DIAGNOSTIC_ASSERT(aStreamReader);
   MOZ_DIAGNOSTIC_ASSERT(aInputStream);
-  MOZ_DIAGNOSTIC_ASSERT(!BodyUsed());
+  MOZ_DIAGNOSTIC_ASSERT(!CheckBodyUsed());
 
   aBodyOut.set(nullptr);
   *aStreamReader = nullptr;
   *aInputStream = nullptr;
 
   if (!mReadableStreamBody) {
     return;
   }
 
   JS::Rooted<JSObject*> stream(aCx, mReadableStreamBody);
 
   // If this is a ReadableStream with an external source, this has been
   // generated by a Fetch. In this case, Fetch will be able to recreate it
   // again when GetBody() is called.
-  if (JS::ReadableStreamGetMode(stream) == JS::ReadableStreamMode::ExternalSource) {
+  JS::ReadableStreamMode streamMode;
+  if (!JS::ReadableStreamGetMode(aCx, stream, &streamMode)) {
+    aRv.StealExceptionFromJSContext(aCx);
+    return;
+  }
+  if (streamMode == JS::ReadableStreamMode::ExternalSource) {
     aBodyOut.set(nullptr);
     return;
   }
 
   JS::Rooted<JSObject*> branch1(aCx);
   JS::Rooted<JSObject*> branch2(aCx);
 
   if (!JS::ReadableStreamTee(aCx, stream, &branch1, &branch2)) {
@@ -1480,17 +1538,18 @@ FetchBody<Derived>::Abort()
   AutoJSAPI jsapi;
   if (!jsapi.Init(mOwner)) {
     return;
   }
 
   JSContext* cx = jsapi.cx();
 
   JS::Rooted<JSObject*> body(cx, mReadableStreamBody);
-  AbortStream(cx, body);
+  IgnoredErrorResult result;
+  AbortStream(cx, body, result);
 }
 
 template
 void
 FetchBody<Request>::Abort();
 
 template
 void
--- a/dom/fetch/Fetch.h
+++ b/dom/fetch/Fetch.h
@@ -145,17 +145,22 @@ public:
 template <class Derived>
 class FetchBody : public FetchStreamHolder
                 , public AbortFollower
 {
 public:
   friend class FetchBodyConsumer<Derived>;
 
   bool
-  BodyUsed() const;
+  GetBodyUsed(ErrorResult& aRv) const;
+
+  // For use in assertions. On success, returns true if the body is used, false
+  // if not. On error, this sweeps the error under the rug and returns true.
+  bool
+  CheckBodyUsed() const;
 
   already_AddRefed<Promise>
   ArrayBuffer(JSContext* aCx, ErrorResult& aRv)
   {
     return ConsumeBody(aCx, CONSUME_ARRAYBUFFER, aRv);
   }
 
   already_AddRefed<Promise>
--- a/dom/fetch/FetchConsumer.cpp
+++ b/dom/fetch/FetchConsumer.cpp
@@ -746,17 +746,17 @@ FetchBodyConsumer<Derived>::ContinueCons
 
   if (mBodyConsumed) {
     return;
   }
   mBodyConsumed = true;
 
   // Just a precaution to ensure ContinueConsumeBody is not called out of
   // sync with a body read.
-  MOZ_ASSERT(mBody->BodyUsed());
+  MOZ_ASSERT(mBody->CheckBodyUsed());
 
   MOZ_ASSERT(mConsumePromise);
   RefPtr<Promise> localPromise = mConsumePromise.forget();
 
   RefPtr<FetchBodyConsumer<Derived>> self = this;
   auto autoReleaseObject = mozilla::MakeScopeExit([self] {
     self->ReleaseObject();
   });
@@ -864,17 +864,17 @@ FetchBodyConsumer<Derived>::ContinueCons
 
   if (mBodyConsumed) {
     return;
   }
   mBodyConsumed = true;
 
   // Just a precaution to ensure ContinueConsumeBody is not called out of
   // sync with a body read.
-  MOZ_ASSERT(mBody->BodyUsed());
+  MOZ_ASSERT(mBody->CheckBodyUsed());
 
   MOZ_ASSERT(mConsumePromise);
   RefPtr<Promise> localPromise = mConsumePromise.forget();
 
   if (!aShuttingDown) {
     RefPtr<dom::Blob> blob = dom::Blob::Create(mGlobal, aBlobImpl);
     MOZ_ASSERT(blob);
 
--- a/dom/fetch/FetchStream.cpp
+++ b/dom/fetch/FetchStream.cpp
@@ -128,17 +128,24 @@ FetchStream::Create(JSContext* aCx, Fetc
 FetchStream::RequestDataCallback(JSContext* aCx,
                                  JS::HandleObject aStream,
                                  void* aUnderlyingSource,
                                  uint8_t aFlags,
                                  size_t aDesiredSize)
 {
   MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource);
   MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG);
-  MOZ_DIAGNOSTIC_ASSERT(JS::ReadableStreamIsDisturbed(aStream));
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+  bool disturbed;
+  if (!JS::ReadableStreamIsDisturbed(aCx, aStream, &disturbed)) {
+    JS_ClearPendingException(aCx);
+  } else {
+    MOZ_DIAGNOSTIC_ASSERT(disturbed);
+  }
+#endif
 
   RefPtr<FetchStream> stream = static_cast<FetchStream*>(aUnderlyingSource);
   stream->AssertIsOnOwningThread();
 
   MutexAutoLock lock(stream->mMutex);
 
   MOZ_DIAGNOSTIC_ASSERT(stream->mState == eInitializing ||
                         stream->mState == eWaiting ||
@@ -486,17 +493,21 @@ FetchStream::CloseAndReleaseObjects(JSCo
                                     JS::HandleObject aStream)
 {
   AssertIsOnOwningThread();
   MOZ_DIAGNOSTIC_ASSERT(mState != eClosed);
 
   ReleaseObjects(aProofOfLock);
 
   MutexAutoUnlock unlock(mMutex);
-  if (JS::ReadableStreamIsReadable(aStream)) {
+  bool readable;
+  if (!JS::ReadableStreamIsReadable(aCx, aStream, &readable)) {
+    return;
+  }
+  if (readable) {
     JS::ReadableStreamClose(aCx, aStream);
   }
 }
 
 void
 FetchStream::ReleaseObjects()
 {
   MutexAutoLock lock(mMutex);
--- a/dom/fetch/FetchUtil.cpp
+++ b/dom/fetch/FetchUtil.cpp
@@ -557,26 +557,30 @@ FetchUtil::StreamResponseToJS(JSContext*
       response->Type() != ResponseType::Default) {
     return ThrowException(aCx, JSMSG_BAD_RESPONSE_CORS_SAME_ORIGIN);
   }
 
   if (!response->Ok()) {
     return ThrowException(aCx, JSMSG_BAD_RESPONSE_STATUS);
   }
 
-  if (response->BodyUsed()) {
+  IgnoredErrorResult result;
+  bool used = response->GetBodyUsed(result);
+  if (NS_WARN_IF(result.Failed())) {
+    return ThrowException(aCx, JSMSG_ERROR_CONSUMING_RESPONSE);
+  }
+  if (used) {
     return ThrowException(aCx, JSMSG_RESPONSE_ALREADY_CONSUMED);
   }
 
   switch (aMimeType) {
     case JS::MimeType::Wasm:
       nsAutoString url;
       response->GetUrl(url);
 
-      IgnoredErrorResult result;
       nsCString sourceMapUrl;
       response->GetInternalHeaders()->Get(NS_LITERAL_CSTRING("SourceMap"), sourceMapUrl, result);
       if (NS_WARN_IF(result.Failed())) {
         return ThrowException(aCx, JSMSG_ERROR_CONSUMING_RESPONSE);
       }
       NS_ConvertUTF16toUTF8 urlUTF8(url);
       aConsumer->noteResponseURLs(urlUTF8.get(),
                                   sourceMapUrl.IsVoid() ? nullptr : sourceMapUrl.get());
--- a/dom/fetch/Request.cpp
+++ b/dom/fetch/Request.cpp
@@ -264,30 +264,35 @@ private:
   nsresult& mResult;
 };
 
 } // namespace
 
 /*static*/ already_AddRefed<Request>
 Request::Constructor(const GlobalObject& aGlobal,
                      const RequestOrUSVString& aInput,
-                     const RequestInit& aInit, ErrorResult& aRv)
+                     const RequestInit& aInit,
+                     ErrorResult& aRv)
 {
   bool hasCopiedBody = false;
   RefPtr<InternalRequest> request;
 
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
 
   RefPtr<AbortSignal> signal;
 
   if (aInput.IsRequest()) {
     RefPtr<Request> inputReq = &aInput.GetAsRequest();
     nsCOMPtr<nsIInputStream> body;
     inputReq->GetBody(getter_AddRefs(body));
-    if (inputReq->BodyUsed()) {
+    bool used = inputReq->GetBodyUsed(aRv);
+    if (NS_WARN_IF(aRv.Failed())) {
+      return nullptr;
+    }
+    if (used) {
       aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
       return nullptr;
     }
 
     // The body will be copied when GetRequestConstructorCopy() is executed.
     if (body) {
       hasCopiedBody = true;
     }
@@ -593,17 +598,21 @@ Request::Constructor(const GlobalObject&
     }
   }
   return domRequest.forget();
 }
 
 already_AddRefed<Request>
 Request::Clone(ErrorResult& aRv)
 {
-  if (BodyUsed()) {
+  bool used = GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  if (used) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   RefPtr<InternalRequest> ir = mRequest->Clone();
   if (!ir) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
--- a/dom/fetch/Response.cpp
+++ b/dom/fetch/Response.cpp
@@ -231,50 +231,64 @@ Response::Constructor(const GlobalObject
     }
 
     nsCString contentTypeWithCharset;
     nsCOMPtr<nsIInputStream> bodyStream;
     int64_t bodySize = InternalResponse::UNKNOWN_BODY_SIZE;
 
     const fetch::ResponseBodyInit& body = aBody.Value().Value();
     if (body.IsReadableStream()) {
+      JSContext* cx = aGlobal.Context();
       const ReadableStream& readableStream = body.GetAsReadableStream();
 
-      JS::Rooted<JSObject*> readableStreamObj(aGlobal.Context(),
-                                              readableStream.Obj());
+      JS::Rooted<JSObject*> readableStreamObj(cx, readableStream.Obj());
 
-      if (JS::ReadableStreamIsDisturbed(readableStreamObj) ||
-          JS::ReadableStreamIsLocked(readableStreamObj) ||
-          !JS::ReadableStreamIsReadable(readableStreamObj)) {
+      bool disturbed;
+      bool locked;
+      bool readable;
+      if (!JS::ReadableStreamIsDisturbed(cx, readableStreamObj, &disturbed) ||
+          !JS::ReadableStreamIsLocked(cx, readableStreamObj, &locked) ||
+          !JS::ReadableStreamIsReadable(cx, readableStreamObj, &readable)) {
+        aRv.StealExceptionFromJSContext(cx);
+        return nullptr;
+      }
+      if (disturbed || locked || !readable) {
         aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
         return nullptr;
       }
 
-      r->SetReadableStreamBody(aGlobal.Context(), readableStreamObj);
+      r->SetReadableStreamBody(cx, readableStreamObj);
 
-      if (JS::ReadableStreamGetMode(readableStreamObj) ==
-            JS::ReadableStreamMode::ExternalSource) {
+      JS::ReadableStreamMode streamMode;
+      if (!JS::ReadableStreamGetMode(cx, readableStreamObj, &streamMode)) {
+        aRv.StealExceptionFromJSContext(cx);
+        return nullptr;
+      }
+      if (streamMode == JS::ReadableStreamMode::ExternalSource) {
         // If this is a DOM generated ReadableStream, we can extract the
         // inputStream directly.
         void* underlyingSource = nullptr;
-        if (!JS::ReadableStreamGetExternalUnderlyingSource(aGlobal.Context(),
+        if (!JS::ReadableStreamGetExternalUnderlyingSource(cx,
                                                            readableStreamObj,
                                                            &underlyingSource)) {
-          aRv.StealExceptionFromJSContext(aGlobal.Context());
+          aRv.StealExceptionFromJSContext(cx);
           return nullptr;
         }
 
         MOZ_ASSERT(underlyingSource);
 
         aRv = FetchStream::RetrieveInputStream(underlyingSource,
                                                getter_AddRefs(bodyStream));
 
         // The releasing of the external source is needed in order to avoid an
         // extra stream lock.
-        JS::ReadableStreamReleaseExternalUnderlyingSource(readableStreamObj);
+        if (!JS::ReadableStreamReleaseExternalUnderlyingSource(cx, readableStreamObj)) {
+          aRv.StealExceptionFromJSContext(cx);
+          return nullptr;
+        }
         if (NS_WARN_IF(aRv.Failed())) {
           return nullptr;
         }
       } else {
         // If this is a JS-created ReadableStream, let's create a
         // FetchStreamReader.
         aRv = FetchStreamReader::Create(aGlobal.Context(), global,
                                         getter_AddRefs(r->mFetchStreamReader),
@@ -315,17 +329,21 @@ Response::Constructor(const GlobalObject
 
   r->SetMimeType();
   return r.forget();
 }
 
 already_AddRefed<Response>
 Response::Clone(JSContext* aCx, ErrorResult& aRv)
 {
-  if (BodyUsed()) {
+  bool bodyUsed = GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  if (bodyUsed) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   RefPtr<FetchStreamReader> streamReader;
   nsCOMPtr<nsIInputStream> inputStream;
 
   JS::Rooted<JSObject*> body(aCx);
@@ -357,17 +375,17 @@ Response::Clone(JSContext* aCx, ErrorRes
   }
 
   return response.forget();
 }
 
 already_AddRefed<Response>
 Response::CloneUnfiltered(JSContext* aCx, ErrorResult& aRv)
 {
-  if (BodyUsed()) {
+  if (GetBodyUsed(aRv)) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   RefPtr<FetchStreamReader> streamReader;
   nsCOMPtr<nsIInputStream> inputStream;
 
   JS::Rooted<JSObject*> body(aCx);
@@ -400,17 +418,17 @@ Response::CloneUnfiltered(JSContext* aCx
   }
 
   return ref.forget();
 }
 
 void
 Response::SetBody(nsIInputStream* aBody, int64_t aBodySize)
 {
-  MOZ_ASSERT(!BodyUsed());
+  MOZ_ASSERT(!CheckBodyUsed());
   mInternalResponse->SetBody(aBody, aBodySize);
 }
 
 already_AddRefed<InternalResponse>
 Response::GetInternalResponse() const
 {
   RefPtr<InternalResponse> ref = mInternalResponse;
   return ref.forget();
--- a/dom/performance/PerformanceTiming.h
+++ b/dom/performance/PerformanceTiming.h
@@ -449,16 +449,30 @@ public:
     if (mPerformance->IsSystemPrincipal()) {
       return GetDOMTiming()->GetTimeToDOMContentFlushed();
     }
     return nsRFPService::ReduceTimePrecisionAsMSecs(
       GetDOMTiming()->GetTimeToDOMContentFlushed(),
       mPerformance->GetRandomTimelineSeed());
   }
 
+  DOMTimeMilliSec TimeToFirstInteractive() const
+  {
+    if (!nsContentUtils::IsPerformanceTimingEnabled() ||
+        nsContentUtils::ShouldResistFingerprinting()) {
+      return 0;
+    }
+    if (mPerformance->IsSystemPrincipal()) {
+      return GetDOMTiming()->GetTimeToTTFI();
+    }
+    return nsRFPService::ReduceTimePrecisionAsMSecs(
+      GetDOMTiming()->GetTimeToTTFI(),
+      mPerformance->GetRandomTimelineSeed());
+  }
+
   PerformanceTimingData* Data() const
   {
     return mTimingData.get();
   }
 
 private:
   ~PerformanceTiming();
 
--- a/dom/serviceworkers/ServiceWorkerEvents.cpp
+++ b/dom/serviceworkers/ServiceWorkerEvents.cpp
@@ -669,20 +669,28 @@ RespondWithHandler::ResolvedCallback(JSC
   }
 
   if (mRequestRedirectMode != RequestRedirect::Follow && response->Redirected()) {
     autoCancel.SetCancelMessage(
       NS_LITERAL_CSTRING("BadRedirectModeInterceptionWithURL"), mRequestURL);
     return;
   }
 
-  if (NS_WARN_IF(response->BodyUsed())) {
-    autoCancel.SetCancelMessage(
-      NS_LITERAL_CSTRING("InterceptedUsedResponseWithURL"), mRequestURL);
-    return;
+  {
+    ErrorResult error;
+    bool bodyUsed = response->GetBodyUsed(error);
+    if (NS_WARN_IF(error.Failed())) {
+      autoCancel.SetCancelErrorResult(aCx, error);
+      return;
+    }
+    if (NS_WARN_IF(bodyUsed)) {
+      autoCancel.SetCancelMessage(
+        NS_LITERAL_CSTRING("InterceptedUsedResponseWithURL"), mRequestURL);
+      return;
+    }
   }
 
   RefPtr<InternalResponse> ir = response->GetInternalResponse();
   if (NS_WARN_IF(!ir)) {
     return;
   }
 
   // An extra safety check to make sure our invariant that opaque and cors
--- a/dom/webidl/Fetch.webidl
+++ b/dom/webidl/Fetch.webidl
@@ -7,16 +7,17 @@
  * http://fetch.spec.whatwg.org/
  */
 
 typedef object JSON;
 typedef (Blob or BufferSource or FormData or URLSearchParams or USVString) BodyInit;
 
 [NoInterfaceObject, Exposed=(Window,Worker)]
 interface Body {
+  [Throws]
   readonly attribute boolean bodyUsed;
   [Throws]
   Promise<ArrayBuffer> arrayBuffer();
   [Throws]
   Promise<Blob> blob();
   [Throws]
   Promise<FormData> formData();
   [Throws]
--- a/dom/webidl/PerformanceTiming.webidl
+++ b/dom/webidl/PerformanceTiming.webidl
@@ -40,10 +40,16 @@ interface PerformanceTiming {
   readonly attribute unsigned long long timeToNonBlankPaint;
 
   // This is a Mozilla proprietary extension and not part of the
   // performance/navigation timing specification. It marks the
   // completion of the first presentation flush after DOMContentLoaded.
   [Pref="dom.performance.time_to_dom_content_flushed.enabled"]
   readonly attribute unsigned long long timeToDOMContentFlushed;
 
+  // This is a Chrome proprietary extension and not part of the
+  // performance/navigation timing specification.
+  // Returns 0 if a time-to-interactive measurement has not happened.
+  [Pref="dom.performance.time_to_first_interactive.enabled"]
+  readonly attribute unsigned long long timeToFirstInteractive;
+
   [Default] object toJSON();
 };
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -372,33 +372,35 @@ private:
 
   DECL_GFX_PREF(Live, "dom.ipc.plugins.asyncdrawing.enabled",  PluginAsyncDrawingEnabled, bool, false);
   DECL_GFX_PREF(Live, "dom.meta-viewport.enabled",             MetaViewportEnabled, bool, false);
   DECL_GFX_PREF(Live, "dom.visualviewport.enabled",            VisualViewportEnabled, bool, false);
   DECL_GFX_PREF(Once, "dom.vr.enabled",                        VREnabled, bool, false);
   DECL_GFX_PREF(Live, "dom.vr.autoactivate.enabled",           VRAutoActivateEnabled, bool, false);
   DECL_GFX_PREF(Live, "dom.vr.controller_trigger_threshold",   VRControllerTriggerThreshold, float, 0.1f);
   DECL_GFX_PREF(Once, "dom.vr.external.enabled",               VRExternalEnabled, bool, true);
+  DECL_GFX_PREF(Live, "dom.vr.external.notdetected.timeout",   VRExternalNotDetectedTimeout, int32_t, 60000);
+  DECL_GFX_PREF(Live, "dom.vr.external.quit.timeout",          VRExternalQuitTimeout, int32_t, 10000);
   DECL_GFX_PREF(Live, "dom.vr.navigation.timeout",             VRNavigationTimeout, int32_t, 1000);
   DECL_GFX_PREF(Once, "dom.vr.oculus.enabled",                 VROculusEnabled, bool, true);
   DECL_GFX_PREF(Live, "dom.vr.oculus.invisible.enabled",       VROculusInvisibleEnabled, bool, true);
   DECL_GFX_PREF(Live, "dom.vr.oculus.present.timeout",         VROculusPresentTimeout, int32_t, 500);
   DECL_GFX_PREF(Live, "dom.vr.oculus.quit.timeout",            VROculusQuitTimeout, int32_t, 10000);
   DECL_GFX_PREF(Once, "dom.vr.openvr.enabled",                 VROpenVREnabled, bool, false);
   DECL_GFX_PREF(Once, "dom.vr.osvr.enabled",                   VROSVREnabled, bool, false);
   DECL_GFX_PREF(Live, "dom.vr.controller.enumerate.interval",  VRControllerEnumerateInterval, int32_t, 1000);
   DECL_GFX_PREF(Live, "dom.vr.display.enumerate.interval",     VRDisplayEnumerateInterval, int32_t, 5000);
   DECL_GFX_PREF(Live, "dom.vr.inactive.timeout",               VRInactiveTimeout, int32_t, 5000);
   DECL_GFX_PREF(Live, "dom.vr.poseprediction.enabled",         VRPosePredictionEnabled, bool, true);
   DECL_GFX_PREF(Live, "dom.vr.require-gesture",                VRRequireGesture, bool, true);
   DECL_GFX_PREF(Live, "dom.vr.puppet.enabled",                 VRPuppetEnabled, bool, false);
   DECL_GFX_PREF(Live, "dom.vr.puppet.submitframe",             VRPuppetSubmitFrame, uint32_t, 0);
   DECL_GFX_PREF(Live, "dom.vr.display.rafMaxDuration",         VRDisplayRafMaxDuration, uint32_t, 50);
   DECL_GFX_PREF(Once, "dom.vr.process.enabled",                VRProcessEnabled, bool, false);
-  DECL_GFX_PREF(Once, "dom.vr.service.enabled",                VRServiceEnabled, bool, false);
+  DECL_GFX_PREF(Once, "dom.vr.service.enabled",                VRServiceEnabled, bool, true);
   DECL_GFX_PREF(Live, "dom.w3c_pointer_events.enabled",        PointerEventsEnabled, bool, false);
 
   DECL_GFX_PREF(Live, "general.smoothScroll",                  SmoothScrollEnabled, bool, true);
   DECL_GFX_PREF(Live, "general.smoothScroll.currentVelocityWeighting",
                 SmoothScrollCurrentVelocityWeighting, float, 0.25);
   DECL_GFX_PREF(Live, "general.smoothScroll.durationToIntervalRatio",
                 SmoothScrollDurationToIntervalRatio, int32_t, 200);
   DECL_GFX_PREF(Live, "general.smoothScroll.lines.durationMaxMS",
--- a/gfx/vr/VRManager.cpp
+++ b/gfx/vr/VRManager.cpp
@@ -511,16 +511,45 @@ VRManager::EnumerateVRDisplays()
 
   /**
    * If we get this far, don't try again until
    * the VRDisplayEnumerateInterval elapses
    */
   mLastDisplayEnumerationTime = TimeStamp::Now();
 
   /**
+   * We must start the VR Service thread
+   * and VR Process before enumeration.
+   * We don't want to start this until we will
+   * actualy enumerate, to avoid continuously
+   * re-launching the thread/process when
+   * no hardware is found or a VR software update
+   * is in progress
+   */
+#if !defined(MOZ_WIDGET_ANDROID)
+    // Tell VR process to start VR service.
+    if (gfxPrefs::VRProcessEnabled() && !mVRServiceStarted) {
+      RefPtr<Runnable> task = NS_NewRunnableFunction(
+        "VRGPUChild::SendStartVRService",
+        [] () -> void {
+          VRGPUChild* vrGPUChild = VRGPUChild::Get();
+          vrGPUChild->SendStartVRService();
+      });
+
+      NS_DispatchToMainThread(task.forget());
+      mVRServiceStarted = true;
+    } else if (!gfxPrefs::VRProcessEnabled()){
+      if (mVRService) {
+        mVRService->Start();
+        mVRServiceStarted = true;
+      }
+    }
+#endif
+
+  /**
    * VRSystemManagers are inserted into mManagers in
    * a strict order of priority.  The managers for the
    * most device-specialized API's will have a chance
    * to enumerate devices before the more generic
    * device-agnostic APIs.
    */
   for (const auto& manager : mManagers) {
     manager->Enumerate();
@@ -541,37 +570,23 @@ void
 VRManager::RefreshVRDisplays(bool aMustDispatch)
 {
   /**
   * If we aren't viewing WebVR content, don't enumerate
   * new hardware, as it will cause some devices to power on
   * or interrupt other VR activities.
   */
   if (mVRDisplaysRequested || aMustDispatch) {
-#if !defined(MOZ_WIDGET_ANDROID)
-    // Tell VR process to start VR service.
-    if (gfxPrefs::VRProcessEnabled() && !mVRServiceStarted) {
-      RefPtr<Runnable> task = NS_NewRunnableFunction(
-        "VRGPUChild::SendStartVRService",
-        [] () -> void {
-          VRGPUChild* vrGPUChild = VRGPUChild::Get();
-          vrGPUChild->SendStartVRService();
-      });
-
-      NS_DispatchToMainThread(task.forget());
-      mVRServiceStarted = true;
-    } else if (!gfxPrefs::VRProcessEnabled()){
-      if (mVRService) {
-        mVRService->Start();
-        mVRServiceStarted = true;
-      }
-    }
-#endif
     EnumerateVRDisplays();
   }
+#if !defined(MOZ_WIDGET_ANDROID)
+  if (mVRService) {
+    mVRService->Refresh();
+  }
+#endif
 
   /**
    * VRSystemManager::GetHMDs will not activate new hardware
    * or result in interruption of other VR activities.
    * We can call it even when suppressing enumeration to get
    * the already-enumerated displays.
    */
   nsTArray<RefPtr<gfx::VRDisplayHost> > displays;
--- a/gfx/vr/external_api/moz_external_vr.h
+++ b/gfx/vr/external_api/moz_external_vr.h
@@ -30,17 +30,17 @@ namespace mozilla {
 #ifdef MOZILLA_INTERNAL_API
 namespace dom {
   enum class GamepadHand : uint8_t;
   enum class GamepadCapabilityFlags : uint16_t;
 }
 #endif //  MOZILLA_INTERNAL_API
 namespace gfx {
 
-static const int32_t kVRExternalVersion = 4;
+static const int32_t kVRExternalVersion = 5;
 
 // We assign VR presentations to groups with a bitmask.
 // Currently, we will only display either content or chrome.
 // Later, we will have more groups to support VR home spaces and
 // multitasking environments.
 // These values are not exposed to regular content and only affect
 // chrome-only API's.  They may be changed at any time.
 static const uint32_t kVRGroupNone = 0;
@@ -262,19 +262,21 @@ struct VRFieldOfView {
 struct VRDisplayState
 {
   enum Eye {
     Eye_Left,
     Eye_Right,
     NumEyes
   };
 
-#if defined(__ANDROID__)
+  // When true, indicates that the VR service has shut down
   bool shutdown;
-#endif // defined(__ANDROID__)
+  // Minimum number of milliseconds to wait before attempting
+  // to start the VR service again
+  uint32_t mMinRestartInterval;
   char mDisplayName[kVRDisplayNameMaxLen];
   // eight byte character code identifier
   // LSB first, so "ABCDEFGH" -> ('H'<<56) + ('G'<<48) + ('F'<<40) + ('E'<<32) + ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
   uint64_t mEightCC;
   VRDisplayCapabilityFlags mCapabilityFlags;
   VRFieldOfView mEyeFOV[VRDisplayState::NumEyes];
   Point3D_POD mEyeTranslation[VRDisplayState::NumEyes];
   IntSize_POD mEyeResolution;
--- a/gfx/vr/gfxVRExternal.cpp
+++ b/gfx/vr/gfxVRExternal.cpp
@@ -460,20 +460,20 @@ VRSystemManagerExternal::VRSystemManager
  , mSameProcess(aAPIShmem != nullptr)
 #endif
 {
 #if defined(XP_MACOSX)
   mShmemFD = 0;
 #elif defined(XP_WIN)
   mShmemFile = NULL;
 #elif defined(MOZ_WIDGET_ANDROID)
-  mDoShutdown = false;
   mExternalStructFailed = false;
   mEnumerationCompleted = false;
 #endif
+  mDoShutdown = false;
 
   if (!aAPIShmem) {
     OpenShmem();
   }
 }
 
 VRSystemManagerExternal::~VRSystemManagerExternal()
 {
@@ -572,27 +572,19 @@ VRSystemManagerExternal::OpenShmem()
   }
 #endif
   CheckForShutdown();
 }
 
 void
 VRSystemManagerExternal::CheckForShutdown()
 {
-#if defined(MOZ_WIDGET_ANDROID)
   if (mDoShutdown) {
     Shutdown();
   }
-#else
-  if (mExternalShmem) {
-    if (mExternalShmem->generationA == -1 && mExternalShmem->generationB == -1) {
-      Shutdown();
-    }
-  }
-#endif // defined(MOZ_WIDGET_ANDROID)
 }
 
 void
 VRSystemManagerExternal::CloseShmem()
 {
 #if !defined(MOZ_WIDGET_ANDROID)
   if (mSameProcess) {
     return;
@@ -650,19 +642,17 @@ VRSystemManagerExternal::Destroy()
 
 void
 VRSystemManagerExternal::Shutdown()
 {
   if (mDisplay) {
     mDisplay = nullptr;
   }
   CloseShmem();
-#if defined(MOZ_WIDGET_ANDROID)
   mDoShutdown = false;
-#endif
 }
 
 void
 VRSystemManagerExternal::Run100msTasks()
 {
   VRSystemManager::Run100msTasks();
   // 1ms and 10ms tasks will always be run before
   // the 100ms tasks, so no need to run them
@@ -704,16 +694,23 @@ VRSystemManagerExternal::Enumerate()
 }
 
 bool
 VRSystemManagerExternal::ShouldInhibitEnumeration()
 {
   if (VRSystemManager::ShouldInhibitEnumeration()) {
     return true;
   }
+  if (!mEarliestRestartTime.IsNull() && mEarliestRestartTime > TimeStamp::Now()) {
+    // When the VR Service shuts down it informs us of how long we
+    // must wait until we can re-start it.
+    // We must wait until mEarliestRestartTime before attempting
+    // to enumerate again.
+    return true;
+  }
   if (mDisplay) {
     // When we find an a VR device, don't
     // allow any further enumeration as it
     // may get picked up redundantly by other
     // API's.
     return true;
   }
   return false;
@@ -825,17 +822,23 @@ VRSystemManagerExternal::PullState(VRDis
         memcpy(aDisplayState, (void*)&(mExternalShmem->state.displayState), sizeof(VRDisplayState));
         if (aSensorState) {
           memcpy(aSensorState, (void*)&(mExternalShmem->state.sensorState), sizeof(VRHMDSensorState));
         }
         if (aControllerState) {
           memcpy(aControllerState, (void*)&(mExternalShmem->state.controllerState), sizeof(VRControllerState) * kVRControllerMaxCount);
         }
         mEnumerationCompleted = mExternalShmem->state.enumerationCompleted;
-        mDoShutdown = aDisplayState->shutdown;
+        if (aDisplayState->shutdown) {
+          mDoShutdown = true;
+          TimeStamp now = TimeStamp::Now();
+          if (!mEarliestRestartTime.IsNull() && mEarliestRestartTime < now) {
+            mEarliestRestartTime = now + TimeDuration::FromMilliseconds((double)aDisplayState->mMinRestartInterval);
+          }
+        }
         if (!aWaitCondition || aWaitCondition()) {
           done = true;
           break;
         }
         // Block current thead using the condition variable until data changes
         pthread_cond_wait((pthread_cond_t*)&mExternalShmem->systemCond, (pthread_mutex_t*)&mExternalShmem->systemMutex);
       }
       pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->systemMutex));
@@ -861,25 +864,31 @@ VRSystemManagerExternal::PullState(VRDis
     if (tmp.generationA == tmp.generationB && tmp.generationA != 0 && tmp.generationA != -1 && tmp.state.enumerationCompleted) {
       memcpy(aDisplayState, &tmp.state.displayState, sizeof(VRDisplayState));
       if (aSensorState) {
         memcpy(aSensorState, &tmp.state.sensorState, sizeof(VRHMDSensorState));
       }
       if (aControllerState) {
         memcpy(aControllerState, (void*)&(mExternalShmem->state.controllerState), sizeof(VRControllerState) * kVRControllerMaxCount);
       }
+      if (aDisplayState->shutdown) {
+        mDoShutdown = true;
+        TimeStamp now = TimeStamp::Now();
+        if (!mEarliestRestartTime.IsNull() && mEarliestRestartTime < now) {
+          mEarliestRestartTime = now + TimeDuration::FromMilliseconds((double)aDisplayState->mMinRestartInterval);
+        }
+      }
       success = true;
     }
   }
 
   return success;
 }
 #endif // defined(MOZ_WIDGET_ANDROID)
 
-
 void
 VRSystemManagerExternal::PushState(VRBrowserState* aBrowserState, bool aNotifyCond)
 {
   MOZ_ASSERT(aBrowserState);
   MOZ_ASSERT(mExternalShmem);
   if (mExternalShmem) {
 #if defined(MOZ_WIDGET_ANDROID)
     if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->browserMutex)) == 0) {
--- a/gfx/vr/gfxVRExternal.h
+++ b/gfx/vr/gfxVRExternal.h
@@ -126,25 +126,27 @@ protected:
 private:
   // there can only be one
   RefPtr<impl::VRDisplayExternal> mDisplay;
 #if defined(XP_MACOSX)
   int mShmemFD;
 #elif defined(XP_WIN)
   base::ProcessHandle mShmemFile;
 #elif defined(MOZ_WIDGET_ANDROID)
-  bool mDoShutdown;
+  
   bool mExternalStructFailed;
   bool mEnumerationCompleted;
 #endif
+  bool mDoShutdown;
 
   volatile VRExternalShmem* mExternalShmem;
 #if !defined(MOZ_WIDGET_ANDROID)
   bool mSameProcess;
 #endif
+  TimeStamp mEarliestRestartTime;
 
   void OpenShmem();
   void CloseShmem();
   void CheckForShutdown();
 };
 
 } // namespace gfx
 } // namespace mozilla
--- a/gfx/vr/service/OSVRSession.cpp
+++ b/gfx/vr/service/OSVRSession.cpp
@@ -493,40 +493,45 @@ OSVRSession::UpdateHeadsetPose(mozilla::
     result.pose.position[1] = position.data[1];
     result.pose.position[2] = position.data[2];
   }
 
   result.CalcViewMatrices(mHeadToEye);
 }
 
 bool
-OSVRSession::ShouldQuit() const
-{
-  return false;
-}
-
-bool
 OSVRSession::StartPresentation()
 {
   return false;
   // TODO Implement
 }
 
 void
 OSVRSession::StopPresentation()
 {
   // TODO Implement
 }
 
+#if defined(XP_WIN)
 bool
-OSVRSession::SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer)
+OSVRSession::SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                         ID3D11Texture2D* aTexture)
 {
   return false;
-   // TODO Implement
+  // TODO Implement
 }
+#elif defined(XP_MACOSX)
+bool
+OSVRSession::SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                         MacIOSurface* aTexture)
+{
+  return false;
+  // TODO Implement
+}
+#endif
 
 void
 OSVRSession::VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
                            float aIntensity, float aDuration)
 {
 
 }
 
--- a/gfx/vr/service/OSVRSession.h
+++ b/gfx/vr/service/OSVRSession.h
@@ -30,25 +30,32 @@ class OSVRSession : public VRSession
 public:
   OSVRSession();
   virtual ~OSVRSession();
 
   bool Initialize(mozilla::gfx::VRSystemState& aSystemState) override;
   void Shutdown() override;
   void ProcessEvents(mozilla::gfx::VRSystemState& aSystemState) override;
   void StartFrame(mozilla::gfx::VRSystemState& aSystemState) override;
-  bool ShouldQuit() const override;
   bool StartPresentation() override;
   void StopPresentation() override;
-  bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer) override;
   void VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
                     float aIntensity, float aDuration) override;
   void StopVibrateHaptic(uint32_t aControllerIdx) override;
   void StopAllHaptics() override;
 
+protected:
+#if defined(XP_WIN)
+  bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                   ID3D11Texture2D* aTexture) override;
+#elif defined(XP_MACOSX)
+  bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                   MacIOSurface* aTexture) override;
+#endif
+
 private:
   bool InitState(mozilla::gfx::VRSystemState& aSystemState);
   void UpdateHeadsetPose(mozilla::gfx::VRSystemState& aState);
   bool mRuntimeLoaded;
   bool mOSVRInitialized;
   bool mClientContextInitialized;
   bool mDisplayConfigInitialized;
   bool mInterfaceInitialized;
new file mode 100644
--- /dev/null
+++ b/gfx/vr/service/OculusSession.cpp
@@ -0,0 +1,1586 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef XP_WIN
+#error "Oculus support only available for Windows"
+#endif
+
+#include <math.h>
+#include <d3d11.h>
+
+#include "gfxPrefs.h"
+#include "mozilla/dom/GamepadEventTypes.h"
+#include "mozilla/dom/GamepadBinding.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/SharedLibrary.h"
+#include "OculusSession.h"
+
+/** XXX The DX11 objects and quad blitting could be encapsulated
+ *    into a separate object if either Oculus starts supporting
+ *     non-Windows platforms or the blit is needed by other HMD\
+ *     drivers.
+ *     Alternately, we could remove the extra blit for
+ *     Oculus as well with some more refactoring.
+ */
+
+// See CompositorD3D11Shaders.h
+namespace mozilla {
+namespace layers {
+struct ShaderBytes
+{
+  const void* mData;
+  size_t mLength;
+};
+extern ShaderBytes sRGBShader;
+extern ShaderBytes sLayerQuadVS;
+} // namespace layers
+} // namespace mozilla
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+
+namespace {
+
+static pfn_ovr_Initialize ovr_Initialize = nullptr;
+static pfn_ovr_Shutdown ovr_Shutdown = nullptr;
+static pfn_ovr_GetLastErrorInfo ovr_GetLastErrorInfo = nullptr;
+static pfn_ovr_GetVersionString ovr_GetVersionString = nullptr;
+static pfn_ovr_TraceMessage ovr_TraceMessage = nullptr;
+static pfn_ovr_IdentifyClient ovr_IdentifyClient = nullptr;
+static pfn_ovr_GetHmdDesc ovr_GetHmdDesc = nullptr;
+static pfn_ovr_GetTrackerCount ovr_GetTrackerCount = nullptr;
+static pfn_ovr_GetTrackerDesc ovr_GetTrackerDesc = nullptr;
+static pfn_ovr_Create ovr_Create = nullptr;
+static pfn_ovr_Destroy ovr_Destroy = nullptr;
+static pfn_ovr_GetSessionStatus ovr_GetSessionStatus = nullptr;
+static pfn_ovr_IsExtensionSupported ovr_IsExtensionSupported = nullptr;
+static pfn_ovr_EnableExtension ovr_EnableExtension = nullptr;
+static pfn_ovr_SetTrackingOriginType ovr_SetTrackingOriginType = nullptr;
+static pfn_ovr_GetTrackingOriginType ovr_GetTrackingOriginType = nullptr;
+static pfn_ovr_RecenterTrackingOrigin ovr_RecenterTrackingOrigin = nullptr;
+static pfn_ovr_SpecifyTrackingOrigin ovr_SpecifyTrackingOrigin = nullptr;
+static pfn_ovr_ClearShouldRecenterFlag ovr_ClearShouldRecenterFlag = nullptr;
+static pfn_ovr_GetTrackingState ovr_GetTrackingState = nullptr;
+static pfn_ovr_GetDevicePoses ovr_GetDevicePoses = nullptr;
+static pfn_ovr_GetTrackerPose ovr_GetTrackerPose = nullptr;
+static pfn_ovr_GetInputState ovr_GetInputState = nullptr;
+static pfn_ovr_GetConnectedControllerTypes ovr_GetConnectedControllerTypes =
+  nullptr;
+static pfn_ovr_GetTouchHapticsDesc ovr_GetTouchHapticsDesc = nullptr;
+static pfn_ovr_SetControllerVibration ovr_SetControllerVibration = nullptr;
+static pfn_ovr_SubmitControllerVibration ovr_SubmitControllerVibration =
+  nullptr;
+static pfn_ovr_GetControllerVibrationState ovr_GetControllerVibrationState =
+  nullptr;
+static pfn_ovr_TestBoundary ovr_TestBoundary = nullptr;
+static pfn_ovr_TestBoundaryPoint ovr_TestBoundaryPoint = nullptr;
+static pfn_ovr_SetBoundaryLookAndFeel ovr_SetBoundaryLookAndFeel = nullptr;
+static pfn_ovr_ResetBoundaryLookAndFeel ovr_ResetBoundaryLookAndFeel = nullptr;
+static pfn_ovr_GetBoundaryGeometry ovr_GetBoundaryGeometry = nullptr;
+static pfn_ovr_GetBoundaryDimensions ovr_GetBoundaryDimensions = nullptr;
+static pfn_ovr_GetBoundaryVisible ovr_GetBoundaryVisible = nullptr;
+static pfn_ovr_RequestBoundaryVisible ovr_RequestBoundaryVisible = nullptr;
+static pfn_ovr_GetTextureSwapChainLength ovr_GetTextureSwapChainLength =
+  nullptr;
+static pfn_ovr_GetTextureSwapChainCurrentIndex
+  ovr_GetTextureSwapChainCurrentIndex = nullptr;
+static pfn_ovr_GetTextureSwapChainDesc ovr_GetTextureSwapChainDesc = nullptr;
+static pfn_ovr_CommitTextureSwapChain ovr_CommitTextureSwapChain = nullptr;
+static pfn_ovr_DestroyTextureSwapChain ovr_DestroyTextureSwapChain = nullptr;
+static pfn_ovr_DestroyMirrorTexture ovr_DestroyMirrorTexture = nullptr;
+static pfn_ovr_GetFovTextureSize ovr_GetFovTextureSize = nullptr;
+static pfn_ovr_GetRenderDesc2 ovr_GetRenderDesc2 = nullptr;
+static pfn_ovr_WaitToBeginFrame ovr_WaitToBeginFrame = nullptr;
+static pfn_ovr_BeginFrame ovr_BeginFrame = nullptr;
+static pfn_ovr_EndFrame ovr_EndFrame = nullptr;
+static pfn_ovr_SubmitFrame ovr_SubmitFrame = nullptr;
+static pfn_ovr_GetPerfStats ovr_GetPerfStats = nullptr;
+static pfn_ovr_ResetPerfStats ovr_ResetPerfStats = nullptr;
+static pfn_ovr_GetPredictedDisplayTime ovr_GetPredictedDisplayTime = nullptr;
+static pfn_ovr_GetTimeInSeconds ovr_GetTimeInSeconds = nullptr;
+static pfn_ovr_GetBool ovr_GetBool = nullptr;
+static pfn_ovr_SetBool ovr_SetBool = nullptr;
+static pfn_ovr_GetInt ovr_GetInt = nullptr;
+static pfn_ovr_SetInt ovr_SetInt = nullptr;
+static pfn_ovr_GetFloat ovr_GetFloat = nullptr;
+static pfn_ovr_SetFloat ovr_SetFloat = nullptr;
+static pfn_ovr_GetFloatArray ovr_GetFloatArray = nullptr;
+static pfn_ovr_SetFloatArray ovr_SetFloatArray = nullptr;
+static pfn_ovr_GetString ovr_GetString = nullptr;
+static pfn_ovr_SetString ovr_SetString = nullptr;
+static pfn_ovr_GetExternalCameras ovr_GetExternalCameras = nullptr;
+static pfn_ovr_SetExternalCameraProperties ovr_SetExternalCameraProperties =
+  nullptr;
+
+#ifdef XP_WIN
+static pfn_ovr_CreateTextureSwapChainDX ovr_CreateTextureSwapChainDX = nullptr;
+static pfn_ovr_GetTextureSwapChainBufferDX ovr_GetTextureSwapChainBufferDX =
+  nullptr;
+static pfn_ovr_CreateMirrorTextureDX ovr_CreateMirrorTextureDX = nullptr;
+static pfn_ovr_GetMirrorTextureBufferDX ovr_GetMirrorTextureBufferDX = nullptr;
+#endif
+
+static pfn_ovr_CreateTextureSwapChainGL ovr_CreateTextureSwapChainGL = nullptr;
+static pfn_ovr_GetTextureSwapChainBufferGL ovr_GetTextureSwapChainBufferGL =
+  nullptr;
+static pfn_ovr_CreateMirrorTextureGL ovr_CreateMirrorTextureGL = nullptr;
+static pfn_ovr_GetMirrorTextureBufferGL ovr_GetMirrorTextureBufferGL = nullptr;
+
+#ifdef HAVE_64BIT_BUILD
+#define BUILD_BITS 64
+#else
+#define BUILD_BITS 32
+#endif
+
+#define OVR_PRODUCT_VERSION 1
+#define OVR_MAJOR_VERSION 1
+#define OVR_MINOR_VERSION 19
+
+static const uint32_t kNumOculusButtons = 6;
+static const uint32_t kNumOculusHaptcs = 1;
+static const uint32_t kNumOculusAxes = 2;
+ovrControllerType OculusControllerTypes[2] = { ovrControllerType_LTouch,
+                                               ovrControllerType_RTouch };
+const char* OculusControllerNames[2] = { "Oculus Touch (Left)",
+                                         "Oculus Touch (Right)" };
+dom::GamepadHand OculusControllerHand[2] = { dom::GamepadHand::Left,
+                                             dom::GamepadHand::Right };
+ovrButton OculusControllerButtons[2][kNumOculusButtons] = {
+  { ovrButton_LThumb,
+    (ovrButton)0,
+    (ovrButton)0,
+    ovrButton_X,
+    ovrButton_Y,
+    (ovrButton)0 },
+  { ovrButton_RThumb,
+    (ovrButton)0,
+    (ovrButton)0,
+    ovrButton_A,
+    ovrButton_B,
+    (ovrButton)0 },
+};
+
+ovrTouch OculusControllerTouches[2][kNumOculusButtons] = {
+  { (ovrTouch)0,
+    ovrTouch_LIndexTrigger,
+    (ovrTouch)0,
+    (ovrTouch)0,
+    (ovrTouch)0,
+    ovrTouch_LThumbRest },
+  { (ovrTouch)0,
+    ovrTouch_RIndexTrigger,
+    (ovrTouch)0,
+    (ovrTouch)0,
+    (ovrTouch)0,
+    ovrTouch_RThumbRest },
+};
+
+void
+UpdateButton(const ovrInputState& aInputState,
+             uint32_t aHandIdx,
+             uint32_t aButtonIdx,
+             VRControllerState& aControllerState)
+{
+  if (aInputState.Buttons & OculusControllerButtons[aHandIdx][aButtonIdx]) {
+    aControllerState.buttonPressed |= (1 << aButtonIdx);
+  }
+  if (aInputState.Touches & OculusControllerTouches[aHandIdx][aButtonIdx]) {
+    aControllerState.buttonTouched |= (1 << aButtonIdx);
+  }
+}
+
+VRFieldOfView
+FromFovPort(const ovrFovPort& aFOV)
+{
+  VRFieldOfView fovInfo;
+  fovInfo.leftDegrees = atan(aFOV.LeftTan) * 180.0 / M_PI;
+  fovInfo.rightDegrees = atan(aFOV.RightTan) * 180.0 / M_PI;
+  fovInfo.upDegrees = atan(aFOV.UpTan) * 180.0 / M_PI;
+  fovInfo.downDegrees = atan(aFOV.DownTan) * 180.0 / M_PI;
+  return fovInfo;
+}
+
+} // anonymous namespace
+
+namespace mozilla {
+namespace gfx {
+
+OculusSession::OculusSession()
+  : VRSession()
+  , mOvrLib(nullptr)
+  , mSession(nullptr)
+  , mInitFlags((ovrInitFlags)0)
+  , mTextureSet(nullptr)
+  , mQuadVS(nullptr)
+  , mQuadPS(nullptr)
+  , mLinearSamplerState(nullptr)
+  , mVSConstantBuffer(nullptr)
+  , mPSConstantBuffer(nullptr)
+  , mVertexBuffer(nullptr)
+  , mInputLayout(nullptr)
+  , mRemainingVibrateTime{}
+  , mHapticPulseIntensity{}
+  , mIsPresenting(false)
+{
+}
+
+OculusSession::~OculusSession()
+{
+  Shutdown();
+}
+
+bool
+OculusSession::Initialize(mozilla::gfx::VRSystemState& aSystemState)
+{
+  if (!CreateD3DObjects()) {
+    return false;
+  }
+  if (!CreateShaders()) {
+    return false;
+  }
+
+  if (!LoadOvrLib()) {
+    return false;
+  }
+  // We start off with an invisible session, then re-initialize
+  // with visible session once WebVR content starts rendering.
+  if (!ChangeVisibility(false)) {
+    return false;
+  }
+  if (!InitState(aSystemState)) {
+    return false;
+  }
+
+  mPresentationSize =
+    IntSize(aSystemState.displayState.mEyeResolution.width * 2,
+            aSystemState.displayState.mEyeResolution.height);
+  return true;
+}
+
+void
+OculusSession::UpdateVisibility()
+{
+  // Do not immediately re-initialize with an invisible session after
+  // the end of a VR presentation.  Waiting for the configured duraction
+  // ensures that the user will not drop to Oculus Home during VR link
+  // traversal.
+  if (mIsPresenting) {
+    // We are currently rendering immersive content.
+    // Avoid interrupting the session
+    return;
+  }
+  if (mInitFlags & ovrInit_Invisible) {
+    // We are already invisible
+    return;
+  }
+  if (mLastPresentationEnd.IsNull()) {
+    // There has been no presentation yet
+    return;
+  }
+
+  TimeDuration duration = TimeStamp::Now() - mLastPresentationEnd;
+  TimeDuration timeout = TimeDuration::FromMilliseconds(gfxPrefs::VROculusPresentTimeout());
+  if (timeout <= TimeDuration(0) || duration >= timeout) {
+    if (!ChangeVisibility(false)) {
+      gfxWarning() << "OculusSession::ChangeVisibility(false) failed";
+    }
+  }
+}
+
+void
+OculusSession::CoverTransitions()
+{
+  // While content is loading or during immersive-mode link
+  // traversal, we need to prevent the user from seeing the
+  // last rendered frame.
+  // We render black frames to cover up the transition.
+  MOZ_ASSERT(mSession);
+  if (mIsPresenting) {
+    // We are currently rendering immersive content.
+    // Avoid interrupting the session
+    return;
+  }
+
+  if (mInitFlags & ovrInit_Invisible) {
+    // We are invisible, nothing to cover up
+    return;
+  }
+
+  // Render a black frame
+  ovrLayerEyeFov layer;
+  memset(&layer, 0, sizeof(layer));
+  layer.Header.Type = ovrLayerType_Disabled;
+  ovrLayerHeader* layers = &layer.Header;
+  ovr_SubmitFrame(mSession, 0, nullptr, &layers, 1);
+}
+
+bool
+OculusSession::ChangeVisibility(bool bVisible)
+{
+  ovrInitFlags flags =
+    (ovrInitFlags)(ovrInit_RequestVersion | ovrInit_MixedRendering);
+  if (gfxPrefs::VROculusInvisibleEnabled() && !bVisible) {
+    flags = (ovrInitFlags)(flags | ovrInit_Invisible);
+  }
+  if (mInitFlags == flags) {
+    // The new state is the same, nothing to do
+    return true;
+  }
+
+  // Tear everything down
+  StopRendering();
+  StopSession();
+  StopLib();
+
+  // Start it back up
+  if (!StartLib(flags)) {
+    return false;
+  }
+  if (!StartSession()) {
+    return false;
+  }
+  return true;
+}
+
+void OculusSession::Shutdown()
+{
+  StopRendering();
+  StopSession();
+  StopLib();
+  UnloadOvrLib();
+  DestroyShaders();
+}
+
+void
+OculusSession::ProcessEvents(mozilla::gfx::VRSystemState& aSystemState)
+{
+  if (!mSession) {
+    return;
+  }
+
+  ovrSessionStatus status;
+  if (OVR_SUCCESS(ovr_GetSessionStatus(mSession, &status))) {
+    aSystemState.displayState.mIsConnected = status.HmdPresent;
+    aSystemState.displayState.mIsMounted = status.HmdMounted;
+    mShouldQuit = status.ShouldQuit;
+\
+  } else {
+    aSystemState.displayState.mIsConnected = false;
+    aSystemState.displayState.mIsMounted = false;
+  }
+  UpdateHaptics();
+  UpdateVisibility();
+  CoverTransitions();
+}
+
+void
+OculusSession::StartFrame(mozilla::gfx::VRSystemState& aSystemState)
+{
+  UpdateHeadsetPose(aSystemState);
+  UpdateEyeParameters(aSystemState);
+  UpdateControllers(aSystemState);
+  UpdateTelemetry(aSystemState);
+  aSystemState.sensorState.inputFrameID++;
+}
+
+bool
+OculusSession::StartPresentation()
+{
+  /**
+   * XXX - We should resolve fail the promise returned by
+   *       VRDisplay.requestPresent() when the DX11 resources fail allocation
+   *       in VRDisplayOculus::StartPresentation().
+   *       Bailing out here prevents the crash but content should be aware
+   *       that frames are not being presented.
+   *       See Bug 1299309.
+   **/
+  if (!ChangeVisibility(true)) {
+    return false;
+  }
+  if (!StartRendering()) {
+    StopRendering();
+    return false;
+  }
+  mIsPresenting = true;
+  return true;
+}
+
+void
+OculusSession::StopPresentation()
+{
+  mLastPresentationEnd = TimeStamp::Now();
+  mIsPresenting = false;
+}
+
+bool
+OculusSession::SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                           ID3D11Texture2D* aTexture)
+{
+  if (!IsPresentationReady()) {
+    return false;
+  }
+
+  D3D11_TEXTURE2D_DESC textureDesc = { 0 };
+  aTexture->GetDesc(&textureDesc);
+
+  int currentRenderTarget = 0;
+  ovrResult orv = ovr_GetTextureSwapChainCurrentIndex(
+    mSession, mTextureSet, &currentRenderTarget);
+  if (orv != ovrSuccess) {
+    NS_WARNING("ovr_GetTextureSwapChainCurrentIndex failed.");
+    return false;
+  }
+
+  ID3D11RenderTargetView* view = mRTView[currentRenderTarget];
+
+  float clear[] = { 0.0f, 0.0f, 0.0f, 1.0f };
+  mContext->ClearRenderTargetView(view, clear);
+  mContext->OMSetRenderTargets(1, &view, nullptr);
+
+  Matrix viewMatrix = Matrix::Translation(-1.0, 1.0);
+  viewMatrix.PreScale(2.0f / float(textureDesc.Width),
+                      2.0f / float(textureDesc.Height));
+  viewMatrix.PreScale(1.0f, -1.0f);
+  Matrix4x4 projection = Matrix4x4::From2D(viewMatrix);
+  projection._33 = 0.0f;
+
+  Matrix transform2d;
+  gfx::Matrix4x4 transform = gfx::Matrix4x4::From2D(transform2d);
+
+  D3D11_VIEWPORT viewport;
+  viewport.MinDepth = 0.0f;
+  viewport.MaxDepth = 1.0f;
+  viewport.Width = textureDesc.Width;
+  viewport.Height = textureDesc.Height;
+  viewport.TopLeftX = 0;
+  viewport.TopLeftY = 0;
+
+  D3D11_RECT scissor;
+  scissor.left = 0;
+  scissor.right = textureDesc.Width;
+  scissor.top = 0;
+  scissor.bottom = textureDesc.Height;
+
+  memcpy(&mVSConstants.layerTransform,
+         &transform._11,
+         sizeof(mVSConstants.layerTransform));
+  memcpy(
+    &mVSConstants.projection, &projection._11, sizeof(mVSConstants.projection));
+  mVSConstants.renderTargetOffset[0] = 0.0f;
+  mVSConstants.renderTargetOffset[1] = 0.0f;
+  mVSConstants.layerQuad =
+    Rect(0.0f, 0.0f, textureDesc.Width, textureDesc.Height);
+  mVSConstants.textureCoords = Rect(0.0f, 1.0f, 1.0f, -1.0f);
+
+  mPSConstants.layerOpacity[0] = 1.0f;
+
+  ID3D11Buffer* vbuffer = mVertexBuffer;
+  UINT vsize = sizeof(Vertex);
+  UINT voffset = 0;
+  mContext->IASetVertexBuffers(0, 1, &vbuffer, &vsize, &voffset);
+  mContext->IASetIndexBuffer(nullptr, DXGI_FORMAT_R16_UINT, 0);
+  mContext->IASetInputLayout(mInputLayout);
+  mContext->RSSetViewports(1, &viewport);
+  mContext->RSSetScissorRects(1, &scissor);
+  mContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
+  mContext->VSSetShader(mQuadVS, nullptr, 0);
+  mContext->PSSetShader(mQuadPS, nullptr, 0);
+
+  RefPtr<ID3D11ShaderResourceView> srView;
+  HRESULT hr = mDevice->CreateShaderResourceView(
+    aTexture, nullptr, getter_AddRefs(srView));
+  if (FAILED(hr)) {
+    gfxWarning() << "Could not create shader resource view for Oculus: "
+                 << hexa(hr);
+    return false;
+  }
+  ID3D11ShaderResourceView* viewPtr = srView.get();
+  mContext->PSSetShaderResources(0 /* 0 == TexSlot::RGB */, 1, &viewPtr);
+  // XXX Use Constant from TexSlot in CompositorD3D11.cpp?
+
+  ID3D11SamplerState* sampler = mLinearSamplerState;
+  mContext->PSSetSamplers(0, 1, &sampler);
+
+  if (!UpdateConstantBuffers()) {
+    NS_WARNING("Failed to update constant buffers for Oculus");
+    return false;
+  }
+
+  mContext->Draw(4, 0);
+
+  orv = ovr_CommitTextureSwapChain(mSession, mTextureSet);
+  if (orv != ovrSuccess) {
+    NS_WARNING("ovr_CommitTextureSwapChain failed.");
+    return false;
+  }
+
+  ovrLayerEyeFov layer;
+  memset(&layer, 0, sizeof(layer));
+  layer.Header.Type = ovrLayerType_EyeFov;
+  layer.Header.Flags = 0;
+  layer.ColorTexture[0] = mTextureSet;
+  layer.ColorTexture[1] = nullptr;
+  layer.Fov[0] = mFOVPort[0];
+  layer.Fov[1] = mFOVPort[1];
+  layer.Viewport[0].Pos.x = textureDesc.Width * aLayer.mLeftEyeRect.x;
+  layer.Viewport[0].Pos.y = textureDesc.Height * aLayer.mLeftEyeRect.y;
+  layer.Viewport[0].Size.w = textureDesc.Width * aLayer.mLeftEyeRect.width;
+  layer.Viewport[0].Size.h = textureDesc.Height * aLayer.mLeftEyeRect.height;
+  layer.Viewport[1].Pos.x = textureDesc.Width * aLayer.mRightEyeRect.x;
+  layer.Viewport[1].Pos.y = textureDesc.Height * aLayer.mRightEyeRect.y;
+  layer.Viewport[1].Size.w = textureDesc.Width * aLayer.mRightEyeRect.width;
+  layer.Viewport[1].Size.h = textureDesc.Height * aLayer.mRightEyeRect.height;
+
+  for (uint32_t i = 0; i < 2; ++i) {
+    layer.RenderPose[i].Orientation.x = mFrameStartPose[i].Orientation.x;
+    layer.RenderPose[i].Orientation.y = mFrameStartPose[i].Orientation.y;
+    layer.RenderPose[i].Orientation.z = mFrameStartPose[i].Orientation.z;
+    layer.RenderPose[i].Orientation.w = mFrameStartPose[i].Orientation.w;
+    layer.RenderPose[i].Position.x = mFrameStartPose[i].Position.x;
+    layer.RenderPose[i].Position.y = mFrameStartPose[i].Position.y;
+    layer.RenderPose[i].Position.z = mFrameStartPose[i].Position.z;
+  }
+
+  ovrLayerHeader* layers = &layer.Header;
+  orv = ovr_SubmitFrame(mSession, 0, nullptr, &layers, 1);
+  // ovr_SubmitFrame will fail during the Oculus health and safety warning.
+  // and will start succeeding once the warning has been dismissed by the user.
+
+  if (!OVR_UNQUALIFIED_SUCCESS(orv)) {
+    /**
+     * We wish to throttle the framerate for any case that the rendered
+     * result is not visible.  In some cases, such as during the Oculus
+     * "health and safety warning", orv will be > 0 (OVR_SUCCESS but not
+     * OVR_UNQUALIFIED_SUCCESS) and ovr_SubmitFrame will not block.
+     * In this case, returning true would have resulted in an unthrottled
+     * render loop hiting excessive frame rates and consuming resources.
+     */
+    return false;
+  }
+
+  return true;
+}
+
+bool
+OculusSession::LoadOvrLib()
+{
+  if (mOvrLib) {
+    // Already loaded, early exit
+    return true;
+  }
+#if defined(_WIN32)
+  nsTArray<nsString> libSearchPaths;
+  nsString libName;
+  nsString searchPath;
+
+  static const char dirSep = '\\';
+  static const int pathLen = 260;
+  searchPath.SetCapacity(pathLen);
+  int realLen =
+    ::GetSystemDirectoryW(char16ptr_t(searchPath.BeginWriting()), pathLen);
+  if (realLen != 0 && realLen < pathLen) {
+    searchPath.SetLength(realLen);
+    libSearchPaths.AppendElement(searchPath);
+  }
+  libName.AppendPrintf("LibOVRRT%d_%d.dll", BUILD_BITS, OVR_PRODUCT_VERSION);
+
+  // search the path/module dir
+  libSearchPaths.InsertElementsAt(0, 1, EmptyString());
+
+  // If the env var is present, we override libName
+  if (_wgetenv(L"OVR_LIB_PATH")) {
+    searchPath = _wgetenv(L"OVR_LIB_PATH");
+    libSearchPaths.InsertElementsAt(0, 1, searchPath);
+  }
+
+  if (_wgetenv(L"OVR_LIB_NAME")) {
+    libName = _wgetenv(L"OVR_LIB_NAME");
+  }
+
+  for (uint32_t i = 0; i < libSearchPaths.Length(); ++i) {
+    nsString& libPath = libSearchPaths[i];
+    nsString fullName;
+    if (libPath.Length() == 0) {
+      fullName.Assign(libName);
+    } else {
+      fullName.AppendPrintf("%s%c%s", libPath.get(), dirSep, libName.get());
+    }
+
+    mOvrLib = LoadLibraryWithFlags(fullName.get());
+    if (mOvrLib) {
+      break;
+    }
+  }
+#else
+#error "Unsupported platform!"
+#endif
+
+  if (!mOvrLib) {
+    return false;
+  }
+
+#define REQUIRE_FUNCTION(_x)                                                   \
+  do {                                                                         \
+    *(void**)&_x = (void*)PR_FindSymbol(mOvrLib, #_x);                         \
+    if (!_x) {                                                                 \
+      printf_stderr(#_x " symbol missing\n");                                  \
+      goto fail;                                                               \
+    }                                                                          \
+  } while (0)
+
+  REQUIRE_FUNCTION(ovr_Initialize);
+  REQUIRE_FUNCTION(ovr_Shutdown);
+  REQUIRE_FUNCTION(ovr_GetLastErrorInfo);
+  REQUIRE_FUNCTION(ovr_GetVersionString);
+  REQUIRE_FUNCTION(ovr_TraceMessage);
+  REQUIRE_FUNCTION(ovr_IdentifyClient);
+  REQUIRE_FUNCTION(ovr_GetHmdDesc);
+  REQUIRE_FUNCTION(ovr_GetTrackerCount);
+  REQUIRE_FUNCTION(ovr_GetTrackerDesc);
+  REQUIRE_FUNCTION(ovr_Create);
+  REQUIRE_FUNCTION(ovr_Destroy);
+  REQUIRE_FUNCTION(ovr_GetSessionStatus);
+  REQUIRE_FUNCTION(ovr_IsExtensionSupported);
+  REQUIRE_FUNCTION(ovr_EnableExtension);
+  REQUIRE_FUNCTION(ovr_SetTrackingOriginType);
+  REQUIRE_FUNCTION(ovr_GetTrackingOriginType);
+  REQUIRE_FUNCTION(ovr_RecenterTrackingOrigin);
+  REQUIRE_FUNCTION(ovr_SpecifyTrackingOrigin);
+  REQUIRE_FUNCTION(ovr_ClearShouldRecenterFlag);
+  REQUIRE_FUNCTION(ovr_GetTrackingState);
+  REQUIRE_FUNCTION(ovr_GetDevicePoses);
+  REQUIRE_FUNCTION(ovr_GetTrackerPose);
+  REQUIRE_FUNCTION(ovr_GetInputState);
+  REQUIRE_FUNCTION(ovr_GetConnectedControllerTypes);
+  REQUIRE_FUNCTION(ovr_GetTouchHapticsDesc);
+  REQUIRE_FUNCTION(ovr_SetControllerVibration);
+  REQUIRE_FUNCTION(ovr_SubmitControllerVibration);
+  REQUIRE_FUNCTION(ovr_GetControllerVibrationState);
+  REQUIRE_FUNCTION(ovr_TestBoundary);
+  REQUIRE_FUNCTION(ovr_TestBoundaryPoint);
+  REQUIRE_FUNCTION(ovr_SetBoundaryLookAndFeel);
+  REQUIRE_FUNCTION(ovr_ResetBoundaryLookAndFeel);
+  REQUIRE_FUNCTION(ovr_GetBoundaryGeometry);
+  REQUIRE_FUNCTION(ovr_GetBoundaryDimensions);
+  REQUIRE_FUNCTION(ovr_GetBoundaryVisible);
+  REQUIRE_FUNCTION(ovr_RequestBoundaryVisible);
+  REQUIRE_FUNCTION(ovr_GetTextureSwapChainLength);
+  REQUIRE_FUNCTION(ovr_GetTextureSwapChainCurrentIndex);
+  REQUIRE_FUNCTION(ovr_GetTextureSwapChainDesc);
+  REQUIRE_FUNCTION(ovr_CommitTextureSwapChain);
+  REQUIRE_FUNCTION(ovr_DestroyTextureSwapChain);
+  REQUIRE_FUNCTION(ovr_DestroyMirrorTexture);
+  REQUIRE_FUNCTION(ovr_GetFovTextureSize);
+  REQUIRE_FUNCTION(ovr_GetRenderDesc2);
+  REQUIRE_FUNCTION(ovr_WaitToBeginFrame);
+  REQUIRE_FUNCTION(ovr_BeginFrame);
+  REQUIRE_FUNCTION(ovr_EndFrame);
+  REQUIRE_FUNCTION(ovr_SubmitFrame);
+  REQUIRE_FUNCTION(ovr_GetPerfStats);
+  REQUIRE_FUNCTION(ovr_ResetPerfStats);
+  REQUIRE_FUNCTION(ovr_GetPredictedDisplayTime);
+  REQUIRE_FUNCTION(ovr_GetTimeInSeconds);
+  REQUIRE_FUNCTION(ovr_GetBool);
+  REQUIRE_FUNCTION(ovr_SetBool);
+  REQUIRE_FUNCTION(ovr_GetInt);
+  REQUIRE_FUNCTION(ovr_SetInt);
+  REQUIRE_FUNCTION(ovr_GetFloat);
+  REQUIRE_FUNCTION(ovr_SetFloat);
+  REQUIRE_FUNCTION(ovr_GetFloatArray);
+  REQUIRE_FUNCTION(ovr_SetFloatArray);
+  REQUIRE_FUNCTION(ovr_GetString);
+  REQUIRE_FUNCTION(ovr_SetString);
+  REQUIRE_FUNCTION(ovr_GetExternalCameras);
+  REQUIRE_FUNCTION(ovr_SetExternalCameraProperties);
+
+#ifdef XP_WIN
+
+  REQUIRE_FUNCTION(ovr_CreateTextureSwapChainDX);
+  REQUIRE_FUNCTION(ovr_GetTextureSwapChainBufferDX);
+  REQUIRE_FUNCTION(ovr_CreateMirrorTextureDX);
+  REQUIRE_FUNCTION(ovr_GetMirrorTextureBufferDX);
+
+#endif
+
+  REQUIRE_FUNCTION(ovr_CreateTextureSwapChainGL);
+  REQUIRE_FUNCTION(ovr_GetTextureSwapChainBufferGL);
+  REQUIRE_FUNCTION(ovr_CreateMirrorTextureGL);
+  REQUIRE_FUNCTION(ovr_GetMirrorTextureBufferGL);
+
+#undef REQUIRE_FUNCTION
+
+  return true;
+
+fail:
+  ovr_Initialize = nullptr;
+  PR_UnloadLibrary(mOvrLib);
+  mOvrLib = nullptr;
+  return false;
+}
+
+void
+OculusSession::UnloadOvrLib()
+{
+  if (mOvrLib) {
+    PR_UnloadLibrary(mOvrLib);
+    mOvrLib = nullptr;
+  }
+}
+
+bool
+OculusSession::StartLib(ovrInitFlags aFlags)
+{
+  if (mInitFlags == 0) {
+    ovrInitParams params;
+    memset(&params, 0, sizeof(params));
+    params.Flags = aFlags;
+    params.RequestedMinorVersion = OVR_MINOR_VERSION;
+    params.LogCallback = nullptr;
+    params.ConnectionTimeoutMS = 0;
+
+    ovrResult orv = ovr_Initialize(&params);
+
+    if (orv == ovrSuccess) {
+      mInitFlags = aFlags;
+    } else {
+      return false;
+    }
+  }
+  MOZ_ASSERT(mInitFlags == aFlags);
+  return true;
+}
+
+void
+OculusSession::StopLib()
+{
+  if (mInitFlags) {
+    ovr_Shutdown();
+    mInitFlags = (ovrInitFlags)0;
+  }
+}
+
+bool
+OculusSession::StartSession()
+{
+  // ovr_Create can be slow when no HMD is present and we wish
+  // to keep the same oculus session when possible, so we detect
+  // presence of an HMD with ovr_GetHmdDesc before calling ovr_Create
+  ovrHmdDesc desc = ovr_GetHmdDesc(NULL);
+  if (desc.Type == ovrHmd_None) {
+    // No HMD connected, destroy any existing session
+    if (mSession) {
+      ovr_Destroy(mSession);
+      mSession = nullptr;
+    }
+    return false;
+  }
+  if (mSession != nullptr) {
+    // HMD Detected and we already have a session, let's keep using it.
+    return true;
+  }
+
+  // HMD Detected and we don't have a session yet,
+  // try to create a new session
+  ovrSession session;
+  ovrGraphicsLuid luid;
+  ovrResult orv = ovr_Create(&session, &luid);
+  if (orv == ovrSuccess) {
+    orv = ovr_SetTrackingOriginType(session, ovrTrackingOrigin_FloorLevel);
+    if (orv != ovrSuccess) {
+      NS_WARNING("ovr_SetTrackingOriginType failed.\n");
+    }
+    mSession = session;
+    return true;
+  }
+
+  // Failed to create a session for the HMD
+  return false;
+}
+
+void
+OculusSession::StopSession()
+{
+  if (mSession) {
+    ovr_Destroy(mSession);
+    mSession = nullptr;
+  }
+}
+
+bool
+OculusSession::CreateD3DObjects()
+{
+  RefPtr<ID3D11Device> device = gfx::DeviceManagerDx::Get()->GetVRDevice();
+  if (!device) {
+    return false;
+  }
+  if (!CreateD3DContext(device)) {
+    return false;
+  }
+  return true;
+}
+
+bool
+OculusSession::CreateShaders()
+{
+  if (!mQuadVS) {
+    if (FAILED(mDevice->CreateVertexShader(
+          sLayerQuadVS.mData, sLayerQuadVS.mLength, nullptr, &mQuadVS))) {
+      NS_WARNING("Failed to create vertex shader for Oculus");
+      return false;
+    }
+  }
+
+  if (!mQuadPS) {
+    if (FAILED(mDevice->CreatePixelShader(
+          sRGBShader.mData, sRGBShader.mLength, nullptr, &mQuadPS))) {
+      NS_WARNING("Failed to create pixel shader for Oculus");
+      return false;
+    }
+  }
+
+  CD3D11_BUFFER_DESC cBufferDesc(sizeof(layers::VertexShaderConstants),
+                                 D3D11_BIND_CONSTANT_BUFFER,
+                                 D3D11_USAGE_DYNAMIC,
+                                 D3D11_CPU_ACCESS_WRITE);
+
+  if (!mVSConstantBuffer) {
+    if (FAILED(mDevice->CreateBuffer(
+          &cBufferDesc, nullptr, getter_AddRefs(mVSConstantBuffer)))) {
+      NS_WARNING("Failed to vertex shader constant buffer for Oculus");
+      return false;
+    }
+  }
+
+  if (!mPSConstantBuffer) {
+    cBufferDesc.ByteWidth = sizeof(layers::PixelShaderConstants);
+    if (FAILED(mDevice->CreateBuffer(
+          &cBufferDesc, nullptr, getter_AddRefs(mPSConstantBuffer)))) {
+      NS_WARNING("Failed to pixel shader constant buffer for Oculus");
+      return false;
+    }
+  }
+
+  if (!mLinearSamplerState) {
+    CD3D11_SAMPLER_DESC samplerDesc(D3D11_DEFAULT);
+    if (FAILED(mDevice->CreateSamplerState(
+          &samplerDesc, getter_AddRefs(mLinearSamplerState)))) {
+      NS_WARNING("Failed to create sampler state for Oculus");
+      return false;
+    }
+  }
+
+  if (!mInputLayout) {
+    D3D11_INPUT_ELEMENT_DESC layout[] = {
+      { "POSITION",
+        0,
+        DXGI_FORMAT_R32G32_FLOAT,
+        0,
+        0,
+        D3D11_INPUT_PER_VERTEX_DATA,
+        0 },
+    };
+
+    if (FAILED(mDevice->CreateInputLayout(layout,
+                                          sizeof(layout) /
+                                            sizeof(D3D11_INPUT_ELEMENT_DESC),
+                                          sLayerQuadVS.mData,
+                                          sLayerQuadVS.mLength,
+                                          getter_AddRefs(mInputLayout)))) {
+      NS_WARNING("Failed to create input layout for Oculus");
+      return false;
+    }
+  }
+
+  if (!mVertexBuffer) {
+    Vertex vertices[] = {
+      { { 0.0, 0.0 } }, { { 1.0, 0.0 } }, { { 0.0, 1.0 } }, { { 1.0, 1.0 } }
+    };
+    CD3D11_BUFFER_DESC bufferDesc(sizeof(vertices), D3D11_BIND_VERTEX_BUFFER);
+    D3D11_SUBRESOURCE_DATA data;
+    data.pSysMem = (void*)vertices;
+
+    if (FAILED(mDevice->CreateBuffer(
+          &bufferDesc, &data, getter_AddRefs(mVertexBuffer)))) {
+      NS_WARNING("Failed to create vertex buffer for Oculus");
+      return false;
+    }
+  }
+
+  memset(&mVSConstants, 0, sizeof(mVSConstants));
+  memset(&mPSConstants, 0, sizeof(mPSConstants));
+  return true;
+}
+
+void
+OculusSession::DestroyShaders()
+{
+}
+
+bool
+OculusSession::UpdateConstantBuffers()
+{
+  HRESULT hr;
+  D3D11_MAPPED_SUBRESOURCE resource;
+  resource.pData = nullptr;
+
+  hr =
+    mContext->Map(mVSConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &resource);
+  if (FAILED(hr) || !resource.pData) {
+    return false;
+  }
+  *(VertexShaderConstants*)resource.pData = mVSConstants;
+  mContext->Unmap(mVSConstantBuffer, 0);
+  resource.pData = nullptr;
+
+  hr =
+    mContext->Map(mPSConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &resource);
+  if (FAILED(hr) || !resource.pData) {
+    return false;
+  }
+  *(PixelShaderConstants*)resource.pData = mPSConstants;
+  mContext->Unmap(mPSConstantBuffer, 0);
+
+  ID3D11Buffer* buffer = mVSConstantBuffer;
+  mContext->VSSetConstantBuffers(0, 1, &buffer);
+  buffer = mPSConstantBuffer;
+  mContext->PSSetConstantBuffers(0, 1, &buffer);
+  return true;
+}
+
+bool
+OculusSession::StartRendering()
+{
+  if (!mTextureSet) {
+    /**
+     * The presentation format is determined by content, which describes the
+     * left and right eye rectangles in the VRLayer.  The default, if no
+     * coordinates are passed is to place the left and right eye textures
+     * side-by-side within the buffer.
+     *
+     * XXX - An optimization would be to dynamically resize this buffer
+     *       to accomodate sites that are choosing to render in a lower
+     *       resolution or are using space outside of the left and right
+     *       eye textures for other purposes.  (Bug 1291443)
+     */
+
+    ovrTextureSwapChainDesc desc;
+    memset(&desc, 0, sizeof(desc));
+    desc.Type = ovrTexture_2D;
+    desc.ArraySize = 1;
+    desc.Format = OVR_FORMAT_B8G8R8A8_UNORM_SRGB;
+    desc.Width = mPresentationSize.width;
+    desc.Height = mPresentationSize.height;
+    desc.MipLevels = 1;
+    desc.SampleCount = 1;
+    desc.StaticImage = false;
+    desc.MiscFlags = ovrTextureMisc_DX_Typeless;
+    desc.BindFlags = ovrTextureBind_DX_RenderTarget;
+
+    ovrResult orv =
+      ovr_CreateTextureSwapChainDX(mSession, mDevice, &desc, &mTextureSet);
+    if (orv != ovrSuccess) {
+      NS_WARNING("ovr_CreateTextureSwapChainDX failed");
+      return false;
+    }
+
+    int textureCount = 0;
+    orv = ovr_GetTextureSwapChainLength(mSession, mTextureSet, &textureCount);
+    if (orv != ovrSuccess) {
+      NS_WARNING("ovr_GetTextureSwapChainLength failed");
+      return false;
+    }
+    mTexture.SetLength(textureCount);
+    mRTView.SetLength(textureCount);
+    mSRV.SetLength(textureCount);
+    for (int i = 0; i < textureCount; ++i) {
+
+      ID3D11Texture2D* texture = nullptr;
+      orv = ovr_GetTextureSwapChainBufferDX(
+        mSession, mTextureSet, i, IID_PPV_ARGS(&texture));
+      if (orv != ovrSuccess) {
+        NS_WARNING("Failed to create Oculus texture swap chain.");
+        return false;
+      }
+
+      RefPtr<ID3D11RenderTargetView> rtView;
+      CD3D11_RENDER_TARGET_VIEW_DESC rtvDesc(D3D11_RTV_DIMENSION_TEXTURE2D,
+                                             DXGI_FORMAT_B8G8R8A8_UNORM);
+      HRESULT hr = mDevice->CreateRenderTargetView(
+        texture, &rtvDesc, getter_AddRefs(rtView));
+      if (FAILED(hr)) {
+        NS_WARNING(
+          "Failed to create RenderTargetView for Oculus texture swap chain.");
+        texture->Release();
+        return false;
+      }
+
+      RefPtr<ID3D11ShaderResourceView> srv;
+      CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(D3D11_SRV_DIMENSION_TEXTURE2D,
+                                               DXGI_FORMAT_B8G8R8A8_UNORM);
+      hr = mDevice->CreateShaderResourceView(
+        texture, &srvDesc, getter_AddRefs(srv));
+      if (FAILED(hr)) {
+        NS_WARNING(
+          "Failed to create ShaderResourceView for Oculus texture swap chain.");
+        texture->Release();
+        return false;
+      }
+
+      mTexture[i] = texture;
+      mRTView[i] = rtView;
+      mSRV[i] = srv;
+      texture->Release();
+    }
+  }
+  return true;
+}
+
+bool
+OculusSession::IsPresentationReady() const
+{
+  return mTextureSet != nullptr;
+}
+
+void
+OculusSession::StopRendering()
+{
+  mSRV.Clear();
+  mRTView.Clear();
+  mTexture.Clear();
+
+  if (mTextureSet && mSession) {
+    ovr_DestroyTextureSwapChain(mSession, mTextureSet);
+  }
+  mTextureSet = nullptr;
+  mIsPresenting = false;
+}
+
+bool
+OculusSession::InitState(VRSystemState& aSystemState)
+{
+  VRDisplayState& state = aSystemState.displayState;
+  strncpy(state.mDisplayName, "Oculus VR HMD", kVRDisplayNameMaxLen);
+  state.mIsConnected = true;
+  state.mIsMounted = false;
+
+  ovrHmdDesc desc = ovr_GetHmdDesc(mSession);
+
+  state.mCapabilityFlags = VRDisplayCapabilityFlags::Cap_None;
+  if (desc.AvailableTrackingCaps & ovrTrackingCap_Orientation) {
+    state.mCapabilityFlags |= VRDisplayCapabilityFlags::Cap_Orientation;
+    state.mCapabilityFlags |= VRDisplayCapabilityFlags::Cap_AngularAcceleration;
+  }
+  if (desc.AvailableTrackingCaps & ovrTrackingCap_Position) {
+    state.mCapabilityFlags |= VRDisplayCapabilityFlags::Cap_Position;
+    state.mCapabilityFlags |= VRDisplayCapabilityFlags::Cap_LinearAcceleration;
+    state.mCapabilityFlags |= VRDisplayCapabilityFlags::Cap_StageParameters;
+  }
+  state.mCapabilityFlags |= VRDisplayCapabilityFlags::Cap_External;
+  state.mCapabilityFlags |= VRDisplayCapabilityFlags::Cap_MountDetection;
+  state.mCapabilityFlags |= VRDisplayCapabilityFlags::Cap_Present;
+  state.mReportsDroppedFrames = true;
+
+  mFOVPort[VRDisplayState::Eye_Left] = desc.DefaultEyeFov[ovrEye_Left];
+  mFOVPort[VRDisplayState::Eye_Right] = desc.DefaultEyeFov[ovrEye_Right];
+
+  state.mEyeFOV[VRDisplayState::Eye_Left] =
+    FromFovPort(mFOVPort[VRDisplayState::Eye_Left]);
+  state.mEyeFOV[VRDisplayState::Eye_Right] =
+    FromFovPort(mFOVPort[VRDisplayState::Eye_Right]);
+
+  float pixelsPerDisplayPixel = 1.0;
+  ovrSizei texSize[2];
+
+  // get eye texture sizes
+  for (uint32_t eye = 0; eye < VRDisplayState::NumEyes; eye++) {
+    texSize[eye] = ovr_GetFovTextureSize(
+      mSession, (ovrEyeType)eye, mFOVPort[eye], pixelsPerDisplayPixel);
+  }
+
+  // take the max of both for eye resolution
+  state.mEyeResolution.width = std::max(texSize[VRDisplayState::Eye_Left].w,
+                                        texSize[VRDisplayState::Eye_Right].w);
+  state.mEyeResolution.height = std::max(texSize[VRDisplayState::Eye_Left].h,
+                                         texSize[VRDisplayState::Eye_Right].h);
+
+  // default to an identity quaternion
+  aSystemState.sensorState.pose.orientation[3] = 1.0f;
+
+  UpdateStageParameters(state);
+  UpdateEyeParameters(aSystemState);
+
+  VRHMDSensorState& sensorState = aSystemState.sensorState;
+  sensorState.flags =
+    (VRDisplayCapabilityFlags)((int)VRDisplayCapabilityFlags::Cap_Orientation |
+                               (int)VRDisplayCapabilityFlags::Cap_Position);
+  sensorState.pose.orientation[3] = 1.0f; // Default to an identity quaternion
+
+  return true;
+}
+
+void
+OculusSession::UpdateStageParameters(VRDisplayState& aState)
+{
+  ovrVector3f playArea;
+  ovrResult res =
+    ovr_GetBoundaryDimensions(mSession, ovrBoundary_PlayArea, &playArea);
+  if (res == ovrSuccess) {
+    aState.mStageSize.width = playArea.x;
+    aState.mStageSize.height = playArea.z;
+  } else {
+    // If we fail, fall back to reasonable defaults.
+    // 1m x 1m space
+    aState.mStageSize.width = 1.0f;
+    aState.mStageSize.height = 1.0f;
+  }
+
+  float eyeHeight =
+    ovr_GetFloat(mSession, OVR_KEY_EYE_HEIGHT, OVR_DEFAULT_EYE_HEIGHT);
+
+  aState.mSittingToStandingTransform[0] = 1.0f;
+  aState.mSittingToStandingTransform[1] = 0.0f;
+  aState.mSittingToStandingTransform[2] = 0.0f;
+  aState.mSittingToStandingTransform[3] = 0.0f;
+
+  aState.mSittingToStandingTransform[4] = 0.0f;
+  aState.mSittingToStandingTransform[5] = 1.0f;
+  aState.mSittingToStandingTransform[6] = 0.0f;
+  aState.mSittingToStandingTransform[7] = 0.0f;
+
+  aState.mSittingToStandingTransform[8] = 0.0f;
+  aState.mSittingToStandingTransform[9] = 0.0f;
+  aState.mSittingToStandingTransform[10] = 1.0f;
+  aState.mSittingToStandingTransform[11] = 0.0f;
+
+  aState.mSittingToStandingTransform[12] = 0.0f;
+  aState.mSittingToStandingTransform[13] = eyeHeight;
+  aState.mSittingToStandingTransform[14] = 0.0f;
+  aState.mSittingToStandingTransform[15] = 1.0f;
+}
+
+void
+OculusSession::UpdateEyeParameters(VRSystemState& aState)
+{
+  if (!mSession) {
+    return;
+  }
+  // This must be called every frame in order to
+  // account for continuous adjustments to ipd.
+  gfx::Matrix4x4 headToEyeTransforms[2];
+  for (uint32_t eye = 0; eye < VRDisplayState::NumEyes; eye++) {
+    // As of Oculus 1.17 SDK, we must use the ovr_GetRenderDesc2 function to
+    // return the updated version of ovrEyeRenderDesc.  This is normally done by
+    // the Oculus static lib shim, but we need to do this explicitly as we are
+    // loading the Oculus runtime dll directly.
+    ovrEyeRenderDesc renderDesc =
+      ovr_GetRenderDesc2(mSession, (ovrEyeType)eye, mFOVPort[eye]);
+    aState.displayState.mEyeTranslation[eye].x =
+      renderDesc.HmdToEyePose.Position.x;
+    aState.displayState.mEyeTranslation[eye].y =
+      renderDesc.HmdToEyePose.Position.y;
+    aState.displayState.mEyeTranslation[eye].z =
+      renderDesc.HmdToEyePose.Position.z;
+
+    Matrix4x4 pose;
+    pose.SetRotationFromQuaternion(
+      gfx::Quaternion(renderDesc.HmdToEyePose.Orientation.x,
+                      renderDesc.HmdToEyePose.Orientation.y,
+                      renderDesc.HmdToEyePose.Orientation.z,
+                      renderDesc.HmdToEyePose.Orientation.w));
+    pose.PreTranslate(renderDesc.HmdToEyePose.Position.x,
+                      renderDesc.HmdToEyePose.Position.y,
+                      renderDesc.HmdToEyePose.Position.z);
+    pose.Invert();
+    headToEyeTransforms[eye] = pose;
+  }
+  aState.sensorState.CalcViewMatrices(headToEyeTransforms);
+
+  Matrix4x4 matView[2];
+  memcpy(matView[0].components,
+         aState.sensorState.leftViewMatrix,
+         sizeof(float) * 16);
+  memcpy(matView[1].components,
+         aState.sensorState.rightViewMatrix,
+         sizeof(float) * 16);
+
+  for (uint32_t eye = 0; eye < VRDisplayState::NumEyes; eye++) {
+    Point3D eyeTranslation;
+    Quaternion eyeRotation;
+    Point3D eyeScale;
+    if (!matView[eye].Decompose(eyeTranslation, eyeRotation, eyeScale)) {
+      NS_WARNING("Failed to decompose eye pose matrix for Oculus");
+    }
+
+    mFrameStartPose[eye].Orientation.x = eyeRotation.x;
+    mFrameStartPose[eye].Orientation.y = eyeRotation.y;
+    mFrameStartPose[eye].Orientation.z = eyeRotation.z;
+    mFrameStartPose[eye].Orientation.w = eyeRotation.w;
+    mFrameStartPose[eye].Position.x = eyeTranslation.x;
+    mFrameStartPose[eye].Position.y = eyeTranslation.y;
+    mFrameStartPose[eye].Position.z = eyeTranslation.z;
+  }
+}
+
+void
+OculusSession::UpdateHeadsetPose(VRSystemState& aState)
+{
+  if (!mSession) {
+    return;
+  }
+  double predictedFrameTime = 0.0f;
+  if (gfxPrefs::VRPosePredictionEnabled()) {
+    // XXX We might need to call ovr_GetPredictedDisplayTime even if we don't
+    // use the result. If we don't call it, the Oculus driver will spew out many
+    // warnings...
+    predictedFrameTime = ovr_GetPredictedDisplayTime(mSession, 0);
+  }
+  ovrTrackingState trackingState =
+    ovr_GetTrackingState(mSession, predictedFrameTime, true);
+  ovrPoseStatef& pose(trackingState.HeadPose);
+
+  aState.sensorState.timestamp = pose.TimeInSeconds;
+
+  if (trackingState.StatusFlags & ovrStatus_OrientationTracked) {
+    aState.sensorState.flags |= VRDisplayCapabilityFlags::Cap_Orientation;
+
+    aState.sensorState.pose.orientation[0] = pose.ThePose.Orientation.x;
+    aState.sensorState.pose.orientation[1] = pose.ThePose.Orientation.y;
+    aState.sensorState.pose.orientation[2] = pose.ThePose.Orientation.z;
+    aState.sensorState.pose.orientation[3] = pose.ThePose.Orientation.w;
+
+    aState.sensorState.pose.angularVelocity[0] = pose.AngularVelocity.x;
+    aState.sensorState.pose.angularVelocity[1] = pose.AngularVelocity.y;
+    aState.sensorState.pose.angularVelocity[2] = pose.AngularVelocity.z;
+
+    aState.sensorState.flags |=
+      VRDisplayCapabilityFlags::Cap_AngularAcceleration;
+
+    aState.sensorState.pose.angularAcceleration[0] = pose.AngularAcceleration.x;
+    aState.sensorState.pose.angularAcceleration[1] = pose.AngularAcceleration.y;
+    aState.sensorState.pose.angularAcceleration[2] = pose.AngularAcceleration.z;
+  } else {
+    // default to an identity quaternion
+    aState.sensorState.pose.orientation[3] = 1.0f;
+  }
+
+  if (trackingState.StatusFlags & ovrStatus_PositionTracked) {
+    float eyeHeight =
+      ovr_GetFloat(mSession, OVR_KEY_EYE_HEIGHT, OVR_DEFAULT_EYE_HEIGHT);
+    aState.sensorState.flags |= VRDisplayCapabilityFlags::Cap_Position;
+
+    aState.sensorState.pose.position[0] = pose.ThePose.Position.x;
+    aState.sensorState.pose.position[1] = pose.ThePose.Position.y - eyeHeight;
+    aState.sensorState.pose.position[2] = pose.ThePose.Position.z;
+
+    aState.sensorState.pose.linearVelocity[0] = pose.LinearVelocity.x;
+    aState.sensorState.pose.linearVelocity[1] = pose.LinearVelocity.y;
+    aState.sensorState.pose.linearVelocity[2] = pose.LinearVelocity.z;
+
+    aState.sensorState.flags |=
+      VRDisplayCapabilityFlags::Cap_LinearAcceleration;
+
+    aState.sensorState.pose.linearAcceleration[0] = pose.LinearAcceleration.x;
+    aState.sensorState.pose.linearAcceleration[1] = pose.LinearAcceleration.y;
+    aState.sensorState.pose.linearAcceleration[2] = pose.LinearAcceleration.z;
+  }
+  aState.sensorState.flags |= VRDisplayCapabilityFlags::Cap_External;
+  aState.sensorState.flags |= VRDisplayCapabilityFlags::Cap_MountDetection;
+  aState.sensorState.flags |= VRDisplayCapabilityFlags::Cap_Present;
+}
+
+void
+OculusSession::UpdateControllers(VRSystemState& aState)
+{
+  if (!mSession) {
+    return;
+  }
+
+  ovrInputState inputState;
+  bool hasInputState =
+    ovr_GetInputState(mSession, ovrControllerType_Touch, &inputState) ==
+    ovrSuccess;
+
+  if (!hasInputState) {
+    return;
+  }
+
+  EnumerateControllers(aState, inputState);
+  UpdateControllerInputs(aState, inputState);
+  UpdateControllerPose(aState, inputState);
+}
+
+void
+OculusSession::UpdateControllerPose(VRSystemState& aState,
+                                    const ovrInputState& aInputState)
+{
+  ovrTrackingState trackingState = ovr_GetTrackingState(mSession, 0.0, false);
+  for (uint32_t handIdx = 0; handIdx < 2; handIdx++) {
+    // Left Touch Controller will always be at index 0 and
+    // and Right Touch Controller will always be at index 1
+    VRControllerState& controllerState = aState.controllerState[handIdx];
+    if (aInputState.ControllerType & OculusControllerTypes[handIdx]) {
+      ovrPoseStatef& pose = trackingState.HandPoses[handIdx];
+      bool bNewController =
+        !(controllerState.flags & dom::GamepadCapabilityFlags::Cap_Orientation);
+      if (bNewController) {
+        controllerState.flags |= dom::GamepadCapabilityFlags::Cap_Orientation;
+        controllerState.flags |= dom::GamepadCapabilityFlags::Cap_Position;
+        controllerState.flags |=
+          dom::GamepadCapabilityFlags::Cap_AngularAcceleration;
+        controllerState.flags |=
+          dom::GamepadCapabilityFlags::Cap_LinearAcceleration;
+      }
+
+      if (bNewController || trackingState.HandStatusFlags[handIdx] &
+                              ovrStatus_OrientationTracked) {
+        controllerState.pose.orientation[0] = pose.ThePose.Orientation.x;
+        controllerState.pose.orientation[1] = pose.ThePose.Orientation.y;
+        controllerState.pose.orientation[2] = pose.ThePose.Orientation.z;
+        controllerState.pose.orientation[3] = pose.ThePose.Orientation.w;
+        controllerState.pose.angularVelocity[0] = pose.AngularVelocity.x;
+        controllerState.pose.angularVelocity[1] = pose.AngularVelocity.y;
+        controllerState.pose.angularVelocity[2] = pose.AngularVelocity.z;
+        controllerState.pose.angularAcceleration[0] =
+          pose.AngularAcceleration.x;
+        controllerState.pose.angularAcceleration[1] =
+          pose.AngularAcceleration.y;
+        controllerState.pose.angularAcceleration[2] =
+          pose.AngularAcceleration.z;
+        controllerState.isOrientationValid = true;
+      } else {
+        controllerState.isOrientationValid = false;
+      }
+      if (bNewController ||
+          trackingState.HandStatusFlags[handIdx] & ovrStatus_PositionTracked) {
+        controllerState.pose.position[0] = pose.ThePose.Position.x;
+        controllerState.pose.position[1] = pose.ThePose.Position.y;
+        controllerState.pose.position[2] = pose.ThePose.Position.z;
+        controllerState.pose.linearVelocity[0] = pose.LinearVelocity.x;
+        controllerState.pose.linearVelocity[1] = pose.LinearVelocity.y;
+        controllerState.pose.linearVelocity[2] = pose.LinearVelocity.z;
+        controllerState.pose.linearAcceleration[0] = pose.LinearAcceleration.x;
+        controllerState.pose.linearAcceleration[1] = pose.LinearAcceleration.y;
+        controllerState.pose.linearAcceleration[2] = pose.LinearAcceleration.z;
+
+        float eyeHeight =
+          ovr_GetFloat(mSession, OVR_KEY_EYE_HEIGHT, OVR_DEFAULT_EYE_HEIGHT);
+        controllerState.pose.position[1] -= eyeHeight;
+        controllerState.isPositionValid = true;
+      } else {
+        controllerState.isPositionValid = false;
+      }
+    }
+  }
+}
+
+void
+OculusSession::EnumerateControllers(VRSystemState& aState,
+                                    const ovrInputState& aInputState)
+{
+  for (uint32_t handIdx = 0; handIdx < 2; handIdx++) {
+    // Left Touch Controller will always be at index 0 and
+    // and Right Touch Controller will always be at index 1
+    VRControllerState& controllerState = aState.controllerState[handIdx];
+    if (aInputState.ControllerType & OculusControllerTypes[handIdx]) {
+      bool bNewController = false;
+      // Left Touch Controller detected
+      if (controllerState.controllerName[0] == '\0') {
+        // Controller has been just enumerated
+        strncpy(controllerState.controllerName,
+                OculusControllerNames[handIdx],
+                kVRControllerNameMaxLen);
+        controllerState.hand = OculusControllerHand[handIdx];
+        controllerState.numButtons = kNumOculusButtons;
+        controllerState.numAxes = kNumOculusAxes;
+        controllerState.numHaptics = kNumOculusHaptcs;
+        bNewController = true;
+      }
+    } else {
+      // Left Touch Controller not detected
+      if (controllerState.controllerName[0] != '\0') {
+        // Clear any newly disconnected ontrollers
+        memset(&controllerState, 0, sizeof(VRControllerState));
+      }
+    }
+  }
+}
+
+void
+OculusSession::UpdateControllerInputs(VRSystemState& aState,
+                                      const ovrInputState& aInputState)
+{
+  const float triggerThreshold = gfxPrefs::VRControllerTriggerThreshold();
+
+  for (uint32_t handIdx = 0; handIdx < 2; handIdx++) {
+    // Left Touch Controller will always be at index 0 and
+    // and Right Touch Controller will always be at index 1
+    VRControllerState& controllerState = aState.controllerState[handIdx];
+    if (aInputState.ControllerType & OculusControllerTypes[handIdx]) {
+      // Update Button States
+      controllerState.buttonPressed = 0;
+      controllerState.buttonTouched = 0;
+      uint32_t buttonIdx = 0;
+
+      UpdateButton(aInputState, handIdx, buttonIdx, controllerState);
+      buttonIdx++;
+      UpdateTrigger(controllerState,
+                    buttonIdx,
+                    aInputState.IndexTrigger[handIdx],
+                    triggerThreshold);
+      UpdateButton(aInputState, handIdx, buttonIdx, controllerState);
+      buttonIdx++;
+      UpdateTrigger(controllerState,
+                    buttonIdx,
+                    aInputState.HandTrigger[handIdx],
+                    triggerThreshold);
+      buttonIdx++;
+      UpdateButton(aInputState, handIdx, buttonIdx, controllerState);
+      buttonIdx++;
+      UpdateButton(aInputState, handIdx, buttonIdx, controllerState);
+      buttonIdx++;
+      UpdateButton(aInputState, handIdx, buttonIdx, controllerState);
+      buttonIdx++;
+
+      MOZ_ASSERT(buttonIdx == kNumOculusButtons);
+
+      // Update Thumbstick axis
+      uint32_t axisIdx = 0;
+      float axisValue = aInputState.Thumbstick[handIdx].x;
+      if (abs(axisValue) < 0.0000009f) {
+        axisValue = 0.0f; // Clear noise signal
+      }
+      controllerState.axisValue[axisIdx] = axisValue;
+      axisIdx++;
+
+      // Note that y axis is intentionally inverted!
+      axisValue = -aInputState.Thumbstick[handIdx].y;
+      if (abs(axisValue) < 0.0000009f) {
+        axisValue = 0.0f; // Clear noise signal
+      }
+      controllerState.axisValue[axisIdx] = axisValue;
+      axisIdx++;
+
+      MOZ_ASSERT(axisIdx == kNumOculusAxes);
+    }
+  }
+}
+
+void
+OculusSession::UpdateTelemetry(VRSystemState& aSystemState)
+{
+  if (!mSession) {
+    return;
+  }
+  ovrPerfStats perfStats;
+  if (ovr_GetPerfStats(mSession, &perfStats) == ovrSuccess) {
+    if (perfStats.FrameStatsCount) {
+      aSystemState.displayState.mDroppedFrameCount =
+        perfStats.FrameStats[0].AppDroppedFrameCount;
+    }
+  }
+}
+
+void
+OculusSession::VibrateHaptic(uint32_t aControllerIdx,
+                             uint32_t aHapticIndex,
+                             float aIntensity,
+                             float aDuration)
+{
+  if (!mSession) {
+    return;
+  }
+
+  if (aDuration <= 0.0f) {
+    StopVibrateHaptic(aControllerIdx);
+    return;
+  }
+
+  // Vibration amplitude in the [0.0, 1.0] range
+  MOZ_ASSERT(aControllerIdx >= 0 && aControllerIdx <= 1);
+  mHapticPulseIntensity[aControllerIdx] = aIntensity > 1.0 ? 1.0 : aIntensity;
+  mRemainingVibrateTime[aControllerIdx] = aDuration;
+  ovrControllerType hand = OculusControllerTypes[aControllerIdx];
+
+  // The gamepad extensions API does not yet have independent control
+  // of frequency and amplitude.  We are always sending 0.0f (160hz)
+  // to the frequency argument.
+  ovrResult result = ovr_SetControllerVibration(
+    mSession, hand, 0.0f, mHapticPulseIntensity[aControllerIdx]);
+  if (result != ovrSuccess) {
+    // This may happen if called when not presenting.
+    gfxWarning() << "ovr_SetControllerVibration failed.";
+  }
+}
+
+void
+OculusSession::StopVibrateHaptic(uint32_t aControllerIdx)
+{
+  if (!mSession) {
+    return;
+  }
+  MOZ_ASSERT(aControllerIdx >= 0 && aControllerIdx <= 1);
+  ovrControllerType hand = OculusControllerTypes[aControllerIdx];
+  mRemainingVibrateTime[aControllerIdx] = 0.0f;
+  mHapticPulseIntensity[aControllerIdx] = 0.0f;
+
+  ovrResult result = ovr_SetControllerVibration(mSession, hand, 0.0f, 0.0f);
+  if (result != ovrSuccess) {
+    // This may happen if called when not presenting.
+    gfxWarning() << "ovr_SetControllerVibration failed.";
+  }
+}
+
+void
+OculusSession::StopAllHaptics()
+{
+  // Left Oculus Touch
+  StopVibrateHaptic(0);
+  // Right Oculus Touch
+  StopVibrateHaptic(1);
+}
+
+void
+OculusSession::UpdateHaptics()
+{
+  if (!mSession) {
+    return;
+  }
+  // The Oculus API and hardware takes at least 33ms to respond
+  // to haptic state changes, so it is not beneficial to create
+  // a dedicated haptic feedback thread and update multiple
+  // times per frame.
+  // If we wish to support more accurate effects with sub-frame timing,
+  // we should use the buffered haptic feedback API's.
+
+  TimeStamp now = TimeStamp::Now();
+  if (mLastHapticUpdate.IsNull()) {
+    mLastHapticUpdate = now;
+    return;
+  }
+  float deltaTime = (float)(now - mLastHapticUpdate).ToSeconds();
+  mLastHapticUpdate = now;
+  for (int i = 0; i < 2; i++) {
+    if (mRemainingVibrateTime[i] <= 0.0f) {
+      continue;
+    }
+    mRemainingVibrateTime[i] -= deltaTime;
+    ovrControllerType hand = OculusControllerTypes[i];
+    if (mRemainingVibrateTime[i] > 0.0f) {
+      ovrResult result = ovr_SetControllerVibration(
+        mSession, hand, 0.0f, mHapticPulseIntensity[i]);
+      if (result != ovrSuccess) {
+        // This may happen if called when not presenting.
+        gfxWarning() << "ovr_SetControllerVibration failed.";
+      }
+    } else {
+      StopVibrateHaptic(i);
+    }
+  }
+}
+
+} // namespace mozilla
+} // namespace gfx
new file mode 100644
--- /dev/null
+++ b/gfx/vr/service/OculusSession.h
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_VR_SERVICE_OCULUSSESSION_H
+#define GFX_VR_SERVICE_OCULUSSESSION_H
+
+#include "VRSession.h"
+
+#include "mozilla/gfx/2D.h"
+#include "moz_external_vr.h"
+#include "nsTArray.h"
+#include "../ovr_capi_dynamic.h"
+#include "prlink.h"
+#include "ShaderDefinitionsD3D11.h" // for VertexShaderConstants and PixelShaderConstants
+
+struct ID3D11Device;
+
+namespace mozilla {
+namespace layers {
+struct VertexShaderConstants;
+struct PixelShaderConstants;
+}
+namespace gfx {
+
+class OculusSession : public VRSession
+{
+public:
+  OculusSession();
+  virtual ~OculusSession();
+
+  bool Initialize(mozilla::gfx::VRSystemState& aSystemState) override;
+  void Shutdown() override;
+  void ProcessEvents(mozilla::gfx::VRSystemState& aSystemState) override;
+  void StartFrame(mozilla::gfx::VRSystemState& aSystemState) override;
+  bool StartPresentation() override;
+  void StopPresentation() override;
+  bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                   ID3D11Texture2D* aTexture) override;
+  void VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
+                    float aIntensity, float aDuration) override;
+  void StopVibrateHaptic(uint32_t aControllerIdx) override;
+  void StopAllHaptics() override;
+
+private:
+  bool LoadOvrLib();
+  void UnloadOvrLib();
+  bool StartLib(ovrInitFlags aFlags);
+  void StopLib();
+  bool StartSession();
+  void StopSession();
+  bool StartRendering();
+  void StopRendering();
+  bool CreateD3DObjects();
+  bool CreateShaders();
+  void DestroyShaders();
+  void CoverTransitions();
+  void UpdateVisibility();
+  bool ChangeVisibility(bool bVisible);
+  bool InitState(mozilla::gfx::VRSystemState& aSystemState);
+  void UpdateStageParameters(mozilla::gfx::VRDisplayState& aState);
+  void UpdateEyeParameters(mozilla::gfx::VRSystemState& aState);
+  void UpdateHeadsetPose(mozilla::gfx::VRSystemState& aState);
+  void UpdateControllers(VRSystemState& aState);
+  void UpdateControllerInputs(VRSystemState& aState,
+                              const ovrInputState& aInputState);
+  void UpdateHaptics();
+  void EnumerateControllers(VRSystemState& aState,
+                            const ovrInputState& aInputState);
+  void UpdateControllerPose(VRSystemState& aState,
+                            const ovrInputState& aInputState);
+  void UpdateTelemetry(VRSystemState& aSystemState);
+  bool IsPresentationReady() const;
+  bool UpdateConstantBuffers();
+
+  PRLibrary* mOvrLib;
+  ovrSession mSession;
+  ovrInitFlags mInitFlags;
+  ovrTextureSwapChain mTextureSet;
+  nsTArray<RefPtr<ID3D11RenderTargetView>> mRTView;
+  nsTArray<RefPtr<ID3D11Texture2D>> mTexture;
+  nsTArray<RefPtr<ID3D11ShaderResourceView>> mSRV;
+
+  ID3D11VertexShader* mQuadVS;
+  ID3D11PixelShader* mQuadPS;
+  RefPtr<ID3D11SamplerState> mLinearSamplerState;
+  layers::VertexShaderConstants mVSConstants;
+  layers::PixelShaderConstants mPSConstants;
+  RefPtr<ID3D11Buffer> mVSConstantBuffer;
+  RefPtr<ID3D11Buffer> mPSConstantBuffer;
+  RefPtr<ID3D11Buffer> mVertexBuffer;
+  RefPtr<ID3D11InputLayout> mInputLayout;
+
+  IntSize mPresentationSize;
+  ovrFovPort mFOVPort[2];
+
+  // Most recent HMD eye poses, from start of frame
+  ovrPosef mFrameStartPose[2];
+
+  float mRemainingVibrateTime[2];
+  float mHapticPulseIntensity[2];
+  TimeStamp mLastHapticUpdate;
+
+  // The timestamp of the last ending presentation
+  TimeStamp mLastPresentationEnd;
+  bool mIsPresenting;
+};
+
+} // namespace mozilla
+} // namespace gfx
+
+#endif // GFX_VR_SERVICE_OCULUSSESSION_H
--- a/gfx/vr/service/OpenVRSession.cpp
+++ b/gfx/vr/service/OpenVRSession.cpp
@@ -74,45 +74,26 @@ UpdateButton(VRControllerState& aState, 
     // not touched
     aState.buttonTouched &= ~mask;
   } else {
     // touched
     aState.buttonTouched |= mask;
   }
 }
 
-void
-UpdateTrigger(VRControllerState& aState, uint32_t aButtonIndex, float aValue, float aThreshold)
-{
-  // For OpenVR, the threshold value of ButtonPressed and ButtonTouched is 0.55.
-  // We prefer to let developers to set their own threshold for the adjustment.
-  // Therefore, we don't check ButtonPressed and ButtonTouched with ButtonMask here.
-  // we just check the button value is larger than the threshold value or not.
-  uint64_t mask = (1ULL << aButtonIndex);
-  aState.triggerValue[aButtonIndex] = aValue;
-  if (aValue > aThreshold) {
-    aState.buttonPressed |= mask;
-    aState.buttonTouched |= mask;
-  } else {
-    aState.buttonPressed &= ~mask;
-    aState.buttonTouched &= ~mask;
-  }
-}
-
 }; // anonymous namespace
 
 OpenVRSession::OpenVRSession()
   : VRSession()
   , mVRSystem(nullptr)
   , mVRChaperone(nullptr)
   , mVRCompositor(nullptr)
   , mControllerDeviceIndex{}
   , mHapticPulseRemaining{}
   , mHapticPulseIntensity{}
-  , mShouldQuit(false)
   , mIsWindowsMR(false)
   , mControllerHapticStateMutex("OpenVRSession::mControllerHapticStateMutex")
 {
 }
 
 OpenVRSession::~OpenVRSession()
 {
   Shutdown();
@@ -779,22 +760,16 @@ OpenVRSession::StartFrame(mozilla::gfx::
   UpdateHeadsetPose(aSystemState);
   UpdateEyeParameters(aSystemState);
   EnumerateControllers(aSystemState);
   UpdateControllerButtons(aSystemState);
   UpdateControllerPoses(aSystemState);
   UpdateTelemetry(aSystemState);
 }
 
-bool
-OpenVRSession::ShouldQuit() const
-{
-  return mShouldQuit;
-}
-
 void
 OpenVRSession::ProcessEvents(mozilla::gfx::VRSystemState& aSystemState)
 {
   bool isHmdPresent = ::vr::VR_IsHmdPresent();
   if (!isHmdPresent) {
     mShouldQuit = true;
   }
 
@@ -830,75 +805,37 @@ OpenVRSession::ProcessEvents(mozilla::gf
         break;
       default:
         // ignore
         break;
     }
   }
 }
 
+#if defined(XP_WIN)
 bool
-OpenVRSession::SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer)
+OpenVRSession::SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                           ID3D11Texture2D* aTexture)
 {
-#if defined(XP_WIN)
-
-  if (aLayer.mTextureType == VRLayerTextureType::LayerTextureType_D3D10SurfaceDescriptor) {
-      RefPtr<ID3D11Texture2D> dxTexture;
-      HRESULT hr = mDevice->OpenSharedResource((HANDLE)aLayer.mTextureHandle,
-        __uuidof(ID3D11Texture2D),
-        (void**)(ID3D11Texture2D**)getter_AddRefs(dxTexture));
-      if (FAILED(hr) || !dxTexture) {
-        NS_WARNING("Failed to open shared texture");
-        return false;
-      }
-
-      // Similar to LockD3DTexture in TextureD3D11.cpp
-      RefPtr<IDXGIKeyedMutex> mutex;
-      dxTexture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
-      if (mutex) {
-        HRESULT hr = mutex->AcquireSync(0, 1000);
-        if (hr == WAIT_TIMEOUT) {
-          gfxDevCrash(LogReason::D3DLockTimeout) << "D3D lock mutex timeout";
-        }
-        else if (hr == WAIT_ABANDONED) {
-          gfxCriticalNote << "GFX: D3D11 lock mutex abandoned";
-        }
-        if (FAILED(hr)) {
-          NS_WARNING("Failed to lock the texture");
-          return false;
-        }
-      }
-      bool success = SubmitFrame((void *)dxTexture,
+  return SubmitFrame((void *)aTexture,
                      ::vr::ETextureType::TextureType_DirectX,
                      aLayer.mLeftEyeRect, aLayer.mRightEyeRect);
-      if (mutex) {
-        HRESULT hr = mutex->ReleaseSync(0);
-        if (FAILED(hr)) {
-          NS_WARNING("Failed to unlock the texture");
-        }
-      }
-      if (!success) {
-        return false;
-      }
-      return true;
-  }
+}
 
 #elif defined(XP_MACOSX)
-
-  if (aLayer.mTextureType == VRLayerTextureType::LayerTextureType_MacIOSurface) {
-    return SubmitFrame(aLayer.mTextureHandle,
-                       ::vr::ETextureType::TextureType_IOSurface,
-                       aLayer.mLeftEyeRect, aLayer.mRightEyeRect);
-  }
-
+bool
+OpenVRSession::SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                           MacIOSurface* aTexture)
+{
+  return SubmitFrame((void *)aTexture,
+                     ::vr::ETextureType::TextureType_IOSurface,
+                     aLayer.mLeftEyeRect, aLayer.mRightEyeRect);
+}
 #endif
 
-  return false;
-}
-
 bool
 OpenVRSession::SubmitFrame(void* aTextureHandle,
                            ::vr::ETextureType aTextureType,
                            const VRLayerEyeRect& aLeftEyeRect,
                            const VRLayerEyeRect& aRightEyeRect)
 {
   ::vr::Texture_t tex;
   tex.handle = aTextureHandle;
@@ -933,21 +870,16 @@ OpenVRSession::SubmitFrame(void* aTextur
 
 void
 OpenVRSession::StopPresentation()
 {
   mVRCompositor->ClearLastSubmittedFrame();
 
   ::vr::Compositor_CumulativeStats stats;
   mVRCompositor->GetCumulativeStats(&stats, sizeof(::vr::Compositor_CumulativeStats));
-  // TODO - Need to send telemetry back to browser.
-  // Bug 1473398 will refactor this original gfxVROpenVR code:
-  //   const uint32_t droppedFramesPerSec = (stats.m_nNumReprojectedFrames -
-  //                                      mTelemetry.mLastDroppedFrameCount) / duration.ToSeconds();
-  // Telemetry::Accumulate(Telemetry::WEBVR_DROPPED_FRAMES_IN_OPENVR, droppedFramesPerSec);
 }
 
 bool
 OpenVRSession::StartPresentation()
 {
   return true;
 }
 
--- a/gfx/vr/service/OpenVRSession.h
+++ b/gfx/vr/service/OpenVRSession.h
@@ -32,34 +32,40 @@ class OpenVRSession : public VRSession
 public:
   OpenVRSession();
   virtual ~OpenVRSession();
 
   bool Initialize(mozilla::gfx::VRSystemState& aSystemState) override;
   void Shutdown() override;
   void ProcessEvents(mozilla::gfx::VRSystemState& aSystemState) override;
   void StartFrame(mozilla::gfx::VRSystemState& aSystemState) override;
-  bool ShouldQuit() const override;
   bool StartPresentation() override;
   void StopPresentation() override;
-  bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer) override;
   void VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
                     float aIntensity, float aDuration) override;
   void StopVibrateHaptic(uint32_t aControllerIdx) override;
   void StopAllHaptics() override;
 
+protected:
+#if defined(XP_WIN)
+  bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                   ID3D11Texture2D* aTexture) override;
+#elif defined(XP_MACOSX)
+  bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                   MacIOSurface* aTexture) override;
+#endif
+
 private:
   // OpenVR State
   ::vr::IVRSystem* mVRSystem = nullptr;
   ::vr::IVRChaperone* mVRChaperone = nullptr;
   ::vr::IVRCompositor* mVRCompositor = nullptr;
   ::vr::TrackedDeviceIndex_t mControllerDeviceIndex[kVRControllerMaxCount];
   float mHapticPulseRemaining[kVRControllerMaxCount][kNumOpenVRHaptics];
   float mHapticPulseIntensity[kVRControllerMaxCount][kNumOpenVRHaptics];
-  bool mShouldQuit;
   bool mIsWindowsMR;
   TimeStamp mLastHapticUpdate;
 
   bool InitState(mozilla::gfx::VRSystemState& aSystemState);
   void UpdateStageParameters(mozilla::gfx::VRDisplayState& aState);
   void UpdateEyeParameters(mozilla::gfx::VRSystemState& aState);
   void UpdateHeadsetPose(mozilla::gfx::VRSystemState& aState);
   void EnumerateControllers(VRSystemState& aState);
--- a/gfx/vr/service/VRService.cpp
+++ b/gfx/vr/service/VRService.cpp
@@ -1,48 +1,53 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "VRService.h"
 #include "gfxPrefs.h"
-#include "base/thread.h"                // for Thread
-#include <cstring>                      // for memcmp
+#include "base/thread.h" // for Thread
+#include <cstring>       // for memcmp
 
-#if defined(XP_WIN) || defined(XP_MACOSX) || (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
+#if defined(XP_WIN)
+#include "OculusSession.h"
+#endif
+
+#if defined(XP_WIN) || defined(XP_MACOSX) ||                                   \
+  (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
 #include "OpenVRSession.h"
 #endif
 #if !defined(MOZ_WIDGET_ANDROID)
 #include "OSVRSession.h"
 #endif
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 using namespace std;
 
 namespace {
 
 int64_t
 FrameIDFromBrowserState(const mozilla::gfx::VRBrowserState& aState)
 {
-  for (int iLayer=0; iLayer < kVRLayerMaxCount; iLayer++) {
+  for (int iLayer = 0; iLayer < kVRLayerMaxCount; iLayer++) {
     const VRLayerState& layer = aState.layerState[iLayer];
     if (layer.type == VRLayerType::LayerType_Stereo_Immersive) {
       return layer.layer_stereo_immersive.mFrameId;
     }
   }
   return 0;
 }
 
 bool
 IsImmersiveContentActive(const mozilla::gfx::VRBrowserState& aState)
 {
-  for (int iLayer=0; iLayer < kVRLayerMaxCount; iLayer++) {
+  for (int iLayer = 0; iLayer < kVRLayerMaxCount; iLayer++) {
     const VRLayerState& layer = aState.layerState[iLayer];
     if (layer.type == VRLayerType::LayerType_Stereo_Immersive) {
       return true;
     }
   }
   return false;
 }
 
@@ -57,47 +62,60 @@ VRService::Create()
     return nullptr;
   }
 
   RefPtr<VRService> service = new VRService();
   return service.forget();
 }
 
 VRService::VRService()
- : mSystemState{}
- , mBrowserState{}
- , mBrowserGeneration(0)
- , mServiceThread(nullptr)
- , mShutdownRequested(false)
- , mAPIShmem(nullptr)
- , mTargetShmemFile(0)
- , mLastHapticState{}
- , mFrameStartTime{}
+  : mSystemState{}
+  , mBrowserState{}
+  , mBrowserGeneration(0)
+  , mServiceThread(nullptr)
+  , mShutdownRequested(false)
+  , mAPIShmem(nullptr)
+  , mTargetShmemFile(0)
+  , mLastHapticState{}
+  , mFrameStartTime{}
+  , mVRProcessEnabled(gfxPrefs::VRProcessEnabled())
 {
   // When we have the VR process, we map the memory
   // of mAPIShmem from GPU process.
   // If we don't have the VR process, we will instantiate
   // mAPIShmem in VRService.
-  if (!gfxPrefs::VRProcessEnabled()) {
+  if (!mVRProcessEnabled) {
     mAPIShmem = new VRExternalShmem();
     memset(mAPIShmem, 0, sizeof(VRExternalShmem));
   }
 }
 
 VRService::~VRService()
 {
   Stop();
 
-  if (!gfxPrefs::VRProcessEnabled() && mAPIShmem) {
+  if (!mVRProcessEnabled && mAPIShmem) {
     delete mAPIShmem;
     mAPIShmem = nullptr;
   }
 }
 
 void
+VRService::Refresh()
+{
+  if (!mAPIShmem) {
+    return;
+  }
+
+  if (mAPIShmem->state.displayState.shutdown) {
+    Stop();
+  }
+}
+
+void
 VRService::Start()
 {
   if (!mServiceThread) {
     /**
      * We must ensure that any time the service is re-started, that
      * the VRSystemState is reset, including mSystemState.enumerationCompleted
      * This must happen before VRService::Start returns to the caller, in order
      * to prevent the WebVR/WebXR promises from being resolved before the
@@ -108,30 +126,30 @@ VRService::Start()
 
     mServiceThread = new base::Thread("VRService");
     base::Thread::Options options;
     /* Timeout values are powers-of-two to enable us get better data.
        128ms is chosen for transient hangs because 8Hz should be the minimally
        acceptable goal for Compositor responsiveness (normal goal is 60Hz). */
     options.transient_hang_timeout = 128; // milliseconds
     /* 2048ms is chosen for permanent hangs because it's longer than most
-     * Compositor hangs seen in the wild, but is short enough to not miss getting
-     * native hang stacks. */
+     * Compositor hangs seen in the wild, but is short enough to not miss
+     * getting native hang stacks. */
     options.permanent_hang_timeout = 2048; // milliseconds
 
     if (!mServiceThread->StartWithOptions(options)) {
       delete mServiceThread;
       mServiceThread = nullptr;
       return;
     }
 
-    mServiceThread->message_loop()->PostTask(NewRunnableMethod(
-      "gfx::VRService::ServiceInitialize",
-      this, &VRService::ServiceInitialize
-    ));
+    mServiceThread->message_loop()->PostTask(
+      NewRunnableMethod("gfx::VRService::ServiceInitialize",
+                        this,
+                        &VRService::ServiceInitialize));
   }
 }
 
 void
 VRService::Stop()
 {
   if (mServiceThread) {
     mShutdownRequested = true;
@@ -139,69 +157,70 @@ VRService::Stop()
     mServiceThread = nullptr;
   }
   if (mTargetShmemFile) {
 #if defined(XP_WIN)
     CloseHandle(mTargetShmemFile);
 #endif
     mTargetShmemFile = 0;
   }
-  if (gfxPrefs::VRProcessEnabled() && mAPIShmem) {
+  if (mVRProcessEnabled && mAPIShmem) {
 #if defined(XP_WIN)
-    UnmapViewOfFile((void *)mAPIShmem);
+    UnmapViewOfFile((void*)mAPIShmem);
 #endif
     mAPIShmem = nullptr;
   }
   mSession = nullptr;
 }
 
 bool
 VRService::InitShmem()
 {
-  if (!gfxPrefs::VRProcessEnabled()) {
+  if (!mVRProcessEnabled) {
     return true;
   }
 
 #if defined(XP_WIN)
   const char* kShmemName = "moz.gecko.vr_ext.0.0.1";
   base::ProcessHandle targetHandle = 0;
 
   // Opening a file-mapping object by name
-  targetHandle = OpenFileMappingA(
-                  FILE_MAP_ALL_ACCESS,   // read/write access
-                  FALSE,                 // do not inherit the name
-                  kShmemName);           // name of mapping object
+  targetHandle = OpenFileMappingA(FILE_MAP_ALL_ACCESS, // read/write access
+                                  FALSE,       // do not inherit the name
+                                  kShmemName); // name of mapping object
 
   MOZ_ASSERT(GetLastError() == 0);
 
   LARGE_INTEGER length;
   length.QuadPart = sizeof(VRExternalShmem);
-  mAPIShmem = (VRExternalShmem *)MapViewOfFile(reinterpret_cast<base::ProcessHandle>(targetHandle), // handle to map object
-                                               FILE_MAP_ALL_ACCESS,  // read/write permission
-                                               0,
-                                               0,
-                                               length.QuadPart);
+  mAPIShmem = (VRExternalShmem*)MapViewOfFile(
+    reinterpret_cast<base::ProcessHandle>(targetHandle), // handle to map object
+    FILE_MAP_ALL_ACCESS, // read/write permission
+    0,
+    0,
+    length.QuadPart);
   MOZ_ASSERT(GetLastError() == 0);
   // TODO - Implement logging
   mTargetShmemFile = targetHandle;
   if (!mAPIShmem) {
     MOZ_ASSERT(mAPIShmem);
     return false;
   }
 #else
-  // TODO: Implement shmem for other platforms.
+    // TODO: Implement shmem for other platforms.
 #endif
 
- return true;
+  return true;
 }
 
 bool
 VRService::IsInServiceThread()
 {
-  return mServiceThread && mServiceThread->thread_id() == PlatformThread::CurrentId();
+  return (mServiceThread != nullptr) &&
+         mServiceThread->thread_id() == PlatformThread::CurrentId();
 }
 
 void
 VRService::ServiceInitialize()
 {
   MOZ_ASSERT(IsInServiceThread());
 
   if (!InitShmem()) {
@@ -209,17 +228,29 @@ VRService::ServiceInitialize()
   }
 
   mShutdownRequested = false;
   memset(&mBrowserState, 0, sizeof(mBrowserState));
 
   // Try to start a VRSession
   UniquePtr<VRSession> session;
 
-#if defined(XP_WIN) || defined(XP_MACOSX) || (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
+  // We try Oculus first to ensure we use Oculus
+  // devices trough the most native interface
+  // when possible.
+#if defined(XP_WIN)
+  // Try Oculus
+  session = MakeUnique<OculusSession>();
+  if (!session->Initialize(mSystemState)) {
+    session = nullptr;
+  }
+#endif
+
+#if defined(XP_WIN) || defined(XP_MACOSX) ||                                   \
+  (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
   // Try OpenVR
   if (!session) {
     session = MakeUnique<OpenVRSession>();
     if (!session->Initialize(mSystemState)) {
       session = nullptr;
     }
   }
 #endif
@@ -236,79 +267,84 @@ VRService::ServiceInitialize()
   if (session) {
     mSession = std::move(session);
     // Setting enumerationCompleted to true indicates to the browser
     // that it should resolve any promises in the WebVR/WebXR API
     // waiting for hardware detection.
     mSystemState.enumerationCompleted = true;
     PushState(mSystemState);
 
-    MessageLoop::current()->PostTask(NewRunnableMethod(
-      "gfx::VRService::ServiceWaitForImmersive",
-      this, &VRService::ServiceWaitForImmersive
-    ));
+    MessageLoop::current()->PostTask(
+      NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive",
+                        this,
+                        &VRService::ServiceWaitForImmersive));
   } else {
     // VR hardware was not detected.
     // We must inform the browser of the failure so it may try again
     // later and resolve WebVR promises.  A failure or shutdown is
     // indicated by enumerationCompleted being set to true, with all
     // other fields remaining zeroed out.
     memset(&mSystemState, 0, sizeof(mSystemState));
     mSystemState.enumerationCompleted = true;
+    mSystemState.displayState.mMinRestartInterval =
+      gfxPrefs::VRExternalNotDetectedTimeout();
+    mSystemState.displayState.shutdown = true;
     PushState(mSystemState);
   }
 }
 
 void
 VRService::ServiceShutdown()
 {
   MOZ_ASSERT(IsInServiceThread());
 
-  mSession = nullptr;
-
   // Notify the browser that we have shut down.
   // This is indicated by enumerationCompleted being set
   // to true, with all other fields remaining zeroed out.
   memset(&mSystemState, 0, sizeof(mSystemState));
   mSystemState.enumerationCompleted = true;
+  mSystemState.displayState.shutdown = true;
+  if (mSession && mSession->ShouldQuit()) {
+    mSystemState.displayState.mMinRestartInterval =
+      gfxPrefs::VRExternalQuitTimeout();
+  }
   PushState(mSystemState);
+  mSession = nullptr;
 }
 
 void
 VRService::ServiceWaitForImmersive()
 {
   MOZ_ASSERT(IsInServiceThread());
   MOZ_ASSERT(mSession);
 
   mSession->ProcessEvents(mSystemState);
   PushState(mSystemState);
   PullState(mBrowserState);
 
   if (mSession->ShouldQuit() || mShutdownRequested) {
     // Shut down
     MessageLoop::current()->PostTask(NewRunnableMethod(
-      "gfx::VRService::ServiceShutdown",
-      this, &VRService::ServiceShutdown
-    ));
+      "gfx::VRService::ServiceShutdown", this, &VRService::ServiceShutdown));
   } else if (IsImmersiveContentActive(mBrowserState)) {
     // Enter Immersive Mode
     mSession->StartPresentation();
     mSession->StartFrame(mSystemState);
     PushState(mSystemState);
 
-    MessageLoop::current()->PostTask(NewRunnableMethod(
-      "gfx::VRService::ServiceImmersiveMode",
-      this, &VRService::ServiceImmersiveMode
-    ));
+    MessageLoop::current()->PostTask(
+      NewRunnableMethod("gfx::VRService::ServiceImmersiveMode",
+                        this,
+                        &VRService::ServiceImmersiveMode));
   } else {
     // Continue waiting for immersive mode
-    MessageLoop::current()->PostTask(NewRunnableMethod(
-      "gfx::VRService::ServiceWaitForImmersive",
-      this, &VRService::ServiceWaitForImmersive
-    ));
+    MessageLoop::current()->PostTask(
+      NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive",
+                        this,
+                        &VRService::ServiceWaitForImmersive));
   }
 }
 
 void
 VRService::ServiceImmersiveMode()
 {
   MOZ_ASSERT(IsInServiceThread());
   MOZ_ASSERT(mSession);
@@ -316,37 +352,35 @@ VRService::ServiceImmersiveMode()
   mSession->ProcessEvents(mSystemState);
   UpdateHaptics();
   PushState(mSystemState);
   PullState(mBrowserState);
 
   if (mSession->ShouldQuit() || mShutdownRequested) {
     // Shut down
     MessageLoop::current()->PostTask(NewRunnableMethod(
-      "gfx::VRService::ServiceShutdown",
-      this, &VRService::ServiceShutdown
-    ));
+      "gfx::VRService::ServiceShutdown", this, &VRService::ServiceShutdown));
     return;
   } else if (!IsImmersiveContentActive(mBrowserState)) {
     // Exit immersive mode
     mSession->StopAllHaptics();
     mSession->StopPresentation();
-    MessageLoop::current()->PostTask(NewRunnableMethod(
-      "gfx::VRService::ServiceWaitForImmersive",
-      this, &VRService::ServiceWaitForImmersive
-    ));
+    MessageLoop::current()->PostTask(
+      NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive",
+                        this,
+                        &VRService::ServiceWaitForImmersive));
     return;
   }
 
   uint64_t newFrameId = FrameIDFromBrowserState(mBrowserState);
   if (newFrameId != mSystemState.displayState.mLastSubmittedFrameId) {
     // A new immersive frame has been received.
     // Submit the textures to the VR system compositor.
     bool success = false;
-    for (int iLayer=0; iLayer < kVRLayerMaxCount; iLayer++) {
+    for (int iLayer = 0; iLayer < kVRLayerMaxCount; iLayer++) {
       const VRLayerState& layer = mBrowserState.layerState[iLayer];
       if (layer.type == VRLayerType::LayerType_Stereo_Immersive) {
         // SubmitFrame may block in order to control the timing for
         // the next frame start
         success = mSession->SubmitFrame(layer.layer_stereo_immersive);
         break;
       }
     }
@@ -356,34 +390,35 @@ VRService::ServiceImmersiveMode()
     // used for rendering, such as headset pose, must be pushed
     // atomically to the browser.
     mSystemState.displayState.mLastSubmittedFrameId = newFrameId;
     mSystemState.displayState.mLastSubmittedFrameSuccessful = success;
 
     // StartFrame may block to control the timing for the next frame start
     mSession->StartFrame(mSystemState);
     mSystemState.sensorState.inputFrameID++;
-    size_t historyIndex = mSystemState.sensorState.inputFrameID % ArrayLength(mFrameStartTime);
+    size_t historyIndex =
+      mSystemState.sensorState.inputFrameID % ArrayLength(mFrameStartTime);
     mFrameStartTime[historyIndex] = TimeStamp::Now();
     PushState(mSystemState);
   }
 
   // Continue immersive mode
-  MessageLoop::current()->PostTask(NewRunnableMethod(
-    "gfx::VRService::ServiceImmersiveMode",
-    this, &VRService::ServiceImmersiveMode
-  ));
+  MessageLoop::current()->PostTask(
+    NewRunnableMethod("gfx::VRService::ServiceImmersiveMode",
+                      this,
+                      &VRService::ServiceImmersiveMode));
 }
 
 void
 VRService::UpdateHaptics()
 {
   MOZ_ASSERT(IsInServiceThread());
   MOZ_ASSERT(mSession);
-  
+
   for (size_t i = 0; i < ArrayLength(mBrowserState.hapticState); i++) {
     VRHapticState& state = mBrowserState.hapticState[i];
     VRHapticState& lastState = mLastHapticState[i];
     // Note that VRHapticState is asserted to be a POD type, thus memcmp is safe
     if (memcmp(&state, &lastState, sizeof(VRHapticState)) == 0) {
       // No change since the last update
       continue;
     }
@@ -395,23 +430,25 @@ VRService::UpdateHaptics()
       if (now.IsNull()) {
         // TimeStamp::Now() is expensive, so we
         // must call it only when needed and save the
         // output for further loop iterations.
         now = TimeStamp::Now();
       }
       // This is a new haptic pulse, or we are overriding a prior one
       size_t historyIndex = state.inputFrameID % ArrayLength(mFrameStartTime);
-      float startOffset = (float)(now - mFrameStartTime[historyIndex]).ToSeconds();
+      float startOffset =
+        (float)(now - mFrameStartTime[historyIndex]).ToSeconds();
 
       // state.pulseStart is guaranteed never to be in the future
       mSession->VibrateHaptic(state.controllerIndex,
                               state.hapticIndex,
                               state.pulseIntensity,
-                              state.pulseDuration + state.pulseStart - startOffset);
+                              state.pulseDuration + state.pulseStart -
+                                startOffset);
     }
     // Record the state for comparison in the next run
     memcpy(&lastState, &state, sizeof(VRHapticState));
   }
 }
 
 void
 VRService::PushState(const mozilla::gfx::VRSystemState& aState)
@@ -420,23 +457,24 @@ VRService::PushState(const mozilla::gfx:
     return;
   }
   // Copying the VR service state to the shmem is atomic, infallable,
   // and non-blocking on x86/x64 architectures.  Arm requires a mutex
   // that is locked for the duration of the memcpy to and from shmem on
   // both sides.
 
 #if defined(MOZ_WIDGET_ANDROID)
-    if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->systemMutex)) == 0) {
-      memcpy((void *)&mAPIShmem->state, &aState, sizeof(VRSystemState));
-      pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->systemMutex));
-    }
+  if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->systemMutex)) ==
+      0) {
+    memcpy((void*)&mAPIShmem->state, &aState, sizeof(VRSystemState));
+    pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->systemMutex));
+  }
 #else
   mAPIShmem->generationA++;
-  memcpy((void *)&mAPIShmem->state, &aState, sizeof(VRSystemState));
+  memcpy((void*)&mAPIShmem->state, &aState, sizeof(VRSystemState));
   mAPIShmem->generationB++;
 #endif
 }
 
 void
 VRService::PullState(mozilla::gfx::VRBrowserState& aState)
 {
   if (!mAPIShmem) {
@@ -447,25 +485,27 @@ VRService::PullState(mozilla::gfx::VRBro
   // locked for the duration of the memcpy to and from shmem on
   // both sides.
   // On x86/x64 It is fallable -- If a dirty copy is detected by
   // a mismatch of browserGenerationA and browserGenerationB,
   // the copy is discarded and will not replace the last known
   // browser state.
 
 #if defined(MOZ_WIDGET_ANDROID)
-    if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->browserMutex)) == 0) {
-      memcpy(&aState, &tmp.browserState, sizeof(VRBrowserState));
-      pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->browserMutex));
-    }
+  if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->browserMutex)) ==
+      0) {
+    memcpy(&aState, &tmp.browserState, sizeof(VRBrowserState));
+    pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->browserMutex));
+  }
 #else
   VRExternalShmem tmp;
   if (mAPIShmem->browserGenerationA != mBrowserGeneration) {
     memcpy(&tmp, mAPIShmem, sizeof(VRExternalShmem));
-    if (tmp.browserGenerationA == tmp.browserGenerationB && tmp.browserGenerationA != 0 && tmp.browserGenerationA != -1) {
+    if (tmp.browserGenerationA == tmp.browserGenerationB &&
+        tmp.browserGenerationA != 0 && tmp.browserGenerationA != -1) {
       memcpy(&aState, &tmp.browserState, sizeof(VRBrowserState));
       mBrowserGeneration = tmp.browserGenerationA;
     }
   }
 #endif
 }
 
 VRExternalShmem*
--- a/gfx/vr/service/VRService.h
+++ b/gfx/vr/service/VRService.h
@@ -23,16 +23,17 @@ class VRSession;
 static const int kVRFrameTimingHistoryDepth = 100;
 
 class VRService
 {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VRService)
   static already_AddRefed<VRService> Create();
 
+  void Refresh();
   void Start();
   void Stop();
   VRExternalShmem* GetAPIShmem();
 
 private:
   VRService();
   ~VRService();
   
@@ -59,16 +60,21 @@ private:
   UniquePtr<VRSession> mSession;
   base::Thread* mServiceThread;
   bool mShutdownRequested;
 
   VRExternalShmem* MOZ_OWNING_REF mAPIShmem;
   base::ProcessHandle mTargetShmemFile;
   VRHapticState mLastHapticState[kVRHapticsMaxCount];
   TimeStamp mFrameStartTime[kVRFrameTimingHistoryDepth];
+  // We store the value of gfxPrefs::VRProcessEnabled() in mVRProcessEnabled.
+  // This allows us to read the value in the VRService destructor, after
+  // gfxPrefs has been shut down.  We should investigate why gfxPrefs
+  // is shutting down earlier - See bug xxx
+  bool mVRProcessEnabled;
 
   bool IsInServiceThread();
   void UpdateHaptics();
 
   /**
    * The VR Service thread is a state machine that always has one
    * task queued depending on the state.
    *
--- a/gfx/vr/service/VRSession.cpp
+++ b/gfx/vr/service/VRSession.cpp
@@ -4,43 +4,44 @@
 
 #if defined(XP_WIN)
 #include <d3d11.h>
 #endif // defined(XP_WIN)
 
 using namespace mozilla::gfx;
 
 VRSession::VRSession()
+  : mShouldQuit(false)
 {
 
 }
 
 VRSession::~VRSession()
 {
 
 }
 
 #if defined(XP_WIN)
 bool
 VRSession::CreateD3DContext(RefPtr<ID3D11Device> aDevice)
 {
   if (!mDevice) {
     if (!aDevice) {
-      NS_WARNING("OpenVRSession::CreateD3DObjects failed to get a D3D11Device");
+      NS_WARNING("VRSession::CreateD3DObjects failed to get a D3D11Device");
       return false;
     }
     if (FAILED(aDevice->QueryInterface(__uuidof(ID3D11Device1), getter_AddRefs(mDevice)))) {
-      NS_WARNING("OpenVRSession::CreateD3DObjects failed to get a D3D11Device1");
+      NS_WARNING("VRSession::CreateD3DObjects failed to get a D3D11Device1");
       return false;
     }
   }
   if (!mContext) {
     mDevice->GetImmediateContext1(getter_AddRefs(mContext));
     if (!mContext) {
-      NS_WARNING("OpenVRSession::CreateD3DObjects failed to get an immediate context");
+      NS_WARNING("VRSession::CreateD3DObjects failed to get an immediate context");
       return false;
     }
   }
   if (!mDeviceContextState) {
     D3D_FEATURE_LEVEL featureLevels[] {
       D3D_FEATURE_LEVEL_11_1,
       D3D_FEATURE_LEVEL_11_0
     };
@@ -48,17 +49,17 @@ VRSession::CreateD3DContext(RefPtr<ID3D1
                                       featureLevels,
                                       2,
                                       D3D11_SDK_VERSION,
                                       __uuidof(ID3D11Device1),
                                       nullptr,
                                       getter_AddRefs(mDeviceContextState));
   }
   if (!mDeviceContextState) {
-    NS_WARNING("VRDisplayHost::CreateD3DObjects failed to get a D3D11DeviceContextState");
+    NS_WARNING("VRSession::CreateD3DObjects failed to get a D3D11DeviceContextState");
     return false;
   }
   return true;
 }
 
 ID3D11Device1*
 VRSession::GetD3DDevice()
 {
@@ -73,8 +74,87 @@ VRSession::GetD3DDeviceContext()
 
 ID3DDeviceContextState*
 VRSession::GetD3DDeviceContextState()
 {
   return mDeviceContextState;
 }
 
 #endif // defined(XP_WIN)
+
+bool
+VRSession::SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer)
+{
+#if defined(XP_WIN)
+
+  if (aLayer.mTextureType == VRLayerTextureType::LayerTextureType_D3D10SurfaceDescriptor) {
+      RefPtr<ID3D11Texture2D> dxTexture;
+      HRESULT hr = mDevice->OpenSharedResource((HANDLE)aLayer.mTextureHandle,
+        __uuidof(ID3D11Texture2D),
+        (void**)(ID3D11Texture2D**)getter_AddRefs(dxTexture));
+      if (FAILED(hr) || !dxTexture) {
+        NS_WARNING("Failed to open shared texture");
+        return false;
+      }
+
+      // Similar to LockD3DTexture in TextureD3D11.cpp
+      RefPtr<IDXGIKeyedMutex> mutex;
+      dxTexture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
+      if (mutex) {
+        HRESULT hr = mutex->AcquireSync(0, 1000);
+        if (hr == WAIT_TIMEOUT) {
+          gfxDevCrash(LogReason::D3DLockTimeout) << "D3D lock mutex timeout";
+        }
+        else if (hr == WAIT_ABANDONED) {
+          gfxCriticalNote << "GFX: D3D11 lock mutex abandoned";
+        }
+        if (FAILED(hr)) {
+          NS_WARNING("Failed to lock the texture");
+          return false;
+        }
+      }
+      bool success = SubmitFrame(aLayer, dxTexture);
+      if (mutex) {
+        HRESULT hr = mutex->ReleaseSync(0);
+        if (FAILED(hr)) {
+          NS_WARNING("Failed to unlock the texture");
+        }
+      }
+      if (!success) {
+        return false;
+      }
+      return true;
+  }
+
+#elif defined(XP_MACOSX)
+
+  if (aLayer.mTextureType == VRLayerTextureType::LayerTextureType_MacIOSurface) {
+    return SubmitFrame(aLayer, (MacIOSurface*)aLayer.mTextureHandle);
+  }
+
+#endif
+
+  return false;
+}
+
+void
+VRSession::UpdateTrigger(VRControllerState& aState, uint32_t aButtonIndex, float aValue, float aThreshold)
+{
+  // For OpenVR, the threshold value of ButtonPressed and ButtonTouched is 0.55.
+  // We prefer to let developers to set their own threshold for the adjustment.
+  // Therefore, we don't check ButtonPressed and ButtonTouched with ButtonMask here.
+  // we just check the button value is larger than the threshold value or not.
+  uint64_t mask = (1ULL << aButtonIndex);
+  aState.triggerValue[aButtonIndex] = aValue;
+  if (aValue > aThreshold) {
+    aState.buttonPressed |= mask;
+    aState.buttonTouched |= mask;
+  } else {
+    aState.buttonPressed &= ~mask;
+    aState.buttonTouched &= ~mask;
+  }
+}
+
+bool
+VRSession::ShouldQuit() const
+{
+  return mShouldQuit;
+}
--- a/gfx/vr/service/VRSession.h
+++ b/gfx/vr/service/VRSession.h
@@ -25,33 +25,40 @@ class VRSession
 public:
   VRSession();
   virtual ~VRSession();
 
   virtual bool Initialize(mozilla::gfx::VRSystemState& aSystemState) = 0;
   virtual void Shutdown() = 0;
   virtual void ProcessEvents(mozilla::gfx::VRSystemState& aSystemState) = 0;
   virtual void StartFrame(mozilla::gfx::VRSystemState& aSystemState) = 0;
-  virtual bool ShouldQuit() const = 0;
   virtual bool StartPresentation() = 0;
   virtual void StopPresentation() = 0;
-  virtual bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer) = 0;
   virtual void VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
                              float aIntensity, float aDuration) = 0;
   virtual void StopVibrateHaptic(uint32_t aControllerIdx) = 0;
   virtual void StopAllHaptics() = 0;
+  bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer);
+  bool ShouldQuit() const;
 
+protected:
+  bool mShouldQuit;
 #if defined(XP_WIN)
-protected:
+  virtual bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                           ID3D11Texture2D* aTexture) = 0;
   bool CreateD3DContext(RefPtr<ID3D11Device> aDevice);
   RefPtr<ID3D11Device1> mDevice;
   RefPtr<ID3D11DeviceContext1> mContext;
   ID3D11Device1* GetD3DDevice();
   ID3D11DeviceContext1* GetD3DDeviceContext();
   ID3DDeviceContextState* GetD3DDeviceContextState();
   RefPtr<ID3DDeviceContextState> mDeviceContextState;
+#elif defined(XP_MACOSX)
+  virtual bool SubmitFrame(const mozilla::gfx::VRLayer_Stereo_Immersive& aLayer,
+                           MacIOSurface* aTexture) = 0;
 #endif
+  void UpdateTrigger(VRControllerState& aState, uint32_t aButtonIndex, float aValue, float aThreshold);
 };
 
 } // namespace mozilla
 } // namespace gfx
 
 #endif // GFX_VR_SERVICE_VRSESSION_H
--- a/gfx/vr/service/moz.build
+++ b/gfx/vr/service/moz.build
@@ -1,14 +1,20 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+# Build Oculus support on Windows only
+if CONFIG['OS_TARGET'] == 'WINNT':
+    SOURCES += [
+        'OculusSession.cpp',
+    ]
+
 # Build OSVR on all platforms except Android
 if CONFIG['OS_TARGET'] != 'Android':
     UNIFIED_SOURCES += [
         'OSVRSession.cpp',
         'VRService.cpp',
         'VRSession.cpp',
     ]
     include('/ipc/chromium/chromium-config.mozbuild')
@@ -20,8 +26,14 @@ if CONFIG['OS_TARGET'] in ('WINNT', 'Lin
     ]
     LOCAL_INCLUDES += [
         '/dom/base',
         '/gfx/layers/d3d11',
         '/gfx/thebes',
     ]
 
 FINAL_LIBRARY = 'xul'
+
+# This is intended as a temporary hack to enable VS2015 builds.
+if CONFIG['CC_TYPE'] in ('msvc', 'clang-cl'):
+    # ovr_capi_dynamic.h '<unnamed-tag>': Alignment specifier is less than
+    # actual alignment (8), and will be ignored
+    CXXFLAGS += ['-wd4359']
--- a/gfx/webrender/res/brush_blend.glsl
+++ b/gfx/webrender/res/brush_blend.glsl
@@ -123,16 +123,33 @@ vec3 Invert(vec3 Cs, float amount) {
 
 vec3 Brightness(vec3 Cs, float amount) {
     // Apply the brightness factor.
     // Resulting color needs to be clamped to output range
     // since we are pre-multiplying alpha in the shader.
     return clamp(Cs.rgb * amount, vec3(0.0), vec3(1.0));
 }
 
+// Based on the Gecko's implementation in
+// https://hg.mozilla.org/mozilla-central/file/91b4c3687d75/gfx/src/FilterSupport.cpp#l24
+// These could be made faster by sampling a lookup table stored in a float texture
+// with linear interpolation.
+
+vec3 SrgbToLinear(vec3 color) {
+    vec3 c1 = color / 12.92;
+    vec3 c2 = pow(color / 1.055 + vec3(0.055 / 1.055), vec3(2.4));
+    return mix(c1, c2, lessThanEqual(color, vec3(0.04045)));
+}
+
+vec3 LinearToSrgb(vec3 color) {
+    vec3 c1 = color * 12.92;
+    vec3 c2 = vec3(1.055) * pow(color, vec3(1.0 / 2.4)) - vec3(0.055);
+    return mix(c1, c2, lessThanEqual(color, vec3(0.0031308)));
+}
+
 Fragment brush_fs() {
     vec4 Cs = texture(sColor0, vUv);
 
     if (Cs.a == 0.0) {
         return Fragment(vec4(0.0)); // could also `discard`
     }
 
     // Un-premultiply the input.
@@ -149,16 +166,22 @@ Fragment brush_fs() {
             color = Invert(color, vAmount);
             break;
         case 7:
             color = Brightness(color, vAmount);
             break;
         case 8: // Opacity
             alpha *= vAmount;
             break;
+        case 11:
+            color = SrgbToLinear(color);
+            break;
+        case 12:
+            color = LinearToSrgb(color);
+            break;
         default:
             color = vColorMat * color + vColorOffset;
     }
 
     // Fail-safe to ensure that we don't sample outside the rendered
     // portion of a blend source.
     alpha *= point_inside_rect(vUv.xy, vUvClipBounds.xy, vUvClipBounds.zw);
 
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -840,29 +840,32 @@ impl AlphaBatchBuilder {
                                                     FilterOp::HueRotate(..) => 3,
                                                     FilterOp::Invert(..) => 4,
                                                     FilterOp::Saturate(..) => 5,
                                                     FilterOp::Sepia(..) => 6,
                                                     FilterOp::Brightness(..) => 7,
                                                     FilterOp::Opacity(..) => 8,
                                                     FilterOp::DropShadow(..) => 9,
                                                     FilterOp::ColorMatrix(..) => 10,
+                                                    FilterOp::SrgbToLinear => 11,
+                                                    FilterOp::LinearToSrgb => 12,
                                                 };
 
                                                 let user_data = match filter {
                                                     FilterOp::Identity => 0x10000i32, // matches `Contrast(1)`
                                                     FilterOp::Contrast(amount) |
                                                     FilterOp::Grayscale(amount) |
                                                     FilterOp::Invert(amount) |
                                                     FilterOp::Saturate(amount) |
                                                     FilterOp::Sepia(amount) |
                                                     FilterOp::Brightness(amount) |
                                                     FilterOp::Opacity(_, amount) => {
                                                         (amount * 65536.0) as i32
                                                     }
+                                                    FilterOp::SrgbToLinear | FilterOp::LinearToSrgb => 0,
                                                     FilterOp::HueRotate(angle) => {
                                                         (0.01745329251 * angle * 65536.0) as i32
                                                     }
                                                     // Go through different paths
                                                     FilterOp::Blur(..) |
                                                     FilterOp::DropShadow(..) => {
                                                         unreachable!();
                                                     }
--- a/gfx/webrender/src/debug_render.rs
+++ b/gfx/webrender/src/debug_render.rs
@@ -118,19 +118,19 @@ impl DebugRenderer {
             "",
             &DESC_COLOR,
         )?;
 
         let font_vao = device.create_vao(&DESC_FONT);
         let line_vao = device.create_vao(&DESC_COLOR);
         let tri_vao = device.create_vao(&DESC_COLOR);
 
-        let mut font_texture = device.create_texture(TextureTarget::Array, ImageFormat::R8);
-        device.init_texture(
-            &mut font_texture,
+        let font_texture = device.create_texture::<u8>(
+            TextureTarget::Array,
+            ImageFormat::R8,
             debug_font_data::BMP_WIDTH,
             debug_font_data::BMP_HEIGHT,
             TextureFilter::Linear,
             None,
             1,
             Some(&debug_font_data::FONT_BITMAP),
         );
 
--- a/gfx/webrender/src/device/gl.rs
+++ b/gfx/webrender/src/device/gl.rs
@@ -1177,34 +1177,92 @@ impl Device {
 
         if self.bound_program != program.id {
             self.gl.use_program(program.id);
             self.bound_program = program.id;
             self.program_mode_id = UniformLocation(program.u_mode);
         }
     }
 
-    pub fn create_texture(
+    pub fn create_texture<T: Texel>(
         &mut self,
         target: TextureTarget,
         format: ImageFormat,
+        mut width: u32,
+        mut height: u32,
+        filter: TextureFilter,
+        render_target: Option<RenderTargetInfo>,
+        layer_count: i32,
+        pixels: Option<&[T]>,
     ) -> Texture {
-        Texture {
+        debug_assert!(self.inside_frame);
+
+        if width > self.max_texture_size || height > self.max_texture_size {
+            error!("Attempting to allocate a texture of size {}x{} above the limit, trimming", width, height);
+            width = width.min(self.max_texture_size);
+            height = height.min(self.max_texture_size);
+        }
+
+        // Set up the texture book-keeping.
+        let mut texture = Texture {
             id: self.gl.gen_textures(1)[0],
             target: get_gl_target(target),
-            width: 0,
-            height: 0,
-            layer_count: 0,
+            width,
+            height,
+            layer_count,
             format,
-            filter: TextureFilter::Nearest,
-            render_target: None,
+            filter,
+            render_target,
             fbo_ids: vec![],
             depth_rb: None,
             last_frame_used: self.frame_id,
+        };
+        self.bind_texture(DEFAULT_TEXTURE, &texture);
+        self.set_texture_parameters(texture.target, filter);
+
+        // Allocate storage.
+        let desc = self.gl_describe_format(texture.format);
+        match texture.target {
+            gl::TEXTURE_2D_ARRAY => {
+                self.gl.tex_image_3d(
+                    gl::TEXTURE_2D_ARRAY,
+                    0,
+                    desc.internal,
+                    texture.width as _,
+                    texture.height as _,
+                    texture.layer_count,
+                    0,
+                    desc.external,
+                    desc.pixel_type,
+                    pixels.map(texels_to_u8_slice),
+                )
+            }
+            gl::TEXTURE_2D | gl::TEXTURE_RECTANGLE | gl::TEXTURE_EXTERNAL_OES => {
+                assert_eq!(texture.layer_count, 1);
+                self.gl.tex_image_2d(
+                    texture.target,
+                    0,
+                    desc.internal,
+                    texture.width as _,
+                    texture.height as _,
+                    0,
+                    desc.external,
+                    desc.pixel_type,
+                    pixels.map(texels_to_u8_slice),
+                )
+            },
+            _ => panic!("BUG: Unexpected texture target!"),
         }
+
+        // Set up FBOs, if required.
+        if let Some(rt_info) = render_target {
+            self.init_fbos(&mut texture, rt_info);
+        }
+
+        texture
     }
 
     fn set_texture_parameters(&mut self, target: gl::GLuint, filter: TextureFilter) {
         let mag_filter = match filter {
             TextureFilter::Nearest => gl::NEAREST,
             TextureFilter::Linear | TextureFilter::Trilinear => gl::LINEAR,
         };
 
@@ -1220,245 +1278,128 @@ impl Device {
             .tex_parameter_i(target, gl::TEXTURE_MIN_FILTER, min_filter as gl::GLint);
 
         self.gl
             .tex_parameter_i(target, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as gl::GLint);
         self.gl
             .tex_parameter_i(target, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as gl::GLint);
     }
 
-    /// Resizes a texture with enabled render target views,
-    /// preserves the data by blitting the old texture contents over.
-    pub fn resize_renderable_texture(
+    /// Copies the contents from one renderable texture to another.
+    pub fn blit_renderable_texture(
         &mut self,
-        texture: &mut Texture,
-        new_size: DeviceUintSize,
+        dst: &mut Texture,
+        src: &Texture,
     ) {
         debug_assert!(self.inside_frame);
-
-        let old_size = texture.get_dimensions();
-        let old_fbos = mem::replace(&mut texture.fbo_ids, Vec::new());
-        let old_texture_id = mem::replace(&mut texture.id, self.gl.gen_textures(1)[0]);
-
-        texture.width = new_size.width;
-        texture.height = new_size.height;
-        let rt_info = texture.render_target
-            .clone()
-            .expect("Only renderable textures are expected for resize here");
+        debug_assert!(dst.width >= src.width);
+        debug_assert!(dst.height >= src.height);
 
-        self.bind_texture(DEFAULT_TEXTURE, texture);
-        self.set_texture_parameters(texture.target, texture.filter);
-        self.update_target_storage::<u8>(texture, &rt_info, true, None);
-
-        let rect = DeviceIntRect::new(DeviceIntPoint::zero(), old_size.to_i32());
-        for (read_fbo, &draw_fbo) in old_fbos.into_iter().zip(&texture.fbo_ids) {
-            self.bind_read_target_impl(read_fbo);
-            self.bind_draw_target_impl(draw_fbo);
+        let rect = DeviceIntRect::new(DeviceIntPoint::zero(), src.get_dimensions().to_i32());
+        for (read_fbo, draw_fbo) in src.fbo_ids.iter().zip(&dst.fbo_ids) {
+            self.bind_read_target_impl(*read_fbo);
+            self.bind_draw_target_impl(*draw_fbo);
             self.blit_render_target(rect, rect);
-            self.delete_fbo(read_fbo);
         }
-        self.gl.delete_textures(&[old_texture_id]);
         self.bind_read_target(None);
     }
 
-    pub fn init_texture<T: Texel>(
+    /// Notifies the device that a render target is about to be reused.
+    ///
+    /// This method adds or removes a depth target as necessary.
+    pub fn reuse_render_target<T: Texel>(
         &mut self,
         texture: &mut Texture,
-        mut width: u32,
-        mut height: u32,
-        filter: TextureFilter,
-        render_target: Option<RenderTargetInfo>,
-        layer_count: i32,
-        pixels: Option<&[T]>,
+        rt_info: RenderTargetInfo,
     ) {
-        debug_assert!(self.inside_frame);
-
-        if width > self.max_texture_size || height > self.max_texture_size {
-            error!("Attempting to allocate a texture of size {}x{} above the limit, trimming", width, height);
-            width = width.min(self.max_texture_size);
-            height = height.min(self.max_texture_size);
-        }
-
-        let is_resized = texture.width != width || texture.height != height;
+        texture.last_frame_used = self.frame_id;
+        texture.render_target = Some(rt_info);
 
-        texture.width = width;
-        texture.height = height;
-        texture.filter = filter;
-        texture.layer_count = layer_count;
-        texture.render_target = render_target;
-        texture.last_frame_used = self.frame_id;
-
-        self.bind_texture(DEFAULT_TEXTURE, texture);
-        self.set_texture_parameters(texture.target, filter);
-
-        match render_target {
-            Some(info) => {
-                self.update_target_storage(texture, &info, is_resized, pixels);
-            }
-            None => {
-                self.update_texture_storage(texture, pixels);
-            }
+        // If the depth target requirements changed, just drop the FBOs and
+        // reinitialize.
+        //
+        // FIXME(bholley): I have a patch to do this better.
+        if rt_info.has_depth != texture.has_depth() {
+            self.deinit_fbos(texture);
+            self.init_fbos(texture, rt_info);
         }
     }
 
-    /// Updates the render target storage for the texture, creating FBOs as required.
-    fn update_target_storage<T: Texel>(
-        &mut self,
-        texture: &mut Texture,
-        rt_info: &RenderTargetInfo,
-        is_resized: bool,
-        pixels: Option<&[T]>,
-    ) {
-        assert!(texture.layer_count > 0 || texture.width + texture.height == 0);
+    fn init_fbos(&mut self, texture: &mut Texture, rt_info: RenderTargetInfo) {
+        // Generate the FBOs.
+        assert!(texture.fbo_ids.is_empty());
+        texture.fbo_ids.extend(
+            self.gl.gen_framebuffers(texture.layer_count).into_iter().map(FBOId)
+        );
 
-        let needed_layer_count = texture.layer_count - texture.fbo_ids.len() as i32;
-        let allocate_color = needed_layer_count != 0 || is_resized || pixels.is_some();
+        // Optionally generate a depth target.
+        if rt_info.has_depth {
+            let renderbuffer_ids = self.gl.gen_renderbuffers(1);
+            let depth_rb = renderbuffer_ids[0];
+            texture.depth_rb = Some(RBOId(depth_rb));
+            self.gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
+            self.gl.renderbuffer_storage(
+                gl::RENDERBUFFER,
+                gl::DEPTH_COMPONENT24,
+                texture.width as _,
+                texture.height as _,
+            );
+        }
 
-        if allocate_color {
-            let desc = self.gl_describe_format(texture.format);
+        // Bind the FBOs.
+        let original_bound_fbo = self.bound_draw_fbo;
+        for (fbo_index, &fbo_id) in texture.fbo_ids.iter().enumerate() {
+            self.bind_external_draw_target(fbo_id);
             match texture.target {
                 gl::TEXTURE_2D_ARRAY => {
-                    self.gl.tex_image_3d(
-                        texture.target,
+                    self.gl.framebuffer_texture_layer(
+                        gl::DRAW_FRAMEBUFFER,
+                        gl::COLOR_ATTACHMENT0,
+                        texture.id,
                         0,
-                        desc.internal,
-                        texture.width as _,
-                        texture.height as _,
-                        texture.layer_count,
-                        0,
-                        desc.external,
-                        desc.pixel_type,
-                        pixels.map(texels_to_u8_slice),
+                        fbo_index as _,
                     )
                 }
                 _ => {
-                    assert_eq!(texture.layer_count, 1);
-                    self.gl.tex_image_2d(
+                    assert_eq!(fbo_index, 0);
+                    self.gl.framebuffer_texture_2d(
+                        gl::DRAW_FRAMEBUFFER,
+                        gl::COLOR_ATTACHMENT0,
                         texture.target,
+                        texture.id,
                         0,
-                        desc.internal,
-                        texture.width as _,
-                        texture.height as _,
-                        0,
-                        desc.external,
-                        desc.pixel_type,
-                        pixels.map(texels_to_u8_slice),
                     )
                 }
             }
-        }
 
-        if needed_layer_count > 0 {
-            // Create more framebuffers to fill the gap
-            let new_fbos = self.gl.gen_framebuffers(needed_layer_count);
-            texture
-                .fbo_ids
-                .extend(new_fbos.into_iter().map(FBOId));
-        } else if needed_layer_count < 0 {
-            // Remove extra framebuffers
-            for old in texture.fbo_ids.drain(texture.layer_count as usize ..) {
-                self.gl.delete_framebuffers(&[old.0]);
-            }
-        }
-
-        let (mut depth_rb, allocate_depth) = match texture.depth_rb {
-            Some(rbo) => (rbo.0, is_resized || !rt_info.has_depth),
-            None if rt_info.has_depth => {
-                let renderbuffer_ids = self.gl.gen_renderbuffers(1);
-                let depth_rb = renderbuffer_ids[0];
-                texture.depth_rb = Some(RBOId(depth_rb));
-                (depth_rb, true)
-            },
-            None => (0, false),
-        };
-
-        if allocate_depth {
-            if rt_info.has_depth {
-                self.gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
-                self.gl.renderbuffer_storage(
-                    gl::RENDERBUFFER,
-                    gl::DEPTH_COMPONENT24,
-                    texture.width as _,
-                    texture.height as _,
-                );
-            } else {
-                self.gl.delete_renderbuffers(&[depth_rb]);
-                depth_rb = 0;
-                texture.depth_rb = None;
-            }
-        }
-
-        if allocate_color || allocate_depth {
-            let original_bound_fbo = self.bound_draw_fbo;
-            for (fbo_index, &fbo_id) in texture.fbo_ids.iter().enumerate() {
-                self.bind_external_draw_target(fbo_id);
-                match texture.target {
-                    gl::TEXTURE_2D_ARRAY => {
-                        self.gl.framebuffer_texture_layer(
-                            gl::DRAW_FRAMEBUFFER,
-                            gl::COLOR_ATTACHMENT0,
-                            texture.id,
-                            0,
-                            fbo_index as _,
-                        )
-                    }
-                    _ => {
-                        assert_eq!(fbo_index, 0);
-                        self.gl.framebuffer_texture_2d(
-                            gl::DRAW_FRAMEBUFFER,
-                            gl::COLOR_ATTACHMENT0,
-                            texture.target,
-                            texture.id,
-                            0,
-                        )
-                    }
-                }
-
+            if let Some(depth_rb) = texture.depth_rb {
                 self.gl.framebuffer_renderbuffer(
                     gl::DRAW_FRAMEBUFFER,
                     gl::DEPTH_ATTACHMENT,
                     gl::RENDERBUFFER,
-                    depth_rb,
+                    depth_rb.0,
                 );
             }
-            self.bind_external_draw_target(original_bound_fbo);
         }
+        self.bind_external_draw_target(original_bound_fbo);
     }
 
-    fn update_texture_storage<T: Texel>(&mut self, texture: &Texture, pixels: Option<&[T]>) {
-        let desc = self.gl_describe_format(texture.format);
-        match texture.target {
-            gl::TEXTURE_2D_ARRAY => {
-                self.gl.tex_image_3d(
-                    gl::TEXTURE_2D_ARRAY,
-                    0,
-                    desc.internal,
-                    texture.width as _,
-                    texture.height as _,
-                    texture.layer_count,
-                    0,
-                    desc.external,
-                    desc.pixel_type,
-                    pixels.map(texels_to_u8_slice),
-                );
-            }
-            gl::TEXTURE_2D | gl::TEXTURE_RECTANGLE | gl::TEXTURE_EXTERNAL_OES => {
-                self.gl.tex_image_2d(
-                    texture.target,
-                    0,
-                    desc.internal,
-                    texture.width as _,
-                    texture.height as _,
-                    0,
-                    desc.external,
-                    desc.pixel_type,
-                    pixels.map(texels_to_u8_slice),
-                );
-            }
-            _ => panic!("BUG: Unexpected texture target!"),
+    fn deinit_fbos(&mut self, texture: &mut Texture) {
+        if let Some(RBOId(depth_rb)) = texture.depth_rb.take() {
+            self.gl.delete_renderbuffers(&[depth_rb]);
+            texture.depth_rb = None;
+        }
+
+        if !texture.fbo_ids.is_empty() {
+            let fbo_ids: Vec<_> = texture
+                .fbo_ids
+                .drain(..)
+                .map(|FBOId(fbo_id)| fbo_id)
+                .collect();
+            self.gl.delete_framebuffers(&fbo_ids[..]);
         }
     }
 
     pub fn blit_render_target(&mut self, src_rect: DeviceIntRect, dest_rect: DeviceIntRect) {
         debug_assert!(self.inside_frame);
 
         self.gl.blit_framebuffer(
             src_rect.origin.x,
@@ -1469,100 +1410,33 @@ impl Device {
             dest_rect.origin.y,
             dest_rect.origin.x + dest_rect.size.width,
             dest_rect.origin.y + dest_rect.size.height,
             gl::COLOR_BUFFER_BIT,
             gl::LINEAR,
         );
     }
 
-    fn free_texture_storage_impl(&mut self, target: gl::GLenum, desc: FormatDesc) {
-        match target {
-            gl::TEXTURE_2D_ARRAY => {
-                self.gl.tex_image_3d(
-                    gl::TEXTURE_2D_ARRAY,
-                    0,
-                    desc.internal,
-                    0,
-                    0,
-                    0,
-                    0,
-                    desc.external,
-                    desc.pixel_type,
-                    None,
-                );
-            }
-            _ => {
-                self.gl.tex_image_2d(
-                    target,
-                    0,
-                    desc.internal,
-                    0,
-                    0,
-                    0,
-                    desc.external,
-                    desc.pixel_type,
-                    None,
-                );
-            }
-        }
-    }
-
-    pub fn free_texture_storage(&mut self, texture: &mut Texture) {
+    pub fn delete_texture(&mut self, mut texture: Texture) {
         debug_assert!(self.inside_frame);
-
-        if texture.width + texture.height == 0 {
-            return;
-        }
-
-        self.bind_texture(DEFAULT_TEXTURE, texture);
-        let desc = self.gl_describe_format(texture.format);
-
-        self.free_texture_storage_impl(texture.target, desc);
-
-        if let Some(RBOId(depth_rb)) = texture.depth_rb.take() {
-            self.gl.delete_renderbuffers(&[depth_rb]);
-        }
-
-        if !texture.fbo_ids.is_empty() {
-            let fbo_ids: Vec<_> = texture
-                .fbo_ids
-                .drain(..)
-                .map(|FBOId(fbo_id)| fbo_id)
-                .collect();
-            self.gl.delete_framebuffers(&fbo_ids[..]);
-        }
-
-        texture.width = 0;
-        texture.height = 0;
-        texture.layer_count = 0;
-    }
-
-    pub fn delete_texture(&mut self, mut texture: Texture) {
-        self.free_texture_storage(&mut texture);
+        self.deinit_fbos(&mut texture);
         self.gl.delete_textures(&[texture.id]);
 
         for bound_texture in &mut self.bound_textures {
             if *bound_texture == texture.id {
                 *bound_texture = 0
             }
         }
 
+        // Disarm the assert in Texture::drop().
         texture.id = 0;
     }
 
     #[cfg(feature = "replay")]
     pub fn delete_external_texture(&mut self, mut external: ExternalTexture) {
-        self.bind_external_texture(DEFAULT_TEXTURE, &external);
-        //Note: the format descriptor here doesn't really matter
-        self.free_texture_storage_impl(external.target, FormatDesc {
-            internal: gl::R8 as _,
-            external: gl::RED,
-            pixel_type: gl::UNSIGNED_BYTE,
-        });
         self.gl.delete_textures(&[external.id]);
         external.id = 0;
     }
 
     pub fn delete_program(&mut self, mut program: Program) {
         self.gl.delete_program(program.id);
         program.id = 0;
     }
--- a/gfx/webrender/src/gpu_glyph_renderer.rs
+++ b/gfx/webrender/src/gpu_glyph_renderer.rs
@@ -49,24 +49,26 @@ impl GpuGlyphRenderer {
         debug_assert!(AREA_LUT_TGA_BYTES[16] == 8);
         let area_lut_width = (AREA_LUT_TGA_BYTES[12] as u32) |
             ((AREA_LUT_TGA_BYTES[13] as u32) << 8);
         let area_lut_height = (AREA_LUT_TGA_BYTES[14] as u32) |
             ((AREA_LUT_TGA_BYTES[15] as u32) << 8);
         let area_lut_pixels =
             &AREA_LUT_TGA_BYTES[18..(18 + area_lut_width * area_lut_height) as usize];
 
-        let mut area_lut_texture = device.create_texture(TextureTarget::Default, ImageFormat::R8);
-        device.init_texture(&mut area_lut_texture,
-                            area_lut_width,
-                            area_lut_height,
-                            TextureFilter::Linear,
-                            None,
-                            1,
-                            Some(area_lut_pixels));
+        let area_lut_texture = device.create_texture(
+            TextureTarget::Default,
+            ImageFormat::R8,
+            area_lut_width,
+            area_lut_height,
+            TextureFilter::Linear,
+            None,
+            1,
+            Some(area_lut_pixels)
+        );
 
         let vector_stencil_vao =
             device.create_vao_with_new_instances(&renderer::desc::VECTOR_STENCIL, prim_vao);
         let vector_cover_vao = device.create_vao_with_new_instances(&renderer::desc::VECTOR_COVER,
                                                                     prim_vao);
 
         // Load Pathfinder vector graphics shaders.
         let vector_stencil = try!{
@@ -103,32 +105,36 @@ impl Renderer {
                           stats: &mut RendererStats)
                           -> Option<StenciledGlyphPage> {
         if glyphs.is_empty() {
             return None
         }
 
         let _timer = self.gpu_profile.start_timer(GPU_TAG_GLYPH_STENCIL);
 
+        let texture = self.device.create_texture::<f32>(
+            TextureTarget::Default,
+            ImageFormat::RGBAF32,
+            target_size.width,
+            target_size.height,
+            TextureFilter::Nearest,
+            Some(RenderTargetInfo {
+                has_depth: false,
+            }),
+            1,
+            None
+        );
+
         // Initialize temporary framebuffer.
         // FIXME(pcwalton): Cache this!
         // FIXME(pcwalton): Use RF32, not RGBAF32!
         let mut current_page = StenciledGlyphPage {
-            texture: self.device.create_texture(TextureTarget::Default, ImageFormat::RGBAF32),
+            texture,
             glyphs: vec![],
         };
-        self.device.init_texture::<f32>(&mut current_page.texture,
-                                        target_size.width,
-                                        target_size.height,
-                                        TextureFilter::Nearest,
-                                        Some(RenderTargetInfo {
-                                            has_depth: false,
-                                        }),
-                                        1,
-                                        None);
 
         // Allocate all target rects.
         let mut packer = ShelfBinPacker::new(&target_size.to_i32().to_untyped(),
                                              &Vector2D::new(HORIZONTAL_BIN_PADDING, 0));
         let mut glyph_indices: Vec<_> = (0..(glyphs.len())).collect();
         glyph_indices.sort_by(|&a, &b| {
             glyphs[b].target_rect.size.height.cmp(&glyphs[a].target_rect.size.height)
         });
@@ -145,19 +151,16 @@ impl Renderer {
                         stencil_origin: DeviceIntPoint::from_untyped(&origin),
                         subpixel: (glyph.render_mode == FontRenderMode::Subpixel) as u16,
                     })
                 }
             }
         }
 
         // Initialize path info.
-        // TODO(pcwalton): Cache this texture!
-        let mut path_info_texture = self.device.create_texture(TextureTarget::Default,
-                                                               ImageFormat::RGBAF32);
 
         let mut path_info_texels = Vec::with_capacity(glyphs.len() * 12);
         for (stenciled_glyph_index, &glyph_index) in glyph_indices.iter().enumerate() {
             let glyph = &glyphs[glyph_index];
             let stenciled_glyph = &current_page.glyphs[stenciled_glyph_index];
             let x_scale = x_scale_for_render_mode(glyph.render_mode) as f32;
             let glyph_origin = TypedVector2D::new(-glyph.origin.x as f32 * x_scale,
                                                   -glyph.origin.y as f32);
@@ -171,23 +174,27 @@ impl Renderer {
                 x_scale, 0.0, 0.0, -1.0,
                 rect.origin.x, rect.max_y(), 0.0, 0.0,
                 rect.size.width, rect.size.height,
                 glyph.embolden_amount.x,
                 glyph.embolden_amount.y,
             ]);
         }
 
-        self.device.init_texture(&mut path_info_texture,
-                                 3,
-                                 glyphs.len() as u32,
-                                 TextureFilter::Nearest,
-                                 None,
-                                 1,
-                                 Some(&path_info_texels));
+        // TODO(pcwalton): Cache this texture!
+        let path_info_texture = self.device.create_texture(
+            TextureTarget::Default,
+            ImageFormat::RGBAF32,
+            3,
+            glyphs.len() as u32,
+            TextureFilter::Nearest,
+            None,
+            1,
+            Some(&path_info_texels)
+        );
 
         self.gpu_glyph_renderer.vector_stencil.bind(&mut self.device,
                                                     projection,
                                                     &mut self.renderer_errors);
 
         self.device.bind_draw_target(Some((&current_page.texture, 0)), Some(*target_size));
         self.device.clear_target(Some([0.0, 0.0, 0.0, 0.0]), None, None);
 
--- a/gfx/webrender/src/internal_types.rs
+++ b/gfx/webrender/src/internal_types.rs
@@ -26,20 +26,22 @@ pub type FastHashMap<K, V> = HashMap<K, 
 pub type FastHashSet<K> = HashSet<K, BuildHasherDefault<FxHasher>>;
 
 /// An ID for a texture that is owned by the `texture_cache` module.
 ///
 /// This can include atlases or standalone textures allocated via the texture
 /// cache (e.g.  if an image is too large to be added to an atlas). The texture
 /// cache manages the allocation and freeing of these IDs, and the rendering
 /// thread maintains a map from cache texture ID to native texture.
+///
+/// We never reuse IDs, so we use a u64 here to be safe.
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct CacheTextureId(pub usize);
+pub struct CacheTextureId(pub u64);
 
 /// Identifies a render pass target that is persisted until the end of the frame.
 ///
 /// By default, only the targets of the immediately-preceding pass are bound as
 /// inputs to the next pass. However, tasks can opt into having their target
 /// preserved in a list until the end of the frame, and this type specifies the
 /// index in that list.
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -725,23 +725,18 @@ struct ActiveTexture {
 }
 
 /// Helper struct for resolving device Textures for use during rendering passes.
 ///
 /// Manages the mapping between the at-a-distance texture handles used by the
 /// `RenderBackend` (which does not directly interface with the GPU) and actual
 /// device texture handles.
 struct TextureResolver {
-    /// A vector for fast resolves of texture cache IDs to native texture IDs.
-    /// This maps to a free-list managed by the backend thread / texture cache.
-    /// We free the texture memory associated with a TextureId when its texture
-    /// cache ID is freed by the texture cache, but reuse the TextureId when the
-    /// texture caches's free list reuses the texture cache ID. This saves
-    /// having to use a hashmap, and allows a flat vector for performance.
-    texture_cache_map: Vec<Texture>,
+    /// A map to resolve texture cache IDs to native textures.
+    texture_cache_map: FastHashMap<CacheTextureId, Texture>,
 
     /// Map of external image IDs to native textures.
     external_images: FastHashMap<(ExternalImageId, u8), ExternalTexture>,
 
     /// A special 1x1 dummy texture used for shaders that expect to work with
     /// the output of the previous pass but are actually running in the first
     /// pass.
     dummy_cache_texture: Texture,
@@ -770,43 +765,43 @@ struct TextureResolver {
     ///
     /// See the comments in `allocate_target_texture` for more insight on why
     /// reuse is a win.
     render_target_pool: Vec<Texture>,
 }
 
 impl TextureResolver {
     fn new(device: &mut Device) -> TextureResolver {
-        let mut dummy_cache_texture = device
-            .create_texture(TextureTarget::Array, ImageFormat::BGRA8);
-        device.init_texture::<u8>(
-            &mut dummy_cache_texture,
-            1,
-            1,
-            TextureFilter::Linear,
-            None,
-            1,
-            None,
-        );
+        let dummy_cache_texture = device
+            .create_texture::<u8>(
+                TextureTarget::Array,
+                ImageFormat::BGRA8,
+                1,
+                1,
+                TextureFilter::Linear,
+                None,
+                1,
+                None,
+            );
 
         TextureResolver {
-            texture_cache_map: Vec::new(),
+            texture_cache_map: FastHashMap::default(),
             external_images: FastHashMap::default(),
             dummy_cache_texture,
             prev_pass_alpha: None,
             prev_pass_color: None,
             saved_targets: Vec::default(),
             render_target_pool: Vec::new(),
         }
     }
 
     fn deinit(self, device: &mut Device) {
         device.delete_texture(self.dummy_cache_texture);
 
-        for texture in self.texture_cache_map {
+        for (_id, texture) in self.texture_cache_map {
             device.delete_texture(texture);
         }
 
         for texture in self.render_target_pool {
             device.delete_texture(texture);
         }
     }
 
@@ -900,17 +895,17 @@ impl TextureResolver {
             }
             TextureSource::External(external_image) => {
                 let texture = self.external_images
                     .get(&(external_image.id, external_image.channel_index))
                     .expect(&format!("BUG: External image should be resolved by now"));
                 device.bind_external_texture(sampler, texture);
             }
             TextureSource::TextureCache(index) => {
-                let texture = &self.texture_cache_map[index.0];
+                let texture = &self.texture_cache_map[&index];
                 device.bind_texture(sampler, texture);
             }
             TextureSource::RenderTaskCache(saved_index) => {
                 let texture = &self.saved_targets[saved_index.0];
                 device.bind_texture(sampler, texture)
             }
         }
     }
@@ -932,30 +927,30 @@ impl TextureResolver {
                     Some(ref at) => &at.texture,
                     None => &self.dummy_cache_texture,
                 }
             ),
             TextureSource::External(..) => {
                 panic!("BUG: External textures cannot be resolved, they can only be bound.");
             }
             TextureSource::TextureCache(index) => {
-                Some(&self.texture_cache_map[index.0])
+                Some(&self.texture_cache_map[&index])
             }
             TextureSource::RenderTaskCache(saved_index) => {
                 Some(&self.saved_targets[saved_index.0])
             }
         }
     }
 
     fn report_memory(&self) -> MemoryReport {
         let mut report = MemoryReport::default();
 
         // We're reporting GPU memory rather than heap-allocations, so we don't
         // use size_of_op.
-        for t in self.texture_cache_map.iter() {
+        for t in self.texture_cache_map.values() {
             report.texture_cache_textures += t.size_in_bytes();
         }
         for t in self.render_target_pool.iter() {
             report.render_target_textures += t.size_in_bytes();
         }
 
         report
     }
@@ -1009,26 +1004,90 @@ enum GpuCacheBus {
         buf_position: VBO<[u16; 2]>,
         /// VBO for gpu block data.
         buf_value: VBO<GpuBlockData>,
         /// Currently stored block count.
         count: usize,
     },
 }
 
+impl GpuCacheBus {
+    /// Returns true if this bus uses a render target for a texture.
+    fn uses_render_target(&self) -> bool {
+        match *self {
+            GpuCacheBus::Scatter { .. } => true,
+            GpuCacheBus::PixelBuffer { .. } => false,
+        }
+    }
+}
+
 /// The device-specific representation of the cache texture in gpu_cache.rs
 struct GpuCacheTexture {
-    texture: Texture,
+    texture: Option<Texture>,
     bus: GpuCacheBus,
 }
 
 impl GpuCacheTexture {
+
+    /// Ensures that we have an appropriately-sized texture. Returns true if a
+    /// new texture was created.
+    fn ensure_texture(&mut self, device: &mut Device, height: u32) -> bool {
+        // If we already have a texture that works, we're done.
+        if self.texture.as_ref().map_or(false, |t| t.get_dimensions().height >= height) {
+            if GPU_CACHE_RESIZE_TEST && self.bus.uses_render_target() {
+                // Special debug mode - resize the texture even though it's fine.
+            } else {
+                return false;
+            }
+        }
+
+        // Compute a few parameters for the new texture. We round the height up to
+        // a multiple of 256 to avoid many small resizes.
+        let new_height = (height + 255) & !255;
+        let new_size = DeviceUintSize::new(MAX_VERTEX_TEXTURE_WIDTH as _, new_height);
+        let rt_info = if self.bus.uses_render_target() {
+            Some(RenderTargetInfo { has_depth: false })
+        } else {
+            None
+        };
+
+        // Take the old texture, if any, and deinitialize it unless we're going
+        // to blit it's contents to the new one.
+        let mut blit_source = None;
+        if let Some(t) = self.texture.take() {
+            if rt_info.is_some() {
+                blit_source = Some(t);
+            } else {
+                device.delete_texture(t);
+            }
+        }
+
+        // Create the new texture.
+        let mut texture = device.create_texture::<u8>(
+            TextureTarget::Default,
+            ImageFormat::RGBAF32,
+            new_size.width,
+            new_size.height,
+            TextureFilter::Nearest,
+            rt_info,
+            1,
+            None,
+        );
+
+        // Blit the contents of the previous texture, if applicable.
+        if let Some(blit_source) = blit_source {
+            device.blit_renderable_texture(&mut texture, &blit_source);
+            device.delete_texture(blit_source);
+        }
+
+        self.texture = Some(texture);
+        true
+    }
+
     fn new(device: &mut Device, use_scatter: bool) -> Result<Self, RendererError> {
-        let texture = device.create_texture(TextureTarget::Default, ImageFormat::RGBAF32);
-
         let bus = if use_scatter {
             let program = device.create_program_linked(
                 "gpu_cache_update",
                 "",
                 &desc::GPU_CACHE_UPDATE,
             )?;
             let buf_position = device.create_vbo();
             let buf_value = device.create_vbo();
@@ -1050,65 +1109,52 @@ impl GpuCacheTexture {
             GpuCacheBus::PixelBuffer {
                 buffer,
                 rows: Vec::new(),
                 cpu_blocks: Vec::new(),
             }
         };
 
         Ok(GpuCacheTexture {
-            texture,
+            texture: None,
             bus,
         })
     }
 
-    fn deinit(self, device: &mut Device) {
-        device.delete_texture(self.texture);
+    fn deinit(mut self, device: &mut Device) {
+        if let Some(t) = self.texture.take() {
+            device.delete_texture(t);
+        }
         match self.bus {
             GpuCacheBus::PixelBuffer { buffer, ..} => {
                 device.delete_pbo(buffer);
             }
             GpuCacheBus::Scatter { program, vao, buf_position, buf_value, ..} => {
                 device.delete_program(program);
                 device.delete_custom_vao(vao);
                 device.delete_vbo(buf_position);
                 device.delete_vbo(buf_value);
             }
         }
     }
 
     fn get_height(&self) -> u32 {
-        self.texture.get_dimensions().height
+        self.texture.as_ref().map_or(0, |t| t.get_dimensions().height)
     }
 
     fn prepare_for_updates(
         &mut self,
         device: &mut Device,
         total_block_count: usize,
         max_height: u32,
     ) {
-        // See if we need to create or resize the texture.
-        let old_size = self.texture.get_dimensions();
-        let new_size = DeviceUintSize::new(MAX_VERTEX_TEXTURE_WIDTH as _, max_height);
-
+        let allocated_new_texture = self.ensure_texture(device, max_height);
         match self.bus {
             GpuCacheBus::PixelBuffer { ref mut rows, .. } => {
-                if max_height > old_size.height {
-                    // Create a f32 texture that can be used for the vertex shader
-                    // to fetch data from.
-                    device.init_texture::<u8>(
-                        &mut self.texture,
-                        new_size.width,
-                        new_size.height,
-                        TextureFilter::Nearest,
-                        None,
-                        1,
-                        None,
-                    );
-
+                if allocated_new_texture {
                     // If we had to resize the texture, just mark all rows
                     // as dirty so they will be uploaded to the texture
                     // during the next flush.
                     for row in rows.iter_mut() {
                         row.is_dirty = true;
                     }
                 }
             }
@@ -1118,34 +1164,16 @@ impl GpuCacheTexture {
                 ref mut count,
                 ..
             } => {
                 *count = 0;
                 if total_block_count > buf_value.allocated_count() {
                     device.allocate_vbo(buf_position, total_block_count, VertexUsageHint::Stream);
                     device.allocate_vbo(buf_value,    total_block_count, VertexUsageHint::Stream);
                 }
-
-                if new_size.height > old_size.height || GPU_CACHE_RESIZE_TEST {
-                    if old_size.height > 0 {
-                        device.resize_renderable_texture(&mut self.texture, new_size);
-                    } else {
-                        device.init_texture::<u8>(
-                            &mut self.texture,
-                            new_size.width,
-                            new_size.height,
-                            TextureFilter::Nearest,
-                            Some(RenderTargetInfo {
-                                has_depth: false,
-                            }),
-                            1,
-                            None,
-                        );
-                    }
-                }
             }
         }
     }
 
     fn update(&mut self, device: &mut Device, updates: &GpuCacheUpdateList) {
         match self.bus {
             GpuCacheBus::PixelBuffer { ref mut rows, ref mut cpu_blocks, .. } => {
                 for update in &updates.updates {
@@ -1185,17 +1213,17 @@ impl GpuCacheTexture {
                 ref buf_value,
                 ref mut count,
                 ..
             } => {
                 //TODO: re-use this heap allocation
                 // Unused positions will be left as 0xFFFF, which translates to
                 // (1.0, 1.0) in the vertex output position and gets culled out
                 let mut position_data = vec![[!0u16; 2]; updates.blocks.len()];
-                let size = self.texture.get_dimensions().to_usize();
+                let size = self.texture.as_ref().unwrap().get_dimensions().to_usize();
 
                 for update in &updates.updates {
                     match *update {
                         GpuCacheUpdate::Copy {
                             block_index,
                             block_count,
                             address,
                         } => {
@@ -1212,28 +1240,29 @@ impl GpuCacheTexture {
                 device.fill_vbo(buf_value, &updates.blocks, *count);
                 device.fill_vbo(buf_position, &position_data, *count);
                 *count += position_data.len();
             }
         }
     }
 
     fn flush(&mut self, device: &mut Device) -> usize {
+        let texture = self.texture.as_ref().unwrap();
         match self.bus {
             GpuCacheBus::PixelBuffer { ref buffer, ref mut rows, ref cpu_blocks } => {
                 let rows_dirty = rows
                     .iter()
                     .filter(|row| row.is_dirty)
                     .count();
                 if rows_dirty == 0 {
                     return 0
                 }
 
                 let mut uploader = device.upload_texture(
-                    &self.texture,
+                    texture,
                     buffer,
                     rows_dirty * MAX_VERTEX_TEXTURE_WIDTH,
                 );
 
                 for (row_index, row) in rows.iter_mut().enumerate() {
                     if !row.is_dirty {
                         continue;
                     }
@@ -1254,43 +1283,49 @@ impl GpuCacheTexture {
                 rows_dirty
             }
             GpuCacheBus::Scatter { ref program, ref vao, count, .. } => {
                 device.disable_depth();
                 device.set_blend(false);
                 device.bind_program(program);
                 device.bind_custom_vao(vao);
                 device.bind_draw_target(
-                    Some((&self.texture, 0)),
-                    Some(self.texture.get_dimensions()),
+                    Some((texture, 0)),
+                    Some(texture.get_dimensions()),
                 );
                 device.draw_nonindexed_points(0, count as _);
                 0
             }
         }
     }
 }
 
 struct VertexDataTexture {
-    texture: Texture,
+    texture: Option<Texture>,
+    format: ImageFormat,
     pbo: PBO,
 }
 
 impl VertexDataTexture {
     fn new(
         device: &mut Device,
         format: ImageFormat,
     ) -> VertexDataTexture {
-        let texture = device.create_texture(
-            TextureTarget::Default,
-            format,
-        );
         let pbo = device.create_pbo();
-
-        VertexDataTexture { texture, pbo }
+        VertexDataTexture { texture: None, format, pbo }
+    }
+
+    /// Returns a borrow of the GPU texture. Panics if it hasn't been initialized.
+    fn texture(&self) -> &Texture {
+        self.texture.as_ref().unwrap()
+    }
+
+    /// Returns an estimate of the GPU memory consumed by this VertexDataTexture.
+    fn size_in_bytes(&self) -> usize {
+        self.texture.as_ref().map_or(0, |t| t.size_in_bytes())
     }
 
     fn update<T>(&mut self, device: &mut Device, data: &mut Vec<T>) {
         if data.is_empty() {
             return;
         }
 
         debug_assert!(mem::size_of::<T>() % 16 == 0);
@@ -1304,46 +1339,53 @@ impl VertexDataTexture {
             while data.len() % items_per_row != 0 {
                 data.push(unsafe { mem::uninitialized() });
             }
         }
 
         let width =
             (MAX_VERTEX_TEXTURE_WIDTH - (MAX_VERTEX_TEXTURE_WIDTH % texels_per_item)) as u32;
         let needed_height = (data.len() / items_per_row) as u32;
-
-        // Determine if the texture needs to be resized.
-        let texture_size = self.texture.get_dimensions();
-
-        if needed_height > texture_size.height {
+        let existing_height = self.texture.as_ref().map_or(0, |t| t.get_dimensions().height);
+
+        // Create a new texture if needed.
+        if needed_height > existing_height {
+            // Drop the existing texture, if any.
+            if let Some(t) = self.texture.take() {
+                device.delete_texture(t);
+            }
             let new_height = (needed_height + 127) & !127;
 
-            device.init_texture::<u8>(
-                &mut self.texture,
+            let texture = device.create_texture::<u8>(
+                TextureTarget::Default,
+                self.format,
                 width,
                 new_height,
                 TextureFilter::Nearest,
                 None,
                 1,
                 None,
             );
+            self.texture = Some(texture);
         }
 
         let rect = DeviceUintRect::new(
             DeviceUintPoint::zero(),
             DeviceUintSize::new(width, needed_height),
         );
         device
-            .upload_texture(&self.texture, &self.pbo, 0)
+            .upload_texture(self.texture(), &self.pbo, 0)
             .upload(rect, 0, None, data);
     }
 
-    fn deinit(self, device: &mut Device) {
+    fn deinit(mut self, device: &mut Device) {
         device.delete_pbo(self.pbo);
-        device.delete_texture(self.texture);
+        if let Some(t) = self.texture.take() {
+            device.delete_texture(t);
+        }
     }
 }
 
 struct FrameOutput {
     last_access: FrameId,
     fbo_id: FBOId,
 }
 
@@ -1662,20 +1704,19 @@ impl Renderer {
                 38,
                 22,
                 41,
                 25,
                 37,
                 21,
             ];
 
-            let mut texture = device
-                .create_texture(TextureTarget::Default, ImageFormat::R8);
-            device.init_texture(
-                &mut texture,
+            let mut texture = device.create_texture::<u8>(
+                TextureTarget::Default,
+                ImageFormat::R8,
                 8,
                 8,
                 TextureFilter::Nearest,
                 None,
                 1,
                 Some(&dither_matrix),
             );
 
@@ -2661,17 +2702,17 @@ impl Renderer {
         self.pending_gpu_cache_updates.extend(deferred_update_list);
 
         self.update_gpu_cache();
 
         // Note: the texture might have changed during the `update`,
         // so we need to bind it here.
         self.device.bind_texture(
             TextureSampler::GpuCache,
-            &self.gpu_cache_texture.texture,
+            self.gpu_cache_texture.texture.as_ref().unwrap(),
         );
     }
 
     fn update_texture_cache(&mut self) {
         let _gm = self.gpu_profile.start_marker("texture cache update");
         let mut pending_texture_updates = mem::replace(&mut self.pending_texture_updates, vec![]);
 
         for update_list in pending_texture_updates.drain(..) {
@@ -2680,46 +2721,40 @@ impl Renderer {
                     TextureUpdateOp::Create {
                         width,
                         height,
                         layer_count,
                         format,
                         filter,
                         render_target,
                     } => {
-                        let CacheTextureId(cache_texture_index) = update.id;
-                        if self.texture_resolver.texture_cache_map.len() == cache_texture_index {
-                            // Create a new native texture, as requested by the texture cache.
-                            let texture = self.device.create_texture(TextureTarget::Array, format);
-                            self.texture_resolver.texture_cache_map.push(texture);
-                        }
-                        let texture =
-                            &mut self.texture_resolver.texture_cache_map[cache_texture_index];
-                        assert_eq!(texture.get_format(), format);
-
+                        // Create a new native texture, as requested by the texture cache.
+                        //
                         // Ensure no PBO is bound when creating the texture storage,
                         // or GL will attempt to read data from there.
-                        self.device.init_texture::<u8>(
-                            texture,
+                        let texture = self.device.create_texture::<u8>(
+                            TextureTarget::Array,
+                            format,
                             width,
                             height,
                             filter,
                             render_target,
                             layer_count,
                             None,
                         );
+                        self.texture_resolver.texture_cache_map.insert(update.id, texture);
                     }
                     TextureUpdateOp::Update {
                         rect,
                         source,
                         stride,
                         layer_index,
                         offset,
                     } => {
-                        let texture = &self.texture_resolver.texture_cache_map[update.id.0];
+                        let texture = &self.texture_resolver.texture_cache_map[&update.id];
                         let mut uploader = self.device.upload_texture(
                             texture,
                             &self.texture_cache_upload_pbo,
                             0,
                         );
 
                         let bytes_uploaded = match source {
                             TextureUpdateSource::Bytes { data } => {
@@ -2757,18 +2792,18 @@ impl Renderer {
                                 handler.unlock(id, channel_index);
                                 size
                             }
                         };
 
                         self.profile_counters.texture_data_uploaded.add(bytes_uploaded >> 10);
                     }
                     TextureUpdateOp::Free => {
-                        let texture = &mut self.texture_resolver.texture_cache_map[update.id.0];
-                        self.device.free_texture_storage(texture);
+                        let texture = self.texture_resolver.texture_cache_map.remove(&update.id).unwrap();
+                        self.device.delete_texture(texture);
                     }
                 }
             }
         }
     }
 
     pub(crate) fn draw_instanced_batch<T>(
         &mut self,
@@ -3676,86 +3711,75 @@ impl Renderer {
                 handler.unlock(ext_data.0, ext_data.1);
             }
         }
     }
 
     /// Allocates a texture to be used as the output for a rendering pass.
     ///
     /// We make an effort to reuse render targe textures across passes and
-    /// across frames. Reusing a texture with the same dimensions (width,
-    /// height, and layer-count) and format is obviously ideal. Reusing a
-    /// texture with different dimensions but the same format can be faster
-    /// than allocating a new texture, since it basically boils down to
-    /// a realloc in GPU memory, which can be very cheap if the existing
-    /// region can be resized. However, some drivers/GPUs require textures
-    /// with different formats to be allocated in different arenas,
-    /// reinitializing with a different format can force a large copy. As
-    /// such, we just allocate a new texture in that case.
+    /// across frames when the format and dimensions match. Because we use
+    /// immutable storage, we can't resize textures.
+    ///
+    /// We could consider approaches to re-use part of a larger target, if
+    /// available. However, we'd need to be careful about eviction. Currently,
+    /// render targets are freed if they haven't been used in 30 frames. If we
+    /// used partial targets, we'd need to track how _much_ of the target has
+    /// been used in the last 30 frames, since we could otherwise end up
+    /// keeping an enormous target alive indefinitely by constantly using it
+    /// in situations where a much smaller target would suffice.
     fn allocate_target_texture<T: RenderTarget>(
         &mut self,
         list: &mut RenderTargetList<T>,
         counters: &mut FrameProfileCounters,
-        frame_id: FrameId,
     ) -> Option<ActiveTexture> {
         debug_assert_ne!(list.max_size, DeviceUintSize::zero());
         if list.targets.is_empty() {
             return None
         }
 
         counters.targets_used.inc();
 
-        // First, try finding a perfect match
+        // Try finding a match in the existing pool. If there's no match, we'll
+        // create a new texture.
         let selector = TargetSelector {
             size: list.max_size,
             num_layers: list.targets.len() as _,
             format: list.format,
         };
-        let mut index = self.texture_resolver.render_target_pool
+        let index = self.texture_resolver.render_target_pool
             .iter()
             .position(|texture| {
-                //TODO: re-use a part of a larger target, if available
                 selector == TargetSelector {
                     size: texture.get_dimensions(),
                     num_layers: texture.get_render_target_layer_count(),
                     format: texture.get_format(),
                 }
             });
 
-        // Next, try at least finding a matching format
-        if index.is_none() {
-            counters.targets_changed.inc();
-            index = self.texture_resolver.render_target_pool
-                .iter()
-                .position(|texture| texture.get_format() == list.format && !texture.used_in_frame(frame_id));
-        }
-
-        let mut texture = match index {
-            Some(pos) => {
-                self.texture_resolver.render_target_pool.swap_remove(pos)
-            }
-            None => {
-                counters.targets_created.inc();
-                // finally, give up and create a new one
-                self.device.create_texture(TextureTarget::Array, list.format)
-            }
+        let rt_info = RenderTargetInfo { has_depth: list.needs_depth() };
+        let texture = if let Some(idx) = index {
+            let mut t = self.texture_resolver.render_target_pool.swap_remove(idx);
+            self.device.reuse_render_target::<u8>(&mut t, rt_info);
+            t
+        } else {
+            counters.targets_created.inc();
+            let mut t = self.device.create_texture::<u8>(
+                TextureTarget::Array,
+                list.format,
+                list.max_size.width,
+                list.max_size.height,
+                TextureFilter::Linear,
+                Some(rt_info),
+                list.targets.len() as _,
+                None,
+            );
+            t
         };
 
-        self.device.init_texture::<u8>(
-            &mut texture,
-            list.max_size.width,
-            list.max_size.height,
-            TextureFilter::Linear,
-            Some(RenderTargetInfo {
-                has_depth: list.needs_depth(),
-            }),
-            list.targets.len() as _,
-            None,
-        );
-
         list.check_ready(&texture);
         Some(ActiveTexture {
             texture,
             saved_index: list.saved_index.clone(),
         })
     }
 
     fn bind_frame_data(&mut self, frame: &mut Frame) {
@@ -3763,42 +3787,42 @@ impl Renderer {
         self.device.set_device_pixel_ratio(frame.device_pixel_ratio);
 
         self.prim_header_f_texture.update(
             &mut self.device,
             &mut frame.prim_headers.headers_float,
         );
         self.device.bind_texture(
             TextureSampler::PrimitiveHeadersF,
-            &self.prim_header_f_texture.texture,
+            &self.prim_header_f_texture.texture(),
         );
 
         self.prim_header_i_texture.update(
             &mut self.device,
             &mut frame.prim_headers.headers_int,
         );
         self.device.bind_texture(
             TextureSampler::PrimitiveHeadersI,
-            &self.prim_header_i_texture.texture,
+            &self.prim_header_i_texture.texture(),
         );
 
         self.transforms_texture.update(
             &mut self.device,
             &mut frame.transform_palette,
         );
         self.device.bind_texture(
             TextureSampler::TransformPalette,
-            &self.transforms_texture.texture,
+            &self.transforms_texture.texture(),
         );
 
         self.render_task_texture
             .update(&mut self.device, &mut frame.render_tasks.task_data);
         self.device.bind_texture(
             TextureSampler::RenderTasks,
-            &self.render_task_texture.texture,
+            &self.render_task_texture.texture(),
         );
 
         debug_assert!(self.texture_resolver.prev_pass_alpha.is_none());
         debug_assert!(self.texture_resolver.prev_pass_color.is_none());
     }
 
     fn draw_tile_frame(
         &mut self,
@@ -3863,18 +3887,18 @@ impl Renderer {
                             frame_id,
                             stats,
                         );
                     }
 
                     (None, None)
                 }
                 RenderPassKind::OffScreen { ref mut alpha, ref mut color, ref mut texture_cache } => {
-                    let alpha_tex = self.allocate_target_texture(alpha, &mut frame.profile_counters, frame_id);
-                    let color_tex = self.allocate_target_texture(color, &mut frame.profile_counters, frame_id);
+                    let alpha_tex = self.allocate_target_texture(alpha, &mut frame.profile_counters);
+                    let color_tex = self.allocate_target_texture(color, &mut frame.profile_counters);
 
                     // If this frame has already been drawn, then any texture
                     // cache targets have already been updated and can be
                     // skipped this time.
                     if !frame.has_been_rendered {
                         for (&(texture_id, target_index), target) in texture_cache {
                             self.draw_texture_cache_target(
                                 &texture_id,
@@ -4058,28 +4082,28 @@ impl Renderer {
             return;
         }
 
         let mut spacing = 16;
         let mut size = 512;
         let fb_width = framebuffer_size.width as i32;
         let num_layers: i32 = self.texture_resolver
             .texture_cache_map
-            .iter()
+            .values()
             .map(|texture| texture.get_layer_count())
             .sum();
 
         if num_layers * (size + spacing) > fb_width {
             let factor = fb_width as f32 / (num_layers * (size + spacing)) as f32;
             size = (size as f32 * factor) as i32;
             spacing = (spacing as f32 * factor) as i32;
         }
 
         let mut i = 0;
-        for texture in &self.texture_resolver.texture_cache_map {
+        for texture in self.texture_resolver.texture_cache_map.values() {
             let y = spacing + if self.debug_flags.contains(DebugFlags::RENDER_TARGET_DBG) {
                 528
             } else {
                 0
             };
             let dimensions = texture.get_dimensions();
             let src_rect = DeviceIntRect::new(
                 DeviceIntPoint::zero(),
@@ -4186,20 +4210,21 @@ impl Renderer {
 
     pub fn read_pixels_rgba8(&mut self, rect: DeviceUintRect) -> Vec<u8> {
         let mut pixels = vec![0; (rect.size.width * rect.size.height * 4) as usize];
         self.device.read_pixels_into(rect, ReadPixelsFormat::Rgba8, &mut pixels);
         pixels
     }
 
     pub fn read_gpu_cache(&mut self) -> (DeviceUintSize, Vec<u8>) {
-        let size = self.gpu_cache_texture.texture.get_dimensions();
+        let texture = self.gpu_cache_texture.texture.as_ref().unwrap();
+        let size = texture.get_dimensions();
         let mut texels = vec![0; (size.width * size.height * 16) as usize];
         self.device.begin_frame();
-        self.device.bind_read_target(Some((&self.gpu_cache_texture.texture, 0)));
+        self.device.bind_read_target(Some((texture, 0)));
         self.device.read_pixels_into(
             DeviceUintRect::new(DeviceUintPoint::zero(), size),
             ReadPixelsFormat::Standard(ImageFormat::RGBAF32),
             &mut texels,
         );
         self.device.bind_read_target(None);
         self.device.end_frame();
         (size, texels)
@@ -4255,29 +4280,30 @@ impl Renderer {
         let mut report = MemoryReport::default();
 
         // GPU cache CPU memory.
         if let GpuCacheBus::PixelBuffer{ref cpu_blocks, ..} = self.gpu_cache_texture.bus {
             report.gpu_cache_cpu_mirror += self.size_of(cpu_blocks.as_ptr());
         }
 
         // GPU cache GPU memory.
-        report.gpu_cache_textures += self.gpu_cache_texture.texture.size_in_bytes();
+        report.gpu_cache_textures +=
+            self.gpu_cache_texture.texture.as_ref().map_or(0, |t| t.size_in_bytes());
 
         // Render task CPU memory.
         for (_id, doc) in &self.active_documents {
             report.render_tasks += self.size_of(doc.frame.render_tasks.tasks.as_ptr());
             report.render_tasks += self.size_of(doc.frame.render_tasks.task_data.as_ptr());
         }
 
         // Vertex data GPU memory.
-        report.vertex_data_textures += self.prim_header_f_texture.texture.size_in_bytes();
-        report.vertex_data_textures += self.prim_header_i_texture.texture.size_in_bytes();
-        report.vertex_data_textures += self.transforms_texture.texture.size_in_bytes();
-        report.vertex_data_textures += self.render_task_texture.texture.size_in_bytes();
+        report.vertex_data_textures += self.prim_header_f_texture.size_in_bytes();
+        report.vertex_data_textures += self.prim_header_i_texture.size_in_bytes();
+        report.vertex_data_textures += self.transforms_texture.size_in_bytes();
+        report.vertex_data_textures += self.render_task_texture.size_in_bytes();
 
         // Texture cache and render target GPU memory.
         report += self.texture_resolver.report_memory();
 
         report
     }
 
     // Sets the blend mode. Blend is unconditionally set if the "show overdraw" debugging mode is
@@ -4549,17 +4575,17 @@ struct PlainTexture {
 
 
 #[cfg(any(feature = "capture", feature = "replay"))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct PlainRenderer {
     gpu_cache: PlainTexture,
     gpu_cache_frame_id: FrameId,
-    textures: Vec<PlainTexture>,
+    textures: FastHashMap<CacheTextureId, PlainTexture>,
     external_images: Vec<ExternalCaptureImage>
 }
 
 #[cfg(feature = "replay")]
 enum CapturedExternalImageData {
     NativeTexture(gl::GLuint),
     Buffer(Arc<Vec<u8>>),
 }
@@ -4654,34 +4680,44 @@ impl Renderer {
             size: (rect.size.width, rect.size.height, texture.get_layer_count()),
             format: texture.get_format(),
             filter: texture.get_filter(),
             render_target: texture.get_render_target(),
         }
     }
 
     #[cfg(feature = "replay")]
-    fn load_texture(texture: &mut Texture, plain: &PlainTexture, root: &PathBuf, device: &mut Device) -> Vec<u8> {
+    fn load_texture(
+        target: TextureTarget,
+        plain: &PlainTexture,
+        root: &PathBuf,
+        device: &mut Device
+    ) -> (Texture, Vec<u8>)
+    {
         use std::fs::File;
         use std::io::Read;
 
         let mut texels = Vec::new();
-        assert_eq!(plain.format, texture.get_format());
         File::open(root.join(&plain.data))
             .expect(&format!("Unable to open texture at {}", plain.data))
             .read_to_end(&mut texels)
             .unwrap();
 
-        device.init_texture(
-            texture, plain.size.0, plain.size.1,
-            plain.filter, plain.render_target,
-            plain.size.2, Some(texels.as_slice()),
+        let texture = device.create_texture(
+            target,
+            plain.format,
+            plain.size.0,
+            plain.size.1,
+            plain.filter,
+            plain.render_target,
+            plain.size.2,
+            Some(texels.as_slice()),
         );
 
-        texels
+        (texture, texels)
     }
 
     #[cfg(feature = "capture")]
     fn save_capture(
         &mut self,
         config: CaptureConfig,
         deferred_images: Vec<ExternalCaptureImage>,
     ) {
@@ -4766,30 +4802,30 @@ impl Renderer {
             if !path_textures.is_dir() {
                 fs::create_dir(&path_textures).unwrap();
             }
 
             info!("saving GPU cache");
             self.update_gpu_cache(); // flush pending updates
             let mut plain_self = PlainRenderer {
                 gpu_cache: Self::save_texture(
-                    &self.gpu_cache_texture.texture,
+                    &self.gpu_cache_texture.texture.as_ref().unwrap(),
                     "gpu", &config.root, &mut self.device,
                 ),
                 gpu_cache_frame_id: self.gpu_cache_frame_id,
-                textures: Vec::new(),
+                textures: FastHashMap::default(),
                 external_images: deferred_images,
             };
 
             info!("saving cached textures");
-            for texture in &self.texture_resolver.texture_cache_map {
+            for (id, texture) in &self.texture_resolver.texture_cache_map {
                 let file_name = format!("cache-{}", plain_self.textures.len() + 1);
                 info!("\t{}", file_name);
                 let plain = Self::save_texture(texture, &file_name, &config.root, &mut self.device);
-                plain_self.textures.push(plain);
+                plain_self.textures.insert(*id, plain);
             }
 
             config.serialize(&plain_self, "renderer");
         }
 
         self.device.bind_read_target(None);
         self.device.end_frame();
         info!("done.");
@@ -4829,36 +4865,44 @@ impl Renderer {
             let value = (CapturedExternalImageData::Buffer(data), plain_ext.uv);
             image_handler.data.insert((ext.id, ext.channel_index), value);
         }
 
         if let Some(renderer) = CaptureConfig::deserialize::<PlainRenderer, _>(&root, "renderer") {
             info!("loading cached textures");
             self.device.begin_frame();
 
-            for texture in self.texture_resolver.texture_cache_map.drain(..) {
+            for (_id, texture) in self.texture_resolver.texture_cache_map.drain() {
                 self.device.delete_texture(texture);
             }
-            for texture in renderer.textures {
+            for (id, texture) in renderer.textures {
                 info!("\t{}", texture.data);
-                let mut t = self.device.create_texture(TextureTarget::Array, texture.format);
-                Self::load_texture(&mut t, &texture, &root, &mut self.device);
-                self.texture_resolver.texture_cache_map.push(t);
+                let t = Self::load_texture(
+                    TextureTarget::Array,
+                    &texture,
+                    &root,
+                    &mut self.device
+                );
+                self.texture_resolver.texture_cache_map.insert(id, t.0);
             }
 
             info!("loading gpu cache");
-            let gpu_cache_data = Self::load_texture(
-                &mut self.gpu_cache_texture.texture,
+            if let Some(t) = self.gpu_cache_texture.texture.take() {
+                self.device.delete_texture(t);
+            }
+            let (t, gpu_cache_data) = Self::load_texture(
+                TextureTarget::Default,
                 &renderer.gpu_cache,
                 &root,
                 &mut self.device,
             );
+            self.gpu_cache_texture.texture = Some(t);
             match self.gpu_cache_texture.bus {
                 GpuCacheBus::PixelBuffer { ref mut rows, ref mut cpu_blocks, .. } => {
-                    let dim = self.gpu_cache_texture.texture.get_dimensions();
+                    let dim = self.gpu_cache_texture.texture.as_ref().unwrap().get_dimensions();
                     let blocks = unsafe {
                         slice::from_raw_parts(
                             gpu_cache_data.as_ptr() as *const GpuBlockData,
                             gpu_cache_data.len() / mem::size_of::<GpuBlockData>(),
                         )
                     };
                     // fill up the CPU cache from the contents we just loaded
                     rows.clear();
@@ -4888,19 +4932,23 @@ impl Renderer {
                         let (layer_count, filter) = (1, TextureFilter::Linear);
                         let plain_tex = PlainTexture {
                             data: e.key().clone(),
                             size: (descriptor.size.width, descriptor.size.height, layer_count),
                             format: descriptor.format,
                             filter,
                             render_target: None,
                         };
-                        let mut t = self.device.create_texture(target, plain_tex.format);
-                        Self::load_texture(&mut t, &plain_tex, &root, &mut self.device);
-                        let extex = t.into_external();
+                        let t = Self::load_texture(
+                            target,
+                            &plain_tex,
+                            &root,
+                            &mut self.device
+                        );
+                        let extex = t.0.into_external();
                         self.owned_external_images.insert(key, extex.clone());
                         e.insert(extex.internal_id()).clone()
                     }
                 };
 
                 let value = (CapturedExternalImageData::NativeTexture(tid), plain_ext.uv);
                 image_handler.data.insert(key, value);
             }
--- a/gfx/webrender/src/scene.rs
+++ b/gfx/webrender/src/scene.rs
@@ -214,17 +214,19 @@ impl FilterOpHelpers for FilterOp {
             FilterOp::Brightness(..) |
             FilterOp::Contrast(..) |
             FilterOp::Grayscale(..) |
             FilterOp::HueRotate(..) |
             FilterOp::Invert(..) |
             FilterOp::Saturate(..) |
             FilterOp::Sepia(..) |
             FilterOp::DropShadow(..) |
-            FilterOp::ColorMatrix(..) => true,
+            FilterOp::ColorMatrix(..) |
+            FilterOp::SrgbToLinear |
+            FilterOp::LinearToSrgb  => true,
             FilterOp::Opacity(_, amount) => {
                 amount > OPACITY_EPSILON
             }
         }
     }
 
     fn is_noop(&self) -> bool {
         match *self {
@@ -243,16 +245,17 @@ impl FilterOpHelpers for FilterOp {
             },
             FilterOp::ColorMatrix(matrix) => {
                 matrix == [1.0, 0.0, 0.0, 0.0,
                            0.0, 1.0, 0.0, 0.0,
                            0.0, 0.0, 1.0, 0.0,
                            0.0, 0.0, 0.0, 1.0,
                            0.0, 0.0, 0.0, 0.0]
             }
+            FilterOp::SrgbToLinear | FilterOp::LinearToSrgb => false,
         }
     }
 }
 
 pub trait StackingContextHelpers {
     fn mix_blend_mode_for_compositing(&self) -> Option<MixBlendMode>;
     fn filter_ops_for_compositing(
         &self,
--- a/gfx/webrender/src/texture_cache.rs
+++ b/gfx/webrender/src/texture_cache.rs
@@ -4,17 +4,17 @@
 
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize};
 use api::{ExternalImageType, ImageData, ImageFormat};
 use api::ImageDescriptor;
 use device::TextureFilter;
 use freelist::{FreeList, FreeListHandle, UpsertResult, WeakFreeListHandle};
 use gpu_cache::{GpuCache, GpuCacheHandle};
 use gpu_types::{ImageSource, UvRectKind};
-use internal_types::{CacheTextureId, FastHashMap, TextureUpdateList, TextureUpdateSource};
+use internal_types::{CacheTextureId, TextureUpdateList, TextureUpdateSource};
 use internal_types::{RenderTargetInfo, TextureSource, TextureUpdate, TextureUpdateOp};
 use profiler::{ResourceProfileCounter, TextureCacheProfileCounters};
 use render_backend::FrameId;
 use resource_cache::CacheItem;
 use std::cell::Cell;
 use std::cmp;
 use std::mem;
 use std::rc::Rc;
@@ -25,52 +25,16 @@ const TEXTURE_ARRAY_LAYERS_LINEAR: usize
 const TEXTURE_ARRAY_LAYERS_NEAREST: usize = 1;
 
 // The dimensions of each layer in the texture cache.
 const TEXTURE_LAYER_DIMENSIONS: u32 = 2048;
 
 // The size of each region (page) in a texture layer.
 const TEXTURE_REGION_DIMENSIONS: u32 = 512;
 
-// Maintains a simple freelist of texture IDs that are mapped
-// to real API-specific texture IDs in the renderer.
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-struct CacheTextureIdList {
-    free_lists: FastHashMap<ImageFormat, Vec<CacheTextureId>>,
-    next_id: usize,
-}
-
-impl CacheTextureIdList {
-    fn new() -> Self {
-        CacheTextureIdList {
-            next_id: 0,
-            free_lists: FastHashMap::default(),
-        }
-    }
-
-    fn allocate(&mut self, format: ImageFormat) -> CacheTextureId {
-        // If nothing on the free list of texture IDs,
-        // allocate a new one.
-        self.free_lists.get_mut(&format)
-            .and_then(|fl| fl.pop())
-            .unwrap_or_else(|| {
-                self.next_id += 1;
-                CacheTextureId(self.next_id - 1)
-            })
-    }
-
-    fn free(&mut self, id: CacheTextureId, format: ImageFormat) {
-        self.free_lists
-            .entry(format)
-            .or_insert(Vec::new())
-            .push(id);
-    }
-}
-
 // Items in the texture cache can either be standalone textures,
 // or a sub-rect inside the shared cache.
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 enum EntryKind {
     Standalone,
     Cache {
@@ -239,21 +203,18 @@ pub struct TextureCache {
     array_rgba8_nearest: TextureArray,
     array_a8_linear: TextureArray,
     array_a16_linear: TextureArray,
     array_rgba8_linear: TextureArray,
 
     // Maximum texture size supported by hardware.
     max_texture_size: u32,
 
-    // A list of texture IDs that represent native
-    // texture handles. This indirection allows the texture
-    // cache to create / destroy / reuse texture handles
-    // without knowing anything about the device code.
-    cache_textures: CacheTextureIdList,
+    // The next unused virtual texture ID. Monotonically increasing.
+    next_id: CacheTextureId,
 
     // A list of updates that need to be applied to the
     // texture cache in the rendering thread this frame.
     #[cfg_attr(all(feature = "serde", any(feature = "capture", feature = "replay")), serde(skip))]
     pending_updates: TextureUpdateList,
 
     // The current frame ID. Used for cache eviction policies.
     frame_id: FrameId,
@@ -292,17 +253,17 @@ impl TextureCache {
                 TextureFilter::Linear,
                 TEXTURE_ARRAY_LAYERS_LINEAR,
             ),
             array_rgba8_nearest: TextureArray::new(
                 ImageFormat::BGRA8,
                 TextureFilter::Nearest,
                 TEXTURE_ARRAY_LAYERS_NEAREST,
             ),
-            cache_textures: CacheTextureIdList::new(),
+            next_id: CacheTextureId(1),
             pending_updates: TextureUpdateList::new(),
             frame_id: FrameId(0),
             entries: FreeList::new(),
             standalone_entry_handles: Vec::new(),
             shared_entry_handles: Vec::new(),
         }
     }
 
@@ -331,41 +292,37 @@ impl TextureCache {
 
         assert!(self.entries.len() == 0);
 
         if let Some(texture_id) = self.array_a8_linear.clear() {
             self.pending_updates.push(TextureUpdate {
                 id: texture_id,
                 op: TextureUpdateOp::Free,
             });
-            self.cache_textures.free(texture_id, self.array_a8_linear.format);
         }
 
         if let Some(texture_id) = self.array_a16_linear.clear() {
             self.pending_updates.push(TextureUpdate {
                 id: texture_id,
                 op: TextureUpdateOp::Free,
             });
-            self.cache_textures.free(texture_id, self.array_a16_linear.format);
         }
 
         if let Some(texture_id) = self.array_rgba8_linear.clear() {
             self.pending_updates.push(TextureUpdate {
                 id: texture_id,
                 op: TextureUpdateOp::Free,
             });
-            self.cache_textures.free(texture_id, self.array_rgba8_linear.format);
         }
 
         if let Some(texture_id) = self.array_rgba8_nearest.clear() {
             self.pending_updates.push(TextureUpdate {
                 id: texture_id,
                 op: TextureUpdateOp::Free,
             });
-            self.cache_textures.free(texture_id, self.array_rgba8_nearest.format);
         }
     }
 
     pub fn begin_frame(&mut self, frame_id: FrameId) {
         self.frame_id = frame_id;
     }
 
     pub fn end_frame(&mut self, texture_cache_profile: &mut TextureCacheProfileCounters) {
@@ -731,17 +688,16 @@ impl TextureCache {
         match entry.kind {
             EntryKind::Standalone { .. } => {
                 // This is a standalone texture allocation. Just push it back onto the free
                 // list.
                 self.pending_updates.push(TextureUpdate {
                     id: entry.texture_id,
                     op: TextureUpdateOp::Free,
                 });
-                self.cache_textures.free(entry.texture_id, entry.format);
                 None
             }
             EntryKind::Cache {
                 origin,
                 region_index,
                 ..
             } => {
                 // Free the block in the given region.
@@ -777,17 +733,18 @@ impl TextureCache {
             (ImageFormat::R16, TextureFilter::Nearest) |
             (ImageFormat::R16, TextureFilter::Trilinear) |
             (ImageFormat::BGRA8, TextureFilter::Trilinear) |
             (ImageFormat::RG8, _) => unreachable!(),
         };
 
         // Lazy initialize this texture array if required.
         if texture_array.texture_id.is_none() {
-            let texture_id = self.cache_textures.allocate(descriptor.format);
+            let texture_id = self.next_id;
+            self.next_id.0 += 1;
 
             let update_op = TextureUpdate {
                 id: texture_id,
                 op: TextureUpdateOp::Create {
                     width: TEXTURE_LAYER_DIMENSIONS,
                     height: TEXTURE_LAYER_DIMENSIONS,
                     format: descriptor.format,
                     filter: texture_array.filter,
@@ -890,17 +847,18 @@ impl TextureCache {
                 );
             }
         }
 
         // If not allowed in the cache, or if the shared cache is full, then it
         // will just have to be in a unique texture. This hurts batching but should
         // only occur on a small number of images (or pathological test cases!).
         if new_cache_entry.is_none() {
-            let texture_id = self.cache_textures.allocate(descriptor.format);
+            let texture_id = self.next_id;
+            self.next_id.0 += 1;
 
             // Create an update operation to allocate device storage
             // of the right size / format.
             let update_op = TextureUpdate {
                 id: texture_id,
                 op: TextureUpdateOp::Create {
                     width: descriptor.size.width,
                     height: descriptor.size.height,
--- a/gfx/webrender_api/src/display_item.rs
+++ b/gfx/webrender_api/src/display_item.rs
@@ -607,16 +607,18 @@ pub enum FilterOp {
     Grayscale(f32),
     HueRotate(f32),
     Invert(f32),
     Opacity(PropertyBinding<f32>, f32),
     Saturate(f32),
     Sepia(f32),
     DropShadow(LayoutVector2D, f32, ColorF),
     ColorMatrix([f32; 20]),
+    SrgbToLinear,
+    LinearToSrgb,
 }
 
 impl FilterOp {
     /// Ensure that the parameters for a filter operation
     /// are sensible.
     pub fn sanitize(self) -> FilterOp {
         match self {
             FilterOp::Blur(radius) => {
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-1396114d80fb19df2295a40b0b14abc8f24afa03
+f83c387824b156e7f97f88edee96956bd0de482d
--- a/gfx/wrench/src/yaml_frame_writer.rs
+++ b/gfx/wrench/src/yaml_frame_writer.rs
@@ -245,16 +245,22 @@ fn write_stacking_context(
                 filters.push(Yaml::String(format!("drop-shadow([{},{}],{},[{}])",
                                                   offset.x, offset.y,
                                                   blur,
                                                   color_to_string(color))))
             }
             FilterOp::ColorMatrix(matrix) => {
                 filters.push(Yaml::String(format!("color-matrix({:?})", matrix)))
             }
+            FilterOp::SrgbToLinear => {
+                filters.push(Yaml::String("srgb-to-linear".to_string()))
+            }
+            FilterOp::LinearToSrgb => {
+                filters.push(Yaml::String("linear-to-srgb".to_string()))
+            }
         }
     }
 
     yaml_node(parent, "filters", Yaml::Array(filters));
 }
 
 #[cfg(target_os = "windows")]
 fn native_font_handle_to_yaml(
--- a/gfx/wrench/src/yaml_helper.rs
+++ b/gfx/wrench/src/yaml_helper.rs
@@ -571,16 +571,18 @@ impl YamlHelper for Yaml {
                     Some(FilterOp::Opacity(amount.into(), amount))
                 }
                 ("saturate", ref args, _) if args.len() == 1 => {
                     Some(FilterOp::Saturate(args[0].parse().unwrap()))
                 }
                 ("sepia", ref args, _) if args.len() == 1 => {
                     Some(FilterOp::Sepia(args[0].parse().unwrap()))
                 }
+                ("srgb-to-linear", _, _)  => Some(FilterOp::SrgbToLinear),
+                ("linear-to-srgb", _, _)  => Some(FilterOp::LinearToSrgb),
                 ("drop-shadow", ref args, _) if args.len() == 3 => {
                     let str = format!("---\noffset: {}\nblur-radius: {}\ncolor: {}\n", args[0], args[1], args[2]);
                     let mut yaml_doc = YamlLoader::load_from_str(&str).expect("Failed to parse drop-shadow");
                     let yaml = yaml_doc.pop().unwrap();
                     Some(FilterOp::DropShadow(yaml["offset"].as_vector().unwrap(),
                                               yaml["blur-radius"].as_f32().unwrap(),
                                               yaml["color"].as_colorf().unwrap()))
                 }
--- a/js/public/ProtoKey.h
+++ b/js/public/ProtoKey.h
@@ -101,20 +101,18 @@ IF_BDATA(real,imaginary)(TypedObject,   
     real(Reflect,               InitReflect,            nullptr) \
     real(WeakSet,               InitWeakSetClass,       OCLASP(WeakSet)) \
     real(TypedArray,            InitViaClassSpec,       &js::TypedArrayObject::sharedTypedArrayPrototypeClass) \
 IF_SAB(real,imaginary)(Atomics, InitAtomicsClass, OCLASP(Atomics)) \
     real(SavedFrame,            InitViaClassSpec,       &js::SavedFrame::class_) \
     real(Promise,               InitViaClassSpec,       OCLASP(Promise)) \
     real(ReadableStream,        InitViaClassSpec,       &js::ReadableStream::class_) \
     real(ReadableStreamDefaultReader,           InitViaClassSpec, &js::ReadableStreamDefaultReader::class_) \
-    real(ReadableStreamBYOBReader,              InitViaClassSpec, &js::ReadableStreamBYOBReader::class_) \
     real(ReadableStreamDefaultController,       InitViaClassSpec, &js::ReadableStreamDefaultController::class_) \
     real(ReadableByteStreamController,          InitViaClassSpec, &js::ReadableByteStreamController::class_) \
-    real(ReadableStreamBYOBRequest,             InitViaClassSpec, &js::ReadableStreamBYOBRequest::class_) \
     imaginary(WritableStream,   dummy,                  dummy) \
     imaginary(WritableStreamDefaultWriter,      dummy,  dummy) \
     imaginary(WritableStreamDefaultController,  dummy,  dummy) \
     real(ByteLengthQueuingStrategy,             InitViaClassSpec, &js::ByteLengthQueuingStrategy::class_) \
     real(CountQueuingStrategy,  InitViaClassSpec,       &js::CountQueuingStrategy::class_) \
     real(WebAssembly,           InitWebAssemblyClass,   CLASP(WebAssembly)) \
     imaginary(WasmModule,       dummy,                  dummy) \
     imaginary(WasmInstance,     dummy,                  dummy) \
--- a/js/public/Stream.h
+++ b/js/public/Stream.h
@@ -181,26 +181,16 @@ HasReadableStreamCallbacks(JSContext* cx
  */
 extern JS_PUBLIC_API(JSObject*)
 NewReadableDefaultStreamObject(JSContext* cx, HandleObject underlyingSource = nullptr,
                                HandleFunction size = nullptr, double highWaterMark = 1,
                                HandleObject proto = nullptr);
 
 /**
  * Returns a new instance of the ReadableStream builtin class in the current
- * compartment, configured as a byte stream.
- * If a |proto| is passed, that gets set as the instance's [[Prototype]]
- * instead of the original value of |ReadableStream.prototype|.
- */
-extern JS_PUBLIC_API(JSObject*)
-NewReadableByteStreamObject(JSContext* cx, HandleObject underlyingSource = nullptr,
-                            double highWaterMark = 0, HandleObject proto = nullptr);
-
-/**
- * Returns a new instance of the ReadableStream builtin class in the current
  * compartment, with the right slot layout. If a |proto| is passed, that gets
  * set as the instance's [[Prototype]] instead of the original value of
  * |ReadableStream.prototype|.
  *
  * The instance is optimized for operating as a byte stream backed by an
  * embedding-provided underlying source, using the callbacks set via
  * |JS::SetReadableStreamCallbacks|.
  *
@@ -208,29 +198,34 @@ NewReadableByteStreamObject(JSContext* c
  * used to disambiguate between different types of stream sources the
  * embedding might support.
  *
  * Note: the embedding is responsible for ensuring that the pointer to the
  * underlying source stays valid as long as the stream can be read from.
  * The underlying source can be freed if the tree is canceled or errored.
  * It can also be freed if the stream is destroyed. The embedding is notified
  * of that using ReadableStreamFinalizeCallback.
+ *
+ * Note: |underlyingSource| must have an even address.
  */
 extern JS_PUBLIC_API(JSObject*)
 NewReadableExternalSourceStreamObject(JSContext* cx, void* underlyingSource,
                                       uint8_t flags = 0, HandleObject proto = nullptr);
 
 /**
  * Returns the flags that were passed to NewReadableExternalSourceStreamObject
  * when creating the given stream.
  *
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
+ *
  * Asserts that the given stream has an embedding-provided underlying source.
  */
-extern JS_PUBLIC_API(uint8_t)
-ReadableStreamGetEmbeddingFlags(const JSObject* stream);
+extern JS_PUBLIC_API(bool)
+ReadableStreamGetEmbeddingFlags(JSContext* cx, const JSObject* stream, uint8_t* flags);
 
 /**
  * Returns the embedding-provided underlying source of the given |stream|.
  *
  * Can be used to optimize operations if both the underlying source and the
  * intended sink are embedding-provided. In that case it might be
  * preferrable to pipe data directly from source to sink without interacting
  * with the stream at all.
@@ -241,143 +236,152 @@ ReadableStreamGetEmbeddingFlags(const JS
  * Throws an exception if the stream is locked, i.e. if a reader has been
  * acquired for the stream, or if ReadableStreamGetExternalUnderlyingSource
  * has been used previously without releasing the external source again.
  *
  * Throws an exception if the stream isn't readable, i.e if it is errored or
  * closed. This is different from ReadableStreamGetReader because we don't
  * have a Promise to resolve/reject, which a reader provides.
  *
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
+ *
  * Asserts that the stream has an embedding-provided underlying source.
  */
 extern JS_PUBLIC_API(bool)
 ReadableStreamGetExternalUnderlyingSource(JSContext* cx, HandleObject stream, void** source);
 
 /**
  * Releases the embedding-provided underlying source of the given |stream|,
  * returning the stream into an unlocked state.
  *
  * Asserts that the stream was locked through
  * ReadableStreamGetExternalUnderlyingSource.
  *
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
+ *
  * Asserts that the stream has an embedding-provided underlying source.
  */
-extern JS_PUBLIC_API(void)
-ReadableStreamReleaseExternalUnderlyingSource(JSObject* stream);
+extern JS_PUBLIC_API(bool)
+ReadableStreamReleaseExternalUnderlyingSource(JSContext* cx, JSObject* stream);
 
 /**
  * Update the amount of data available at the underlying source of the given
  * |stream|.
  *
  * Can only be used for streams with an embedding-provided underlying source.
  * The JS engine will use the given value to satisfy read requests for the
  * stream by invoking the JS::WriteIntoReadRequestBuffer callback.
+ *
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
 ReadableStreamUpdateDataAvailableFromSource(JSContext* cx, HandleObject stream,
                                             uint32_t availableData);
 
 /**
- * Returns true if the given object is an unwrapped ReadableStream object,
- * false otherwise.
+ * Returns true if the given object is a ReadableStream object or an
+ * unwrappable wrapper for one, false otherwise.
  */
 extern JS_PUBLIC_API(bool)
 IsReadableStream(const JSObject* obj);
 
 /**
- * Returns true if the given object is an unwrapped
- * ReadableStreamDefaultReader or ReadableStreamBYOBReader object,
- * false otherwise.
+ * Returns true if the given object is a ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader object or an unwrappable wrapper for one, false
+ * otherwise.
  */
 extern JS_PUBLIC_API(bool)
 IsReadableStreamReader(const JSObject* obj);
 
 /**
- * Returns true if the given object is an unwrapped
- * ReadableStreamDefaultReader object, false otherwise.
+ * Returns true if the given object is a ReadableStreamDefaultReader object
+ * or an unwrappable wrapper for one, false otherwise.
  */
 extern JS_PUBLIC_API(bool)
 IsReadableStreamDefaultReader(const JSObject* obj);
 
-/**
- * Returns true if the given object is an unwrapped
- * ReadableStreamBYOBReader object, false otherwise.
- */
-extern JS_PUBLIC_API(bool)
-IsReadableStreamBYOBReader(const JSObject* obj);
-
 enum class ReadableStreamMode {
     Default,
     Byte,
     ExternalSource
 };
 
 /**
  * Returns the stream's ReadableStreamMode. If the mode is |Byte| or
  * |ExternalSource|, it's possible to acquire a BYOB reader for more optimized
  * operations.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
-extern JS_PUBLIC_API(ReadableStreamMode)
-ReadableStreamGetMode(const JSObject* stream);
+extern JS_PUBLIC_API(bool)
+ReadableStreamGetMode(JSContext* cx, const JSObject* stream, ReadableStreamMode* mode);
 
 enum class ReadableStreamReaderMode {
-    Default,
-    BYOB
+    Default
 };
 
 /**
  * Returns true if the given ReadableStream is readable, false if not.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
-ReadableStreamIsReadable(const JSObject* stream);
+ReadableStreamIsReadable(JSContext* cx, const JSObject* stream, bool* result);
 
 /**
  * Returns true if the given ReadableStream is locked, false if not.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
-ReadableStreamIsLocked(const JSObject* stream);
+ReadableStreamIsLocked(JSContext* cx, const JSObject* stream, bool* result);
 
 /**
  * Returns true if the given ReadableStream is disturbed, false if not.
  *
- * Asserts that |stream| is an ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
-ReadableStreamIsDisturbed(const JSObject* stream);
+ReadableStreamIsDisturbed(JSContext* cx, const JSObject* stream, bool* result);
 
 /**
  * Cancels the given ReadableStream with the given reason and returns a
  * Promise resolved according to the result.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(JSObject*)
 ReadableStreamCancel(JSContext* cx, HandleObject stream, HandleValue reason);
 
 /**
  * Creates a reader of the type specified by the mode option and locks the
  * stream to the new reader.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one. The returned object will always be created in the
+ * current cx compartment.
  */
 extern JS_PUBLIC_API(JSObject*)
 ReadableStreamGetReader(JSContext* cx, HandleObject stream, ReadableStreamReaderMode mode);
 
 /**
  * Tees the given ReadableStream and stores the two resulting streams in
  * outparams. Returns false if the operation fails, e.g. because the stream is
  * locked.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
 ReadableStreamTee(JSContext* cx, HandleObject stream,
                   MutableHandleObject branch1Stream, MutableHandleObject branch2Stream);
 
 /**
  * Retrieves the desired combined size of additional chunks to fill the given
  * ReadableStream's queue. Stores the result in |value| and sets |hasValue| to
@@ -385,138 +389,112 @@ ReadableStreamTee(JSContext* cx, HandleO
  *
  * If the stream is errored, the call will succeed but no value will be stored
  * in |value| and |hasValue| will be set to false.
  *
  * Note: This is semantically equivalent to the |desiredSize| getter on
  * the stream controller's prototype in JS. We expose it with the stream
  * itself as a target for simplicity.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
-extern JS_PUBLIC_API(void)
-ReadableStreamGetDesiredSize(JSObject* stream, bool* hasValue, double* value);
+extern JS_PUBLIC_API(bool)
+ReadableStreamGetDesiredSize(JSContext* cx, JSObject* stream, bool* hasValue, double* value);
 
 /**
  * Closes the given ReadableStream.
  *
  * Throws a TypeError and returns false if the closing operation fails.
  *
  * Note: This is semantically equivalent to the |close| method on
  * the stream controller's prototype in JS. We expose it with the stream
  * itself as a target for simplicity.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
 ReadableStreamClose(JSContext* cx, HandleObject stream);
 
 /**
  * Returns true if the given ReadableStream reader is locked, false otherwise.
  *
- * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or
- * ReadableStreamBYOBReader instance.
+ * Asserts that |reader| is a ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader object or an unwrappable wrapper for one.
  */
 extern JS_PUBLIC_API(bool)
-ReadableStreamReaderIsClosed(const JSObject* reader);
+ReadableStreamReaderIsClosed(JSContext* cx, const JSObject* reader, bool* result);
 
 /**
  * Enqueues the given chunk in the given ReadableStream.
  *
  * Throws a TypeError and returns false if the enqueing operation fails.
  *
  * Note: This is semantically equivalent to the |enqueue| method on
  * the stream controller's prototype in JS. We expose it with the stream
  * itself as a target for simplicity.
  *
  * If the ReadableStream has an underlying byte source, the given chunk must
  * be a typed array or a DataView. Consider using
  * ReadableByteStreamEnqueueBuffer.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
 ReadableStreamEnqueue(JSContext* cx, HandleObject stream, HandleValue chunk);
 
 /**
- * Enqueues the given buffer as a chunk in the given ReadableStream.
- *
- * Throws a TypeError and returns false if the enqueing operation fails.
- *
- * Note: This is semantically equivalent to the |enqueue| method on
- * the stream controller's prototype in JS. We expose it with the stream
- * itself as a target for simplicity. Additionally, the JS version only
- * takes typed arrays and ArrayBufferView instances as arguments, whereas
- * this takes an ArrayBuffer, obviating the need to wrap it into a typed
- * array.
- *
- * Asserts that |stream| is an unwrapped ReadableStream instance and |buffer|
- * an unwrapped ArrayBuffer instance.
- */
-extern JS_PUBLIC_API(bool)
-ReadableByteStreamEnqueueBuffer(JSContext* cx, HandleObject stream, HandleObject buffer);
-
-/**
  * Errors the given ReadableStream, causing all future interactions to fail
  * with the given error value.
  *
  * Throws a TypeError and returns false if the erroring operation fails.
  *
  * Note: This is semantically equivalent to the |error| method on
  * the stream controller's prototype in JS. We expose it with the stream
  * itself as a target for simplicity.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
 ReadableStreamError(JSContext* cx, HandleObject stream, HandleValue error);
 
 /**
  * Cancels the given ReadableStream reader's associated stream.
  *
  * Throws a TypeError and returns false if the given reader isn't active.
  *
- * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or
- * ReadableStreamBYOBReader instance.
+ * Asserts that |reader| is a ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader object or an unwrappable wrapper for one.
  */
 extern JS_PUBLIC_API(bool)
 ReadableStreamReaderCancel(JSContext* cx, HandleObject reader, HandleValue reason);
 
 /**
  * Cancels the given ReadableStream reader's associated stream.
  *
  * Throws a TypeError and returns false if the given reader has pending
  * read or readInto (for default or byob readers, respectively) requests.
  *
- * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or
- * ReadableStreamBYOBReader instance.
+ * Asserts that |reader| is a ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader object or an unwrappable wrapper for one.
  */
 extern JS_PUBLIC_API(bool)
 ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader);
 
 /**
  * Requests a read from the reader's associated ReadableStream and returns the
  * resulting PromiseObject.
  *
  * Returns a Promise that's resolved with the read result once available or
  * rejected immediately if the stream is errored or the operation failed.
  *
- * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader instance.
+ * Asserts that |reader| is a ReadableStreamDefaultReader object or an
+ * unwrappable wrapper for one.
  */
 extern JS_PUBLIC_API(JSObject*)
 ReadableStreamDefaultReaderRead(JSContext* cx, HandleObject reader);
 
-/**
- * Requests a read from the reader's associated ReadableStream into the given
- * ArrayBufferView and returns the resulting PromiseObject.
- *
- * Returns a Promise that's resolved with the read result once available or
- * rejected immediately if the stream is errored or the operation failed.
- *
- * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader and
- * |view| an unwrapped typed array or DataView instance.
- */
-extern JS_PUBLIC_API(JSObject*)
-ReadableStreamBYOBReaderRead(JSContext* cx, HandleObject reader, HandleObject view);
-
 } // namespace JS
 
 #endif // js_Realm_h
--- a/js/src/builtin/ModuleObject.cpp
+++ b/js/src/builtin/ModuleObject.cpp
@@ -984,28 +984,32 @@ AssertModuleScopesMatch(ModuleObject* mo
 void
 ModuleObject::fixEnvironmentsAfterCompartmentMerge()
 {
     AssertModuleScopesMatch(this);
     initialEnvironment().fixEnclosingEnvironmentAfterCompartmentMerge(script()->global());
     AssertModuleScopesMatch(this);
 }
 
-bool
-ModuleObject::hasScript() const
+JSScript*
+ModuleObject::maybeScript() const
 {
-    // When modules are parsed via the Reflect.parse() API, the module object
-    // doesn't have a script.
-    return !getReservedSlot(ScriptSlot).isUndefined();
+    Value value = getReservedSlot(ScriptSlot);
+    if (value.isUndefined())
+        return nullptr;
+
+    return value.toGCThing()->as<JSScript>();
 }
 
 JSScript*
 ModuleObject::script() const
 {
-    return getReservedSlot(ScriptSlot).toGCThing()->as<JSScript>();
+    JSScript* ptr = maybeScript();
+    MOZ_RELEASE_ASSERT(ptr);
+    return ptr;
 }
 
 static inline void
 AssertValidModuleStatus(ModuleStatus status)
 {
     MOZ_ASSERT(status >= MODULE_STATUS_UNINSTANTIATED &&
                status <= MODULE_STATUS_EVALUATED_ERROR);
 }
@@ -1152,16 +1156,21 @@ ModuleObject::execute(JSContext* cx, Han
 #ifdef DEBUG
     MOZ_ASSERT(self->status() == MODULE_STATUS_EVALUATING);
     if (!AssertFrozen(cx, self)) {
         return false;
     }
 #endif
 
     RootedScript script(cx, self->script());
+
+    // The top-level script if a module is only ever executed once. Clear the
+    // reference to prevent us keeping this alive unnecessarily.
+    self->setReservedSlot(ScriptSlot, UndefinedValue());
+
     RootedModuleEnvironmentObject scope(cx, self->environment());
     if (!scope) {
         JS_ReportErrorASCII(cx, "Module declarations have not yet been instantiated");
         return false;
     }
 
     return Execute(cx, script, *scope, rval.address());
 }
--- a/js/src/builtin/ModuleObject.h
+++ b/js/src/builtin/ModuleObject.h
@@ -297,16 +297,17 @@ class ModuleObject : public NativeObject
                               HandleArrayObject indiretExportEntries,
                               HandleArrayObject starExportEntries);
     static bool Freeze(JSContext* cx, HandleModuleObject self);
 #ifdef DEBUG
     static bool AssertFrozen(JSContext* cx, HandleModuleObject self);
 #endif
     void fixEnvironmentsAfterCompartmentMerge();
 
+    JSScript* maybeScript() const;
     JSScript* script() const;
     Scope* enclosingScope() const;
     ModuleEnvironmentObject& initialEnvironment() const;
     ModuleEnvironmentObject* environment() const;
     ModuleNamespaceObject* namespace_();
     ModuleStatus status() const;
     bool hadEvaluationError() const;
     Value evaluationError() const;
@@ -343,17 +344,16 @@ class ModuleObject : public NativeObject
                                                   HandleObject exports);
 
   private:
     static const ClassOps classOps_;
 
     static void trace(JSTracer* trc, JSObject* obj);
     static void finalize(js::FreeOp* fop, JSObject* obj);
 
-    bool hasScript() const;
     bool hasImportBindings() const;
     FunctionDeclarationVector* functionDeclarations();
 };
 
 // Process a module's parse tree to collate the import and export data used when
 // creating a ModuleObject.
 class MOZ_STACK_CLASS ModuleBuilder
 {
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -3317,26 +3317,21 @@ PromiseThenNewPromiseCapability(JSContex
         }
     }
 
     return true;
 }
 
 // ES2016, 25.4.5.3., steps 3-5.
 MOZ_MUST_USE bool
-js::OriginalPromiseThen(JSContext* cx, Handle<PromiseObject*> promise,
+js::OriginalPromiseThen(JSContext* cx, HandleObject promiseObj,
                         HandleValue onFulfilled, HandleValue onRejected,
                         MutableHandleObject dependent, CreateDependentPromise createDependent)
 {
-    RootedObject promiseObj(cx, promise);
-    if (promise->compartment() != cx->compartment()) {
-        if (!cx->compartment()->wrap(cx, &promiseObj)) {
-            return false;
-        }
-    }
+    Rooted<PromiseObject*> promise(cx, &CheckedUnwrap(promiseObj)->as<PromiseObject>());
 
     // Steps 3-4.
     Rooted<PromiseCapability> resultCapability(cx);
     if (!PromiseThenNewPromiseCapability(cx, promiseObj, createDependent, &resultCapability)) {
         return false;
     }
 
     // Step 5.
@@ -4057,40 +4052,37 @@ Promise_then_impl(JSContext* cx, HandleV
     }
 
     // Fast path when the default Promise state is intact.
     if (CanCallOriginalPromiseThenBuiltin(cx, promiseVal)) {
         return OriginalPromiseThenBuiltin(cx, promiseVal, onFulfilled, onRejected, rval, rvalUsed);
     }
 
     RootedObject promiseObj(cx, &promiseVal.toObject());
-    Rooted<PromiseObject*> promise(cx);
-
-    if (promiseObj->is<PromiseObject>()) {
-        promise = &promiseObj->as<PromiseObject>();
-    } else {
+
+    if (!promiseObj->is<PromiseObject>()) {
         JSObject* unwrappedPromiseObj = CheckedUnwrap(promiseObj);
         if (!unwrappedPromiseObj) {
             ReportAccessDenied(cx);
             return false;
         }
         if (!unwrappedPromiseObj->is<PromiseObject>()) {
-            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
-                                      "Promise", "then", "value");
+            JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                                       "Promise", "then",
+                                       InformalValueTypeName(ObjectValue(*promiseObj)));
             return false;
         }
-        promise = &unwrappedPromiseObj->as<PromiseObject>();
     }
 
     // Steps 3-5.
     CreateDependentPromise createDependent = rvalUsed
                                              ? CreateDependentPromise::Always
                                              : CreateDependentPromise::SkipIfCtorUnobservable;
     RootedObject resultPromise(cx);
-    if (!OriginalPromiseThen(cx, promise, onFulfilled, onRejected, &resultPromise,
+    if (!OriginalPromiseThen(cx, promiseObj, onFulfilled, onRejected, &resultPromise,
                              createDependent))
     {
         return false;
     }
 
     if (rvalUsed) {
         rval.setObject(*resultPromise);
     } else {
--- a/js/src/builtin/Promise.h
+++ b/js/src/builtin/Promise.h
@@ -203,19 +203,21 @@ enum class CreateDependentPromise {
  * Enqueues resolve/reject reactions in the given Promise's reactions lists
  * as though calling the original value of Promise.prototype.then.
  *
  * If the `createDependent` flag is not set, no dependent Promise will be
  * created. This is used internally to implement DOM functionality.
  * Note: In this case, the reactions pushed using this function contain a
  * `promise` field that can contain null. That field is only ever used by
  * devtools, which have to treat these reactions specially.
+ *
+ * Asserts that `promiseObj` is a, maybe wrapped, instance of Promise.
  */
 MOZ_MUST_USE bool
-OriginalPromiseThen(JSContext* cx, Handle<PromiseObject*> promise,
+OriginalPromiseThen(JSContext* cx, HandleObject promiseObj,
                     HandleValue onFulfilled, HandleValue onRejected,
                     MutableHandleObject dependent, CreateDependentPromise createDependent);
 
 /**
  * PromiseResolve ( C, x )
  *
  * The abstract operation PromiseResolve, given a constructor and a value,
  * returns a new promise resolved with that value.
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -7,64 +7,135 @@
 #include "builtin/Stream.h"
 
 #include "js/Stream.h"
 
 #include "gc/Heap.h"
 #include "vm/JSContext.h"
 #include "vm/SelfHosting.h"
 
+#include "vm/Compartment-inl.h"
 #include "vm/List-inl.h"
 #include "vm/NativeObject-inl.h"
 
 using namespace js;
 
+/**
+ * Memory layout of Stream instances.
+ *
+ * See https://streams.spec.whatwg.org/#rs-internal-slots for details on the
+ * stored state. [[state]] and [[disturbed]] are stored in StreamSlot_State as
+ * ReadableStream::State enum values.
+ *
+ * Of the stored values, Reader and StoredError might be cross-compartment
+ * wrappers. This can happen if the Reader was created by applying a different
+ * compartment's ReadableStream.prototype.getReader method.
+ *
+ * A stream's associated controller is always created from under the stream's
+ * constructor and thus cannot be in a different compartment.
+ */
 enum StreamSlots {
     StreamSlot_Controller,
     StreamSlot_Reader,
     StreamSlot_State,
     StreamSlot_StoredError,
     StreamSlotCount
 };
 
+/**
+ * Memory layout of Stream Reader instances.
+ *
+ * See https://streams.spec.whatwg.org/#default-reader-internal-slots and
+ * https://streams.spec.whatwg.org/#byob-reader-internal-slots for details.
+ *
+ * Note that [[readRequests]] and [[readIntoRequests]] are treated the same in
+ * our implementation.
+ *
+ * Of the stored values, Stream and ClosedPromise might be cross-compartment
+ * wrapper wrappers.
+ *
+ * For Stream, this can happen if the Reader was created by applying a
+ * different compartment's ReadableStream.prototype.getReader method.
+ *
+ * For ClosedPromise, it can be caused by applying a different compartment's
+ * ReadableStream*Reader.prototype.releaseLock method.
+ *
+ * Requests is guaranteed to be in the same compartment as the Reader, but can
+ * contain wrapped request objects from other globals.
+ */
 enum ReaderSlots {
     ReaderSlot_Stream,
     ReaderSlot_Requests,
     ReaderSlot_ClosedPromise,
     ReaderSlotCount,
 };
 
 enum ReaderType {
     ReaderType_Default,
     ReaderType_BYOB
 };
 
-// ReadableStreamDefaultController and ReadableByteStreamController are both
-// queue containers and must have these slots at identical offsets.
+/**
+ * Memory layout for queue containers.
+ *
+ * Both ReadableStreamDefaultController and ReadableByteStreamController are
+ * queue containers and must have these slots at identical offsets.
+ *
+ * The queue is guaranteed to be in the  same compartment as the container,
+ * but might contain wrappers for objects from other compartments.
+ */
 enum QueueContainerSlots {
     QueueContainerSlot_Queue,
     QueueContainerSlot_TotalSize,
     QueueContainerSlotCount
 };
 
-// These slots are identical between the two types of ReadableStream
-// controllers.
+/**
+ * Memory layout for ReadableStream controllers, starting after the slots
+ * reserved for queue container usage.
+ *
+ * UnderlyingSource is usually treated as an opaque value. It might be a
+ * wrapped object from another compartment, but that case is handled
+ * correctly by all operations the controller might invoke on it.
+ *
+ * The only case where we don't treat underlyingSource as an opaque value is
+ * if it's a TeeState. All functions operating on TeeState properly handle
+ * TeeState instances from other compartments.
+ *
+ * StrategyHWM and Flags are both primitive (numeric) values.
+ */
 enum ControllerSlots {
     ControllerSlot_Stream = QueueContainerSlotCount,
     ControllerSlot_UnderlyingSource,
     ControllerSlot_StrategyHWM,
     ControllerSlot_Flags,
     ControllerSlotCount
 };
 
+/**
+ * Memory layout for ReadableStreamDefaultControllers, starting after the
+ * slots shared among all types of controllers.
+ *
+ * StrategySize is treated as an opaque value when stored. The only use site
+ * ensures that it's wrapped into the current cx compartment.
+ */
 enum DefaultControllerSlots {
     DefaultControllerSlot_StrategySize = ControllerSlotCount,
     DefaultControllerSlotCount
 };
 
+/**
+ * Memory layout for ReadableByteStreamControllers, starting after the
+ * slots shared among all types of controllers.
+ *
+ * PendingPullIntos is guaranteed to be in the  same compartment as the
+ * controller, but might contain wrappers for objects from other compartments.
+ *
+ * AutoAllocateSize is a primitive (numeric) value.
+ */
 enum ByteControllerSlots {
     ByteControllerSlot_BYOBRequest = ControllerSlotCount,
     ByteControllerSlot_PendingPullIntos,
     ByteControllerSlot_AutoAllocateSize,
     ByteControllerSlotCount
 };
 
 enum ControllerFlags {
@@ -90,44 +161,50 @@ enum BYOBRequestSlots {
 
 template<class T>
 MOZ_ALWAYS_INLINE bool
 Is(const HandleValue v)
 {
     return v.isObject() && v.toObject().is<T>();
 }
 
-#ifdef DEBUG
-static bool
-IsReadableStreamController(const JSObject* controller)
+template<class T>
+MOZ_ALWAYS_INLINE bool
+IsMaybeWrapped(const HandleValue v)
 {
-    return controller->is<ReadableStreamDefaultController>() ||
-           controller->is<ReadableByteStreamController>();
+    if (!v.isObject()) {
+        return false;
+    }
+    JSObject* obj = &v.toObject();
+    if (obj->is<T>()) {
+        return true;
+    }
+    obj = CheckedUnwrap(obj);
+    if (!obj) {
+        return false;
+    }
+    return obj->is<T>();
 }
-#endif // DEBUG
 
 static inline uint32_t
-ControllerFlags(const NativeObject* controller)
+ControllerFlags(const ReadableStreamController* controller)
 {
-    MOZ_ASSERT(IsReadableStreamController(controller));
     return controller->getFixedSlot(ControllerSlot_Flags).toInt32();
 }
 
 static inline void
-AddControllerFlags(NativeObject* controller, uint32_t flags)
+AddControllerFlags(ReadableStreamController* controller, uint32_t flags)
 {
-    MOZ_ASSERT(IsReadableStreamController(controller));
     controller->setFixedSlot(ControllerSlot_Flags,
                              Int32Value(ControllerFlags(controller) | flags));
 }
 
 static inline void
-RemoveControllerFlags(NativeObject* controller, uint32_t flags)
+RemoveControllerFlags(ReadableStreamController* controller, uint32_t flags)
 {
-    MOZ_ASSERT(IsReadableStreamController(controller));
     controller->setFixedSlot(ControllerSlot_Flags,
                              Int32Value(ControllerFlags(controller) & ~flags));
 }
 
 static inline uint32_t
 StreamState(const ReadableStream* stream)
 {
     return stream->getFixedSlot(StreamSlot_State).toInt32();
@@ -161,84 +238,245 @@ ReadableStream::errored() const
 
 bool
 ReadableStream::disturbed() const
 {
     return StreamState(this) & Disturbed;
 }
 
 inline static bool
-ReaderHasStream(const NativeObject* reader)
+ReaderHasStream(const ReadableStreamReader* reader)
 {
-    MOZ_ASSERT(JS::IsReadableStreamReader(reader));
     return !reader->getFixedSlot(ReaderSlot_Stream).isUndefined();
 }
 
 bool
 js::ReadableStreamReaderIsClosed(const JSObject* reader)
 {
-    return !ReaderHasStream(&reader->as<NativeObject>());
+    return !ReaderHasStream(&reader->as<ReadableStreamReader>());
 }
 
 inline static MOZ_MUST_USE ReadableStream*
-StreamFromController(const NativeObject* controller)
+StreamFromController(const ReadableStreamController* controller)
 {
-    MOZ_ASSERT(IsReadableStreamController(controller));
     return &controller->getFixedSlot(ControllerSlot_Stream).toObject().as<ReadableStream>();
 }
 
-inline static MOZ_MUST_USE NativeObject*
+inline static MOZ_MUST_USE ReadableStreamController*
 ControllerFromStream(const ReadableStream* stream)
 {
-    Value controllerVal = stream->getFixedSlot(StreamSlot_Controller);
-    MOZ_ASSERT(IsReadableStreamController(&controllerVal.toObject()));
-    return &controllerVal.toObject().as<NativeObject>();
+    return &stream->getFixedSlot(StreamSlot_Controller).toObject().as<ReadableStreamController>();
 }
 
 inline static bool
 HasController(const ReadableStream* stream)
 {
     return !stream->getFixedSlot(StreamSlot_Controller).isUndefined();
 }
 
 JS::ReadableStreamMode
 ReadableStream::mode() const
 {
-    NativeObject* controller = ControllerFromStream(this);
+    ReadableStreamController* controller = ControllerFromStream(this);
     if (controller->is<ReadableStreamDefaultController>()) {
         return JS::ReadableStreamMode::Default;
     }
     return controller->as<ReadableByteStreamController>().hasExternalSource()
            ? JS::ReadableStreamMode::ExternalSource
            : JS::ReadableStreamMode::Byte;
 }
 
-inline static MOZ_MUST_USE ReadableStream*
-StreamFromReader(const NativeObject* reader)
+template <>
+inline bool
+JSObject::is<ReadableStreamController>() const
+{
+    return is<ReadableStreamDefaultController>() || is<ReadableByteStreamController>();
+}
+
+template <>
+inline bool
+JSObject::is<ReadableStreamReader>() const
+{
+    return is<ReadableStreamDefaultReader>();
+}
+
+/**
+ * Checks that |obj| is an unwrapped instance of T or throws an error.
+ *
+ * This overload must only be used if the caller can ensure that failure to
+ * unwrap is the only possible source of exceptions.
+ */
+template<class T>
+static MOZ_ALWAYS_INLINE T*
+ToUnwrapped(JSContext* cx, JSObject* obj)
+{
+    if (IsWrapper(obj)) {
+        obj = CheckedUnwrap(obj);
+        if (!obj) {
+            ReportAccessDenied(cx);
+            return nullptr;
+        }
+    }
+
+    return &obj->as<T>();
+}
+
+/**
+ * Checks that |obj| is an unwrapped instance of T or throws an error.
+ */
+template<class T>
+static MOZ_ALWAYS_INLINE T*
+ToUnwrapped(JSContext* cx, JSObject* obj, const char* description)
+{
+    if (IsWrapper(obj)) {
+        obj = CheckedUnwrap(obj);
+        if (!obj) {
+            ReportAccessDenied(cx);
+            return nullptr;
+        }
+    }
+
+    if (!obj->is<T>()) {
+        JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,
+                                   description, T::class_->name,
+                                   InformalValueTypeName(ObjectValue(*obj)));
+        return nullptr;
+    }
+
+    return &obj->as<T>();
+}
+
+/**
+ * Checks that |obj| is an unwrapped instance of T or throws an error.
+ *
+ * If the caller can ensure that failure to unwrap is the only possible
+ * source of exceptions, it may omit the error messages.
+ */
+template<class T>
+static MOZ_ALWAYS_INLINE T*
+ToUnwrapped(JSContext* cx, JSObject* obj, const char* className, const char* methodName)
+{
+    if (IsWrapper(obj)) {
+        obj = CheckedUnwrap(obj);
+        if (!obj) {
+            ReportAccessDenied(cx);
+            return nullptr;
+        }
+    }
+
+    if (!obj->is<T>()) {
+        JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                                   className, methodName, InformalValueTypeName(ObjectValue(*obj)));
+        return nullptr;
+    }
+
+    return &obj->as<T>();
+}
+
+/**
+ * Converts the given value to an object and ensures that it is an unwrapped
+ * instance of T.
+ *
+ * Throws exceptions if the value isn't an object, cannot be unwrapped, or
+ * isn't an instance of the expected type.
+ *
+ * If the caller can ensure that failure to unwrap is the only possible
+ * source of exceptions, it may omit the error messages.
+ */
+template<class T>
+static MOZ_ALWAYS_INLINE T*
+ToUnwrapped(JSContext* cx,
+            HandleValue val,
+            const char* className = "",
+            const char* methodName = "")
+{
+    if (!val.isObject()) {
+        JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                                   className, methodName, InformalValueTypeName(val));
+        return nullptr;
+    }
+
+    return ToUnwrapped<T>(cx, &val.toObject(), className, methodName);
+}
+
+/**
+ * Returns the stream associated with the given reader.
+ *
+ * The returned object will always be unwrapped, but might be from another
+ * compartment.
+ *
+ * If the stream cannot be unwrapped, which can only happen if it's a dead
+ * wrapper object, a nullptr is returned. If a JSContext is available, this
+ * case will also report an exception. The JSContext isn't mandatory to make
+ * use easier for call sites that don't otherwise need a JSContext and can
+ * provide useful defaults in case the stream is a dead wrapper.
+ */
+MOZ_ALWAYS_INLINE static MOZ_MUST_USE ReadableStream*
+StreamFromReader(JSContext *maybeCx, const ReadableStreamReader* reader)
 {
     MOZ_ASSERT(ReaderHasStream(reader));
-    return &reader->getFixedSlot(ReaderSlot_Stream).toObject().as<ReadableStream>();
+    JSObject* streamObj = &reader->getFixedSlot(ReaderSlot_Stream).toObject();
+    if (IsProxy(streamObj)) {
+        if (JS_IsDeadWrapper(streamObj)) {
+            if (maybeCx) {
+                JS_ReportErrorNumberASCII(maybeCx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT);
+            }
+            return nullptr;
+        }
+
+        // It's ok to do an unchecked unwrap here: the stream wouldn't have
+        // been stored on the reader if it couldn't be unwrapped.
+        streamObj = UncheckedUnwrap(streamObj);
+    }
+    return &streamObj->as<ReadableStream>();
 }
 
-inline static MOZ_MUST_USE NativeObject*
-ReaderFromStream(const NativeObject* stream)
+/**
+ * Returns the reader associated with the given stream.
+ *
+ * Must only be called on ReadableStreams that already have a reader
+ * associated with them.
+ *
+ * If the reader is a wrapper, it will be unwrapped, so it might not be an
+ * object from the currently active compartment.
+ *
+ * If the reader cannot be unwrapped, which can only happen if it's a dead
+ * wrapper object, a nullptr is returned. If a JSContext is available, an
+ * exception is reported, too. The JSContext isn't mandatory to make use
+ * easier for call sites that don't otherwise need a JSContext and can provide
+ * useful defaults in case the reader is a dead object wrapper.
+ */
+MOZ_ALWAYS_INLINE static MOZ_MUST_USE ReadableStreamReader*
+ReaderFromStream(JSContext* maybeCx, const ReadableStream* stream)
 {
-    Value readerVal = stream->getFixedSlot(StreamSlot_Reader);
-    MOZ_ASSERT(JS::IsReadableStreamReader(&readerVal.toObject()));
-    return &readerVal.toObject().as<NativeObject>();
+    JSObject* readerObj = &stream->getFixedSlot(StreamSlot_Reader).toObject();
+    if (IsProxy(readerObj)) {
+        if (JS_IsDeadWrapper(readerObj)) {
+            if (maybeCx) {
+                JS_ReportErrorNumberASCII(maybeCx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT);
+            }
+            return nullptr;
+        }
+
+        // It's ok to do an unchecked unwrap here: the reader wouldn't have
+        // been stored on the stream if it couldn't be unwrapped.
+        readerObj = UncheckedUnwrap(readerObj);
+    }
+
+    return &readerObj->as<ReadableStreamReader>();
 }
 
 inline static bool
 HasReader(const ReadableStream* stream)
 {
     return !stream->getFixedSlot(StreamSlot_Reader).isUndefined();
 }
 
 inline static MOZ_MUST_USE JSFunction*
-NewHandler(JSContext *cx, Native handler, HandleObject target)
+NewHandler(JSContext* cx, Native handler, HandleObject target)
 {
     RootedAtom funName(cx, cx->names().empty);
     RootedFunction handlerFun(cx, NewNativeFunction(cx, handler, 0, funName,
                                                     gc::AllocKind::FUNCTION_EXTENDED,
                                                     GenericObject));
     if (!handlerFun) {
         return nullptr;
     }
@@ -249,17 +487,17 @@ NewHandler(JSContext *cx, Native handler
 template<class T>
 inline static MOZ_MUST_USE T*
 TargetFromHandler(JSObject& handler)
 {
     return &handler.as<JSFunction>().getExtendedSlot(0).toObject().as<T>();
 }
 
 inline static MOZ_MUST_USE bool
-ResetQueue(JSContext* cx, HandleNativeObject container);
+ResetQueue(JSContext* cx, Handle<ReadableStreamController*> container);
 
 inline static MOZ_MUST_USE bool
 InvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg,
              MutableHandleValue rval);
 
 static MOZ_MUST_USE JSObject*
 PromiseInvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg);
 
@@ -302,29 +540,33 @@ ReturnPromiseRejectedWithPendingError(JS
     if (!promise) {
         return false;
     }
 
     args.rval().setObject(*promise);
     return true;
 }
 
-static MOZ_MUST_USE bool
-RejectNonGenericMethod(JSContext* cx, const CallArgs& args,
-                       const char* className, const char* methodName)
-{
-    ReportValueError(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(),
-                     nullptr, className, methodName);
-
-    return ReturnPromiseRejectedWithPendingError(cx, args);
-}
-
+/**
+ * Creates a NativeObject to be used as a list and stores it on the given
+ * container at the given fixed slot offset.
+ *
+ * Note: to make handling of lists easier, SetNewList ensures that the list
+ * is created in the container's compartment. If the container isn't from the
+ * currently entered compartment, then it's compartment is entered prior to
+ * creating the list. The list is returned unwrapped in that case, so won't
+ * be in the currently entered compartment, either.
+ */
 inline static MOZ_MUST_USE NativeObject*
 SetNewList(JSContext* cx, HandleNativeObject container, uint32_t slot)
 {
+    mozilla::Maybe<AutoRealm> ar;
+    if (container->compartment() != cx->compartment()) {
+        ar.emplace(cx, container);
+    }
     NativeObject* list = NewList(cx);
     if (!list) {
         return nullptr;
     }
     container->setFixedSlot(slot, ObjectValue(*list));
     return list;
 }
 
@@ -459,29 +701,48 @@ class QueueEntry : public NativeObject
 const Class QueueEntry::class_ = {
     "QueueEntry",
     JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
 };
 
 class TeeState : public NativeObject
 {
   private:
+    /**
+     * Memory layout for TeeState instances.
+     *
+     * The Reason1 and Reason2 slots store opaque values, which might be
+     * wrapped objects from other compartments. Since we don't treat them as
+     * objects in Streams-specific code, we don't have to worry about that
+     * apart from ensuring that the values are properly wrapped before storing
+     * them.
+     *
+     * Promise is always created in TeeState::create below, so is guaranteed
+     * to be in the same compartment as the TeeState instance itself.
+     *
+     * Stream can be from another compartment. It is automatically wrapped
+     * before storing it and unwrapped upon retrieval. That means that
+     * TeeState consumers need to be able to deal with unwrapped
+     * ReadableStream instances from non-current compartments.
+     *
+     * Branch1 and Branch2 are always created in the same compartment as the
+     * TeeState instance, so cannot be from another compartment.
+     */
     enum Slots {
         Slot_Flags = 0,
         Slot_Reason1,
         Slot_Reason2,
         Slot_Promise,
         Slot_Stream,
         Slot_Branch1,
         Slot_Branch2,
         SlotCount
     };
 
-    enum Flags
-    {
+    enum Flags {
         Flag_ClosedOrErrored = 1 << 0,
         Flag_Canceled1 =       1 << 1,
         Flag_Canceled2 =       1 << 2,
         Flag_CloneForBranch2 = 1 << 3,
     };
     uint32_t flags() const { return getFixedSlot(Slot_Flags).toInt32(); }
     void setFlags(uint32_t flags) { setFixedSlot(Slot_Flags, Int32Value(flags)); }
 
@@ -518,21 +779,20 @@ class TeeState : public NativeObject
     Value reason2() const {
         MOZ_ASSERT(canceled2());
         return getFixedSlot(Slot_Reason2);
     }
 
     PromiseObject* promise() {
         return &getFixedSlot(Slot_Promise).toObject().as<PromiseObject>();
     }
-    ReadableStream* stream() {
-        return &getFixedSlot(Slot_Stream).toObject().as<ReadableStream>();
-    }
-    ReadableStreamDefaultReader* reader() {
-        return &ReaderFromStream(stream())->as<ReadableStreamDefaultReader>();
+
+    static MOZ_MUST_USE ReadableStream* stream(JSContext* cx, TeeState* teeState) {
+        RootedValue streamVal(cx, teeState->getFixedSlot(Slot_Stream));
+        return ToUnwrapped<ReadableStream>(cx, streamVal);
     }
 
     ReadableStreamDefaultController* branch1() {
         ReadableStreamDefaultController* controller = &getFixedSlot(Slot_Branch1).toObject()
                                                        .as<ReadableStreamDefaultController>();
         MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch);
         MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch1);
         return controller;
@@ -564,17 +824,21 @@ class TeeState : public NativeObject
 
         Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
         if (!promise) {
             return nullptr;
         }
 
         state->setFixedSlot(Slot_Flags, Int32Value(0));
         state->setFixedSlot(Slot_Promise, ObjectValue(*promise));
-        state->setFixedSlot(Slot_Stream, ObjectValue(*stream));
+        RootedObject wrappedStream(cx, stream);
+        if (!cx->compartment()->wrap(cx, &wrappedStream)) {
+            return nullptr;
+        }
+        state->setFixedSlot(Slot_Stream, ObjectValue(*wrappedStream));
 
         return state;
    }
 };
 
 const Class TeeState::class_ = {
     "TeeState",
     JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
@@ -657,60 +921,29 @@ ReadableStream::createDefaultStream(JSCo
 
     stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
 
     return stream;
 }
 
 static MOZ_MUST_USE ReadableByteStreamController*
 CreateReadableByteStreamController(JSContext* cx, Handle<ReadableStream*> stream,
-                                   HandleValue underlyingByteSource,
-                                   HandleValue highWaterMarkVal);
-
-// Streams spec, 3.2.3., steps 1-4, 7.
-ReadableStream*
-ReadableStream::createByteStream(JSContext* cx, HandleValue underlyingSource,
-                                 HandleValue highWaterMark, HandleObject proto /* = nullptr */)
-{
-    // Steps 1-4.
-    Rooted<ReadableStream*> stream(cx, createStream(cx, proto));
-    if (!stream) {
-        return nullptr;
-    }
-
-    // Step 7.b: Set this.[[readableStreamController]] to
-    //           ? Construct(ReadableByteStreamController,
-    //                       « this, underlyingSource, highWaterMark »).
-    RootedObject controller(cx, CreateReadableByteStreamController(cx, stream,
-                                                                   underlyingSource,
-                                                                   highWaterMark));
-    if (!controller) {
-        return nullptr;
-    }
-
-    stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
-
-    return stream;
-}
-
-static MOZ_MUST_USE ReadableByteStreamController*
-CreateReadableByteStreamController(JSContext* cx, Handle<ReadableStream*> stream,
                                    void* underlyingSource);
 
 ReadableStream*
 ReadableStream::createExternalSourceStream(JSContext* cx, void* underlyingSource,
                                            uint8_t flags, HandleObject proto /* = nullptr */)
 {
     Rooted<ReadableStream*> stream(cx, createStream(cx, proto));
     if (!stream) {
         return nullptr;
     }
 
-    RootedNativeObject controller(cx, CreateReadableByteStreamController(cx, stream,
-                                                                         underlyingSource));
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = CreateReadableByteStreamController(cx, stream, underlyingSource);
     if (!controller) {
         return nullptr;
     }
 
     stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
     AddControllerFlags(controller, flags << ControllerEmbeddingFlagsOffset);
 
     return stream;
@@ -776,17 +1009,19 @@ ReadableStream::constructor(JSContext* c
 
     Rooted<ReadableStream*> stream(cx);
 
     // Step 7: If typeString is "bytes",
     if (!notByteStream) {
         // Step 7.b: Set this.[[readableStreamController]] to
         //           ? Construct(ReadableByteStreamController,
         //                       « this, underlyingSource, highWaterMark »).
-        stream = createByteStream(cx, underlyingSource, highWaterMark);
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED);
+        return false;
     } else if (typeVal.isUndefined()) {
         // Step 8: Otherwise, if type is undefined,
         // Step 8.b: Set this.[[readableStreamController]] to
         //           ? Construct(ReadableStreamDefaultController,
         //                       « this, underlyingSource, size, highWaterMark »).
         stream = createDefaultStream(cx, underlyingSource, size, highWaterMark);
     } else {
         // Step 9: Otherwise, throw a RangeError exception.
@@ -801,45 +1036,45 @@ ReadableStream::constructor(JSContext* c
     args.rval().setObject(*stream);
     return true;
 }
 
 // Streams spec, 3.2.4.1. get locked
 static MOZ_MUST_USE bool
 ReadableStream_locked_impl(JSContext* cx, const CallArgs& args)
 {
-    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
+    Rooted<ReadableStream*> stream(cx);
+    stream = &UncheckedUnwrap(&args.thisv().toObject())->as<ReadableStream>();
 
     // Step 2: Return ! IsReadableStreamLocked(this).
     args.rval().setBoolean(stream->locked());
     return true;
 }
 
 static bool
 ReadableStream_locked(JSContext* cx, unsigned argc, Value* vp)
 {
     // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
     CallArgs args = CallArgsFromVp(argc, vp);
-    return CallNonGenericMethod<Is<ReadableStream>, ReadableStream_locked_impl>(cx, args);
+    return CallNonGenericMethod<IsMaybeWrapped<ReadableStream>, ReadableStream_locked_impl>(cx,
+                                                                                            args);
 }
 
 // Streams spec, 3.2.4.2. cancel ( reason )
 static MOZ_MUST_USE bool
 ReadableStream_cancel(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
+
     // Step 1: If ! IsReadableStream(this) is false, return a promise rejected
     //         with a TypeError exception.
-    if (!Is<ReadableStream>(args.thisv())) {
-        ReportValueError(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(),
-                         nullptr, "cancel", "");
+    Rooted<ReadableStream*> stream(cx);
+    stream = ToUnwrapped<ReadableStream>(cx, args.thisv(), "ReadableStream", "cancel");
+    if (!stream)
         return ReturnPromiseRejectedWithPendingError(cx, args);
-    }
-
-    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
 
     // Step 2: If ! IsReadableStreamLocked(this) is true, return a promise
     //         rejected with a TypeError exception.
     if (stream->locked()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAM_LOCKED_METHOD, "cancel");
         return ReturnPromiseRejectedWithPendingError(cx, args);
     }
@@ -851,24 +1086,28 @@ ReadableStream_cancel(JSContext* cx, uns
     }
     args.rval().setObject(*cancelPromise);
     return true;
 }
 
 static MOZ_MUST_USE ReadableStreamDefaultReader*
 CreateReadableStreamDefaultReader(JSContext* cx, Handle<ReadableStream*> stream);
 
-static MOZ_MUST_USE ReadableStreamBYOBReader*
-CreateReadableStreamBYOBReader(JSContext* cx, Handle<ReadableStream*> stream);
-
 // Streams spec, 3.2.4.3. getReader()
-static MOZ_MUST_USE bool
-ReadableStream_getReader_impl(JSContext* cx, const CallArgs& args)
+static bool
+ReadableStream_getReader(JSContext* cx, unsigned argc, Value* vp)
 {
-    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
+    Rooted<ReadableStream*> stream(cx);
+    stream = ToUnwrapped<ReadableStream>(cx, args.thisv(), "ReadableStream", "getReader");
+    if (!stream)
+        return false;
+
     RootedObject reader(cx);
 
     // Step 2: If mode is undefined, return
     //         ? AcquireReadableStreamDefaultReader(this).
     RootedValue modeVal(cx);
     HandleValue optionsVal = args.get(0);
     if (!optionsVal.isUndefined()) {
         if (!GetProperty(cx, optionsVal, cx->names().mode, &modeVal)) {
@@ -890,48 +1129,40 @@ ReadableStream_getReader_impl(JSContext*
         if (!CompareStrings(cx, mode, cx->names().byob, &notByob)) {
             return false;
         }
         if (notByob) {
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                       JSMSG_READABLESTREAM_INVALID_READER_MODE);
             // Step 5: Throw a RangeError exception.
             return false;
-
         }
-        reader = CreateReadableStreamBYOBReader(cx, stream);
+
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED);
     }
 
     // Reordered second part of steps 2 and 4.
     if (!reader) {
         return false;
     }
     args.rval().setObject(*reader);
     return true;
 }
 
-static bool
-ReadableStream_getReader(JSContext* cx, unsigned argc, Value* vp)
-{
-    // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
-    CallArgs args = CallArgsFromVp(argc, vp);
-    return CallNonGenericMethod<Is<ReadableStream>, ReadableStream_getReader_impl>(cx, args);
-}
-
 // Streams spec, 3.2.4.4. pipeThrough({ writable, readable }, options)
 static MOZ_MUST_USE bool
 ReadableStream_pipeThrough(JSContext* cx, unsigned argc, Value* vp)
 {
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                               JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, "pipeThrough");
     return false;
     // // Step 1: Perform ? Invoke(this, "pipeTo", « writable, options »).
 
     // // Step 2: Return readable.
-    // return readable;
 }
 
 // Streams spec, 3.2.4.5. pipeTo(dest, { preventClose, preventAbort, preventCancel } = {})
 // TODO: Unimplemented since spec is not complete yet.
 static MOZ_MUST_USE bool
 ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp)
 {
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
@@ -939,20 +1170,26 @@ ReadableStream_pipeTo(JSContext* cx, uns
     return false;
 }
 
 static MOZ_MUST_USE bool
 ReadableStreamTee(JSContext* cx, Handle<ReadableStream*> stream, bool cloneForBranch2,
                   MutableHandle<ReadableStream*> branch1, MutableHandle<ReadableStream*> branch2);
 
 // Streams spec, 3.2.4.6. tee()
-static MOZ_MUST_USE bool
-ReadableStream_tee_impl(JSContext* cx, const CallArgs& args)
+static bool
+ReadableStream_tee(JSContext* cx, unsigned argc, Value* vp)
 {
-    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
+    Rooted<ReadableStream*> stream(cx);
+    stream = ToUnwrapped<ReadableStream>(cx, args.thisv(), "ReadableStream", "tee");
+    if (!stream)
+        return false;
 
     // Step 2: Let branches be ? ReadableStreamTee(this, false).
     Rooted<ReadableStream*> branch1(cx);
     Rooted<ReadableStream*> branch2(cx);
     if (!ReadableStreamTee(cx, stream, false, &branch1, &branch2)) {
         return false;
     }
 
@@ -964,24 +1201,16 @@ ReadableStream_tee_impl(JSContext* cx, c
     branches->setDenseInitializedLength(2);
     branches->initDenseElement(0, ObjectValue(*branch1));
     branches->initDenseElement(1, ObjectValue(*branch2));
 
     args.rval().setObject(*branches);
     return true;
 }
 
-static bool
-ReadableStream_tee(JSContext* cx, unsigned argc, Value* vp)
-{
-    // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception
-    CallArgs args = CallArgsFromVp(argc, vp);
-    return CallNonGenericMethod<Is<ReadableStream>, ReadableStream_tee_impl>(cx, args);
-}
-
 static const JSFunctionSpec ReadableStream_methods[] = {
     JS_FN("cancel",         ReadableStream_cancel,      1, 0),
     JS_FN("getReader",      ReadableStream_getReader,   0, 0),
     JS_FN("pipeThrough",    ReadableStream_pipeThrough, 2, 0),
     JS_FN("pipeTo",         ReadableStream_pipeTo,      1, 0),
     JS_FN("tee",            ReadableStream_tee,         0, 0),
     JS_FS_END
 };
@@ -1128,120 +1357,175 @@ ReadableStreamTee_Pull(JSContext* cx, Ha
     // Step 1: Let reader be F.[[reader]], branch1 be F.[[branch1]],
     //         branch2 be F.[[branch2]], teeState be F.[[teeState]], and
     //         cloneForBranch2 be F.[[cloneForBranch2]].
 
     // Step 2: Return the result of transforming
     //         ! ReadableStreamDefaultReaderRead(reader) by a fulfillment
     //         handler which takes the argument result and performs the
     //         following steps:
-    Rooted<ReadableStreamDefaultReader*> reader(cx, teeState->reader());
+    Rooted<ReadableStream*> stream(cx, TeeState::stream(cx, teeState));
+    if (!stream)
+        return nullptr;
+    RootedObject readerObj(cx, ReaderFromStream(cx, stream));
+    if (!readerObj)
+        return nullptr;
+    Rooted<ReadableStreamDefaultReader*> reader(cx,
+                                                &readerObj->as<ReadableStreamDefaultReader>());
+
     RootedObject readPromise(cx, ReadableStreamDefaultReader::read(cx, reader));
     if (!readPromise) {
         return nullptr;
     }
 
     RootedObject onFulfilled(cx, NewHandler(cx, TeeReaderReadHandler, teeState));
     if (!onFulfilled) {
         return nullptr;
     }
 
     return JS::CallOriginalPromiseThen(cx, readPromise, onFulfilled, nullptr);
 }
 
+/**
+ * Cancel a tee'd stream's |branch| with the given |reason_|.
+ *
+ * Note: can operate on unwrapped values for |teeState| and |branch|.
+ *
+ * Objects created in the course of this function's operation are always
+ * created in the current cx compartment.
+ */
 static MOZ_MUST_USE JSObject*
 ReadableStreamTee_Cancel(JSContext* cx, Handle<TeeState*> teeState,
-                         Handle<ReadableStreamDefaultController*> branch, HandleValue reason)
+                         Handle<ReadableStreamDefaultController*> branch, HandleValue reason_)
 {
     // Step 1: Let stream be F.[[stream]] and teeState be F.[[teeState]].
-    Rooted<ReadableStream*> stream(cx, teeState->stream());
+    Rooted<ReadableStream*> stream(cx, TeeState::stream(cx, teeState));
+    if (!stream)
+        return nullptr;
 
     bool bothBranchesCanceled = false;
 
     // Step 2: Set teeState.[[canceled1]] to true.
     // Step 3: Set teeState.[[reason1]] to reason.
-    if (ControllerFlags(branch) & ControllerFlag_TeeBranch1) {
-        teeState->setCanceled1(reason);
-        bothBranchesCanceled = teeState->canceled2();
-    } else {
-        MOZ_ASSERT(ControllerFlags(branch) & ControllerFlag_TeeBranch2);
-        teeState->setCanceled2(reason);
-        bothBranchesCanceled = teeState->canceled1();
+    {
+        RootedValue reason(cx, reason_);
+        if (reason.isGCThing() &&
+            reason.toGCThing()->maybeCompartment() != teeState->compartment())
+        {
+            AutoRealm ar(cx, teeState);
+            if (!cx->compartment()->wrap(cx, &reason))
+                return nullptr;
+        }
+        if (ControllerFlags(branch) & ControllerFlag_TeeBranch1) {
+            teeState->setCanceled1(reason);
+            bothBranchesCanceled = teeState->canceled2();
+        } else {
+            MOZ_ASSERT(ControllerFlags(branch) & ControllerFlag_TeeBranch2);
+            teeState->setCanceled2(reason);
+            bothBranchesCanceled = teeState->canceled1();
+        }
     }
 
     // Step 4: If teeState.[[canceled1]] is true,
     // Step 4: If teeState.[[canceled2]] is true,
     if (bothBranchesCanceled) {
         // Step a: Let compositeReason be
         //         ! CreateArrayFromList(« teeState.[[reason1]], teeState.[[reason2]] »).
         RootedNativeObject compositeReason(cx, NewDenseFullyAllocatedArray(cx, 2));
         if (!compositeReason) {
             return nullptr;
         }
 
         compositeReason->setDenseInitializedLength(2);
-        compositeReason->initDenseElement(0, teeState->reason1());
-        compositeReason->initDenseElement(1, teeState->reason2());
+
+        RootedValue reason1(cx, teeState->reason1());
+        RootedValue reason2(cx, teeState->reason2());
+        if (teeState->compartment() != cx->compartment()) {
+            if (!cx->compartment()->wrap(cx, &reason1) || !cx->compartment()->wrap(cx, &reason2))
+                return nullptr;
+        }
+        compositeReason->initDenseElement(0, reason1);
+        compositeReason->initDenseElement(1, reason2);
         RootedValue compositeReasonVal(cx, ObjectValue(*compositeReason));
 
         Rooted<PromiseObject*> promise(cx, teeState->promise());
 
         // Step b: Let cancelResult be ! ReadableStreamCancel(stream, compositeReason).
         RootedObject cancelResult(cx, ReadableStream::cancel(cx, stream, compositeReasonVal));
-        if (!cancelResult) {
-            if (!RejectWithPendingError(cx, promise)) {
-                return nullptr;
-            }
-        } else {
-            // Step c: Resolve teeState.[[promise]] with cancelResult.
-            RootedValue resultVal(cx, ObjectValue(*cancelResult));
-            if (!PromiseObject::resolve(cx, promise, resultVal)) {
-                return nullptr;
+        {
+            AutoRealm ar(cx, promise);
+            if (!cancelResult) {
+                if (!RejectWithPendingError(cx, promise)) {
+                    return nullptr;
+                }
+            } else {
+                // Step c: Resolve teeState.[[promise]] with cancelResult.
+                RootedValue resultVal(cx, ObjectValue(*cancelResult));
+                if (!cx->compartment()->wrap(cx, &resultVal))
+                    return nullptr;
+                if (!PromiseObject::resolve(cx, promise, resultVal)) {
+                    return nullptr;
+                }
             }
         }
     }
 
     // Step 5: Return teeState.[[promise]].
-    return teeState->promise();
+    RootedObject promise(cx, teeState->promise());
+    if (promise->compartment() != cx->compartment()) {
+        if (!cx->compartment()->wrap(cx, &promise))
+            return nullptr;
+    }
+    return promise;
 }
 
 static MOZ_MUST_USE bool
-ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e);
+ReadableStreamDefaultControllerErrorIfNeeded(JSContext* cx,
+                                             Handle<ReadableStreamDefaultController*> controller,
+                                             HandleValue e);
 
 // Streams spec, 3.3.6. step 21:
 // Upon rejection of reader.[[closedPromise]] with reason r,
 static bool
 TeeReaderClosedHandler(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     Rooted<TeeState*> teeState(cx, TargetFromHandler<TeeState>(args.callee()));
     HandleValue reason = args.get(0);
 
     // Step a: If teeState.[[closedOrErrored]] is false, then:
     if (!teeState->closedOrErrored()) {
-        // Step a.i: Perform ! ReadableStreamDefaultControllerError(pull.[[branch1]], r).
+        // Step a.iii: Set teeState.[[closedOrErrored]] to true.
+        // Reordered to ensure that internal errors in the other steps don't
+        // leave the teeState in an undefined state.
+        teeState->setClosedOrErrored();
+
+        // Step a.i: Perform ! ReadableStreamDefaultControllerErrorIfNeeded(pull.[[branch1]], r).
         Rooted<ReadableStreamDefaultController*> branch1(cx, teeState->branch1());
-        if (!ReadableStreamControllerError(cx, branch1, reason)) {
+        if (!ReadableStreamDefaultControllerErrorIfNeeded(cx, branch1, reason)) {
             return false;
         }
 
-        // Step a.ii: Perform ! ReadableStreamDefaultControllerError(pull.[[branch2]], r).
+        // Step a.ii: Perform ! ReadableStreamDefaultControllerErrorIfNeeded(pull.[[branch2]], r).
         Rooted<ReadableStreamDefaultController*> branch2(cx, teeState->branch2());
-        if (!ReadableStreamControllerError(cx, branch2, reason)) {
+        if (!ReadableStreamDefaultControllerErrorIfNeeded(cx, branch2, reason)) {
             return false;
         }
-
-        // Step a.iii: Set teeState.[[closedOrErrored]] to true.
-        teeState->setClosedOrErrored();
     }
 
     return true;
 }
 
-// Streams spec, 3.3.6. ReadableStreamTee ( stream, cloneForBranch2 )
+/**
+ * Streams spec, 3.3.6. ReadableStreamTee ( stream, cloneForBranch2 )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment. The returned branch streams and their associated
+ * controllers  are always created in the current cx compartment.
+ */
 static MOZ_MUST_USE bool
 ReadableStreamTee(JSContext* cx, Handle<ReadableStream*> stream, bool cloneForBranch2,
                   MutableHandle<ReadableStream*> branch1Stream,
                   MutableHandle<ReadableStream*> branch2Stream)
 {
     // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
     // Step 2: Assert: Type(cloneForBranch2) is Boolean (implicit).
 
@@ -1308,16 +1592,17 @@ ReadableStreamTee(JSContext* cx, Handle<
 
     Rooted<ReadableStreamDefaultController*> branch2(cx);
     branch2 = &ControllerFromStream(branch2Stream)->as<ReadableStreamDefaultController>();
     AddControllerFlags(branch2, ControllerFlag_TeeBranch | ControllerFlag_TeeBranch2);
     teeState->setBranch2(branch2);
 
     // Step 19: Set pull.[[branch1]] to branch1Stream.[[readableStreamController]].
     // Step 20: Set pull.[[branch2]] to branch2Stream.[[readableStreamController]].
+    // Our implementation stores the controllers on the TeeState instead.
 
     // Step 21: Upon rejection of reader.[[closedPromise]] with reason r,
     RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject());
 
     RootedObject onRejected(cx, NewHandler(cx, TeeReaderClosedHandler, teeState));
     if (!onRejected) {
         return false;
     }
@@ -1325,401 +1610,488 @@ ReadableStreamTee(JSContext* cx, Handle<
     if (!JS::AddPromiseReactions(cx, closedPromise, nullptr, onRejected)) {
         return false;
     }
 
     // Step 22: Return « branch1, branch2 ».
     return true;
 }
 
-// Streams spec, 3.4.1. ReadableStreamAddReadIntoRequest ( stream )
-static MOZ_MUST_USE PromiseObject*
-ReadableStreamAddReadIntoRequest(JSContext* cx, Handle<ReadableStream*> stream)
+inline static MOZ_MUST_USE bool
+AppendToListAtSlot(JSContext* cx, HandleNativeObject container, uint32_t slot, HandleObject obj);
+
+/**
+ * Streams spec, 3.4.1. ReadableStreamAddReadIntoRequest ( stream )
+ * Streams spec, 3.4.2. ReadableStreamAddReadRequest ( stream )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from another
+ * compartment.
+ *
+ * Note: The returned Promise is created in the current cx compartment.
+ */
+static MOZ_MUST_USE JSObject*
+ReadableStreamAddReadOrReadIntoRequest(JSContext* cx, Handle<ReadableStream*> stream)
 {
-    // Step 1: MOZ_ASSERT: ! IsReadableStreamBYOBReader(stream.[[reader]]) is true.
-    RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader));
-    RootedNativeObject reader(cx, &val.toObject().as<ReadableStreamBYOBReader>());
-
-    // Step 2: MOZ_ASSERT: stream.[[state]] is "readable" or "closed".
-    MOZ_ASSERT(stream->readable() || stream->closed());
+    // Step 1: Assert: ! IsReadableStreamBYOBReader(stream.[[reader]]) is true.
+    // Skipped: handles both kinds of readers.
+    Rooted<ReadableStreamReader*> reader(cx, ReaderFromStream(cx, stream));
+    if (!reader)
+        return nullptr;
+
+    // Step 2 of 3.4.2: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT_IF(reader->is<ReadableStreamDefaultReader>(), stream->readable());
 
     // Step 3: Let promise be a new promise.
-    Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
+    RootedObject promise(cx, PromiseObject::createSkippingExecutor(cx));
     if (!promise) {
         return nullptr;
     }
 
-    // Step 4: Let readIntoRequest be Record {[[promise]]: promise}.
-    // Step 5: Append readIntoRequest as the last element of stream.[[reader]].[[readIntoRequests]].
-    val = reader->getFixedSlot(ReaderSlot_Requests);
-    RootedNativeObject readIntoRequests(cx, &val.toObject().as<NativeObject>());
+    // Step 4: Let read{Into}Request be Record {[[promise]]: promise}.
+    // Step 5: Append read{Into}Request as the last element of
+    //         stream.[[reader]].[[read{Into}Requests]].
     // Since [[promise]] is the Record's only field, we store it directly.
-    val = ObjectValue(*promise);
-    if (!AppendToList(cx, readIntoRequests, val)) {
+    if (!AppendToListAtSlot(cx, reader, ReaderSlot_Requests, promise)) {
         return nullptr;
     }
 
     // Step 6: Return promise.
     return promise;
 }
 
-// Streams spec, 3.4.2. ReadableStreamAddReadRequest ( stream )
-static MOZ_MUST_USE PromiseObject*
-ReadableStreamAddReadRequest(JSContext* cx, Handle<ReadableStream*> stream)
-{
-  MOZ_ASSERT(stream->is<ReadableStream>());
-
-  // Step 1: Assert: ! IsReadableStreamDefaultReader(stream.[[reader]]) is true.
-  RootedNativeObject reader(cx, ReaderFromStream(stream));
-
-  // Step 2: Assert: stream.[[state]] is "readable".
-  MOZ_ASSERT(stream->readable());
-
-  // Step 3: Let promise be a new promise.
-  Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
-  if (!promise) {
-      return nullptr;
-  }
-
-  // Step 4: Let readRequest be Record {[[promise]]: promise}.
-  // Step 5: Append readRequest as the last element of stream.[[reader]].[[readRequests]].
-  RootedValue val(cx, reader->getFixedSlot(ReaderSlot_Requests));
-  RootedNativeObject readRequests(cx, &val.toObject().as<NativeObject>());
-
-  // Since [[promise]] is the Record's only field, we store it directly.
-  val = ObjectValue(*promise);
-  if (!AppendToList(cx, readRequests, val)) {
-      return nullptr;
-  }
-
-  // Step 6: Return promise.
-  return promise;
-}
-
 static MOZ_MUST_USE JSObject*
-ReadableStreamControllerCancelSteps(JSContext* cx,
-                                    HandleNativeObject controller, HandleValue reason);
+ReadableStreamControllerCancelSteps(JSContext* cx, Handle<ReadableStreamController*> controller,
+                                    HandleValue reason);
 
 // Used for transforming the result of promise fulfillment/rejection.
 static bool
 ReturnUndefined(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     args.rval().setUndefined();
     return true;
 }
 
 MOZ_MUST_USE bool
 ReadableStreamCloseInternal(JSContext* cx, Handle<ReadableStream*> stream);
 
-// Streams spec, 3.4.3. ReadableStreamCancel ( stream, reason )
+/**
+ * Streams spec, 3.4.3. ReadableStreamCancel ( stream, reason )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment. `reason` must be in the cx compartment.
+ */
 /* static */ MOZ_MUST_USE JSObject*
 ReadableStream::cancel(JSContext* cx, Handle<ReadableStream*> stream, HandleValue reason)
 {
+    AssertSameCompartment(cx, reason);
+
     // Step 1: Set stream.[[disturbed]] to true.
     uint32_t state = StreamState(stream) | ReadableStream::Disturbed;
     SetStreamState(stream, state);
 
     // Step 2: If stream.[[state]] is "closed", return a new promise resolved
     //         with undefined.
     if (stream->closed()) {
         return PromiseObject::unforgeableResolve(cx, UndefinedHandleValue);
     }
 
     // Step 3: If stream.[[state]] is "errored", return a new promise rejected
     //         with stream.[[storedError]].
     if (stream->errored()) {
         RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError));
+        if (!cx->compartment()->wrap(cx, &storedError))
+            return nullptr;
         return PromiseObject::unforgeableReject(cx, storedError);
     }
 
     // Step 4: Perform ! ReadableStreamClose(stream).
     if (!ReadableStreamCloseInternal(cx, stream)) {
         return nullptr;
     }
 
     // Step 5: Let sourceCancelPromise be
     //         ! stream.[[readableStreamController]].[[CancelSteps]](reason).
-    RootedNativeObject controller(cx, ControllerFromStream(stream));
+    Rooted<ReadableStreamController*> controller(cx, ControllerFromStream(stream));
     RootedObject sourceCancelPromise(cx);
     sourceCancelPromise = ReadableStreamControllerCancelSteps(cx, controller, reason);
     if (!sourceCancelPromise) {
         return nullptr;
     }
 
     // Step 6: Return the result of transforming sourceCancelPromise by a
     //         fulfillment handler that returns undefined.
     RootedAtom funName(cx, cx->names().empty);
     RootedFunction returnUndefined(cx, NewNativeFunction(cx, ReturnUndefined, 0, funName));
     if (!returnUndefined) {
         return nullptr;
     }
     return JS::CallOriginalPromiseThen(cx, sourceCancelPromise, returnUndefined, nullptr);
 }
 
-// Streams spec, 3.4.4. ReadableStreamClose ( stream )
+/**
+ * Streams spec, 3.4.4. ReadableStreamClose ( stream )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment.
+ */
 MOZ_MUST_USE bool
 ReadableStreamCloseInternal(JSContext* cx, Handle<ReadableStream*> stream)
 {
-  // Step 1: Assert: stream.[[state]] is "readable".
-  MOZ_ASSERT(stream->readable());
-
-  uint32_t state = StreamState(stream);
-  // Step 2: Set stream.[[state]] to "closed".
-  SetStreamState(stream, (state & ReadableStream::Disturbed) | ReadableStream::Closed);
-
-  // Step 3: Let reader be stream.[[reader]].
-  RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader));
-
-  // Step 4: If reader is undefined, return.
-  if (val.isUndefined()) {
-      return true;
-  }
-
-  // Step 5: If ! IsReadableStreamDefaultReader(reader) is true,
-  RootedNativeObject reader(cx, &val.toObject().as<NativeObject>());
-  if (reader->is<ReadableStreamDefaultReader>()) {
-      // Step a: Repeat for each readRequest that is an element of
-      //         reader.[[readRequests]],
-      val = reader->getFixedSlot(ReaderSlot_Requests);
-      if (!val.isUndefined()) {
-          RootedNativeObject readRequests(cx, &val.toObject().as<NativeObject>());
-          uint32_t len = readRequests->getDenseInitializedLength();
-          RootedObject readRequest(cx);
-          RootedObject resultObj(cx);
-          RootedValue resultVal(cx);
-          for (uint32_t i = 0; i < len; i++) {
-              // Step i: Resolve readRequest.[[promise]] with
-              //         ! CreateIterResultObject(undefined, true).
-              readRequest = &readRequests->getDenseElement(i).toObject();
-              resultObj = CreateIterResultObject(cx, UndefinedHandleValue, true);
-              if (!resultObj) {
-                  return false;
-              }
-              resultVal = ObjectValue(*resultObj);
-              if (!ResolvePromise(cx, readRequest, resultVal)) {
-                  return false;
-              }
-          }
-
-          // Step b: Set reader.[[readRequests]] to an empty List.
-          reader->setFixedSlot(ReaderSlot_Requests, UndefinedValue());
-      }
-  }
-
-  // Step 6: Resolve reader.[[closedPromise]] with undefined.
-  // Step 7: Return (implicit).
-  RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject());
-  if (!ResolvePromise(cx, closedPromise, UndefinedHandleValue)) {
-      return false;
-  }
-
-  if (stream->mode() == JS::ReadableStreamMode::ExternalSource &&
-      cx->runtime()->readableStreamClosedCallback)
-  {
-      NativeObject* controller = ControllerFromStream(stream);
-      void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
-      cx->runtime()->readableStreamClosedCallback(cx, stream, source, stream->embeddingFlags());
-  }
-
-  return true;
+    // Step 1: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT(stream->readable());
+
+    uint32_t state = StreamState(stream);
+    // Step 2: Set stream.[[state]] to "closed".
+    SetStreamState(stream, (state & ReadableStream::Disturbed) | ReadableStream::Closed);
+
+    // Step 4: If reader is undefined, return (reordered).
+    if (!HasReader(stream)) {
+        return true;
+    }
+
+    // Step 3: Let reader be stream.[[reader]].
+    Rooted<ReadableStreamReader*> reader(cx, ReaderFromStream(cx, stream));
+    if (!reader) {
+        return false;
+    }
+
+    // If the close operation was triggered from another global,
+    // the reader's read requests and close Promise might not be objects or
+    // wrappers from the current compartment.
+    bool needsWrapping = reader->compartment() != cx->compartment();
+
+    // Step 5: If ! IsReadableStreamDefaultReader(reader) is true,
+    if (reader->is<ReadableStreamDefaultReader>()) {
+        // Step a: Repeat for each readRequest that is an element of
+        //         reader.[[readRequests]],
+        RootedValue val(cx, reader->getFixedSlot(ReaderSlot_Requests));
+        if (!val.isUndefined()) {
+            RootedNativeObject readRequests(cx, &val.toObject().as<NativeObject>());
+            uint32_t len = readRequests->getDenseInitializedLength();
+            RootedObject readRequest(cx);
+            RootedObject resultObj(cx);
+            RootedValue resultVal(cx);
+            for (uint32_t i = 0; i < len; i++) {
+                // Step i: Resolve readRequest.[[promise]] with
+                //         ! CreateIterResultObject(undefined, true).
+                readRequest = &readRequests->getDenseElement(i).toObject();
+                if (needsWrapping && !cx->compartment()->wrap(cx, &readRequest)) {
+                    return false;
+                }
+
+                resultObj = CreateIterResultObject(cx, UndefinedHandleValue, true);
+                if (!resultObj) {
+                    return false;
+                }
+                resultVal = ObjectValue(*resultObj);
+                if (!ResolvePromise(cx, readRequest, resultVal))
+                    return false;
+            }
+
+            // Step b: Set reader.[[readRequests]] to an empty List.
+            reader->setFixedSlot(ReaderSlot_Requests, UndefinedValue());
+        }
+    }
+
+    // Step 6: Resolve reader.[[closedPromise]] with undefined.
+    // Step 7: Return (implicit).
+    RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject());
+    if (needsWrapping && !cx->compartment()->wrap(cx, &closedPromise)) {
+        return false;
+    }
+    if (!ResolvePromise(cx, closedPromise, UndefinedHandleValue)) {
+        return false;
+    }
+
+    if (stream->mode() == JS::ReadableStreamMode::ExternalSource &&
+        cx->runtime()->readableStreamClosedCallback)
+    {
+        // Make sure we're in the stream's compartment.
+        AutoRealm ar(cx, stream);
+        ReadableStreamController* controller = ControllerFromStream(stream);
+        void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+        cx->runtime()->readableStreamClosedCallback(cx, stream, source, stream->embeddingFlags());
+    }
+
+    return true;
 }
 
-// Streams spec, 3.4.5. ReadableStreamError ( stream, e )
+/**
+ * Streams spec, 3.4.5. ReadableStreamError ( stream, e )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment.
+ */
 MOZ_MUST_USE bool
 ReadableStreamErrorInternal(JSContext* cx, Handle<ReadableStream*> stream, HandleValue e)
 {
     // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
 
     // Step 2: Assert: stream.[[state]] is "readable".
     MOZ_ASSERT(stream->readable());
 
     // Step 3: Set stream.[[state]] to "errored".
     uint32_t state = StreamState(stream);
     SetStreamState(stream, (state & ReadableStream::Disturbed) | ReadableStream::Errored);
 
     // Step 4: Set stream.[[storedError]] to e.
     stream->setFixedSlot(StreamSlot_StoredError, e);
 
+    // Step 6: If reader is undefined, return (reordered).
+    if (!HasReader(stream)) {
+        return true;
+    }
+
     // Step 5: Let reader be stream.[[reader]].
-    RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader));
-
-    // Step 6: If reader is undefined, return.
-    if (val.isUndefined()) {
-        return true;
-    }
-    RootedNativeObject reader(cx, &val.toObject().as<NativeObject>());
+    Rooted<ReadableStreamReader*> reader(cx, ReaderFromStream(cx, stream));
+    if (!reader) {
+        return false;
+    }
 
     // Steps 7,8: (Identical in our implementation.)
     // Step a: Repeat for each readRequest that is an element of
     //         reader.[[readRequests]],
-    val = reader->getFixedSlot(ReaderSlot_Requests);
+    RootedValue val(cx, reader->getFixedSlot(ReaderSlot_Requests));
     RootedNativeObject readRequests(cx, &val.toObject().as<NativeObject>());
-    Rooted<PromiseObject*> readRequest(cx);
+    RootedObject readRequest(cx);
     uint32_t len = readRequests->getDenseInitializedLength();
     for (uint32_t i = 0; i < len; i++) {
         // Step i: Reject readRequest.[[promise]] with e.
         val = readRequests->getDenseElement(i);
-        readRequest = &val.toObject().as<PromiseObject>();
-        if (!PromiseObject::reject(cx, readRequest, e)) {
+        readRequest = &val.toObject();
+
+        // Responses have to be created in the compartment from which the
+        // error was triggered, which might not be the same as the one the
+        // request was created in, so we have to wrap requests here.
+        if (!cx->compartment()->wrap(cx, &readRequest))
+            return false;
+
+        if (!RejectPromise(cx, readRequest, e)) {
             return false;
         }
     }
 
     // Step b: Set reader.[[readRequests]] to a new empty List.
     if (!SetNewList(cx, reader, ReaderSlot_Requests)) {
         return false;
     }
 
     // Step 9: Reject reader.[[closedPromise]] with e.
-    val = reader->getFixedSlot(ReaderSlot_ClosedPromise);
-    Rooted<PromiseObject*> closedPromise(cx, &val.toObject().as<PromiseObject>());
-    if (!PromiseObject::reject(cx, closedPromise, e)) {
+    RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject());
+
+    // The closedPromise might have been created in another compartment.
+    // RejectPromise can deal with wrapped Promise objects, but has to be
+    // with all arguments in the current compartment, so we do need to wrap
+    // the Promise.
+    if (!cx->compartment()->wrap(cx, &closedPromise)) {
+        return false;
+    }
+    if (!RejectPromise(cx, closedPromise, e)) {
         return false;
     }
 
     if (stream->mode() == JS::ReadableStreamMode::ExternalSource &&
         cx->runtime()->readableStreamErroredCallback)
     {
-        NativeObject* controller = ControllerFromStream(stream);
+        // Make sure we're in the stream's compartment.
+        AutoRealm ar(cx, stream);
+        ReadableStreamController* controller = ControllerFromStream(stream);
         void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+
+        // Ensure that the embedding doesn't have to deal with
+        // mixed-compartment arguments to the callback.
+        RootedValue error(cx, e);
+        if (!cx->compartment()->wrap(cx, &error))
+            return false;
+
         cx->runtime()->readableStreamErroredCallback(cx, stream, source,
-                                                     stream->embeddingFlags(), e);
+                                                     stream->embeddingFlags(), error);
     }
 
     return true;
 }
 
-// Streams spec, 3.4.6. ReadableStreamFulfillReadIntoRequest( stream, chunk, done )
-// Streams spec, 3.4.7. ReadableStreamFulfillReadRequest ( stream, chunk, done )
-// These two spec functions are identical in our implementation.
+/**
+ * Streams spec, 3.4.6. ReadableStreamFulfillReadIntoRequest( stream, chunk, done )
+ * Streams spec, 3.4.7. ReadableStreamFulfillReadRequest ( stream, chunk, done )
+ * These two spec functions are identical in our implementation.
+ *
+ * Note: can operate on unwrapped values from other compartments for either
+ * |stream| and/or |chunk|. The iteration result object created in the course
+ * of this function's operation is created in the current cx compartment.
+ */
 static MOZ_MUST_USE bool
 ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, Handle<ReadableStream*> stream,
                                            HandleValue chunk, bool done)
 {
     // Step 1: Let reader be stream.[[reader]].
-    RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader));
-    RootedNativeObject reader(cx, &val.toObject().as<NativeObject>());
+    Rooted<ReadableStreamReader*> reader(cx, ReaderFromStream(cx, stream));
+    if (!reader)
+        return false;
 
     // Step 2: Let readIntoRequest be the first element of
     //         reader.[[readIntoRequests]].
     // Step 3: Remove readIntoRequest from reader.[[readIntoRequests]], shifting
     //         all other elements downward (so that the second becomes the first,
     //         and so on).
-    val = reader->getFixedSlot(ReaderSlot_Requests);
+    RootedValue val(cx, reader->getFixedSlot(ReaderSlot_Requests));
     RootedNativeObject readIntoRequests(cx, &val.toObject().as<NativeObject>());
-    Rooted<PromiseObject*> readIntoRequest(cx);
-    readIntoRequest = ShiftFromList<PromiseObject>(cx, readIntoRequests);
+    RootedObject readIntoRequest(cx, ShiftFromList<JSObject>(cx, readIntoRequests));
     MOZ_ASSERT(readIntoRequest);
+    if (!cx->compartment()->wrap(cx, &readIntoRequest))
+        return false;
 
     // Step 4: Resolve readIntoRequest.[[promise]] with
     //         ! CreateIterResultObject(chunk, done).
-    RootedObject iterResult(cx, CreateIterResultObject(cx, chunk, done));
+    RootedValue wrappedChunk(cx, chunk);
+    if (!cx->compartment()->wrap(cx, &wrappedChunk))
+        return false;
+    RootedObject iterResult(cx, CreateIterResultObject(cx, wrappedChunk, done));
     if (!iterResult) {
         return false;
     }
     val = ObjectValue(*iterResult);
-    return PromiseObject::resolve(cx, readIntoRequest, val);
+    return ResolvePromise(cx, readIntoRequest, val);
 }
 
 // Streams spec, 3.4.8. ReadableStreamGetNumReadIntoRequests ( stream )
 // Streams spec, 3.4.9. ReadableStreamGetNumReadRequests ( stream )
 // (Identical implementation.)
 static uint32_t
 ReadableStreamGetNumReadRequests(ReadableStream* stream)
 {
     // Step 1: Return the number of elements in
     //         stream.[[reader]].[[readRequests]].
     if (!HasReader(stream)) {
         return 0;
     }
-    NativeObject* reader = ReaderFromStream(stream);
+
+    JS::AutoSuppressGCAnalysis nogc;
+    ReadableStreamReader* reader = ReaderFromStream(nullptr, stream);
+
+    // Reader is a dead wrapper, treat it as non-existent.
+    if (!reader) {
+        return 0;
+    }
+
     Value readRequests = reader->getFixedSlot(ReaderSlot_Requests);
     return readRequests.toObject().as<NativeObject>().getDenseInitializedLength();
 }
 
-// Stream spec 3.4.10. ReadableStreamHasBYOBReader ( stream )
+enum class ReaderMode
+{
+    None,
+    Default,
+};
+
+#if DEBUG
+// Streams spec 3.4.11. ReadableStreamHasDefaultReader ( stream )
 static MOZ_MUST_USE bool
-ReadableStreamHasBYOBReader(ReadableStream* stream)
+ReadableStreamHasDefaultReader(JSContext* cx, ReadableStream* stream, bool* result)
 {
     // Step 1: Let reader be stream.[[reader]].
     // Step 2: If reader is undefined, return false.
-    // Step 3: If ! IsReadableStreamBYOBReader(reader) is false, return false.
-    // Step 4: Return true.
-    Value reader = stream->getFixedSlot(StreamSlot_Reader);
-    return reader.isObject() && reader.toObject().is<ReadableStreamBYOBReader>();
-}
-
-// Streap spec 3.4.11. ReadableStreamHasDefaultReader ( stream )
-static MOZ_MUST_USE bool
-ReadableStreamHasDefaultReader(ReadableStream* stream)
-{
-    // Step 1: Let reader be stream.[[reader]].
-    // Step 2: If reader is undefined, return false.
+    if (stream->getFixedSlot(StreamSlot_Reader).isUndefined()) {
+        *result = false;
+        return true;
+    }
+
     // Step 3: If ! ReadableStreamDefaultReader(reader) is false, return false.
     // Step 4: Return true.
-    Value reader = stream->getFixedSlot(StreamSlot_Reader);
-    return reader.isObject() && reader.toObject().is<ReadableStreamDefaultReader>();
+    JSObject* readerObj = ReaderFromStream(cx, stream);
+    if (!readerObj)
+        return false;
+
+    *result = readerObj->is<ReadableStreamDefaultReader>();
+    return true;
+}
+#endif // DEBUG
+
+static MOZ_MUST_USE bool
+ReadableStreamGetReaderMode(JSContext* cx, ReadableStream* stream, ReaderMode* mode)
+{
+    if (stream->getFixedSlot(StreamSlot_Reader).isUndefined()) {
+        *mode = ReaderMode::None;
+        return true;
+    }
+
+    JSObject* readerObj = ReaderFromStream(cx, stream);
+    if (!readerObj)
+        return false;
+
+    *mode = ReaderMode::Default;
+
+    return true;
 }
 
 static MOZ_MUST_USE bool
 ReadableStreamReaderGenericInitialize(JSContext* cx,
-                                      HandleNativeObject reader,
+                                      Handle<ReadableStreamReader*> reader,
                                       Handle<ReadableStream*> stream);
 
-// Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
-// Steps 2-4.
+/**
+ * Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
+ * Steps 2-4.
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment. The returned object will always be created in the
+ * current cx compartment.
+ */
 static MOZ_MUST_USE ReadableStreamDefaultReader*
 CreateReadableStreamDefaultReader(JSContext* cx, Handle<ReadableStream*> stream)
 {
     Rooted<ReadableStreamDefaultReader*> reader(cx);
     reader = NewBuiltinClassInstance<ReadableStreamDefaultReader>(cx);
     if (!reader) {
         return nullptr;
     }
 
     // Step 2: If ! IsReadableStreamLocked(stream) is true, throw a TypeError
     //         exception.
     if (stream->locked()) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLESTREAM_LOCKED);
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED);
         return nullptr;
     }
 
     // Step 3: Perform ! ReadableStreamReaderGenericInitialize(this, stream).
     if (!ReadableStreamReaderGenericInitialize(cx, reader, stream)) {
         return nullptr;
     }
 
     // Step 4: Set this.[[readRequests]] to a new empty List.
     if (!SetNewList(cx, reader, ReaderSlot_Requests)) {
         return nullptr;
     }
 
     return reader;
 }
 
-// Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
+/**
+ * Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
+ *
+ * Note: can handle ReadableStream instances from another compartment.
+ */
 bool
 ReadableStreamDefaultReader::constructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     if (!ThrowIfNotConstructing(cx, args, "ReadableStreamDefaultReader")) {
         return false;
     }
 
     // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception.
-    if (!Is<ReadableStream>(args.get(0))) {
-        ReportArgTypeError(cx, "ReadableStreamDefaultReader", "ReadableStream",
-                           args.get(0));
+    if (!IsMaybeWrapped<ReadableStream>(args.get(0))) {
+        ReportArgTypeError(cx, "ReadableStreamDefaultReader", "ReadableStream", args.get(0));
         return false;
     }
 
-    Rooted<ReadableStream*> stream(cx, &args.get(0).toObject().as<ReadableStream>());
+    Rooted<ReadableStream*> stream(cx,
+                                   &CheckedUnwrap(&args.get(0).toObject())->as<ReadableStream>());
 
     RootedObject reader(cx, CreateReadableStreamDefaultReader(cx, stream));
     if (!reader) {
         return false;
     }
 
     args.rval().setObject(*reader);
     return true;
@@ -1728,44 +2100,54 @@ ReadableStreamDefaultReader::constructor
 // Streams spec, 3.5.4.1 get closed
 static MOZ_MUST_USE bool
 ReadableStreamDefaultReader_closed(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
     //         rejected with a TypeError exception.
-    if (!Is<ReadableStreamDefaultReader>(args.thisv())) {
-        return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "get closed");
+    auto reader = ToUnwrapped<ReadableStreamDefaultReader>(cx, args.thisv(),
+                                                           "ReadableStreamDefaultReader",
+                                                           "get closed");
+    if (!reader) {
+        return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     // Step 2: Return this.[[closedPromise]].
-    NativeObject* reader = &args.thisv().toObject().as<NativeObject>();
-    args.rval().set(reader->getFixedSlot(ReaderSlot_ClosedPromise));
+    RootedValue closedPromise(cx, reader->getFixedSlot(ReaderSlot_ClosedPromise));
+    if (!cx->compartment()->wrap(cx, &closedPromise)) {
+        return false;
+    }
+
+    args.rval().set(closedPromise);
     return true;
 }
 
 static MOZ_MUST_USE JSObject*
-ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason);
+ReadableStreamReaderGenericCancel(JSContext* cx, Handle<ReadableStreamReader*> reader,
+                                  HandleValue reason);
 
 // Streams spec, 3.5.4.2. cancel ( reason )
 static MOZ_MUST_USE bool
 ReadableStreamDefaultReader_cancel(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
     //         rejected with a TypeError exception.
-    if (!Is<ReadableStreamDefaultReader>(args.thisv())) {
-        return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "cancel");
+    Rooted<ReadableStreamDefaultReader*> reader(cx);
+    reader = ToUnwrapped<ReadableStreamDefaultReader>(cx, args.thisv(),
+                                                      "ReadableStreamDefaultReader", "cancel");
+    if (!reader) {
+        return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
     //         rejected with a TypeError exception.
-    RootedNativeObject reader(cx, &args.thisv().toObject().as<NativeObject>());
     if (!ReaderHasStream(reader)) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel");
         return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     // Step 3: Return ! ReadableStreamReaderGenericCancel(this, reason).
     JSObject* cancelPromise = ReadableStreamReaderGenericCancel(cx, reader, args.get(0));
@@ -1779,353 +2161,163 @@ ReadableStreamDefaultReader_cancel(JSCon
 // Streams spec, 3.5.4.3 read ( )
 static MOZ_MUST_USE bool
 ReadableStreamDefaultReader_read(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
     //         rejected with a TypeError exception.
-    if (!Is<ReadableStreamDefaultReader>(args.thisv())) {
-        return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "read");
+    Rooted<ReadableStreamDefaultReader*> reader(cx);
+    reader = ToUnwrapped<ReadableStreamDefaultReader>(cx, args.thisv(),
+                                                      "ReadableStreamDefaultReader", "read");
+    if (!reader) {
+        return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
     //         rejected with a TypeError exception.
-    Rooted<ReadableStreamDefaultReader*> reader(cx);
-    reader = &args.thisv().toObject().as<ReadableStreamDefaultReader>();
     if (!ReaderHasStream(reader)) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMREADER_NOT_OWNED, "read");
         return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     // Step 3: Return ! ReadableStreamDefaultReaderRead(this).
     JSObject* readPromise = ReadableStreamDefaultReader::read(cx, reader);
     if (!readPromise) {
         return false;
     }
     args.rval().setObject(*readPromise);
     return true;
 }
 
 static MOZ_MUST_USE bool
-ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader);
-
-// Streams spec, 3.5.4.4. releaseLock ( )
-static MOZ_MUST_USE bool
+ReadableStreamReaderGenericRelease(JSContext* cx, Handle<ReadableStreamReader*> reader);
+
+/**
+ * Streams spec, 3.5.4.4. releaseLock ( )
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultReader instances from
+ * another compartment.
+ */
+static bool
 ReadableStreamDefaultReader_releaseLock_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStreamDefaultReader*> reader(cx);
-    reader = &args.thisv().toObject().as<ReadableStreamDefaultReader>();
+    reader = &UncheckedUnwrap(&args.thisv().toObject())->as<ReadableStreamDefaultReader>();
 
     // Step 2: If this.[[ownerReadableStream]] is undefined, return.
     if (!ReaderHasStream(reader)) {
         args.rval().setUndefined();
         return true;
     }
 
     // Step 3: If this.[[readRequests]] is not empty, throw a TypeError exception.
     Value val = reader->getFixedSlot(ReaderSlot_Requests);
     if (!val.isUndefined()) {
         NativeObject* readRequests = &val.toObject().as<NativeObject>();
         uint32_t len = readRequests->getDenseInitializedLength();
         if (len != 0) {
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                      JSMSG_READABLESTREAMREADER_NOT_EMPTY,
-                                      "releaseLock");
+                                        JSMSG_READABLESTREAMREADER_NOT_EMPTY,
+                                        "releaseLock");
             return false;
         }
     }
 
     // Step 4: Perform ! ReadableStreamReaderGenericRelease(this).
     return ReadableStreamReaderGenericRelease(cx, reader);
 }
 
 static bool
 ReadableStreamDefaultReader_releaseLock(JSContext* cx, unsigned argc, Value* vp)
 {
     // Step 1: If ! IsReadableStreamDefaultReader(this) is false,
     //         throw a TypeError exception.
     CallArgs args = CallArgsFromVp(argc, vp);
-    return CallNonGenericMethod<Is<ReadableStreamDefaultReader>,
+    return CallNonGenericMethod<IsMaybeWrapped<ReadableStreamDefaultReader>,
                                 ReadableStreamDefaultReader_releaseLock_impl>(cx, args);
 }
 
 static const JSFunctionSpec ReadableStreamDefaultReader_methods[] = {
     JS_FN("cancel",         ReadableStreamDefaultReader_cancel,         1, 0),
     JS_FN("read",           ReadableStreamDefaultReader_read,           0, 0),
     JS_FN("releaseLock",    ReadableStreamDefaultReader_releaseLock,    0, 0),
     JS_FS_END
 };
 
 static const JSPropertySpec ReadableStreamDefaultReader_properties[] = {
     JS_PSG("closed", ReadableStreamDefaultReader_closed, 0),
     JS_PS_END
 };
 
+const Class ReadableStreamReader::class_ = {
+    "ReadableStreamReader"
+};
+
 CLASS_SPEC(ReadableStreamDefaultReader, 1, ReaderSlotCount, ClassSpec::DontDefineConstructor, 0,
            JS_NULL_CLASS_OPS);
 
-
-// Streams spec, 3.6.3 new ReadableStreamBYOBReader ( stream )
-// Steps 2-5.
-static MOZ_MUST_USE ReadableStreamBYOBReader*
-CreateReadableStreamBYOBReader(JSContext* cx, Handle<ReadableStream*> stream)
-{
-    // Step 2: If ! IsReadableByteStreamController(stream.[[readableStreamController]])
-    //         is false, throw a TypeError exception.
-    if (!ControllerFromStream(stream)->is<ReadableByteStreamController>()) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER,
-                                  "ReadableStream.getReader('byob')");
-        return nullptr;
-    }
-
-    // Step 3: If ! IsReadableStreamLocked(stream) is true, throw a TypeError
-    //         exception.
-    if (stream->locked()) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED);
-        return nullptr;
-    }
-
-    Rooted<ReadableStreamBYOBReader*> reader(cx);
-    reader = NewBuiltinClassInstance<ReadableStreamBYOBReader>(cx);
-    if (!reader) {
-        return nullptr;
-    }
-
-    // Step 4: Perform ! ReadableStreamReaderGenericInitialize(this, stream).
-    if (!ReadableStreamReaderGenericInitialize(cx, reader, stream)) {
-        return nullptr;
-    }
-
-    // Step 5: Set this.[[readIntoRequests]] to a new empty List.
-    if (!SetNewList(cx, reader, ReaderSlot_Requests)) {
-        return nullptr;
-    }
-
-    return reader;
-}
-
-// Streams spec, 3.6.3 new ReadableStreamBYOBReader ( stream )
-bool
-ReadableStreamBYOBReader::constructor(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-
-    if (!ThrowIfNotConstructing(cx, args, "ReadableStreamBYOBReader")) {
-        return false;
-    }
-
-    // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception.
-    if (!Is<ReadableStream>(args.get(0))) {
-        ReportArgTypeError(cx, "ReadableStreamBYOBReader", "ReadableStream", args.get(0));
-        return false;
-    }
-
-    Rooted<ReadableStream*> stream(cx, &args.get(0).toObject().as<ReadableStream>());
-    RootedObject reader(cx, CreateReadableStreamBYOBReader(cx, stream));
-    if (!reader) {
-        return false;
-    }
-
-    args.rval().setObject(*reader);
-    return true;
-}
-
-// Streams spec, 3.6.4.1 get closed
-static MOZ_MUST_USE bool
-ReadableStreamBYOBReader_closed(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-
-    // Step 1: If ! IsReadableStreamBYOBReader(this) is false, return a promise
-    //         rejected with a TypeError exception.
-    if (!Is<ReadableStreamBYOBReader>(args.thisv())) {
-        return RejectNonGenericMethod(cx, args, "ReadableStreamBYOBReader", "get closed");
-    }
-
-    // Step 2: Return this.[[closedPromise]].
-    NativeObject* reader = &args.thisv().toObject().as<NativeObject>();
-    args.rval().set(reader->getFixedSlot(ReaderSlot_ClosedPromise));
-    return true;
-}
-
-// Streams spec, 3.6.4.2. cancel ( reason )
-static MOZ_MUST_USE bool
-ReadableStreamBYOBReader_cancel(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-
-    // Step 1: If ! IsReadableStreamBYOBReader(this) is false, return a promise
-    //         rejected with a TypeError exception.
-    if (!Is<ReadableStreamBYOBReader>(args.thisv())) {
-        return RejectNonGenericMethod(cx, args, "ReadableStreamBYOBReader", "cancel");
-    }
-
-    // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
-    //         rejected with a TypeError exception.
-    RootedNativeObject reader(cx, &args.thisv().toObject().as<NativeObject>());
-    if (!ReaderHasStream(reader)) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel");
-        return ReturnPromiseRejectedWithPendingError(cx, args);
-    }
-
-    // Step 3: Return ! ReadableStreamReaderGenericCancel(this, reason).
-    JSObject* cancelPromise = ReadableStreamReaderGenericCancel(cx, reader, args.get(0));
-    if (!cancelPromise) {
-        return false;
-    }
-    args.rval().setObject(*cancelPromise);
-    return true;
-}
-
-// Streams spec, 3.6.4.3 read ( )
-static MOZ_MUST_USE bool
-ReadableStreamBYOBReader_read(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    HandleValue viewVal = args.get(0);
-
-    // Step 1: If ! IsReadableStreamBYOBReader(this) is false, return a promise
-    //         rejected with a TypeError exception.
-    if (!Is<ReadableStreamBYOBReader>(args.thisv())) {
-        return RejectNonGenericMethod(cx, args, "ReadableStreamBYOBReader", "read");
-    }
-
-    // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
-    //         rejected with a TypeError exception.
-    Rooted<ReadableStreamBYOBReader*> reader(cx);
-    reader = &args.thisv().toObject().as<ReadableStreamBYOBReader>();
-    if (!ReaderHasStream(reader)) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLESTREAMREADER_NOT_OWNED, "read");
-        return ReturnPromiseRejectedWithPendingError(cx, args);
-    }
-
-    // Step 3: If Type(view) is not Object, return a promise rejected with a
-    //         TypeError exception.
-    // Step 4: If view does not have a [[ViewedArrayBuffer]] internal slot,
-    //         return a promise rejected with a TypeError exception.
-    if (!Is<ArrayBufferViewObject>(viewVal)) {
-        ReportArgTypeError(cx, "ReadableStreamBYOBReader.read", "Typed Array", viewVal);
-        return ReturnPromiseRejectedWithPendingError(cx, args);
-    }
-
-    Rooted<ArrayBufferViewObject*> view(cx, &viewVal.toObject().as<ArrayBufferViewObject>());
-
-    // Step 5: If view.[[ByteLength]] is 0, return a promise rejected with a
-    //         TypeError exception.
-    // Note: It's ok to use the length in number of elements here because all we
-    // want to know is whether it's < 0.
-    if (JS_GetArrayBufferViewByteLength(view) == 0) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLESTREAMBYOBREADER_READ_EMPTY_VIEW);
-        return ReturnPromiseRejectedWithPendingError(cx, args);
-    }
-
-    // Step 6: Return ! ReadableStreamBYOBReaderRead(this, view).
-    JSObject* readPromise = ReadableStreamBYOBReader::read(cx, reader, view);
-    if (!readPromise) {
-        return false;
-    }
-    args.rval().setObject(*readPromise);
-    return true;
-}
-
-static MOZ_MUST_USE bool
-ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader);
-
-// Streams spec, 3.6.4.4. releaseLock ( )
-static MOZ_MUST_USE bool
-ReadableStreamBYOBReader_releaseLock_impl(JSContext* cx, const CallArgs& args)
-{
-    Rooted<ReadableStreamBYOBReader*> reader(cx);
-    reader = &args.thisv().toObject().as<ReadableStreamBYOBReader>();
-
-    // Step 2: If this.[[ownerReadableStream]] is undefined, return.
-    if (!ReaderHasStream(reader)) {
-        args.rval().setUndefined();
-        return true;
-    }
-
-    // Step 3: If this.[[readRequests]] is not empty, throw a TypeError exception.
-    Value val = reader->getFixedSlot(ReaderSlot_Requests);
-    if (!val.isUndefined()) {
-        NativeObject* readRequests = &val.toObject().as<NativeObject>();
-        uint32_t len = readRequests->getDenseInitializedLength();
-        if (len != 0) {
-            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                      JSMSG_READABLESTREAMREADER_NOT_EMPTY, "releaseLock");
-            return false;
-        }
-    }
-
-    // Step 4: Perform ! ReadableStreamReaderGenericRelease(this).
-    return ReadableStreamReaderGenericRelease(cx, reader);
-}
-
-static bool
-ReadableStreamBYOBReader_releaseLock(JSContext* cx, unsigned argc, Value* vp)
-{
-    // Step 1: If ! IsReadableStreamBYOBReader(this) is false,
-    //         throw a TypeError exception.
-    CallArgs args = CallArgsFromVp(argc, vp);
-    return CallNonGenericMethod<Is<ReadableStreamBYOBReader>,
-                                ReadableStreamBYOBReader_releaseLock_impl>(cx, args);
-}
-
-static const JSPropertySpec ReadableStreamBYOBReader_properties[] = {
-    JS_PSG("closed", ReadableStreamBYOBReader_closed, 0),
-    JS_PS_END
-};
-
-static const JSFunctionSpec ReadableStreamBYOBReader_methods[] = {
-    JS_FN("cancel",         ReadableStreamBYOBReader_cancel,        1, 0),
-    JS_FN("read",           ReadableStreamBYOBReader_read,          1, 0),
-    JS_FN("releaseLock",    ReadableStreamBYOBReader_releaseLock,   0, 0),
-    JS_FS_END
-};
-
-CLASS_SPEC(ReadableStreamBYOBReader, 1, 3, ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS);
-
 inline static MOZ_MUST_USE bool
-ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller);
+ReadableStreamControllerCallPullIfNeeded(JSContext* cx,
+                                         Handle<ReadableStreamController*> controller);
 
 // Streams spec, 3.7.1. IsReadableStreamDefaultReader ( x )
 // Implemented via intrinsic_isInstanceOfBuiltin<ReadableStreamDefaultReader>()
 
 // Streams spec, 3.7.2. IsReadableStreamBYOBReader ( x )
 // Implemented via intrinsic_isInstanceOfBuiltin<ReadableStreamBYOBReader>()
 
-// Streams spec, 3.7.3. ReadableStreamReaderGenericCancel ( reader, reason )
+/**
+ * Streams spec, 3.7.3. ReadableStreamReaderGenericCancel ( reader, reason )
+ *
+ * Note: can operate on unwrapped ReadableStream reader instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE JSObject*
-ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason)
+ReadableStreamReaderGenericCancel(JSContext* cx, Handle<ReadableStreamReader*> reader,
+                                  HandleValue reason)
 {
     // Step 1: Let stream be reader.[[ownerReadableStream]].
-    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
-
     // Step 2: Assert: stream is not undefined (implicit).
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(cx, reader));
+    if (!stream)
+        return nullptr;
 
     // Step 3: Return ! ReadableStreamCancel(stream, reason).
-    return &ReadableStreamCancel(cx, stream, reason)->as<PromiseObject>();
+    return ReadableStream::cancel(cx, stream, reason);
 }
 
-// Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream )
+/**
+ * Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream )
+ *
+ * Note: can operate on unwrapped ReadableStream reader instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
-ReadableStreamReaderGenericInitialize(JSContext* cx, HandleNativeObject reader,
+ReadableStreamReaderGenericInitialize(JSContext* cx, Handle<ReadableStreamReader*> reader,
                                       Handle<ReadableStream*> stream)
 {
     // Step 1: Set reader.[[ownerReadableStream]] to stream.
-    reader->setFixedSlot(ReaderSlot_Stream, ObjectValue(*stream));
-
     // Step 2: Set stream.[[reader]] to reader.
-    stream->setFixedSlot(StreamSlot_Reader, ObjectValue(*reader));
+    if (!IsObjectInContextCompartment(stream, cx)) {
+        RootedObject wrappedStream(cx, stream);
+        if (!cx->compartment()->wrap(cx, &wrappedStream))
+            return false;
+        reader->setFixedSlot(ReaderSlot_Stream, ObjectValue(*wrappedStream));
+        AutoRealm ar(cx, stream);
+        RootedObject wrappedReader(cx, reader);
+        if (!cx->compartment()->wrap(cx, &wrappedReader))
+            return false;
+        stream->setFixedSlot(StreamSlot_Reader, ObjectValue(*wrappedReader));
+    } else {
+        reader->setFixedSlot(ReaderSlot_Stream, ObjectValue(*stream));
+        stream->setFixedSlot(StreamSlot_Reader, ObjectValue(*reader));
+    }
 
     // Step 3: If stream.[[state]] is "readable",
     RootedObject promise(cx);
     if (stream->readable()) {
         // Step a: Set reader.[[closedPromise]] to a new promise.
         promise = PromiseObject::createSkippingExecutor(cx);
     } else if (stream->closed()) {
         // Step 4: Otherwise
@@ -2136,115 +2328,116 @@ ReadableStreamReaderGenericInitialize(JS
     } else {
         // Step b: Otherwise,
         // Step i: Assert: stream.[[state]] is "errored".
         MOZ_ASSERT(stream->errored());
 
         // Step ii: Set reader.[[closedPromise]] to a new promise rejected with
         //          stream.[[storedError]].
         RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError));
+        if (!cx->compartment()->wrap(cx, &storedError))
+            return false;
         promise = PromiseObject::unforgeableReject(cx, storedError);
     }
 
     if (!promise) {
         return false;
     }
 
     reader->setFixedSlot(ReaderSlot_ClosedPromise, ObjectValue(*promise));
     return true;
 }
 
-// Streams spec, 3.7.5. ReadableStreamReaderGenericRelease ( reader )
+/**
+ * Streams spec, 3.7.5. ReadableStreamReaderGenericRelease ( reader )
+ *
+ * Note: can operate on unwrapped ReadableStream reader instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
-ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader)
+ReadableStreamReaderGenericRelease(JSContext* cx, Handle<ReadableStreamReader*> reader)
 {
     // Step 1: Assert: reader.[[ownerReadableStream]] is not undefined.
-    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(cx, reader));
+    if (!stream)
+        return false;
 
     // Step 2: Assert: reader.[[ownerReadableStream]].[[reader]] is reader.
-    MOZ_ASSERT(&stream->getFixedSlot(StreamSlot_Reader).toObject() == reader);
+    MOZ_ASSERT(ReaderFromStream(cx, stream) == reader);
 
     // Create an exception to reject promises with below. We don't have a
     // clean way to do this, unfortunately.
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAMREADER_RELEASED);
     RootedValue exn(cx);
     // Not much we can do about uncatchable exceptions, just bail.
     if (!GetAndClearException(cx, &exn)) {
         return false;
     }
 
+    // The reader might be from another compartment. In that case we need to
+    // enter the reader's compartment before storing the above-created
+    // exception. We might delay entering the compartment until we have also
+    // created the closedPromise in step 4 below.
+    mozilla::Maybe<AutoRealm> ar;
+
     // Step 3: If reader.[[ownerReadableStream]].[[state]] is "readable", reject
     //         reader.[[closedPromise]] with a TypeError exception.
     if (stream->readable()) {
-            Value val = reader->getFixedSlot(ReaderSlot_ClosedPromise);
-            Rooted<PromiseObject*> closedPromise(cx, &val.toObject().as<PromiseObject>());
-            if (!PromiseObject::reject(cx, closedPromise, exn)) {
+        Value val = reader->getFixedSlot(ReaderSlot_ClosedPromise);
+        Rooted<PromiseObject*> closedPromise(cx, &val.toObject().as<PromiseObject>());
+        if (closedPromise->compartment() != cx->compartment()) {
+            ar.emplace(cx, closedPromise);
+            if (!cx->compartment()->wrap(cx, &exn)) {
                 return false;
             }
+        }
+        if (!PromiseObject::reject(cx, closedPromise, exn))
+            return false;
     } else {
         // Step 4: Otherwise, set reader.[[closedPromise]] to a new promise rejected
         //         with a TypeError exception.
         RootedObject closedPromise(cx, PromiseObject::unforgeableReject(cx, exn));
         if (!closedPromise) {
             return false;
         }
+        if (reader->compartment() != cx->compartment()) {
+            ar.emplace(cx, reader);
+            if (!cx->compartment()->wrap(cx, &closedPromise)) {
+                return false;
+            }
+        }
         reader->setFixedSlot(ReaderSlot_ClosedPromise, ObjectValue(*closedPromise));
     }
 
     // Step 5: Set reader.[[ownerReadableStream]].[[reader]] to undefined.
     stream->setFixedSlot(StreamSlot_Reader, UndefinedValue());
 
     // Step 6: Set reader.[[ownerReadableStream]] to undefined.
     reader->setFixedSlot(ReaderSlot_Stream, UndefinedValue());
 
     return true;
 }
 
 static MOZ_MUST_USE JSObject*
-ReadableByteStreamControllerPullInto(JSContext* cx,
-                                     Handle<ReadableByteStreamController*> controller,
-                                     Handle<ArrayBufferViewObject*> view);
-
-// Streams spec, 3.7.6. ReadableStreamBYOBReaderRead ( reader, view )
+ReadableStreamControllerPullSteps(JSContext* cx, Handle<ReadableStreamController*> controller);
+
+/**
+ * Streams spec, 3.7.7. ReadableStreamDefaultReaderRead ( reader )
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultReader instances from
+ * another compartment.
+ */
 /* static */ MOZ_MUST_USE JSObject*
-ReadableStreamBYOBReader::read(JSContext* cx, Handle<ReadableStreamBYOBReader*> reader,
-                               Handle<ArrayBufferViewObject*> view)
-{
-    MOZ_ASSERT(reader->is<ReadableStreamBYOBReader>());
-
-    // Step 1: Let stream be reader.[[ownerReadableStream]].
-    // Step 2: Assert: stream is not undefined.
-    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
-
-    // Step 3: Set stream.[[disturbed]] to true.
-    SetStreamState(stream, StreamState(stream) | ReadableStream::Disturbed);
-
-    // Step 4: If stream.[[state]] is "errored", return a promise rejected with
-    //         stream.[[storedError]].
-    if (stream->errored()) {
-        RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError));
-        return PromiseObject::unforgeableReject(cx, storedError);
-    }
-
-    // Step 5: Return ! ReadableByteStreamControllerPullInto(stream.[[readableStreamController]], view).
-    Rooted<ReadableByteStreamController*> controller(cx);
-    controller = &ControllerFromStream(stream)->as<ReadableByteStreamController>();
-    return ReadableByteStreamControllerPullInto(cx, controller, view);
-}
-
-static MOZ_MUST_USE JSObject*
-ReadableStreamControllerPullSteps(JSContext* cx, HandleNativeObject controller);
-
-// Streams spec, 3.7.7. ReadableStreamDefaultReaderRead ( reader )
-MOZ_MUST_USE JSObject*
 ReadableStreamDefaultReader::read(JSContext* cx, Handle<ReadableStreamDefaultReader*> reader)
 {
     // Step 1: Let stream be reader.[[ownerReadableStream]].
     // Step 2: Assert: stream is not undefined.
-    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(cx, reader));
+    if (!stream)
+        return nullptr;
 
     // Step 3: Set stream.[[disturbed]] to true.
     SetStreamState(stream, StreamState(stream) | ReadableStream::Disturbed);
 
     // Step 4: If stream.[[state]] is "closed", return a new promise resolved with
     //         ! CreateIterResultObject(undefined, true).
     if (stream->closed()) {
         RootedObject iterResult(cx, CreateIterResultObject(cx, UndefinedHandleValue, true));
@@ -2254,35 +2447,38 @@ ReadableStreamDefaultReader::read(JSCont
         RootedValue iterResultVal(cx, ObjectValue(*iterResult));
         return PromiseObject::unforgeableResolve(cx, iterResultVal);
     }
 
     // Step 5: If stream.[[state]] is "errored", return a new promise rejected with
     //         stream.[[storedError]].
     if (stream->errored()) {
         RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError));
+        if (!cx->compartment()->wrap(cx, &storedError))
+            return nullptr;
         return PromiseObject::unforgeableReject(cx, storedError);
     }
 
     // Step 6: Assert: stream.[[state]] is "readable".
     MOZ_ASSERT(stream->readable());
 
     // Step 7: Return ! stream.[[readableStreamController]].[[PullSteps]]().
-    RootedNativeObject controller(cx, ControllerFromStream(stream));
+    Rooted<ReadableStreamController*> controller(cx, ControllerFromStream(stream));
     return ReadableStreamControllerPullSteps(cx, controller);
 }
 
 // Streams spec, 3.8.3, step 11.a.
 // and
 // Streams spec, 3.10.3, step 16.a.
 static bool
 ControllerStartHandler(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
-    RootedNativeObject controller(cx, TargetFromHandler<NativeObject>(args.callee()));
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = TargetFromHandler<ReadableStreamController>(args.callee());
 
     // Step i: Set controller.[[started]] to true.
     AddControllerFlags(controller, ControllerFlag_Started);
 
     // Step ii: Assert: controller.[[pulling]] is false.
     // Step iii: Assert: controller.[[pullAgain]] is false.
     MOZ_ASSERT(!(ControllerFlags(controller) &
                  (ControllerFlag_Pulling | ControllerFlag_PullAgain)));
@@ -2293,31 +2489,28 @@ ControllerStartHandler(JSContext* cx, un
     if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) {
         return false;
     }
     args.rval().setUndefined();
     return true;
 }
 
 static MOZ_MUST_USE bool
-ReadableStreamDefaultControllerErrorIfNeeded(JSContext* cx,
-                                             Handle<ReadableStreamDefaultController*> controller,
-                                             HandleValue e);
-
-static MOZ_MUST_USE bool
-ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e);
+ReadableStreamControllerError(JSContext* cx, Handle<ReadableStreamController*> controller,
+                              HandleValue e);
 
 // Streams spec, 3.8.3, step 11.b.
 // and
 // Streams spec, 3.10.3, step 16.b.
 static bool
 ControllerStartFailedHandler(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
-    RootedNativeObject controllerObj(cx, TargetFromHandler<NativeObject>(args.callee()));
+    Rooted<ReadableStreamController*> controllerObj(cx);
+    controllerObj = TargetFromHandler<ReadableStreamController>(args.callee());
 
     // 3.8.3, Step 11.b.i:
     // Perform ! ReadableStreamDefaultControllerErrorIfNeeded(controller, r).
     if (controllerObj->is<ReadableStreamDefaultController>()) {
         Rooted<ReadableStreamDefaultController*> controller(cx);
         controller = &controllerObj->as<ReadableStreamDefaultController>();
         return ReadableStreamDefaultControllerErrorIfNeeded(cx, controller, args.get(0));
     }
@@ -2338,19 +2531,25 @@ ValidateAndNormalizeHighWaterMark(JSCont
                                   double* highWaterMark);
 
 static MOZ_MUST_USE bool
 ValidateAndNormalizeQueuingStrategy(JSContext* cx,
                                     HandleValue size,
                                     HandleValue highWaterMarkVal,
                                     double* highWaterMark);
 
-// Streams spec, 3.8.3 new ReadableStreamDefaultController ( stream, underlyingSource,
-//                                                           size, highWaterMark )
-// Steps 3 - 11.
+/**
+ * Streams spec, 3.8.3 new ReadableStreamDefaultController ( stream, underlyingSource,
+ *                                                           size, highWaterMark )
+ * Steps 3 - 11.
+ *
+ * Note: can NOT operate on unwrapped ReadableStream instances from
+ * another compartment: ReadableStream controllers must be created in the same
+ * compartment as the stream.
+ */
 static MOZ_MUST_USE ReadableStreamDefaultController*
 CreateReadableStreamDefaultController(JSContext* cx, Handle<ReadableStream*> stream,
                                       HandleValue underlyingSource, HandleValue size,
                                       HandleValue highWaterMarkVal)
 {
     Rooted<ReadableStreamDefaultController*> controller(cx);
     controller = NewBuiltinClassInstance<ReadableStreamDefaultController>(cx);
     if (!controller) {
@@ -2454,26 +2653,26 @@ ReadableStreamDefaultController::constru
         return false;
     }
 
     args.rval().setObject(*controller);
     return true;
 }
 
 static MOZ_MUST_USE double
-ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller);
+ReadableStreamControllerGetDesiredSizeUnchecked(ReadableStreamController* controller);
 
 // Streams spec, 3.8.4.1. get desiredSize
 // and
 // Streams spec, 3.10.4.2. get desiredSize
 static MOZ_MUST_USE bool
 ReadableStreamController_desiredSize_impl(JSContext* cx, const CallArgs& args)
 {
-    RootedNativeObject controller(cx);
-    controller = &args.thisv().toObject().as<NativeObject>();
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableStreamController>();
 
     // Streams spec, 3.9.8. steps 1-4.
     // 3.9.8. Step 1: Let stream be controller.[[controlledReadableStream]].
     ReadableStream* stream = StreamFromController(controller);
 
     // 3.9.8. Step 2: Let state be stream.[[state]].
     // 3.9.8. Step 3: If state is "errored", return null.
     if (stream->errored()) {
@@ -2501,19 +2700,24 @@ ReadableStreamDefaultController_desiredS
     return CallNonGenericMethod<Is<ReadableStreamDefaultController>,
                                 ReadableStreamController_desiredSize_impl>(cx, args);
 }
 
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerClose(JSContext* cx,
                                      Handle<ReadableStreamDefaultController*> controller);
 
-// Unified implementation of steps 2-3 of 3.8.4.2 and 3.10.4.3.
+/**
+ * Unified implementation of steps 2-3 of 3.8.4.2 and 3.10.4.3.
+ *
+ * Note: can operate on unwrapped ReadableStream controller instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
-VerifyControllerStateForClosing(JSContext* cx, HandleNativeObject controller)
+VerifyControllerStateForClosing(JSContext* cx, Handle<ReadableStreamController*> controller)
 {
     // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception.
     if (ControllerFlags(controller) & ControllerFlag_CloseRequested) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMCONTROLLER_CLOSED, "close");
         return false;
     }
 
@@ -2524,22 +2728,27 @@ VerifyControllerStateForClosing(JSContex
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close");
         return false;
     }
 
     return true;
 }
 
-// Streams spec, 3.8.4.2 close()
+/**
+ * Streams spec, 3.8.4.2 close()
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultController instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
 ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStreamDefaultController*> controller(cx);
-    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
+    controller = &UncheckedUnwrap(&args.thisv().toObject())->as<ReadableStreamDefaultController>();
 
     // Steps 2-3.
     if (!VerifyControllerStateForClosing(cx, controller)) {
         return false;
     }
 
     // Step 4: Perform ! ReadableStreamDefaultControllerClose(this).
     if (!ReadableStreamDefaultControllerClose(cx, controller)) {
@@ -2551,73 +2760,72 @@ ReadableStreamDefaultController_close_im
 
 static bool
 ReadableStreamDefaultController_close(JSContext* cx, unsigned argc, Value* vp)
 {
     // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
     //         TypeError exception.
 
     CallArgs args = CallArgsFromVp(argc, vp);
-    return CallNonGenericMethod<Is<ReadableStreamDefaultController>,
+    return CallNonGenericMethod<IsMaybeWrapped<ReadableStreamDefaultController>,
                                 ReadableStreamDefaultController_close_impl>(cx, args);
 }
 
-static MOZ_MUST_USE bool
-ReadableStreamDefaultControllerEnqueue(JSContext* cx,
-                                       Handle<ReadableStreamDefaultController*> controller,
-                                       HandleValue chunk);
-
 // Streams spec, 3.8.4.3. enqueue ( chunk )
 static MOZ_MUST_USE bool
 ReadableStreamDefaultController_enqueue_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStreamDefaultController*> controller(cx);
-    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
+    controller = &UncheckedUnwrap(&args.thisv().toObject())->as<ReadableStreamDefaultController>();
 
     // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception.
     if (ControllerFlags(controller) & ControllerFlag_CloseRequested) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLESTREAMCONTROLLER_CLOSED, "close");
+                                  JSMSG_READABLESTREAMCONTROLLER_CLOSED, "enqueue");
         return false;
     }
 
     // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable",
     //         throw a TypeError exception.
     ReadableStream* stream = StreamFromController(controller);
     if (!stream->readable()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close");
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "enqueue");
         return false;
     }
 
     // Step 4: Return ! ReadableStreamDefaultControllerEnqueue(this, chunk).
     if (!ReadableStreamDefaultControllerEnqueue(cx, controller, args.get(0))) {
         return false;
     }
     args.rval().setUndefined();
     return true;
 }
 
 static bool
 ReadableStreamDefaultController_enqueue(JSContext* cx, unsigned argc, Value* vp)
 {
     // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
     //         TypeError exception.
-
     CallArgs args = CallArgsFromVp(argc, vp);
-    return CallNonGenericMethod<Is<ReadableStreamDefaultController>,
+    return CallNonGenericMethod<IsMaybeWrapped<ReadableStreamDefaultController>,
                                 ReadableStreamDefaultController_enqueue_impl>(cx, args);
 }
 
-// Streams spec, 3.8.4.4. error ( e )
+/**
+ * Streams spec, 3.8.4.4. error ( e )
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultController instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
 ReadableStreamDefaultController_error_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStreamDefaultController*> controller(cx);
-    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
+    controller = &UncheckedUnwrap(&args.thisv().toObject())->as<ReadableStreamDefaultController>();
 
     // Step 2: Let stream be this.[[controlledReadableStream]].
     // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception.
     if (!StreamFromController(controller)->readable()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error");
         return false;
     }
@@ -2632,99 +2840,140 @@ ReadableStreamDefaultController_error_im
 
 static bool
 ReadableStreamDefaultController_error(JSContext* cx, unsigned argc, Value* vp)
 {
     // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
     //         TypeError exception.
 
     CallArgs args = CallArgsFromVp(argc, vp);
-    return CallNonGenericMethod<Is<ReadableStreamDefaultController>,
+    return CallNonGenericMethod<IsMaybeWrapped<ReadableStreamDefaultController>,
                                 ReadableStreamDefaultController_error_impl>(cx, args);
 }
 
 static const JSPropertySpec ReadableStreamDefaultController_properties[] = {
     JS_PSG("desiredSize", ReadableStreamDefaultController_desiredSize, 0),
     JS_PS_END
 };
 
 static const JSFunctionSpec ReadableStreamDefaultController_methods[] = {
     JS_FN("close",      ReadableStreamDefaultController_close,      0, 0),
     JS_FN("enqueue",    ReadableStreamDefaultController_enqueue,    1, 0),
     JS_FN("error",      ReadableStreamDefaultController_error,      1, 0),
     JS_FS_END
 };
 
+const Class ReadableStreamController::class_ = {
+    "ReadableStreamController"
+};
+
 CLASS_SPEC(ReadableStreamDefaultController, 4, 7, ClassSpec::DontDefineConstructor, 0,
            JS_NULL_CLASS_OPS);
 
 /**
  * Unified implementation of ReadableStream controllers' [[CancelSteps]] internal
  * methods.
  * Streams spec, 3.8.5.1. [[CancelSteps]] ( reason )
  * and
  * Streams spec, 3.10.5.1. [[CancelSteps]] ( reason )
+ *
+ * Note: can operate on unwrapped ReadableStream |controller| instances
+ * from another compartment. |reason| must be in the current cx compartment.
  */
 static MOZ_MUST_USE JSObject*
-ReadableStreamControllerCancelSteps(JSContext* cx, HandleNativeObject controller,
+ReadableStreamControllerCancelSteps(JSContext* cx, Handle<ReadableStreamController*> controller,
                                     HandleValue reason)
 {
-    MOZ_ASSERT(IsReadableStreamController(controller));
+    AssertSameCompartment(cx, reason);
 
     // Step 1 of 3.10.5.1: If this.[[pendingPullIntos]] is not empty,
     if (!controller->is<ReadableStreamDefaultController>()) {
         Value val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos);
         RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
 
         if (pendingPullIntos->getDenseInitializedLength() != 0) {
             // Step a: Let firstDescriptor be the first element of
             //         this.[[pendingPullIntos]].
             // Step b: Set firstDescriptor.[[bytesFilled]] to 0.
-            Rooted<PullIntoDescriptor*> firstDescriptor(cx);
-            firstDescriptor = PeekList<PullIntoDescriptor>(pendingPullIntos);
-            firstDescriptor->setBytesFilled(0);
+            PullIntoDescriptor* descriptor;
+            descriptor = ToUnwrapped<PullIntoDescriptor>(cx, PeekList<JSObject>(pendingPullIntos));
+            if (!descriptor)
+                return nullptr;
+            descriptor->setBytesFilled(0);
         }
     }
 
+    RootedValue underlyingSource(cx);
+    underlyingSource = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+
     // Step 1 of 3.8.5.1, step 2 of 3.10.5.1: Perform ! ResetQueue(this).
     if (!ResetQueue(cx, controller)) {
         return nullptr;
     }
 
     // Step 2 of 3.8.5.1, step 3 of 3.10.5.1:
     // Return ! PromiseInvokeOrNoop(this.[[underlying(Byte)Source]],
     //                              "cancel", « reason »)
-    RootedValue underlyingSource(cx);
-    underlyingSource = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
-
-    if (Is<TeeState>(underlyingSource)) {
-        Rooted<TeeState*> teeState(cx, &underlyingSource.toObject().as<TeeState>());
+    // Note: this special-cases the underlying source of tee'd stream's
+    // branches. Instead of storing a JSFunction as the "cancel" property on
+    // those, we check if the source is a, maybe wrapped, TeeState instance
+    // and manually dispatch to the right internal function. TeeState is fully
+    // under our control, so this isn't content-observable.
+    if (IsMaybeWrapped<TeeState>(underlyingSource)) {
+        Rooted<TeeState*> teeState(cx);
+        teeState = &UncheckedUnwrap(&underlyingSource.toObject())->as<TeeState>();
         Rooted<ReadableStreamDefaultController*> defaultController(cx);
         defaultController = &controller->as<ReadableStreamDefaultController>();
         return ReadableStreamTee_Cancel(cx, teeState, defaultController, reason);
     }
 
     if (ControllerFlags(controller) & ControllerFlag_ExternalSource) {
-        void* source = underlyingSource.toPrivate();
-        Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+        bool needsWrapping = controller->compartment() != cx->compartment();
         RootedValue rval(cx);
-        rval = cx->runtime()->readableStreamCancelCallback(cx, stream, source,
-                                                           stream->embeddingFlags(), reason);
+        {
+            RootedValue wrappedReason(cx, reason);
+            mozilla::Maybe<AutoRealm> ar;
+            if (needsWrapping) {
+                ar.emplace(cx, controller);
+                if (!cx->compartment()->wrap(cx, &wrappedReason))
+                    return nullptr;
+            }
+            void* source = underlyingSource.toPrivate();
+            Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+            rval = cx->runtime()->readableStreamCancelCallback(cx, stream, source,
+                                                                stream->embeddingFlags(),
+                                                                wrappedReason);
+        }
+
+        if (needsWrapping && !cx->compartment()->wrap(cx, &rval))
+            return nullptr;
         return PromiseObject::unforgeableResolve(cx, rval);
     }
 
+    // If the stream and its controller aren't in the cx compartment, we have
+    // to ensure that the underlying source is correctly wrapped before
+    // operating on it.
+    if (!cx->compartment()->wrap(cx, &underlyingSource))
+        return nullptr;
+
     return PromiseInvokeOrNoop(cx, underlyingSource, cx->names().cancel, reason);
 }
 
 inline static MOZ_MUST_USE bool
-DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chunk);
-
-// Streams spec, 3.8.5.2. ReadableStreamDefaultController [[PullSteps]]()
+DequeueValue(JSContext* cx, Handle<ReadableStreamController*> container, MutableHandleValue chunk);
+
+/**
+ * Streams spec, 3.8.5.2. ReadableStreamDefaultController [[PullSteps]]()
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultController instances
+ * from another compartment.
+ */
 static JSObject*
-ReadableStreamDefaultControllerPullSteps(JSContext* cx, HandleNativeObject controller)
+ReadableStreamDefaultControllerPullSteps(JSContext* cx,
+                                         Handle<ReadableStreamController*> controller)
 {
     MOZ_ASSERT(controller->is<ReadableStreamDefaultController>());
 
     // Step 1: Let stream be this.[[controlledReadableStream]].
     Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
 
     // Step 2: If this.[[queue]] is not empty,
     RootedNativeObject queue(cx);
@@ -2752,26 +3001,28 @@ ReadableStreamDefaultControllerPullSteps
         // Step c: Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this).
         else {
         if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) {
             return nullptr;
         }
         }
 
         // Step d: Return a promise resolved with ! CreateIterResultObject(chunk, false).
+        if (!cx->compartment()->wrap(cx, &chunk))
+            return nullptr;
         RootedObject iterResultObj(cx, CreateIterResultObject(cx, chunk, false));
         if (!iterResultObj) {
-          return nullptr;
+            return nullptr;
         }
         RootedValue iterResult(cx, ObjectValue(*iterResultObj));
         return PromiseObject::unforgeableResolve(cx, iterResult);
     }
 
     // Step 3: Let pendingPromise be ! ReadableStreamAddReadRequest(stream).
-    Rooted<PromiseObject*> pendingPromise(cx, ReadableStreamAddReadRequest(cx, stream));
+    RootedObject pendingPromise(cx, ReadableStreamAddReadOrReadIntoRequest(cx, stream));
     if (!pendingPromise) {
         return nullptr;
     }
 
     // Step 4: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this).
     if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) {
         return nullptr;
     }
@@ -2781,17 +3032,23 @@ ReadableStreamDefaultControllerPullSteps
 }
 
 // Streams spec, 3.9.2 and 3.12.3. step 7:
 // Upon fulfillment of pullPromise,
 static bool
 ControllerPullHandler(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
-    RootedNativeObject controller(cx, TargetFromHandler<NativeObject>(args.callee()));
+
+    RootedValue controllerVal(cx, args.callee().as<JSFunction>().getExtendedSlot(0));
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = ToUnwrapped<ReadableStreamController>(cx, controllerVal);
+    if (!controller)
+        return false;
+
     uint32_t flags = ControllerFlags(controller);
 
     // Step a: Set controller.[[pulling]] to false.
     // Step b.i: Set controller.[[pullAgain]] to false.
     RemoveControllerFlags(controller, ControllerFlag_Pulling | ControllerFlag_PullAgain);
 
     // Step b: If controller.[[pullAgain]] is true,
     if (flags & ControllerFlag_PullAgain) {
@@ -2806,42 +3063,52 @@ ControllerPullHandler(JSContext* cx, uns
 }
 
 // Streams spec, 3.9.2 and 3.12.3. step 8:
 // Upon rejection of pullPromise with reason e,
 static bool
 ControllerPullFailedHandler(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
-    RootedNativeObject controller(cx, TargetFromHandler<NativeObject>(args.callee()));
     HandleValue e = args.get(0);
 
+    RootedValue controllerVal(cx, args.callee().as<JSFunction>().getExtendedSlot(0));
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = ToUnwrapped<ReadableStreamController>(cx, controllerVal);
+    if (!controller)
+        return false;
+
     // Step a: If controller.[[controlledReadableStream]].[[state]] is "readable",
     //         perform ! ReadableByteStreamControllerError(controller, e).
     if (StreamFromController(controller)->readable()) {
         if (!ReadableStreamControllerError(cx, controller, e)) {
             return false;
         }
     }
 
     args.rval().setUndefined();
     return true;
 }
 
 static bool
-ReadableStreamControllerShouldCallPull(NativeObject* controller);
+ReadableStreamControllerShouldCallPull(ReadableStreamController* controller);
 
 static MOZ_MUST_USE double
-ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller);
-
-// Streams spec, 3.9.2 ReadableStreamDefaultControllerCallPullIfNeeded ( controller )
-// and
-// Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller )
+ReadableStreamControllerGetDesiredSizeUnchecked(ReadableStreamController* controller);
+
+/**
+ * Streams spec, 3.9.2 ReadableStreamDefaultControllerCallPullIfNeeded ( controller )
+ * Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller )
+ *
+ * Note: can operate on unwrapped instances from other compartments for
+ * |controller|.
+ */
 inline static MOZ_MUST_USE bool
-ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller)
+ReadableStreamControllerCallPullIfNeeded(JSContext* cx,
+                                         Handle<ReadableStreamController*> controller)
 {
     // Step 1: Let shouldPull be
     //         ! ReadableByteStreamControllerShouldCallPull(controller).
     bool shouldPull = ReadableStreamControllerShouldCallPull(controller);
 
     // Step 2: If shouldPull is false, return.
     if (!shouldPull) {
         return true;
@@ -2859,59 +3126,67 @@ ReadableStreamControllerCallPullIfNeeded
     // Step 4: Assert: controller.[[pullAgain]] is false.
     MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_PullAgain));
 
     // Step 5: Set controller.[[pulling]] to true.
     AddControllerFlags(controller, ControllerFlag_Pulling);
 
     // Step 6: Let pullPromise be
     //         ! PromiseInvokeOrNoop(controller.[[underlyingByteSource]], "pull", controller).
-    RootedObject pullPromise(cx);
+    RootedObject wrappedController(cx, controller);
+    if (!cx->compartment()->wrap(cx, &wrappedController))
+        return false;
+    RootedValue controllerVal(cx, ObjectValue(*wrappedController));
     RootedValue underlyingSource(cx);
     underlyingSource = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
-    RootedValue controllerVal(cx, ObjectValue(*controller));
-
-    if (Is<TeeState>(underlyingSource)) {
-        Rooted<TeeState*> teeState(cx, &underlyingSource.toObject().as<TeeState>());
+    RootedObject pullPromise(cx);
+
+    if (IsMaybeWrapped<TeeState>(underlyingSource)) {
+        Rooted<TeeState*> teeState(cx);
+        teeState = &UncheckedUnwrap(&underlyingSource.toObject())->as<TeeState>();
         Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
         pullPromise = ReadableStreamTee_Pull(cx, teeState, stream);
     } else if (ControllerFlags(controller) & ControllerFlag_ExternalSource) {
         void* source = underlyingSource.toPrivate();
         Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
         double desiredSize = ReadableStreamControllerGetDesiredSizeUnchecked(controller);
         cx->runtime()->readableStreamDataRequestCallback(cx, stream, source,
                                                          stream->embeddingFlags(), desiredSize);
         pullPromise = PromiseObject::unforgeableResolve(cx, UndefinedHandleValue);
     } else {
         pullPromise = PromiseInvokeOrNoop(cx, underlyingSource, cx->names().pull, controllerVal);
     }
     if (!pullPromise) {
         return false;
     }
 
-    RootedObject onPullFulfilled(cx, NewHandler(cx, ControllerPullHandler, controller));
+    RootedObject onPullFulfilled(cx, NewHandler(cx, ControllerPullHandler, wrappedController));
     if (!onPullFulfilled) {
         return false;
     }
 
-    RootedObject onPullRejected(cx, NewHandler(cx, ControllerPullFailedHandler, controller));
+    RootedObject onPullRejected(cx, NewHandler(cx, ControllerPullFailedHandler, wrappedController));
     if (!onPullRejected) {
         return false;
     }
 
     return JS::AddPromiseReactions(cx, pullPromise, onPullFulfilled, onPullRejected);
 
     // Steps 7-8 implemented in functions above.
 }
 
-// Streams spec, 3.9.3. ReadableStreamDefaultControllerShouldCallPull ( controller )
-// and
-// Streams spec, 3.12.24. ReadableByteStreamControllerShouldCallPull ( controller )
+/**
+ * Streams spec, 3.9.3. ReadableStreamDefaultControllerShouldCallPull ( controller )
+ * Streams spec, 3.12.24. ReadableByteStreamControllerShouldCallPull ( controller )
+ *
+ * Note: can operate on unwrapped ReadableStream controller instances from
+ * another compartment.
+ */
 static bool
-ReadableStreamControllerShouldCallPull(NativeObject* controller)
+ReadableStreamControllerShouldCallPull(ReadableStreamController* controller)
 {
     // Step 1: Let stream be controller.[[controlledReadableStream]].
     ReadableStream* stream = StreamFromController(controller);
 
     // Step 2: If stream.[[state]] is "closed" or stream.[[state]] is "errored",
     //         return false.
     // or, equivalently
     // Step 2: If stream.[[state]] is not "readable", return false.
@@ -2941,17 +3216,22 @@ ReadableStreamControllerShouldCallPull(N
     double desiredSize = ReadableStreamControllerGetDesiredSizeUnchecked(controller);
 
     // Step 7: If desiredSize > 0, return true.
     // Step 8: Return false.
     // Steps 7-8 of 3.12.24 are equivalent in our implementation.
     return desiredSize > 0;
 }
 
-// Streams spec, 3.9.4. ReadableStreamDefaultControllerClose ( controller )
+/**
+ * Streams spec, 3.9.4. ReadableStreamDefaultControllerClose ( controller )
+ *
+ * Note: can operate on unwrapped ReadableStream controller instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerClose(JSContext* cx,
                                      Handle<ReadableStreamDefaultController*> controller)
 {
     // Step 1: Let stream be controller.[[controlledReadableStream]].
     Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
 
     // Step 2: Assert: controller.[[closeRequested]] is false.
@@ -2969,25 +3249,32 @@ ReadableStreamDefaultControllerClose(JSC
     if (queue->getDenseInitializedLength() == 0) {
         return ReadableStreamCloseInternal(cx, stream);
     }
 
     return true;
 }
 
 static MOZ_MUST_USE bool
-EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue value,
+EnqueueValueWithSize(JSContext* cx, Handle<ReadableStreamController*> container, HandleValue value,
                      HandleValue sizeVal);
 
-// Streams spec, 3.9.5. ReadableStreamDefaultControllerEnqueue ( controller, chunk )
+/**
+ * Streams spec, 3.9.5. ReadableStreamDefaultControllerEnqueue ( controller, chunk )
+ *
+ * Note: can operate on unwrapped instances from other compartments for
+ * |controller|. |chunk| must be in the current cx compartment.
+ */