diff --git a/.gitignore b/.gitignore
index 4af34870cf621d95cb8aa4d1991d8fe993da8e19..355c39de70f86c6fa72ed2c6fc8e3787a294afd7 100755
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,7 @@ indra/newview/search_history.txt
 indra/newview/teleport_history.txt
 indra/newview/typed_locations.txt
 indra/newview/vivox-runtime
+indra/newview/skins/default/html/common/equirectangular/js
 indra/server-linux-*
 indra/temp
 indra/test/linden_file.dat
diff --git a/autobuild.xml b/autobuild.xml
index 4768bd25c67295e05bfabb208e7de949a2e413fd..d44dc2ac7cce534897fa45b9a061fe4e4407e51f 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -367,6 +367,58 @@
         <key>version</key>
         <string>2.3.545362</string>
       </map>
+      <key>cubemaptoequirectangular</key>
+      <map>
+        <key>copyright</key>
+        <string>Copyright (c) 2017 Jaume Sanchez Elias, http://www.clicktorelease.com</string>
+        <key>license</key>
+        <string>MIT</string>
+        <key>license_file</key>
+        <string>LICENSES/CUBEMAPTOEQUIRECTANGULAR_LICENSE.txt</string>
+        <key>name</key>
+        <string>cubemaptoequirectangular</string>
+        <key>platforms</key>
+        <map>
+          <key>darwin64</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>e6da43ea831b2416a5a8b388a511e70b</string>
+              <key>url</key>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85446/792080/cubemaptoequirectangular-1.1.0-darwin64-562268.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>darwin64</string>
+          </map>
+          <key>windows</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>f26636666a056cacea0618bc2648d935</string>
+              <key>url</key>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85451/792109/cubemaptoequirectangular-1.1.0-windows-562268.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows</string>
+          </map>
+          <key>windows64</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>a59ba299bc94e915a8b55e9eef681253</string>
+              <key>url</key>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85449/792108/cubemaptoequirectangular-1.1.0-windows64-562268.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows64</string>
+          </map>
+        </map>
+        <key>version</key>
+        <string>1.1.0</string>
+      </map>
       <key>curl</key>
       <map>
         <key>copyright</key>
@@ -1491,6 +1543,58 @@
         <key>version</key>
         <string>2012.1-2</string>
       </map>
+      <key>jpegencoderbasic</key>
+      <map>
+        <key>copyright</key>
+        <string>Andreas Ritter, www.bytestrom.eu, 11/2009</string>
+        <key>license</key>
+        <string>NONE</string>
+        <key>license_file</key>
+        <string>LICENSES/JPEG_ENCODER_BASIC_LICENSE.txt</string>
+        <key>name</key>
+        <string>jpegencoderbasic</string>
+        <key>platforms</key>
+        <map>
+          <key>darwin64</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>d07453bf10bae71013209874477640bb</string>
+              <key>url</key>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85447/792085/jpegencoderbasic-1.0-darwin64-562269.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>darwin64</string>
+          </map>
+          <key>windows</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>e422fee0c82507ec218597654f8cadbf</string>
+              <key>url</key>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85450/792107/jpegencoderbasic-1.0-windows-562269.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows</string>
+          </map>
+          <key>windows64</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>fbe07040cecc4e8760acc2cdf2436468</string>
+              <key>url</key>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85448/792106/jpegencoderbasic-1.0-windows64-562269.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows64</string>
+          </map>
+        </map>
+        <key>version</key>
+        <string>1.0</string>
+      </map>
       <key>jpeglib</key>
       <map>
         <key>copyright</key>
@@ -3067,6 +3171,58 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
         <key>version</key>
         <string>4.10.0000.32327.5fc3fe7c.539691</string>
       </map>
+      <key>threejs</key>
+      <map>
+        <key>copyright</key>
+        <string>Copyright © 2010-2021 three.js authors</string>
+        <key>license</key>
+        <string>MIT</string>
+        <key>license_file</key>
+        <string>LICENSES/THREEJS_LICENSE.txt</string>
+        <key>name</key>
+        <string>threejs</string>
+        <key>platforms</key>
+        <map>
+          <key>darwin64</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>313a852ddc87be24235319987a42f0f2</string>
+              <key>url</key>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/86208/797009/threejs-0.131.3-darwin64-562835.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>darwin64</string>
+          </map>
+          <key>windows</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>850b2400f91e7704653fe3fd4171ce94</string>
+              <key>url</key>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/86210/797031/threejs-0.131.3-windows-562835.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows</string>
+          </map>
+          <key>windows64</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>821f398169c1743987ac1b99376f767f</string>
+              <key>url</key>
+              <string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/86209/797030/threejs-0.131.3-windows64-562835.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows64</string>
+          </map>
+        </map>
+        <key>version</key>
+        <string>0.131.3</string>
+      </map>
       <key>tut</key>
       <map>
         <key>copyright</key>
diff --git a/indra/cmake/CubemapToEquirectangularJS.cmake b/indra/cmake/CubemapToEquirectangularJS.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..bfe29260051a6f9b5fdae8553037b73b2ee9646e
--- /dev/null
+++ b/indra/cmake/CubemapToEquirectangularJS.cmake
@@ -0,0 +1,5 @@
+# -*- cmake -*-
+use_prebuilt_binary(cubemaptoequirectangular)
+
+# Main JS file
+configure_file("${AUTOBUILD_INSTALL_DIR}/js/CubemapToEquirectangular.js" "${CMAKE_SOURCE_DIR}/newview/skins/default/html/common/equirectangular/js/CubemapToEquirectangular.js" COPYONLY)
diff --git a/indra/cmake/JPEGEncoderBasic.cmake b/indra/cmake/JPEGEncoderBasic.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..0d2a3231bbb8ecf22e5109c51a90ac3cdfb7c67b
--- /dev/null
+++ b/indra/cmake/JPEGEncoderBasic.cmake
@@ -0,0 +1,5 @@
+# -*- cmake -*-
+use_prebuilt_binary(jpegencoderbasic)
+
+# Main JS file
+configure_file("${AUTOBUILD_INSTALL_DIR}/js/jpeg_encoder_basic.js" "${CMAKE_SOURCE_DIR}/newview/skins/default/html/common/equirectangular/js/jpeg_encoder_basic.js" COPYONLY)
diff --git a/indra/cmake/ThreeJS.cmake b/indra/cmake/ThreeJS.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..528adcbb25be14ee228af882ba77d1d344fa409b
--- /dev/null
+++ b/indra/cmake/ThreeJS.cmake
@@ -0,0 +1,8 @@
+# -*- cmake -*-
+use_prebuilt_binary(threejs)
+
+# Main three.js file
+configure_file("${AUTOBUILD_INSTALL_DIR}/js/three.min.js" "${CMAKE_SOURCE_DIR}/newview/skins/default/html/common/equirectangular/js/three.min.js" COPYONLY)
+
+# Controls to move around the scene using mouse or keyboard
+configure_file("${AUTOBUILD_INSTALL_DIR}/js/OrbitControls.js" "${CMAKE_SOURCE_DIR}/newview/skins/default/html/common/equirectangular/js/OrbitControls.js" COPYONLY)
diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp
index 6d51adc685fb0a1a2268942d8e01cf02b5ee2dd2..da352e8dd437b7be3b07382753d0f147c38ff2ec 100644
--- a/indra/llplugin/llpluginclassmedia.cpp
+++ b/indra/llplugin/llpluginclassmedia.cpp
@@ -714,6 +714,15 @@ void LLPluginClassMedia::loadURI(const std::string &uri)
 	sendMessage(message);
 }
 
+void LLPluginClassMedia::executeJavaScript(const std::string &code)
+{
+	LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "execute_javascript");
+
+	message.setValue("code", code);
+
+	sendMessage(message);
+}
+
 const char* LLPluginClassMedia::priorityToString(EPriority priority)
 {
 	const char* result = "UNKNOWN";
@@ -891,6 +900,19 @@ void LLPluginClassMedia::setJavascriptEnabled(const bool enabled)
 	sendMessage(message);
 }
 
+void LLPluginClassMedia::setWebSecurityDisabled(const bool disabled)
+{
+	LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "web_security_disabled");
+	message.setValueBoolean("disabled", disabled);
+	sendMessage(message);
+}
+
+void LLPluginClassMedia::setFileAccessFromFileUrlsEnabled(const bool enabled)
+{
+	LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "file_access_from_file_urls");
+	message.setValueBoolean("enabled", enabled);
+	sendMessage(message);
+}
 
 void LLPluginClassMedia::enableMediaPluginDebugging( bool enable )
 {
diff --git a/indra/llplugin/llpluginclassmedia.h b/indra/llplugin/llpluginclassmedia.h
index 382f891e0c4132b4a0e2680f69d75818a61c5ff5..69622c62db59efd3ac9ee0bfa59c9c7fe26c2c28 100644
--- a/indra/llplugin/llpluginclassmedia.h
+++ b/indra/llplugin/llpluginclassmedia.h
@@ -139,6 +139,8 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner
 
 	void loadURI(const std::string &uri);
 	
+	void executeJavaScript(const std::string &code);
+
 	// "Loading" means uninitialized or any state prior to fully running (processing commands)
 	bool isPluginLoading(void) { return mPlugin?mPlugin->isLoading():false; };
 
@@ -199,6 +201,8 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner
 	void	setLanguageCode(const std::string &language_code);
 	void	setPluginsEnabled(const bool enabled);
 	void	setJavascriptEnabled(const bool enabled);
+	void	setWebSecurityDisabled(const bool disabled);
+	void	setFileAccessFromFileUrlsEnabled(const bool enabled);
 	void	setTarget(const std::string &target);
 	
 	///////////////////////////////////
diff --git a/indra/media_plugins/cef/media_plugin_cef.cpp b/indra/media_plugins/cef/media_plugin_cef.cpp
index 8465285d2b76dd453d9a02732b04ce9b6f2a80df..2f2f25b6125d44f32602b0491d09c67094cdb621 100644
--- a/indra/media_plugins/cef/media_plugin_cef.cpp
+++ b/indra/media_plugins/cef/media_plugin_cef.cpp
@@ -65,7 +65,7 @@ class MediaPluginCEF :
 	void onTooltipCallback(std::string text);
 	void onLoadStartCallback();
 	void onRequestExitCallback();
-	void onLoadEndCallback(int httpStatusCode);
+	void onLoadEndCallback(int httpStatusCode, std::string url);
 	void onLoadError(int status, const std::string error_text);
 	void onAddressChangeCallback(std::string url);
 	void onOpenPopupCallback(std::string url, std::string target);
@@ -92,6 +92,8 @@ class MediaPluginCEF :
 	bool mDisableGPU;
 	bool mDisableNetworkService;
 	bool mUseMockKeyChain;
+	bool mDisableWebSecurity;
+	bool mFileAccessFromFileUrls;
 	std::string mUserAgentSubtring;
 	std::string mAuthUsername;
 	std::string mAuthPassword;
@@ -127,6 +129,8 @@ MediaPluginBase(host_send_func, host_user_data)
 	mDisableGPU = false;
 	mDisableNetworkService = true;
 	mUseMockKeyChain = true;
+	mDisableWebSecurity = false;
+	mFileAccessFromFileUrls = false;
 	mUserAgentSubtring = "";
 	mAuthUsername = "";
 	mAuthPassword = "";
@@ -260,13 +264,14 @@ void MediaPluginCEF::onRequestExitCallback()
 
 ////////////////////////////////////////////////////////////////////////////////
 //
-void MediaPluginCEF::onLoadEndCallback(int httpStatusCode)
+void MediaPluginCEF::onLoadEndCallback(int httpStatusCode, std::string url)
 {
 	LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "navigate_complete");
 	//message.setValue("uri", event.getEventUri());  // not easily available here in CEF - needed?
 	message.setValueS32("result_code", httpStatusCode);
 	message.setValueBoolean("history_back_available", mCEFLib->canGoBack());
 	message.setValueBoolean("history_forward_available", mCEFLib->canGoForward());
+	message.setValue("uri", url);
 	sendMessage(message);
 }
 
@@ -355,14 +360,16 @@ const std::vector<std::string> MediaPluginCEF::onFileDialog(dullahan::EFileDialo
 	}
 	else if (dialog_type == dullahan::FD_SAVE_FILE)
 	{
+		mPickedFiles.clear();
 		mAuthOK = false;
 
 		LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "file_download");
+		message.setValueBoolean("blocking_request", true);
 		message.setValue("filename", default_file);
 
 		sendMessage(message);
 
