Merge mozilla-central to autoland. CLOSED TREE
authorCsoregi Natalia <ncsoregi@mozilla.com>
Fri, 12 Oct 2018 13:18:41 +0300
changeset 499297 73c033dd56a292f249e7e02e6c42ad6b5ba98773
parent 499296 b14e0673f3d8af803fb7dbd601e76ed85c749c4f (current diff)
parent 499281 7a0840d602524d3b4552a867092267bb3fd5e79e (diff)
child 499298 652a138f3ba7fe8059b5d491ae7f80653ff3c080
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland. CLOSED TREE
devtools/client/inspector/layout/components/LayoutApp.js
testing/mochitest/rungeckoview.py
third_party/python/scandir/LICENSE.txt
third_party/python/scandir/MANIFEST.in
third_party/python/scandir/PKG-INFO
third_party/python/scandir/README.rst
third_party/python/scandir/_scandir.c
third_party/python/scandir/benchmark.py
third_party/python/scandir/osdefs.h
third_party/python/scandir/scandir.py
third_party/python/scandir/setup.cfg
third_party/python/scandir/setup.py
third_party/python/scandir/test/run_tests.py
third_party/python/scandir/test/test_scandir.py
third_party/python/scandir/test/test_walk.py
third_party/python/scandir/winreparse.h
--- 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/build/virtualenv_packages.txt
+++ b/build/virtualenv_packages.txt
@@ -26,17 +26,16 @@ mozilla.pth:third_party/python/jsmin
 !windows:mozilla.pth:third_party/python/psutil
 windows:mozilla.pth:third_party/python/psutil-cp27-none-win_amd64
 mozilla.pth:third_party/python/pylru
 mozilla.pth:third_party/python/which
 mozilla.pth:third_party/python/pystache
 mozilla.pth:third_party/python/pyyaml/lib
 mozilla.pth:third_party/python/requests
 mozilla.pth:third_party/python/requests-unixsocket
-mozilla.pth:third_party/python/scandir
 mozilla.pth:third_party/python/slugid
 mozilla.pth:third_party/python/py
 mozilla.pth:third_party/python/pytest/src
 mozilla.pth:third_party/python/pytoml
 mozilla.pth:third_party/python/redo
 mozilla.pth:third_party/python/six
 mozilla.pth:third_party/python/voluptuous
 mozilla.pth:third_party/python/json-e
--- 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);