-		return std::vector<std::string>();
+		return mPickedFiles;
 	}
 
 	return std::vector<std::string>();
@@ -523,7 +530,7 @@ void MediaPluginCEF::receiveMessage(const char* message_string)
 				mCEFLib->setOnTitleChangeCallback(std::bind(&MediaPluginCEF::onTitleChangeCallback, this, std::placeholders::_1));
 				mCEFLib->setOnTooltipCallback(std::bind(&MediaPluginCEF::onTooltipCallback, this, std::placeholders::_1));
 				mCEFLib->setOnLoadStartCallback(std::bind(&MediaPluginCEF::onLoadStartCallback, this));
-				mCEFLib->setOnLoadEndCallback(std::bind(&MediaPluginCEF::onLoadEndCallback, this, std::placeholders::_1));
+				mCEFLib->setOnLoadEndCallback(std::bind(&MediaPluginCEF::onLoadEndCallback, this, std::placeholders::_1, std::placeholders::_2));
 				mCEFLib->setOnLoadErrorCallback(std::bind(&MediaPluginCEF::onLoadError, this, std::placeholders::_1, std::placeholders::_2));
 				mCEFLib->setOnAddressChangeCallback(std::bind(&MediaPluginCEF::onAddressChangeCallback, this, std::placeholders::_1));
 				mCEFLib->setOnOpenPopupCallback(std::bind(&MediaPluginCEF::onOpenPopupCallback, this, std::placeholders::_1, std::placeholders::_2));
@@ -562,6 +569,19 @@ void MediaPluginCEF::receiveMessage(const char* message_string)
 				settings.disable_network_service = mDisableNetworkService;
 				settings.use_mock_keychain = mUseMockKeyChain;
 #endif
+                // these were added to facilitate loading images directly into a local
+                // web page for the prototype 360 project in 2017 - something that is 
+                // disallowed normally by the browser security model. Now the the source
+                // (cubemap) images are stores as JavaScript, we can avoid opening up
+                // this security hole (it was only set for the 360 floater but still 
+                // a concern). Leaving them here, explicitly turn off vs removing 
+                // entirely from this source file so that others are aware of them 
+                // in the future.
+                settings.disable_web_security = false;
+                settings.file_access_from_file_urls = false;
+
+                settings.flash_enabled = mPluginsEnabled;
+
 				// This setting applies to all plugins, not just Flash
 				// Regarding, SL-15559 PDF files do not load in CEF v91,
 				// it turns out that on Windows, PDF support is treated
@@ -688,6 +708,11 @@ void MediaPluginCEF::receiveMessage(const char* message_string)
 				std::string uri = message_in.getValue("uri");
 				mCEFLib->navigate(uri);
 			}
+			else if (message_name == "execute_javascript")
+			{
+				std::string code = message_in.getValue("code");
+				mCEFLib->executeJavaScript(code);
+			}
 			else if (message_name == "set_cookie")
 			{
 				std::string uri = message_in.getValue("uri");
@@ -883,6 +908,14 @@ void MediaPluginCEF::receiveMessage(const char* message_string)
 			{
 				mDisableGPU = message_in.getValueBoolean("disable");
 			}
+			else if (message_name == "web_security_disabled")
+			{
+				mDisableWebSecurity = message_in.getValueBoolean("disabled");
+			}
+			else if (message_name == "file_access_from_file_urls")
+			{
+				mFileAccessFromFileUrls = message_in.getValueBoolean("enabled");
+			}
 		}
         else if (message_class == LLPLUGIN_MESSAGE_CLASS_MEDIA_TIME)
         {
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 87caca56af1d14067632cac3304742b4c1c19551..08462d044bae1f546777241894f59b0763608c87 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -12,12 +12,14 @@ include(bugsplat)
 include(BuildPackagesInfo)
 include(BuildVersion)
 include(CMakeCopyIfDifferent)
+include(CubemapToEquirectangularJS)
 include(DBusGlib)
 include(DragDrop)
 include(EXPAT)
 include(FMODSTUDIO)
 include(GLOD)
 include(Hunspell)
+include(JPEGEncoderBasic)
 include(JsonCpp)
 include(LLAppearance)
 include(LLAudio)
@@ -47,6 +49,7 @@ include(OpenGL)
 include(OpenSSL)
 include(PNG)
 include(TemplateCheck)
+include(ThreeJS)
 include(UI)
 include(UnixInstall)
 include(ViewerMiscLibs)
@@ -205,6 +208,7 @@ set(viewer_SOURCE_FILES
     llfilteredwearablelist.cpp
     llfirstuse.cpp
     llflexibleobject.cpp
+    llfloater360capture.cpp
     llfloaterabout.cpp
     llfloaterbvhpreview.cpp
     llfloateraddpaymentmethod.cpp
@@ -278,7 +282,7 @@ set(viewer_SOURCE_FILES
     llfloaternamedesc.cpp
     llfloaternotificationsconsole.cpp
     llfloaternotificationstabbed.cpp
-    llfloateroutfitphotopreview.cpp 
+    llfloateroutfitphotopreview.cpp
     llfloateroutfitsnapshot.cpp
     llfloaterobjectweights.cpp
     llfloateropenobject.cpp
@@ -844,6 +848,7 @@ set(viewer_HEADER_FILES
     llfilteredwearablelist.h
     llfirstuse.h
     llflexibleobject.h
+    llfloater360capture.h
     llfloaterabout.h
     llfloaterbvhpreview.h
     llfloateraddpaymentmethod.h
@@ -1381,7 +1386,7 @@ file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt"
            "${VIEWER_SHORT_VERSION}.${VIEWER_VERSION_REVISION}\n")
 
 set_source_files_properties(
-   llversioninfo.cpp tests/llversioninfo_test.cpp 
+   llversioninfo.cpp tests/llversioninfo_test.cpp
    PROPERTIES
    COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}" # see BuildVersion.cmake
    )
@@ -2004,7 +2009,7 @@ endif (WINDOWS)
 # one of these being libz where you can find four or more versions in play
 # at once.  On Linux, libz can be found at link and run time via a number
 # of paths:
-#     
+#
 #      => -lfreetype
 #        => libz.so.1 (on install machine, not build)
 #      => -lSDL
@@ -2175,7 +2180,7 @@ if (DARWIN)
 
   # https://blog.kitware.com/upcoming-in-cmake-2-8-12-osx-rpath-support/
   set(CMAKE_MACOSX_RPATH 1)
-  
+
   set_target_properties(
     ${VIEWER_BINARY_NAME}
     PROPERTIES
@@ -2436,7 +2441,7 @@ if (LL_TESTS)
     llworldmap.cpp
     llworldmipmap.cpp
     PROPERTIES
-    LL_TEST_ADDITIONAL_SOURCE_FILES 
+    LL_TEST_ADDITIONAL_SOURCE_FILES
     tests/llviewertexture_stub.cpp
     #llviewertexturelist.cpp
   )
@@ -2470,7 +2475,7 @@ if (LL_TESTS)
     llworldmap.cpp
     llworldmipmap.cpp
     PROPERTIES
-    LL_TEST_ADDITIONAL_SOURCE_FILES 
+    LL_TEST_ADDITIONAL_SOURCE_FILES
     tests/llviewertexture_stub.cpp
     #llviewertexturelist.cpp
     LL_TEST_ADDITIONAL_LIBRARIES "${BOOST_SYSTEM_LIBRARY}"
diff --git a/indra/newview/app_settings/commands.xml b/indra/newview/app_settings/commands.xml
index d0480ca47e9fd8c807a33d36dedc8db18e00e4d3..3dfe3f6634a9ca5043d3f1eeaa4bcaad4ccd015b 100644
--- a/indra/newview/app_settings/commands.xml
+++ b/indra/newview/app_settings/commands.xml
@@ -276,4 +276,15 @@
          is_running_function="Floater.IsOpen"
          is_running_parameters="my_environments"
            />
+  <command name="360capture"
+         available_in_toybox="true"
+         is_flashing_allowed="true"
+         icon="Command_360_Capture_Icon"
+         label_ref="Command_360_Capture_Label"
+         tooltip_ref="Command_360_Capture_Tooltip"
+         execute_function="Floater.ToggleOrBringToFront"
+         execute_parameters="360capture"
+         is_running_function="Floater.IsOpen"
+         is_running_parameters="360capture"
+           />
 </commands>
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index b1120c18b2eb4f928559666e90af73084ba924f7..5c140fb724c71c3d15f0bb153550ab245202a6c1 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -2076,6 +2076,28 @@
       <key>Value</key>
       <integer>1</integer>
     </map>
+    <key>BrowserFileAccessFromFileUrls</key>
+    <map>
+      <key>Comment</key>
+      <string>Allow access to local files via file urls in the embedded browser</string>
+      <key>Persist</key>
+      <integer>1</integer>
+      <key>Type</key>
+      <string>Boolean</string>
+      <key>Value</key>
+      <integer>1</integer>
+    </map>
+    <key>BrowserPluginsEnabled</key>
+    <map>
+      <key>Comment</key>
+      <string>Enable Web plugins in the built-in Web browser?</string>
+      <key>Persist</key>
+      <integer>1</integer>
+      <key>Type</key>
+      <string>Boolean</string>
+      <key>Value</key>
+      <integer>1</integer>
+    </map>
     <key>ChatBarCustomWidth</key>
     <map>
       <key>Comment</key>
@@ -3539,7 +3561,7 @@
       <key>Value</key>
       <integer>0</integer>
     </map>
-    <key>DoubleClickTeleport</key> 
+    <key>DoubleClickTeleport</key>
     <map>
       <key>Comment</key>
       <string>Enable double-click to teleport where allowed (afects minimap and people panel)</string>
@@ -8858,7 +8880,7 @@
     <key>Value</key>
     <integer>0</integer>
   </map>
-   <key>RenderHiDPI</key>
+    <key>RenderHiDPI</key>
   <map>
     <key>Comment</key>
     <string>Enable support for HiDPI displays, like Retina (MacOS X ONLY, requires restart)</string>
@@ -11580,7 +11602,7 @@
             <string>Boolean</string>
         <key>Value</key>
             <integer>0</integer>
-    </map> 
+    </map>
     <key>NearbyListShowMap</key>
     <map>
       <key>Comment</key>
@@ -16632,30 +16654,83 @@
     <string>Boolean</string>
     <key>Value</key>
     <integer>1</integer>
-   </map>
-    <key>CefVerboseLog</key>
-    <map>
-      <key>Comment</key>
-      <string>Enable/disable CEF verbose loggingk</string>
-      <key>Persist</key>
-      <integer>1</integer>
-      <key>Type</key>
-      <string>Boolean</string>
-      <key>Value</key>
-      <integer>0</integer>
-    </map>
-    <key>ResetUIScaleOnFirstRun</key>
-    <map>
-      <key>Comment</key>
-      <string>Resets the UI scale factor on first run due to changed display scaling behavior</string>
-      <key>Persist</key>
-      <integer>1</integer>
-      <key>Type</key>
-      <string>Boolean</string>
-      <key>Value</key>
-      <integer>1</integer>        
+  </map>
+  <key>360CaptureUseInterestListCap</key>
+  <map>
+    <key>Comment</key>
+    <string>Flag if set, uses the new InterestList cap to ask the simulator for full content</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>Boolean</string>
+    <key>Value</key>
+    <integer>1</integer>
+  </map>
+  <key>360CaptureJPEGEncodeQuality</key>
+  <map>
+    <key>Comment</key>
+    <string>Quality value to use in the JPEG encoder (0..100)</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>U32</string>
+    <key>Value</key>
+    <integer>95</integer>
+  </map>
+  <key>360CaptureDebugSaveImage</key>
+  <map>
+    <key>Comment</key>
+    <string>Flag if set, saves off each cube map as an image, as well as the JavaScript data URL, for debugging purposes</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>Boolean</string>
+    <key>Value</key>
+    <integer>0</integer>
+  </map>
+  <key>360CaptureOutputImageWidth</key>
+  <map>
+    <key>Comment</key>
+    <string>Width of the output 360 equirectangular image</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>U32</string>
+    <key>Value</key>
+    <integer>4096</integer>
+  </map>
+  <key>360CaptureHideAvatars</key>
+  <map>
+    <key>Comment</key>
+    <string>Flag if set, removes all the avatars from the 360 snapshot</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>Boolean</string>
+    <key>Value</key>
+    <integer>0</integer>
     </map>
+  <key>360CaptureCameraFOV</key>
+  <map>
+    <key>Comment</key>
+    <string>Field of view of the WebGL camera that converts the cubemap to an equirectangular image</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>U32</string>
+    <key>Value</key>
+    <integer>75</integer>
+  </map>
+  <key>ResetUIScaleOnFirstRun</key>
+  <map>
+  <key>Comment</key>
+  <string>Resets the UI scale factor on first run due to changed display scaling behavior</string>
+  <key>Persist</key>
+  <integer>1</integer>
+  <key>Type</key>
+  <string>Boolean</string>
+  <key>Value</key>
+  <integer>1</integer>
+  </map>
 </map>
 </llsd>
-
-
diff --git a/indra/newview/llfloater360capture.cpp b/indra/newview/llfloater360capture.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..aa226981aa8ae13a4b21b826a6c2b078934f5387
--- /dev/null
+++ b/indra/newview/llfloater360capture.cpp
@@ -0,0 +1,904 @@
+/**
+ * @file llfloater360capture.cpp
+ * @author Callum Prentice (callum@lindenlab.com)
+ * @brief Floater code for the 360 Capture feature
+ *
+ * $LicenseInfo:firstyear=2011&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2011, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
+ * $/LicenseInfo$
+ */
+
+#include "llviewerprecompiledheaders.h"
+
+#include "llfloater360capture.h"
+
+#include "llagent.h"
+#include "llagentui.h"
+#include "llbase64.h"
+#include "llcallbacklist.h"
+#include "llenvironment.h"
+#include "llimagejpeg.h"
+#include "llmediactrl.h"
+#include "llradiogroup.h"
+#include "llslurl.h"
+#include "lltextbox.h"
+#include "lltrans.h"
+#include "lluictrlfactory.h"
+#include "llversioninfo.h"
+#include "llviewercamera.h"
+#include "llviewercontrol.h"
+#include "llviewerpartsim.h"
+#include "llviewerregion.h"
+#include "llviewerwindow.h"
+#include "pipeline.h"
+
+#include <iterator>
+
+LLFloater360Capture::LLFloater360Capture(const LLSD& key)
+    :   LLFloater(key)
+{
+    // The handle to embedded browser that we use to
+    // render the WebGL preview as we as host the
+    // Cube Map to Equirectangular image code
+    mWebBrowser = nullptr;
+
+    // Ask the simulator to send us everything (and not just
+    // what it thinks the connected Viewer can see) until
+    // such time as we ask it not to (the dtor). If we crash or
+    // otherwise, exit before this is turned off, the Simulator
+    // will take care of cleaning up for us.
+    if (gSavedSettings.getBOOL("360CaptureUseInterestListCap"))
+    {
+        // send everything to us for as long as this floater is open
+        const bool send_everything = true;
+        changeInterestListMode(send_everything);
+    }
+}
+
+LLFloater360Capture::~LLFloater360Capture()
+{
+    if (mWebBrowser)
+    {
+        mWebBrowser->navigateStop();
+        mWebBrowser->clearCache();
+        mWebBrowser->unloadMediaSource();
+    }
+
+    // Tell the Simulator not to send us everything anymore
+    // and revert to the regular "keyhole" frustum of interest
+    // list updates.
+    if (gSavedSettings.getBOOL("360CaptureUseInterestListCap"))
+    {
+        const bool send_everything = false;
+        changeInterestListMode(send_everything);
+    }
+}
+
+BOOL LLFloater360Capture::postBuild()
+{
+    mCaptureBtn = getChild<LLUICtrl>("capture_button");
+    mCaptureBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onCapture360ImagesBtn, this));
+
+    mSaveLocalBtn = getChild<LLUICtrl>("save_local_button");
+    mSaveLocalBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onSaveLocalBtn, this));
+    mSaveLocalBtn->setEnabled(false);
+
+    mWebBrowser = getChild<LLMediaCtrl>("360capture_contents");
+    mWebBrowser->addObserver(this);
+
+    // There is a group of radio buttons that define the quality
+    // by each having a 'value' that is returns equal to the pixel
+    // size (width == height)
+    mQualityRadioGroup = getChild<LLRadioGroup>("360_quality_selection");
+    mQualityRadioGroup->setCommitCallback(boost::bind(&LLFloater360Capture::onChooseQualityRadioGroup, this));
+
+    // UX/UI called for preview mode (always the first index/option)
+    // by default each time vs restoring the last value
+    mQualityRadioGroup->setSelectedIndex(0);
+
+    // Construct a URL pointing to the first page to load. Although
+    // we do not use this page for anything (after some significant
+    // design changes), we retain the code to load the start page
+    // in case that changes again one day. It also makes sure the
+    // embedded browser is active and ready to go for when the real
+    // page with the 360 preview is navigated to.
+    std::string url = STRINGIZE(
+                          "file:///" <<
+                          getHTMLBaseFolder() <<
+                          mDefaultHTML
+                      );
+    mWebBrowser->navigateTo(url);
+
+    // initial pass at determining what size (width == height since
+    // the cube map images are square) we should capture at.
+    setSourceImageSize();
+
+    // the size of the output equirectangular image. The height of an EQR image
+    // is always 1/2 of the width so we should not store it but rather,
+    // calculate it from the width directly
+    mOutputImageWidth = gSavedSettings.getU32("360CaptureOutputImageWidth");
+    mOutputImageHeight = mOutputImageWidth / 2;
+
+    // enable resizing and enable for width and for height
+    enableResizeCtrls(true, true, true);
+
+    // initial heading that consumers of the equirectangular image
+    // (such as Facebook or Flickr) use to position initial view -
+    // we set during capture - stored as degrees (0..359)
+    mInitialHeadingDeg = 0.0;
+
+    // save directory in which to store the images (must obliviously be
+    // writable by the viewer). Also create it for users who haven't
+    // used the 360 feature before.
+    mImageSaveDir = gDirUtilp->getLindenUserDir() + gDirUtilp->getDirDelimiter() + "eqrimg";
+    LLFile::mkdir(mImageSaveDir);
+
+    // We do an initial capture when the floater is opened, albeit at a 'preview'
+    // quality level (really low resolution, but really fast)
+    onCapture360ImagesBtn();
+
+    return true;
+}
+
+// called when the user choose a quality level using
+// the buttons in the radio group
+void LLFloater360Capture::onChooseQualityRadioGroup()
+{
+    // set the size of the captured cube map images based
+    // on the quality level chosen
+    setSourceImageSize();
+}
+
+// Using a new capability, tell the simulator that we want it to send everything
+// it knows about and not just what is in front of the camera, in its view
+// frustum. We need this feature so that the contents of the region that appears
+// in the 6 snapshots which we cannot see and is normally not "considered", is
+// also rendered. Typically, this is turned on when the 360 capture floater is
+// opened and turned off when it is closed.
+// Note: for this version, we do not have a way to determine when "everything"
+// has arrived and has been rendered so for now, the proposal is that users
+// will need to experiment with the low resolution version and wait for some
+// (hopefully) small period of time while the full contents resolves.
+// Pass in a flag to ask the simulator/interest list to "send everything" or
+// not (the default mode)
+void LLFloater360Capture::changeInterestListMode(bool send_everything)
+{
+    LLSD body;
+
+    if (send_everything)
+    {
+        body["mode"] = LLSD::String("360");
+    }
+    else
+    {
+        body["mode"] = LLSD::String("default");
+    }
+
+    if (gAgent.requestPostCapability("InterestList", body, [](const LLSD & response)
+    {
+        LL_INFOS("360Capture") <<
+                               "InterestList capability responded: \n" <<
+                               ll_pretty_print_sd(response) <<
+                               LL_ENDL;
+    }))
+    {
+        LL_INFOS("360Capture") <<
+                               "Successfully posted an InterestList capability request with payload: \n" <<
+                               ll_pretty_print_sd(body) <<
+                               LL_ENDL;
+    }
+    else
+    {
+        LL_INFOS("360Capture") <<
+                               "Unable to post an InterestList capability request with payload: \n" <<
+                               ll_pretty_print_sd(body) <<
+                               LL_ENDL;
+    }
+}
+
+// There is is a setting (360CaptureSourceImageSize) that holds the size
+// (width == height since it's a square) of each of the 6 source snapshots.
+// However there are some known (and I dare say, some more unknown conditions
+// where the specified size is not possible and this function tries to figure it
+// out and change that setting to the optimal value for the current conditions.
+void LLFloater360Capture::setSourceImageSize()
+{
+    mSourceImageSize = mQualityRadioGroup->getSelectedValue().asInteger();
+
+    // If deferred rendering is off, we need to shrink the window we capture
+    // until it's smaller than the Viewer window dimensions.
+    if (!LLPipeline::sRenderDeferred)
+    {
+        LLRect window_rect = gViewerWindow->getWindowRectRaw();
+        S32 window_width = window_rect.getWidth();
+        S32 window_height = window_rect.getHeight();
+
+        while (mSourceImageSize > window_width || mSourceImageSize > window_height)
+        {
+            mSourceImageSize /= 2;
+            LL_INFOS("360Capture") << "Deferred rendering is forcing a smaller capture size: " << mSourceImageSize << LL_ENDL;
+        }
+
+        // there has to be an easier way than this to get the value
+        // from the radio group item at index 0. Why doesn't
+        // LLRadioGroup::getSelectedValue(int index) exist?
+        int index = mQualityRadioGroup->getSelectedIndex();
+        mQualityRadioGroup->setSelectedIndex(0);
+        int min_size = mQualityRadioGroup->getSelectedValue().asInteger();
+        mQualityRadioGroup->setSelectedIndex(index);
+
+        // If the maximum size we can support falls below a threshold then
+        // we should display a message in the log so we can try to debug
+        // why this is happening
+        if (mSourceImageSize < min_size)
+        {
+            LL_INFOS("360Capture") << "Small snapshot size due to deferred rendering and small app window" << LL_ENDL;
+        }
+    }
+}
+
+// This function shouldn't exist! We use the tooltip text from
+// the corresponding XUI file (floater_360capture.xml) as the
+// overlay text for the final web page to inform the user
+// about the quality level in play.  There ouught to be a
+// UI function like LLView* getSelectedItemView() or similar
+// but as far as I can tell, there isn't so we have to resort
+// to finding it ourselves with this icky code..
+const std::string LLFloater360Capture::getSelectedQualityTooltip()
+{
+    // safey (or bravery?)
+    if (mQualityRadioGroup != nullptr)
+    {
+        // for all the child widgets for the radio group
+        // (just the radio buttons themselves I think)
+        for (child_list_const_reverse_iter_t iter = mQualityRadioGroup->getChildList()->rbegin();
+                iter != mQualityRadioGroup->getChildList()->rend();
+                ++iter)
+        {
+            // if we match the selected index (which we can get easily)
+            // with our position in the list of children
+            if (mQualityRadioGroup->getSelectedIndex() ==
+                    std::distance(mQualityRadioGroup->getChildList()->rend(), iter) - 1)
+            {
+                // return the plain old tooltip text
+                return (*iter)->getToolTip();
+            }
+        }
+    }
+
+    // if it's not found or not available, return an empty string
+    return std::string();
+}
+
+// Some of the 'magic' happens via a web page in an HTML directory
+// and this code provides a single point of reference for its' location
+const std::string LLFloater360Capture::getHTMLBaseFolder()
+{
+    std::string folder_name = gDirUtilp->getSkinDir();
+    folder_name += gDirUtilp->getDirDelimiter();
+    folder_name += "html";
+    folder_name += gDirUtilp->getDirDelimiter();
+    folder_name += "common";
+    folder_name += gDirUtilp->getDirDelimiter();
+    folder_name += "equirectangular";
+    folder_name += gDirUtilp->getDirDelimiter();
+
+    return folder_name;
+}
+
+// triggered when the 'capture' button in the UI is pressed
+void LLFloater360Capture::onCapture360ImagesBtn()
+{
+    // launch the main capture code in a coroutine so we can
+    // yield/suspend at some points to give the main UI
+    // thread a look-in occasionally.
+    LLCoros::instance().launch("capture360cap", [this]()
+    {
+        capture360Images();
+    });
+}
+
+// Gets the full path name for a given JavaScript file in the HTML folder. We
+// use this ultimately as a parameter to the main web app so it knows where to find
+// the JavaScript array containing the 6 cube map images, stored as data URLs
+const std::string LLFloater360Capture::makeFullPathToJS(const std::string filename)
+{
+    std::string full_js_path = mImageSaveDir;
+    full_js_path += gDirUtilp->getDirDelimiter();
+    full_js_path += filename;
+
+    return full_js_path;
+}
+
+// Write the header/prequel portion of the JavaScript array of data urls
+// that we use to store the cube map images in (so the web code can load
+// them without tweaking browser security - we'd have to do this if they
+// we stored as plain old images) This deliberately overwrites the old
+// one, if it exists
+void LLFloater360Capture::writeDataURLHeader(const std::string filename)
+{
+    std::ofstream file_handle(filename.c_str());
+    if (file_handle.is_open())
+    {
+        file_handle << "// cube map images for Second Life Viewer panorama 360 images" << std::endl;
+        file_handle.close();
+    }
+}
+
+// Write the footer/sequel portion of the JavaScript image code. When this is
+// called, the current file on disk will contain the header and the 6 data
+// URLs, each with a well specified name.  This final piece of JavaScript code
+// creates an array from those data URLs that the main application can
+// reference and read.
+void LLFloater360Capture::writeDataURLFooter(const std::string filename)
+{
+    std::ofstream file_handle(filename.c_str(), std::ios_base::app);
+    if (file_handle.is_open())
+    {
+        file_handle << "var cubemap_img_js = [" << std::endl;
+        file_handle << "    img_posx, img_negx," << std::endl;
+        file_handle << "    img_posy, img_negy," << std::endl;
+        file_handle << "    img_posz, img_negz," << std::endl;
+        file_handle << "];" << std::endl;
+
+        file_handle.close();
+    }
+}
+
+// Given a filename, a chunk of data (representing an image file) and the size
+// of the buffer, we create a BASE64 encoded string and use it to build a JavaScript
+// data URL that represents the image in a web browser environment
+bool LLFloater360Capture::writeDataURL(const std::string filename, const std::string prefix, U8* data, unsigned int data_len)
+{
+    LL_INFOS("360Capture") << "Writing data URL for " << prefix << " to " << filename << LL_ENDL;
+
+    const std::string data_url = LLBase64::encode(data, data_len);
+
+    std::ofstream file_handle(filename.c_str(), std::ios_base::app);
+    if (file_handle.is_open())
+    {
+        file_handle << "var img_";
+        file_handle << prefix;
+        file_handle << " = '";
+        file_handle << "data:image/jpeg; base64,";
+        file_handle << data_url;
+        file_handle << "'";
+        file_handle << std::endl;
+        file_handle.close();
+
+        return true;
+    }
+
+    return false;
+}
+
+// Encode the image from each of the 6 snapshots and save it out to
+// the JavaScript array of data URLs
+void LLFloater360Capture::encodeAndSave(LLPointer<LLImageRaw> raw_image, const std::string filename, const std::string prefix)
+{
+    // the default quality for the JPEG encoding is set quite high
+    // but this still seems to be a reasonable compromise for
+    // quality/size and is still much smaller than equivalent PNGs
+    int jpeg_encode_quality = gSavedSettings.getU32("360CaptureJPEGEncodeQuality");
+    LLPointer<LLImageJPEG> jpeg_image = new LLImageJPEG(jpeg_encode_quality);
+
+    // Actually encode the JPEG image. This is where a lot of time
+    // is spent now that the snapshot capture process has been
+    // optimized.  The encode_time parameter doesn't appear to be
+    // used anymore.
+    const int encode_time = 0;
+    bool resultjpeg = jpeg_image->encode(raw_image, encode_time);
+
+    if (resultjpeg)
+    {
+        // save individual cube map images as real JPEG files
+        // for debugging or curiosity) based on debug settings
+        if (gSavedSettings.getBOOL("360CaptureDebugSaveImage"))
+        {
+            const std::string jpeg_filename = STRINGIZE(
+                                                  gDirUtilp->getLindenUserDir() <<
+                                                  gDirUtilp->getDirDelimiter() <<
+                                                  "eqrimg" <<
+                                                  gDirUtilp->getDirDelimiter() <<
+                                                  prefix <<
+                                                  "." <<
+                                                  jpeg_image->getExtension()
+                                              );
+
+            LL_INFOS("360Capture") << "Saving debug JPEG image as " << jpeg_filename << LL_ENDL;
+            jpeg_image->save(jpeg_filename);
+        }
+
+        // actually write the JPEG image to disk as a data URL
+        writeDataURL(filename, prefix, jpeg_image->getData(), jpeg_image->getDataSize());
+    }
+}
+
+// Defer back to the main loop for a single rendered frame to give
+// the renderer a chance to update the UI if it is needed
+void LLFloater360Capture::suspendForAFrame()
+{
+    const U32 frame_count_delta = 1;
+    U32 curr_frame_count = LLFrameTimer::getFrameCount();
+    while (LLFrameTimer::getFrameCount() <= curr_frame_count + frame_count_delta)
+    {
+        llcoro::suspend();
+    }
+}
+
+// A debug version of the snapshot code that simply fills the
+// buffer with a pattern that can be used to investigate
+// issues with encoding and saving off each RAW image.
+// Probably not needed anymore but saving here just in case.
+void LLFloater360Capture::mockSnapShot(LLImageRaw* raw)
+{
+    unsigned int width = raw->getWidth();
+    unsigned int height = raw->getHeight();
+    unsigned int depth = raw->getComponents();
+    unsigned char* pixels = raw->getData();
+
+    for (int y = 0; y < height; y++)
+    {
+        for (int x = 0; x < width; x++)
+        {
+            unsigned long offset = y * width * depth + x * depth;
+            unsigned char red = x * 256 / width;
+            unsigned char green = y * 256 / height;
+            unsigned char blue = ((x + y) / 2) * 256 / (width + height) / 2;
+            pixels[offset + 0] = red;
+            pixels[offset + 1] = green;
+            pixels[offset + 2] = blue;
+        }
+    }
+}
+
+// The main code that actually captures all 6 images and then saves them out to
+// disk before navigating the embedded web browser to the page with the WebGL
+// application that consumes them and creates an EQR image. This code runs as a
+// coroutine so it can be suspended at certain points.
+void LLFloater360Capture::capture360Images()
+{
+    // recheck the size of the cube map source images in case it changed
+    // since it was set when we opened the floater
+    setSourceImageSize();
+
+    // disable buttons while we are capturing
+    mCaptureBtn->setEnabled(false);
+    mSaveLocalBtn->setEnabled(false);
+
+    // determine whether or not to include avatar in the scene as we capture the 360 panorama
+    if (gSavedSettings.getBOOL("360CaptureHideAvatars"))
+    {
+        // Turn off the avatar if UI tells us to hide it.
+        // Note: the original call to gAvatar.hide(FALSE) did *not* hide
+        // attachments and so for most residents, there would be some debris
+        // left behind in the snapshot.
+        // Note: this toggles so if it set to on, this will turn it off and
+        // the subsequent call to the same thing after capture is finished
+        // will turn it back on again.  Similarly, for the case where it
+        // was set to off - I think this is what we need
+        LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_AVATAR);
+    }
+
+    // these are the 6 directions we will point the camera - essentially,
+    // North, South, East, West, Up, Down
+    LLVector3 look_dirs[6] = { LLVector3(1, 0, 0), LLVector3(0, 1, 0), LLVector3(0, 0, 1), LLVector3(-1, 0, 0), LLVector3(0, -1, 0), LLVector3(0, 0, -1) };
+    LLVector3 look_upvecs[6] = { LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, -1, 0), LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, 1, 0) };
+
+    // save current view/camera settings so we can restore them afterwards
+    S32 old_occlusion = LLPipeline::sUseOcclusion;
+    LLPipeline::sUseOcclusion = 0;
+    LLViewerCamera* camera = LLViewerCamera::getInstance();
+    F32 old_fov = camera->getView();
+    F32 old_aspect = camera->getAspect();
+    F32 old_yaw = camera->getYaw();
+
+    // stop the motion of as much of the world moving as much as we can
+    freezeWorld(true);
+
+    // Save the direction (in degrees) the camera is looking when we
+    // take the shot since that is what we write to image metadata
+    // 'GPano:InitialViewHeadingDegrees' field.
+    // We need to convert from the angle getYaw() gives us into something
+    // the XMP data field wants (N=0, E=90, S=180, W= 270 etc.)
+    mInitialHeadingDeg  = (360 + 90 - (int)(camera->getYaw() * RAD_TO_DEG)) % 360;
+    LL_INFOS("360Capture") << "Recording a heading of " << (int)(mInitialHeadingDeg) << LL_ENDL;
+
+    // camera constants for the square, cube map capture image
+    camera->setAspect(1.0); // must set aspect ratio first to avoid undesirable clamping of vertical FoV
+    camera->setView(F_PI_BY_TWO);
+    camera->yaw(0.0);
+
+    // record how many times we changed camera to try to understand the "all shots are the same issue"
+    unsigned int camera_changed_times = 0;
+
+    // the name of the JavaScript file written out that contains the 6 cube map images
+    // stored as a JavaScript array of data URLs.  If you change this filename, you must
+    // also change the corresponding entry in the HTML file that uses it -
+    // (newview/skins/default/html/common/equirectangular/display_eqr.html)
+    const std::string cumemap_js_filename("cubemap_img.js");
+
+    // construct the full path to this file - typically stored in the users'
+    // Second Life settings / username / eqrimg folder.
+    const std::string cubemap_js_full_path = makeFullPathToJS(cumemap_js_filename);
+
+    // Write the JavaScript file header (the top of the file before the
+    // declarations of the actual data URLs array).  In practice, all this writes
+    // is a comment - it's main purpose is to reset the file from the last time
+    // it was written
+    writeDataURLHeader(cubemap_js_full_path);
+
+    // the names of the prefixes we assign as the name to each data URL and are then
+    // consumed by the WebGL application. Nominally, they stand for positive and
+    // negative in the X/Y/Z directions.
+    static const std::string prefixes[6] =
+    {
+        "posx", "posz", "posy",
+        "negx", "negz", "negy",
+    };
+
+    // time the encode process for later optimization
+    auto encode_time_total = 0.0;
+
+    // for each of the 6 directions we shoot...
+    for (int i = 0; i < 6; i++)
+    {
+        // these buffers are where the raw, captured pixels are stored and
+        // the first time we use them, we have to make a new one
+        if (mRawImages[i] == nullptr)
+        {
+            mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3);
+        }
+        else
+            // subsequent capture with floater open so we resize the buffer from
+            // the previous run
+        {
+            // LLImageRaw deletes the old one via operator= but just to be
+            // sure, we delete its' large data member first...
+            mRawImages[i]->deleteData();
+            mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3);
+        }
+
+        // set up camera to look in each direction
+        camera->lookDir(look_dirs[i], look_upvecs[i]);
+
+        // record if camera changed to try to understand the "all shots are the same issue"
+        if (camera->isChanged())
+        {
+            ++camera_changed_times;
+        }
+
+        // call the (very) simplified snapshot code that simply deals
+        // with a single image, no sub-images etc. but is very fast
+        gViewerWindow->simpleSnapshot(mRawImages[i], mSourceImageSize, mSourceImageSize);
+
+        // encode each image and write to disk while saving how long it took to do so
+        auto t_start = std::chrono::high_resolution_clock::now();
+        encodeAndSave(mRawImages[i], cubemap_js_full_path, prefixes[i]);
+        auto t_end = std::chrono::high_resolution_clock::now();
+        auto duration = std::chrono::duration_cast<std::chrono::duration<double>>(t_end - t_start);
+        encode_time_total += duration.count();
+
+        // ping the main loop in case the snapshot process takes a really long
+        // time and we get disconnected
+        LLAppViewer::instance()->pingMainloopTimeout("LLFloater360Capture::capture360Images");
+    }
+
+    // display time to encode all 6 images.  It tends to be a fairly linear
+    // time for each so we don't need to worry about displaying the time
+    // for each - this gives us plenty to use for optimizing
+    LL_INFOS("360Capture") <<
+                           "Time to encode and save 6 images was " <<
+                           encode_time_total <<
+                           " seconds" <<
+                           LL_ENDL;
+
+    // Write the JavaScript file footer (the bottom of the file after the
+    // declarations of the actual data URLs array). The footer comprises of
+    // a JavaScript array declaration that references the 6 data URLs generated
+    // previously and is what is referred to in the display HTML file
+    // (newview/skins/default/html/common/equirectangular/display_eqr.html)
+    writeDataURLFooter(cubemap_js_full_path);
+
+    // unfreeze the world now we have our shots
+    freezeWorld(false);
+
+    // restore original view/camera/avatar settings settings
+    camera->setAspect(old_aspect);
+    camera->setView(old_fov);
+    camera->yaw(old_yaw);
+    LLPipeline::sUseOcclusion = old_occlusion;
+
+    // if we toggled off the avatar because the Hide check box was ticked,
+    // we should toggle it back to where it was before we started the capture
+    if (gSavedSettings.getBOOL("360CaptureHideAvatars"))
+    {
+        LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_AVATAR);
+    }
+
+    // record that we missed some shots in the log for later debugging
+    // note: we use 5 and not 6 because the first shot isn't regarded
+    // as a change - only the subsequent 5 are
+    if (camera_changed_times < 5)
+    {
+        LL_INFOS("360Capture") << "Warning: we only captured " << camera_changed_times << " images." << LL_ENDL;
+    }
+
+    // now we have the 6 shots saved in a well specified location,
+    // we can load the web content that uses them
+    std::string url = "file:///" + getHTMLBaseFolder() + mEqrGenHTML;
+    mWebBrowser->navigateTo(url);
+
+    // allow the UI to update by suspending and waiting for the
+    // main render loop to update the UI
+    suspendForAFrame();
+
+    // page is loaded and ready so we can turn on the buttons again
+    mCaptureBtn->setEnabled(true);
+    mSaveLocalBtn->setEnabled(true);
+}
+
+// once the request is made to navigate to the web page containing the code
+// to process the 6 images into an EQR one, we have to wait for it to finish
+// loaded - we get a "navigate complete" event when that happens that we can act on
+void LLFloater360Capture::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event)
+{
+    switch (event)
+    {
+        // not used right now but retaining because this event might
+        // be useful for a feature I am hoping to add
+        case MEDIA_EVENT_LOCATION_CHANGED:
+            break;
+
+        // navigation in the browser completed
+        case MEDIA_EVENT_NAVIGATE_COMPLETE:
+        {
+            // Confirm that the navigation event does indeed apply to the
+            // page we are looking for. At the moment, this is the only
+            // one we care about so the test is superfluous but that might change.
+            std::string navigate_url = self->getNavigateURI();
+            if (navigate_url.find(mEqrGenHTML) != std::string::npos)
+            {
+                // this string is being passed across to the web so replace all the windows backslash
+                // characters with forward slashes or (I think) the backslashes are treated as escapes
+                std::replace(mImageSaveDir.begin(), mImageSaveDir.end(), '\\', '/');
+
+                // we store the camera FOV (field of view) in a saved setting since this feels
+                // like something it would be interesting to change and experiment with
+                int camera_fov = gSavedSettings.getU32("360CaptureCameraFOV");
+
+                // compose the overlay for the final web page that tells the user
+                // what level of quality the capture was taken with
+                std::string overlay_label = "'" + getSelectedQualityTooltip() + "'";
+
+                // so now our page is loaded and images are in place - call
+                // the JavaScript init script with some parameters to initialize
+                // the WebGL based preview
+                const std::string cmd = STRINGIZE(
+                                            "init("
+                                            << mOutputImageWidth
+                                            << ", "
+                                            << mOutputImageHeight
+                                            << ", "
+                                            << "'"
+                                            << mImageSaveDir
+                                            << "'"
+                                            << ", "
+                                            << camera_fov
+                                            << ", "
+                                            << LLViewerCamera::getInstance()->getYaw()
+                                            << ", "
+                                            << overlay_label
+                                            << ")"
+                                        );
+
+                // execute the command on the page
+                mWebBrowser->getMediaPlugin()->executeJavaScript(cmd);
+            }
+        }
+        break;
+
+        default:
+            break;
+    }
+}
+
+// called when the user wants to save the cube maps off to the final EQR image
+void LLFloater360Capture::onSaveLocalBtn()
+{
+    // region name and URL
+    std::string region_name; // no sensible default
+    std::string region_url("http://secondlife.com");
+    LLViewerRegion* region = gAgent.getRegion();
+    if (region)
+    {
+        // region names can (and do) contain characters that would make passing
+        // them into a JavaScript function problematic - single quotes for example
+        // so we must escape/encode both
+        region_name = region->getName();
+
+        // escaping/encoding is a minefield - let's just remove any offending characters from the region name
+        region_name.erase(std::remove(region_name.begin(), region_name.end(), '\''), region_name.end());
+        region_name.erase(std::remove(region_name.begin(), region_name.end(), '\"'), region_name.end());
+
+        // fortunately there is already an escaping function built into the SLURL generation code
+        LLSLURL slurl;
+        bool is_escaped = true;
+        LLAgentUI::buildSLURL(slurl, is_escaped);
+        region_url = slurl.getSLURLString();
+    }
+
+    // build suggested filename (the one that appears as the default
+    // in the Save dialog box)
+    const std::string suggested_filename = generate_proposed_filename();
+
+    // This string (the name of the product plus a truncated version number (no build))
+    // is used in the XMP block as the name of the generating and stitching software.
+    // We save the version number here and not in the more generic 'software' item
+    // because that might help us determine something about the image in the future.
+    const std::string client_version = STRINGIZE(
+                                           LLVersionInfo::instance().getChannel() <<
+                                           " " <<
+                                           LLVersionInfo::instance().getShortVersion()
+                                       );
+
+    // save the time the image was created. I don't know if this should be
+    // UTC/ZULU or the users' local time. It probably doesn't matter.
+    std::time_t result = std::time(nullptr);
+    std::string ctime_str = std::ctime(&result);
+    std::string time_str = ctime_str.substr(0, ctime_str.length() - 1);
+
+    // build the JavaScript data structure that is used to pass all the
+    // variables into the JavaScript function on the web page loaded into
+    // the embedded browser component of the floater.
+    const std::string xmp_details = STRINGIZE(
+                                        "{ " <<
+                                        "pano_version: '" << "2.2.1" << "', " <<
+                                        "software: '" << LLVersionInfo::instance().getChannel() << "', " <<
+                                        "capture_software: '" << client_version << "', " <<
+                                        "stitching_software: '" << client_version << "', " <<
+                                        "width: " << mOutputImageWidth << ", " <<
+                                        "height: " << mOutputImageHeight << ", " <<
+                                        "heading: " << mInitialHeadingDeg << ", " <<
+                                        "actual_source_image_size: " << mQualityRadioGroup->getSelectedValue().asInteger() << ", " <<
+                                        "scaled_source_image_size: " << mSourceImageSize << ", " <<
+                                        "first_photo_date: '" << time_str << "', " <<
+                                        "last_photo_date: '" << time_str << "', " <<
+                                        "region_name: '" << region_name << "', " <<
+                                        "region_url: '" << region_url << "', " <<
+                                        " }"
+                                    );
+
+    // build the JavaScript command to send to the web browser
+    const std::string cmd = "saveAsEqrImage(\"" + suggested_filename + "\", " + xmp_details + ")";
+
+    // send it to the browser instance, triggering the equirectangular capture
+    // process and complimentary offer to save the image
+    mWebBrowser->getMediaPlugin()->executeJavaScript(cmd);
+}
+
+// We capture all 6 images sequentially and if parts of the world are moving
+// E.G. clouds, water, objects - then we may get seams or discontinuities
+// when the images are combined to form the EQR image.  This code tries to
+// stop everything so we can shoot for seamless shots.  There is probably more
+// we can do here - e.g. waves in the water probably won't line up.
+void LLFloater360Capture::freezeWorld(bool enable)
+{
+    static bool clouds_scroll_paused = false;
+    if (enable)
+    {
+        // record the cloud scroll current value so we can restore it
+        clouds_scroll_paused = LLEnvironment::instance().isCloudScrollPaused();
+
+        // stop the clouds moving
+        LLEnvironment::instance().pauseCloudScroll();
+
+        // freeze all avatars
+        LLCharacter* avatarp;
+        for (std::vector<LLCharacter*>::iterator iter = LLCharacter::sInstances.begin();
+                iter != LLCharacter::sInstances.end(); ++iter)
+        {
+            avatarp = *iter;
+            mAvatarPauseHandles.push_back(avatarp->requestPause());
+        }
+
+        // freeze everything else
+        gSavedSettings.setBOOL("FreezeTime", true);
+
+        // disable particle system
+        LLViewerPartSim::getInstance()->enable(false);
+    }
+    else // turning off freeze world mode, either temporarily or not.
+    {
+        // restart the clouds moving if they were not paused before
+        // we starting using the 360 capture floater
+        if (clouds_scroll_paused == false)
+        {
+            LLEnvironment::instance().resumeCloudScroll();
+        }
+
+        // thaw all avatars
+        mAvatarPauseHandles.clear();
+
+        // thaw everything else
+        gSavedSettings.setBOOL("FreezeTime", false);
+
+        //enable particle system
+        LLViewerPartSim::getInstance()->enable(true);
+    }
+}
+
+// Build the default filename that appears in the Save dialog box. We try
+// to encode some metadata about too (region name, EQR dimensions, capture
+// time) but the user if free to replace this with anything else before
+// the images is saved.
+const std::string LLFloater360Capture::generate_proposed_filename()
+{
+    std::ostringstream filename("");
+
+    // base name
+    filename << "sl360_";
+
+    LLViewerRegion* region = gAgent.getRegion();
+    if (region)
+    {
+        // this looks complex but it's straightforward - removes all non-alpha chars from a string
+        // which in this case is the SL region name - we use it as a proposed filename but the user is free to change
+        std::string region_name = region->getName();
+        std::replace_if(region_name.begin(), region_name.end(), std::not1(std::ptr_fun(isalnum)), '_');
+        if (region_name.length() > 0)
+        {
+            filename << region_name;
+            filename << "_";
+        }
+    }
+
+    // add in resolution to make it easier to tell what you captured later
+    filename << mOutputImageWidth;
+    filename << "x";
+    filename << mOutputImageHeight;
+    filename << "_";
+
+    // Add in the size of the source image (width == height since it was square)
+    // Might be useful later for quality comparisons
+    filename << mSourceImageSize;
+    filename << "_";
+
+    // add in the current HH-MM-SS (with leading 0's) so users can easily save many shots in same folder
+    std::time_t cur_epoch = std::time(nullptr);
+    std::tm* tm_time = std::localtime(&cur_epoch);
+    filename << std::setfill('0') << std::setw(4) << (tm_time->tm_year + 1900);
+    filename << std::setfill('0') << std::setw(2) << (tm_time->tm_mon + 1);
+    filename << std::setfill('0') << std::setw(2) << tm_time->tm_mday;
+    filename << "_";
+    filename << std::setfill('0') << std::setw(2) << tm_time->tm_hour;
+    filename << std::setfill('0') << std::setw(2) << tm_time->tm_min;
+    filename << std::setfill('0') << std::setw(2) << tm_time->tm_sec;
+
+    // the unusual way we save the output image (originates in the
+    // embedded browser and not the C++ code) means that the system
+    // appends ".jpeg" to the file automatically on macOS at least, 
+    // so we only need to do it ourselves for windows.
+#if LL_WINDOWS
+    filename << ".jpg";
+#endif
+
+    return filename.str();
+}
diff --git a/indra/newview/llfloater360capture.h b/indra/newview/llfloater360capture.h
new file mode 100644
index 0000000000000000000000000000000000000000..6da7ee074a399e4fa25f76af36aac1d0b18ab718
--- /dev/null
+++ b/indra/newview/llfloater360capture.h
@@ -0,0 +1,97 @@
+/**
+ * @file llfloater360capture.h
+ * @author Callum Prentice (callum@lindenlab.com)
+ * @brief Floater for the 360 capture feature
+ *
+ * $LicenseInfo:firstyear=2011&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2011, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_FLOATER_360CAPTURE_H
+#define LL_FLOATER_360CAPTURE_H
+
+#include "llfloater.h"
+#include "llmediactrl.h"
+#include "llcharacter.h"
+
+class LLImageRaw;
+class LLTextBox;
+class LLRadioGroup;
+
+class LLFloater360Capture:
+    public LLFloater,
+    public LLViewerMediaObserver
+{
+        friend class LLFloaterReg;
+
+    private:
+        LLFloater360Capture(const LLSD& key);
+
+        ~LLFloater360Capture();
+        BOOL postBuild() override;
+        void handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) override;
+
+        void changeInterestListMode(bool send_everything);
+
+        const std::string getHTMLBaseFolder();
+        void capture360Images();
+
+        const std::string makeFullPathToJS(const std::string filename);
+        void writeDataURLHeader(const std::string filename);
+        void writeDataURLFooter(const std::string filename);
+        bool writeDataURL(const std::string filename, const std::string prefix, U8* data, unsigned int data_len);
+        void encodeAndSave(LLPointer<LLImageRaw> raw_image, const std::string filename, const std::string prefix);
+
+        std::vector<LLAnimPauseRequest> mAvatarPauseHandles;
+        void freezeWorld(bool enable);
+
+        void mockSnapShot(LLImageRaw* raw);
+
+        void suspendForAFrame();
+
+        const std::string generate_proposed_filename();
+
+        void setSourceImageSize();
+
+        LLMediaCtrl* mWebBrowser;
+        const std::string mDefaultHTML = "default.html";
+        const std::string mEqrGenHTML = "eqr_gen.html";
+
+        LLUICtrl* mCaptureBtn;
+        void onCapture360ImagesBtn();
+
+        void onSaveLocalBtn();
+        LLUICtrl* mSaveLocalBtn;
+
+        LLRadioGroup* mQualityRadioGroup;
+        void onChooseQualityRadioGroup();
+        const std::string getSelectedQualityTooltip();
+
+        int mSourceImageSize;
+        float mInitialHeadingDeg;
+        int mOutputImageWidth;
+        int mOutputImageHeight;
+        std::string mImageSaveDir;
+
+        LLPointer<LLImageRaw> mRawImages[6];
+};
+
+#endif  // LL_FLOATER_360CAPTURE_H
diff --git a/indra/newview/llfloatersnapshot.cpp b/indra/newview/llfloatersnapshot.cpp
index ef7a9fd5366eec9e9cef73482c09926c06aa9f59..83212230e52acafc59fdae2cf08414d48024b07c 100644
--- a/indra/newview/llfloatersnapshot.cpp
+++ b/indra/newview/llfloatersnapshot.cpp
@@ -179,6 +179,10 @@ void LLFloaterSnapshotBase::ImplBase::updateLayout(LLFloaterSnapshotBase* floate
 	thumbnail_placeholder->reshape(panel_width, thumbnail_placeholder->getRect().getHeight());
 	floaterp->getChild<LLUICtrl>("image_res_text")->setVisible(mAdvanced);
 	floaterp->getChild<LLUICtrl>("file_size_label")->setVisible(mAdvanced);
+    if (floaterp->hasChild("360_label", TRUE))
+    { 
+        floaterp->getChild<LLUICtrl>("360_label")->setVisible(mAdvanced);
+    }
 	if(!floaterp->isMinimized())
 	{
 		floaterp->reshape(floater_width, floaterp->getRect().getHeight());
@@ -992,6 +996,10 @@ BOOL LLFloaterSnapshot::postBuild()
     getChild<LLButton>("retract_btn")->setCommitCallback(boost::bind(&LLFloaterSnapshot::onExtendFloater, this));
     getChild<LLButton>("extend_btn")->setCommitCallback(boost::bind(&LLFloaterSnapshot::onExtendFloater, this));
 
+    getChild<LLTextBox>("360_label")->setSoundFlags(LLView::MOUSE_UP);
+    getChild<LLTextBox>("360_label")->setShowCursorHand(false);
+    getChild<LLTextBox>("360_label")->setClickedCallback(boost::bind(&LLFloaterSnapshot::on360Snapshot, this));
+
 	// Filters
 	LLComboBox* filterbox = getChild<LLComboBox>("filters_combobox");
 	std::vector<std::string> filter_list = LLImageFiltersManager::getInstance()->getFiltersList();
@@ -1118,6 +1126,12 @@ void LLFloaterSnapshot::onExtendFloater()
 	impl->setAdvanced(gSavedSettings.getBOOL("AdvanceSnapshot"));
 }
 
+void LLFloaterSnapshot::on360Snapshot()
+{
+    LLFloaterReg::showInstance("360capture");
+    closeFloater();
+}
+
 //virtual
 void LLFloaterSnapshotBase::onClose(bool app_quitting)
 {
diff --git a/indra/newview/llfloatersnapshot.h b/indra/newview/llfloatersnapshot.h
index 8221b0a637043c9a389104e81f1e4b84c180be3a..7ec133ff4585cfa2fd85300452b0ca135afd11c7 100644
--- a/indra/newview/llfloatersnapshot.h
+++ b/indra/newview/llfloatersnapshot.h
@@ -153,6 +153,7 @@ class LLFloaterSnapshot : public LLFloaterSnapshotBase
 	static void update();
 
 	void onExtendFloater();
+    void on360Snapshot();
 
 	static LLFloaterSnapshot* getInstance();
 	static LLFloaterSnapshot* findInstance();
diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp
index 5a05f897588f723e5f975a66eec5be9b33fd59a3..62d73063aa4ca14e03c2f9fabba2a94f4a3dcb76 100644
--- a/indra/newview/llviewerfloaterreg.cpp
+++ b/indra/newview/llviewerfloaterreg.cpp
@@ -33,6 +33,7 @@
 #include "llcommandhandler.h"
 #include "llcompilequeue.h"
 #include "llfasttimerview.h"
+#include "llfloater360capture.h"
 #include "llfloaterabout.h"
 #include "llfloateraddpaymentmethod.h"
 #include "llfloaterauction.h"
@@ -195,6 +196,7 @@ void LLViewerFloaterReg::registerFloaters()
 	// *NOTE: Please keep these alphabetized for easier merges
 
 	LLFloaterAboutUtil::registerFloater();
+	LLFloaterReg::add("360capture", "floater_360capture.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloater360Capture>);
 	LLFloaterReg::add("block_timers", "floater_fast_timers.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFastTimerView>);
 	LLFloaterReg::add("about_land", "floater_about_land.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLand>);
 	LLFloaterReg::add("add_payment_method", "floater_add_payment_method.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterAddPaymentMethod>);
diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp
index d35dbda907d6942119f8b9714ab6b431565c408c..c428e73a6e54fc91ce91b976fe3e7b9368e15bae 100644
--- a/indra/newview/llviewermedia.cpp
+++ b/indra/newview/llviewermedia.cpp
@@ -1746,8 +1746,16 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_
 			media_source->cookies_enabled( cookies_enabled || clean_browser);
 
 			// collect 'javascript enabled' setting from prefs and send to embedded browser
-			bool javascript_enabled = gSavedSettings.getBOOL( "BrowserJavascriptEnabled" );
-			media_source->setJavascriptEnabled( javascript_enabled || clean_browser);
+			bool javascript_enabled = gSavedSettings.getBOOL("BrowserJavascriptEnabled");
+			media_source->setJavascriptEnabled(javascript_enabled || clean_browser);
+
+			// collect 'web security disabled' (see Chrome --web-security-disabled) setting from prefs and send to embedded browser
+			bool web_security_disabled = gSavedSettings.getBOOL("BrowserWebSecurityDisabled");
+			media_source->setWebSecurityDisabled(web_security_disabled || clean_browser);
+
+			// collect setting indicates if local file access from file URLs is allowed from prefs and send to embedded browser
+			bool file_access_from_file_urls = gSavedSettings.getBOOL("BrowserFileAccessFromFileUrls");
+			media_source->setFileAccessFromFileUrlsEnabled(file_access_from_file_urls || clean_browser);
 
 			// As of SL-15559 PDF files do not load in CEF v91 we enable plugins
 			// but explicitly disable Flash (PDF support in CEF is now treated as a plugin)
@@ -1907,6 +1915,15 @@ void LLViewerMediaImpl::loadURI()
 	}
 }
 
+//////////////////////////////////////////////////////////////////////////////////////////
+void LLViewerMediaImpl::executeJavaScript(const std::string& code)
+{
+	if (mMediaSource)
+	{
+		mMediaSource->executeJavaScript(code);
+	}
+}
+
 //////////////////////////////////////////////////////////////////////////////////////////
 void LLViewerMediaImpl::setSize(int width, int height)
 {
@@ -3212,8 +3229,19 @@ void LLViewerMediaImpl::handleMediaEvent(LLPluginClassMedia* plugin, LLPluginCla
 
 		case LLViewerMediaObserver::MEDIA_EVENT_FILE_DOWNLOAD:
 		{
-			//llinfos << "Media event - file download requested - filename is " << self->getFileDownloadFilename() << llendl;
-			LLNotificationsUtil::add("MediaFileDownloadUnsupported");
+			LL_DEBUGS("Media") << "Media event - file download requested - filename is " << plugin->getFileDownloadFilename() << LL_ENDL;
+			// pick a file from SAVE FILE dialog
+
+			// need a better algorithm that this or else, pass in type of save type
+			// from event that initiated it - this is okay for now - only thing
+			// that saves is 360s
+			std::string suggested_filename = plugin->getFileDownloadFilename();
+			LLFilePicker::ESaveFilter filter = LLFilePicker::FFSAVE_ALL;
+			if (suggested_filename.find(".jpg") != std::string::npos || suggested_filename.find(".jpeg") != std::string::npos)
+				filter = LLFilePicker::FFSAVE_JPEG;
+			if (suggested_filename.find(".png") != std::string::npos)
+				filter = LLFilePicker::FFSAVE_PNG;
+			init_threaded_picker_save_dialog(plugin, filter, suggested_filename);
 		}
 		break;
 
diff --git a/indra/newview/llviewermedia.h b/indra/newview/llviewermedia.h
index 8bf1ad2441632294890c71069eae2228499643aa..71cec5125d91aa4f86cf1757b7eb8920af9fa421 100644
--- a/indra/newview/llviewermedia.h
+++ b/indra/newview/llviewermedia.h
@@ -207,6 +207,7 @@ class LLViewerMediaImpl
 	bool initializeMedia(const std::string& mime_type);
 	bool initializePlugin(const std::string& media_type);
 	void loadURI();
+	void executeJavaScript(const std::string& code);
 	LLPluginClassMedia* getMediaPlugin() { return mMediaSource.get(); }
 	void setSize(int width, int height);
 
diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp
index ad81cb07c19a99d68c641770754134fcc3783220..0eda1523ff4d1f7fbd7a06bc1ff7f744ffcef461 100644
--- a/indra/newview/llviewermenu.cpp
+++ b/indra/newview/llviewermenu.cpp
@@ -1263,12 +1263,55 @@ class LLAdvancedDumpScriptedCamera : public view_listener_t
 class LLAdvancedDumpRegionObjectCache : public view_listener_t
 {
 	bool handleEvent(const LLSD& userdata)
-{
+	{
 		handle_dump_region_object_cache(NULL);
 		return true;
 	}
 };
 
+class LLAdvancedInterestListFullUpdate : public view_listener_t
+{
+	bool handleEvent(const LLSD& userdata)
+	{
+		LLSD request;
+		LLSD body;
+		static bool using_360 = false;
+
+		if (using_360)
+		{
+			body["mode"] = LLSD::String("default");
+		}
+		else
+		{
+			body["mode"] = LLSD::String("360");
+		}
+		using_360 = !using_360;
+
+        if (gAgent.requestPostCapability("InterestList", body, [](const LLSD& response)
+        {
+            LL_INFOS("360Capture") <<
+                "InterestList capability responded: \n" <<
+                ll_pretty_print_sd(response) <<
+                LL_ENDL;
+        }))
+        {
+            LL_INFOS("360Capture") <<
+                "Successfully posted an InterestList capability request with payload: \n" <<
+                ll_pretty_print_sd(body) <<
+                LL_ENDL;
+            return true;
+        }
+        else
+        {
+            LL_INFOS("360Capture") <<
+                "Unable to post an InterestList capability request with payload: \n" <<
+                ll_pretty_print_sd(body) <<
+                LL_ENDL;
+            return false;
+        }
+	}
+};
+
 class LLAdvancedBuyCurrencyTest : public view_listener_t
 	{
 	bool handleEvent(const LLSD& userdata)
@@ -2309,11 +2352,10 @@ class LLAdvancedLeaveAdminStatus : public view_listener_t
 // Advanced > Debugging //
 //////////////////////////
 
-
 class LLAdvancedForceErrorBreakpoint : public view_listener_t
 {
 	bool handleEvent(const LLSD& userdata)
-	{
+	{		
 		force_error_breakpoint(NULL);
 		return true;
 	}
@@ -9189,6 +9231,7 @@ void initialize_menus()
 	// Advanced > World
 	view_listener_t::addMenu(new LLAdvancedDumpScriptedCamera(), "Advanced.DumpScriptedCamera");
 	view_listener_t::addMenu(new LLAdvancedDumpRegionObjectCache(), "Advanced.DumpRegionObjectCache");
+	view_listener_t::addMenu(new LLAdvancedInterestListFullUpdate(), "Advanced.InterestListFullUpdate");
 
 	// Advanced > UI
 	commit.add("Advanced.WebBrowserTest", boost::bind(&handle_web_browser_test,	_2));	// sigh! this one opens the MEDIA browser
diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp
index 39c891c9c1624a5c2a7ca59e07455a6fe582e49f..7737a2753fe1d0bda40e29df6035e0ad80db94c4 100644
--- a/indra/newview/llviewermessage.cpp
+++ b/indra/newview/llviewermessage.cpp
@@ -3745,6 +3745,7 @@ void process_kill_object(LLMessageSystem *mesgsys, void **user_data)
 				{
 					LLColor4 color(0.f,1.f,0.f,1.f);
 					gPipeline.addDebugBlip(objectp->getPositionAgent(), color);
+					LL_DEBUGS("MessageBlip") << "Kill blip for local " << local_id << " at " << objectp->getPositionAgent() << LL_ENDL;
 				}
 
 				// Do the kill
diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp
index b88baf6aa72f3b3b5f8cacdb4eee85778f637b67..31e80eb8655041a69b1da519ee8f67eaa06a8254 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -2402,6 +2402,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys,
 			color.setVec(1.f, 0.f, 0.f, 1.f);
 		}
 		gPipeline.addDebugBlip(getPositionAgent(), color);
+		LL_DEBUGS("MessageBlip") << "Update type " << (S32)update_type << " blip for local " << mLocalID << " at " << getPositionAgent() << LL_ENDL;
 	}
 
 	const F32 MAG_CUTOFF = F_APPROXIMATELY_ZERO;
diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp
index 7628a6c7ef16ad3d27f999a251d8ac35e416d763..df84938eac61c96d5a48d10a49bc9169b1bd3533 100644
--- a/indra/newview/llviewerregion.cpp
+++ b/indra/newview/llviewerregion.cpp
@@ -2956,6 +2956,8 @@ void LLViewerRegionImpl::buildCapabilityNames(LLSD& capabilityNames)
 	capabilityNames.append("IncrementCOFVersion");
 	AISAPI::getCapNames(capabilityNames);
 
+	capabilityNames.append("InterestList");
+
 	capabilityNames.append("GetDisplayNames");
 	capabilityNames.append("GetExperiences");
 	capabilityNames.append("AgentExperiences");
diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp
index 1d13a306ef7a5c22aea857cf0c3bb0db44972470..81e19e9fe20d5bc84fc78a9e426738d96c9ec09f 100644
--- a/indra/newview/llviewerwindow.cpp
+++ b/indra/newview/llviewerwindow.cpp
@@ -5160,6 +5160,83 @@ BOOL LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei
 	return ret;
 }
 
+BOOL LLViewerWindow::simpleSnapshot(LLImageRaw* raw, S32 image_width, S32 image_height)
+{
+    gDisplaySwapBuffers = FALSE;
+
+    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+    setCursor(UI_CURSOR_WAIT);
+
+    BOOL prev_draw_ui = gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI) ? TRUE : FALSE;
+    if (prev_draw_ui != false)
+    {
+        LLPipeline::toggleRenderDebugFeature(LLPipeline::RENDER_DEBUG_FEATURE_UI);
+    }
+
+    LLPipeline::sShowHUDAttachments = FALSE;
+    LLRect window_rect = getWorldViewRectRaw();
+
+    S32 original_width = LLPipeline::sRenderDeferred ? gPipeline.mDeferredScreen.getWidth() : gViewerWindow->getWorldViewWidthRaw();
+    S32 original_height = LLPipeline::sRenderDeferred ? gPipeline.mDeferredScreen.getHeight() : gViewerWindow->getWorldViewHeightRaw();
+
+    LLRenderTarget scratch_space;
+    U32 color_fmt = GL_RGBA;
+    const bool use_depth_buffer = true;
+    const bool use_stencil_buffer = true;
+    if (scratch_space.allocate(image_width, image_height, color_fmt, use_depth_buffer, use_stencil_buffer))
+    {
+        if (gPipeline.allocateScreenBuffer(image_width, image_height))
+        {
+            mWorldViewRectRaw.set(0, image_height, image_width, 0);
+
+            scratch_space.bindTarget();
+        }
+        else
+        {
+            scratch_space.release();
+            gPipeline.allocateScreenBuffer(original_width, original_height);
+        }
+    }
+
+    const U32 subfield = 0;
+    const bool do_rebuild = true;
+    const F32 zoom = 1.0;
+    const bool for_snapshot = TRUE;
+    display(do_rebuild, zoom, subfield, for_snapshot);
+
+    glReadPixels(
+        0, 0,
+        image_width,
+        image_height,
+        GL_RGB, GL_UNSIGNED_BYTE,
+        raw->getData()
+    );
+    stop_glerror();
+
+    gDisplaySwapBuffers = FALSE;
+    gDepthDirty = TRUE;
+
+    if (!gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI))
+    {
+        if (prev_draw_ui != false)
+        {
+            LLPipeline::toggleRenderDebugFeature(LLPipeline::RENDER_DEBUG_FEATURE_UI);
+        }
+    }
+
+    LLPipeline::sShowHUDAttachments = TRUE;
+
+    setCursor(UI_CURSOR_ARROW);
+
+    gPipeline.resetDrawOrders();
+    mWorldViewRectRaw = window_rect;
+    scratch_space.flush();
+    scratch_space.release();
+    gPipeline.allocateScreenBuffer(original_width, original_height);
+
+    return true;
+}
+
 void LLViewerWindow::destroyWindow()
 {
 	if (mWindow)
diff --git a/indra/newview/llviewerwindow.h b/indra/newview/llviewerwindow.h
index 8a6df613dc28aa724475bdee092e1ddb3c8b3050..dd1a32edefdf215961120c81e3241ecaaa0e93c9 100644
--- a/indra/newview/llviewerwindow.h
+++ b/indra/newview/llviewerwindow.h
@@ -357,7 +357,10 @@ class LLViewerWindow : public LLWindowCallbacks
 	BOOL			saveSnapshot(const std::string&  filename, S32 image_width, S32 image_height, BOOL show_ui = TRUE, BOOL show_hud = TRUE, BOOL do_rebuild = FALSE, LLSnapshotModel::ESnapshotLayerType type = LLSnapshotModel::SNAPSHOT_TYPE_COLOR, LLSnapshotModel::ESnapshotFormat format = LLSnapshotModel::SNAPSHOT_FORMAT_BMP);
 	BOOL			rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_height, BOOL keep_window_aspect = TRUE, BOOL is_texture = FALSE,
 		BOOL show_ui = TRUE, BOOL show_hud = TRUE, BOOL do_rebuild = FALSE, LLSnapshotModel::ESnapshotLayerType type = LLSnapshotModel::SNAPSHOT_TYPE_COLOR, S32 max_size = MAX_SNAPSHOT_IMAGE_SIZE);
-	BOOL			thumbnailSnapshot(LLImageRaw *raw, S32 preview_width, S32 preview_height, BOOL show_ui, BOOL show_hud, BOOL do_rebuild, LLSnapshotModel::ESnapshotLayerType type);
+
+    BOOL			simpleSnapshot(LLImageRaw *raw, S32 image_width, S32 image_height);
+
+    BOOL			thumbnailSnapshot(LLImageRaw *raw, S32 preview_width, S32 preview_height, BOOL show_ui, BOOL show_hud, BOOL do_rebuild, LLSnapshotModel::ESnapshotLayerType type);
 	BOOL			isSnapshotLocSet() const;
 	void			resetSnapshotLoc() const;
 
diff --git a/indra/newview/skins/default/html/common/equirectangular/default.html b/indra/newview/skins/default/html/common/equirectangular/default.html
new file mode 100644
index 0000000000000000000000000000000000000000..227b3065909d45b120ff120ab8482d59265aa129
--- /dev/null
+++ b/indra/newview/skins/default/html/common/equirectangular/default.html
@@ -0,0 +1,22 @@
+<html>
+<head>
+<style>
+body {
+  background-color:#000;
+  background-image: linear-gradient(white 2px, transparent 2px),
+  linear-gradient(90deg, white 2px, transparent 2px),
+  linear-gradient(rgba(255,255,255,.3) 1px, transparent 1px),
+  linear-gradient(90deg, rgba(255,255,255,.3) 1px, transparent 1px);
+  background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px;
+  background-position:-2px -2px, -2px -2px, -1px -1px, -1px -1px;
+}
+</style>
+</head>
+<body>
+<script>
+function start() {
+}
+document.addEventListener('DOMContentLoaded', start);
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/indra/newview/skins/default/html/common/equirectangular/eqr_gen.html b/indra/newview/skins/default/html/common/equirectangular/eqr_gen.html
new file mode 100644
index 0000000000000000000000000000000000000000..855c26c65136a881b53e1e2c23f453551c3c83df
--- /dev/null
+++ b/indra/newview/skins/default/html/common/equirectangular/eqr_gen.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <style>
+        body {
+            background: #333;
+            padding: 0;
+            margin: 0;
+            overflow: hidden;
+        }
+        #error_message {
+            z-index: 2;
+            background-color: #aa3333;
+            overflow: hidden;
+            display:  none;
+            pointer-events:none;
+            font-family: monospace;
+            font-size: 3em;
+            color: white;
+            border-radius: 1em;
+            padding: 1em;
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            margin-right: -50%;
+            transform: translate(-50%, -50%)
+        }
+        #quality_window {
+            z-index: 100;
+            position:  absolute;
+            left: 8px;
+            top: 8px;
+            width: auto;
+            border-radius: 16px;
+            height: auto;
+            font-size: 1.5em;
+            text-align: center;
+            font-family: monospace;
+            background-color: rgba(200,200,200,0.35);
+            color: #000;
+            padding-left: 16px;
+            padding-right: 16px;
+            padding-top: 8px;
+            padding-bottom: 8px;
+        }
+    </style>
+</head>
+<body>
+    <script src="js/three.min.js"></script>
+    <script src="js/OrbitControls.js"></script>
+    <script src="js/jpeg_encoder_basic.js" type="text/javascript"></script>
+    <script src="js/CubemapToEquirectangular.js"></script>
+    <script>
+        var controls, camera, scene, renderer, equiManaged;
+
+        function init(eqr_width, eqr_height, img_path, camera_fov, initial_heading, overlay_label) {
+
+            camera = new THREE.PerspectiveCamera(camera_fov, window.innerWidth / window.innerHeight, 0.1, 100);
+            camera.position.x = 0.01;
+
+            scene = new THREE.Scene();
+
+            renderer = new THREE.WebGLRenderer();
+            renderer.autoClear = false;
+            renderer.setPixelRatio(window.devicePixelRatio);
+            renderer.setSize(window.innerWidth, window.innerHeight);
+
+            var cubemap_img_js_url = img_path + '/cubemap_img.js';
+            var cubemap_image_js = document.createElement('script');
+            cubemap_image_js.setAttribute('type', 'text/javascript');
+            cubemap_image_js.setAttribute('src', cubemap_img_js_url);
+            document.getElementsByTagName('head')[0].appendChild(cubemap_image_js);
+            cubemap_image_js.onload = function () {
+                document.getElementById("error_message").style.display = 'none' 
+                scene.background = new THREE.CubeTextureLoader().load(cubemap_img_js);
+                equiManaged = new CubemapToEquirectangular(renderer, true, eqr_width, eqr_height);
+            };
+            cubemap_image_js.onerror = function () {
+                document.getElementById("error_message").style.display = 'inline-block' 
+            };
+
+            document.body.appendChild(renderer.domElement);
+            window.addEventListener('resize', onWindowResize, false);
+
+            controls = new THREE.OrbitControls(camera, renderer.domElement);
+            controls.autoRotate = true;
+            controls.autoRotateSpeed = 0.2;
+            controls.enableZoom = false;
+            controls.enablePan = false;
+            controls.enableDamping = true;
+            controls.dampingFactor = 0.15;
+            controls.rotateSpeed = -0.5;
+
+            // initial direction the camera faces
+            // We cannot edit camera rotation directly as the OrbitControls will
+            // immediately reset it so we need some math to tell the controls
+            // there to look at initially. Note there is also an offset of π/2 since
+            // the Viewer and three.js have slightly different coordinate systems 
+            var spherical_target = new THREE.Spherical(1, Math.PI / 2, initial_heading + Math.PI / 2)
+            var target = new THREE.Vector3().setFromSpherical(spherical_target) 
+            camera.position.set(target.x, target.y, target.z);
+            controls.update();
+            controls.saveState();
+
+            // update the text that gets passed in from the C++ app for
+            // the translucent overlay label that tells us what we are seeing
+            document.getElementById('quality_window').innerHTML = overlay_label;
+
+            animate();
+        }
+
+        window.addEventListener(
+            'mousedown',
+            function (event) {
+                controls.autoRotate = false;
+            },
+            false
+        );
+
+        window.addEventListener(
+            'dblclick',
+            function (event) {
+                controls.autoRotate = true;
+            },
+            false
+        );
+
+        function saveAsEqrImage(filename, xmp_details) {
+            equiManaged.update(camera, scene, filename, xmp_details);
+        }
+
+        function onWindowResize() {
+            camera.aspect = window.innerWidth / window.innerHeight;
+            camera.updateProjectionMatrix();
+            renderer.setSize(window.innerWidth, window.innerHeight);
+        }
+
+        function animate() {
+            requestAnimationFrame(animate);
+            controls.update();
+            renderer.render(scene, camera);
+        }
+    </script>
+    <div id="error_message">UNABLE TO LOAD EQR IMAGE</div>
+    <div id="quality_window">Preview Quality</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/indra/newview/skins/default/textures/textures.xml b/indra/newview/skins/default/textures/textures.xml
index 03878d9fe73e187c7f9306cce1c092436c44bc16..a36b859b6c0e2374aea0a7aeee5c447fd931c82d 100644
--- a/indra/newview/skins/default/textures/textures.xml
+++ b/indra/newview/skins/default/textures/textures.xml
@@ -131,7 +131,8 @@ with the same filename but different name
   <texture name="Check_Mark" file_name="icons/check_mark.png" preload="true" />
 
   <texture name="Checker" file_name="checker.png" preload="false" />
-  
+
+  <texture name="Command_360_Capture_Icon"  file_name="toolbar_icons/360_capture.png"  preload="true" />
   <texture name="Command_AboutLand_Icon"    file_name="toolbar_icons/land.png"         preload="true" />
   <texture name="Command_Appearance_Icon"   file_name="toolbar_icons/appearance.png"   preload="true" />
   <texture name="Command_Avatar_Icon"       file_name="toolbar_icons/avatars.png"      preload="true" />
diff --git a/indra/newview/skins/default/textures/toolbar_icons/360_capture.png b/indra/newview/skins/default/textures/toolbar_icons/360_capture.png
new file mode 100644
index 0000000000000000000000000000000000000000..163cebe29fc7ff3c510d78e5769aca0d38aec1bb
Binary files /dev/null and b/indra/newview/skins/default/textures/toolbar_icons/360_capture.png differ
diff --git a/indra/newview/skins/default/xui/en/floater_360capture.xml b/indra/newview/skins/default/xui/en/floater_360capture.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c2959b9853c2ddf0a1d33cd75225d7d0b0a2a6ba
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/floater_360capture.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<floater can_resize="true"
+         height="400"
+         layout="topleft"
+         min_height="300"
+         min_width="400"
+         name="360capture"
+         help_topic="360capture"
+         save_rect="true"
+         title="360 Capture"
+         width="800">
+  <panel layout="topleft"
+       background_visible="true"
+       top="0"
+       follows="left|bottom|top"
+       left="0"
+       width="200"
+       bg_opaque_color="0.195 0.195 0.195 1"
+       background_opaque="true"
+       height="400"
+       name="ui_panel">
+    <text
+       follows="top|left|right"
+       height="16"
+       layout="topleft"
+       left="10"
+       top="10"
+       width="100">
+      Quality level
+    </text>
+    <radio_group
+      control_name="360QualitySelection"
+      follows="left|top"
+      height="94"
+      layout="topleft"
+      left_delta="20"
+      name="360_quality_selection"
+      top_pad="0"
+      width="180">
+      <radio_item
+        height="20"
+        label="Preview (fast)"
+        layout="topleft"
+        left="0"
+        name="preview_quality"
+        value="128"
+        tool_tip="Preview quality"
+        top="0"
+        width="100" />
+      <radio_item
+         height="20"
+         label="Medium"
+         layout="topleft"
+         left_delta="0"
+         name="medium_quality"
+         value="512"
+         tool_tip="Medium quality"
+         top_delta="20"
+         width="100" />
+      <radio_item
+         height="20"
+         label="High"
+         layout="topleft"
+         left_delta="0"
+         name="high_quality"
+         value="1024"
+         tool_tip="High quality"
+         top_delta="20"
+         width="100" />
+      <radio_item
+         height="20"
+         label="Maximum"
+         layout="topleft"
+         left_delta="0"
+         name="maximum_quality"
+         value="2048"
+         tool_tip="Maximum quality"
+         top_delta="20"
+         width="100" />
+    </radio_group>
+    <check_box control_name="360CaptureHideAvatars"
+               follows="left|top"
+               height="15"
+               label="Hide all avatars"
+               layout="left"
+               left="10"
+               name="360_hide_avatar"
+               top_delta="0"
+               width="100"/>
+    <button follows="left|top"
+            height="20"
+            label="Capture 360 image"
+            layout="topleft"
+            left="10"
+            name="capture_button"
+            top_delta="32"
+            width="180" />
+    <button follows="left|top"
+            height="20"
+            label="Save as.."
+            layout="topleft"
+            left="10"
+            name="save_local_button"
+            top_delta="35"
+            width="180" />
+  </panel>
+  <web_browser top="0"
+               follows="all"
+               bg_opaque_color="0.225 0.225 0.225 1"
+               left="200"
+               width="600"
+               height="380"
+               name="360capture_contents"
+               trusted_content="true" />
+  <text follows="bottom"
+        layout="topleft"
+        name="statusbar"
+        height="17"
+        left="210"
+        top="383"
+        width="600">
+       Click and drag on the image to pan
+  </text>
+</floater>
\ No newline at end of file
diff --git a/indra/newview/skins/default/xui/en/floater_snapshot.xml b/indra/newview/skins/default/xui/en/floater_snapshot.xml
index 832c2ee7da62bfcb0c439653c4bc87d69a3987b4..f441e3cbd7eba15775786768876423e3caf7a086 100644
--- a/indra/newview/skins/default/xui/en/floater_snapshot.xml
+++ b/indra/newview/skins/default/xui/en/floater_snapshot.xml
@@ -400,7 +400,7 @@
    layout="topleft"
    name="img_info_border"
    top_pad="0"
-   right="-10"
+   right="-130"
    follows="left|top|right"
    left_delta="0"/>
    <text
@@ -411,11 +411,10 @@
     height="14"
     layout="topleft"
     left="220"
-	right="-20"
     halign="left"
     name="image_res_text"
     top_delta="5"
-    width="200">
+    width="250">
        [WIDTH]px (width) x [HEIGHT]px (height)
    </text>
    <text
@@ -423,7 +422,7 @@
     font="SansSerifSmall"
     height="14"
     layout="topleft"
-    left="-65"
+    left="-185"
     length="1"
     halign="right"
     name="file_size_label"
@@ -432,4 +431,19 @@
     width="50">
        [SIZE] KB
    </text>
+   <text
+    follows="right|top"
+    font="SansSerifSmall"
+    height="14"
+    layout="topleft"
+    left="-130"
+    length="1"
+    halign="right"
+    name="360_label"
+    text_color="0.3 0.82 1 1"
+    top_delta="0"
+    type="string"
+    width="115">
+    Take a 360 snapshot
+   </text>
 </floater>
diff --git a/indra/newview/skins/default/xui/en/floater_toybox.xml b/indra/newview/skins/default/xui/en/floater_toybox.xml
index bc19d6e79f5d70c134c092a52516d7b5c686e9cf..bdc04a8a78864d4e32de2a6c5ab6ec9242bdceaf 100644
--- a/indra/newview/skins/default/xui/en/floater_toybox.xml
+++ b/indra/newview/skins/default/xui/en/floater_toybox.xml
@@ -4,7 +4,7 @@
   can_dock="false"
   can_minimize="false"
   can_resize="false"
-  height="375"
+  height="430"
   help_topic="toybox"
   layout="topleft"
   legacy_header_height="18"
@@ -45,7 +45,7 @@
       Buttons will appear as shown or as icon-only depending on each toolbar's settings.
   </text>
   <toolbar
-    bottom="310"
+    bottom="365"
     button_display_mode="icons_with_text"
     follows="all"
     left="20"
@@ -81,11 +81,11 @@
   <panel
     bevel_style="none"
     border="true"
-    bottom="311"
+    bottom="366"
     follows="left|bottom|right"
     left="20"
     right="-20"
-    top="311" />
+    top="366" />
   <button
     follows="left|bottom|right"
     height="23"
@@ -94,7 +94,7 @@
     layout="topleft"
     left="185"
     name="btn_clear_all"
-    top="330"
+    top="385"
     width="130">
     <button.commit_callback function="Toybox.ClearAll" />
   </button>
@@ -106,7 +106,7 @@
     layout="topleft"
     left="335"
     name="btn_restore_defaults"
-    top="330"
+    top="385"
     width="130">
     <button.commit_callback function="Toybox.RestoreDefaults" />
   </button>
diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml
index 72cce2208f9a7310efa58ffdbda1926db13885a2..52afe9164e3d663ea4e81a4ab2d280f5b65c38dc 100644
--- a/indra/newview/skins/default/xui/en/menu_viewer.xml
+++ b/indra/newview/skins/default/xui/en/menu_viewer.xml
@@ -733,6 +733,15 @@
              function="Floater.Show"
              parameter="snapshot" />
         </menu_item_call>
+        
+<menu_item_call
+         label="Capture 360"
+         name="Capture 360"
+         shortcut="control|alt|shift|s">
+            <menu_item_call.on_click
+             function="Floater.Show"
+             parameter="360capture" />
+        </menu_item_call>
         <menu_item_separator/>
         <menu_item_call
          label="Place profile"
@@ -3427,6 +3436,14 @@ function="World.EnvPreset"
                 <menu_item_call.on_click
                  function="Advanced.DumpRegionObjectCache" />
             </menu_item_call>
+        
+<menu_item_call
+             label="Interest List: Full Update"
+             name="Interest List: Full Update"
+             shortcut="alt|shift|I">
+                <menu_item_call.on_click
+                 function="Advanced.InterestListFullUpdate" />
+            </menu_item_call>
         </menu>
         <menu
          create_jump_keys="true"
diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml
index d115e09d5bae60db6f279b55d84cefd047291590..ae04a7c2b4b17e1d3c36c8f1a5566c94d903bb55 100644
--- a/indra/newview/skins/default/xui/en/strings.xml
+++ b/indra/newview/skins/default/xui/en/strings.xml
@@ -4124,6 +4124,8 @@ Try enclosing path to the editor with double quotes.
 
   <!-- commands -->
 
+  <string 
+name="Command_360_Capture_Label">360 Capture</string>
   <string name="Command_AboutLand_Label">About land</string>
   <string name="Command_Appearance_Label">Outfits</string>
   <string name="Command_Avatar_Label">Complete avatars</string>
@@ -4154,6 +4156,8 @@ Try enclosing path to the editor with double quotes.
   <string name="Command_View_Label">Camera controls</string>
   <string name="Command_Voice_Label">Voice settings</string>
 
+  <string 
+name="Command_360_Capture_Tooltip">Capture a 360 panorama image</string>
   <string name="Command_AboutLand_Tooltip">Information about the land you're visiting</string>
   <string name="Command_Appearance_Tooltip">Change your avatar</string>
   <string name="Command_Avatar_Tooltip">Choose a complete avatar</string>
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index 41da8fa3287e482f5a30361897d123f1b0eeb4cb..7e24c1f800dba5f25d5bbd6d42b0f008a3bc8208 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -158,18 +158,12 @@ def construct(self):
                     self.path("*/xui/*/widgets/*.xml")
                     self.path("*/*.xml")
 
-                    # Local HTML files (e.g. loading screen)
-                    # The claim is that we never use local html files any
-                    # longer. But rather than commenting out this block, let's
-                    # rename every html subdirectory as html.old. That way, if
-                    # we're wrong, a user actually does have the relevant
-                    # files; s/he just needs to rename every html.old
-                    # directory back to html to recover them.
-                    with self.prefix(src="*/html", dst="*/html.old"):
-                            self.path("*.png")
-                            self.path("*/*/*.html")
-                            self.path("*/*/*.gif")
-
+                    # Update: 2017-11-01 CP Now we store app code in the html folder
+                    #         Initially the HTML/JS code to render equirectangular
+                    #         images for the 360 capture feature but more to follow.
+                    with self.prefix(src="*/html", dst="*/html"):
+                        self.path("*/*/*/*.js")
+                        self.path("*/*/*.html")
 
             #build_data.json.  Standard with exception handling is fine.  If we can't open a new file for writing, we have worse problems
             #platform is computed above with other arg parsing