diff --git a/CMakeLists.txt b/CMakeLists.txt
index 008047b9e567579fc3bc8f40ff2dcab5a993273e..0874ce89a2e7a83211ac765c4027997b440195a0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -13,6 +13,7 @@ enable_testing()
 option(WITH_PCL "Use PCL if available" OFF)
 option(WITH_NVPIPE "Use NvPipe for compression if available" ON)
 option(WITH_OPTFLOW "Use NVIDIA Optical Flow if available" OFF)
+option(WITH_OPENVR "Build with OpenVR support" OFF)
 option(WITH_FIXSTARS "Use Fixstars libSGM if available" ON)
 option(BUILD_VISION "Enable the vision component" ON)
 option(BUILD_RECONSTRUCT "Enable the reconstruction component" ON)
@@ -42,19 +43,20 @@ if (LibArchive_FOUND)
 	set(HAVE_LIBARCHIVE true)
 endif()
 
-## OpenVR API path
-find_library(OPENVR_LIBRARIES
-  NAMES
-    openvr_api
-)
-set(OPENVR_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../headers)
-
-if (OPENVR_LIBRARIES)
-	message(STATUS "Found OpenVR: ${OPENVR_LIBRARIES}")
-	set(HAVE_OPENVR true)
+if (WITH_OPENVR)
+	## OpenVR API path
+	find_library(OPENVR_LIBRARIES
+		NAMES
+			openvr_api
+	)
+	set(OPENVR_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../headers)
+
+	if (OPENVR_LIBRARIES)
+		message(STATUS "Found OpenVR: ${OPENVR_LIBRARIES}")
+		set(HAVE_OPENVR true)
+	endif()
 endif()
 
-
 if (WITH_FIXSTARS)
 	find_package( LibSGM )
 	if (LibSGM_FOUND)
diff --git a/applications/gui/CMakeLists.txt b/applications/gui/CMakeLists.txt
index fbed0680dde997c0f0a76519ac272fb3e5c1c64f..fe553becc5ef883873f0385383b06ede5f3cc6ab 100644
--- a/applications/gui/CMakeLists.txt
+++ b/applications/gui/CMakeLists.txt
@@ -15,6 +15,10 @@ set(GUISRC
 	src/thumbview.cpp
 )
 
+if (HAVE_OPENVR)
+	list(APPEND GUISRC "src/vr.cpp")
+endif()
+
 add_executable(ftl-gui ${GUISRC})
 
 target_include_directories(ftl-gui PUBLIC
diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index efd3313224295677153733a9e3c4ca8e3e5eddee..8530915bfa14181d682fb1cad42f5e939d29184d 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -1,9 +1,12 @@
 #include "camera.hpp"
 #include "pose_window.hpp"
 #include "screen.hpp"
-
 #include <nanogui/glutil.h>
 
+#ifdef HAVE_OPENVR
+#include "vr.hpp"
+#endif
+
 using ftl::rgbd::isValidDepth;
 using ftl::gui::GLTexture;
 using ftl::gui::PoseWindow;
@@ -116,13 +119,13 @@ void StatisticsImage::getValidRatio(cv::Mat &out) {
 }
 
 static Eigen::Affine3d create_rotation_matrix(float ax, float ay, float az) {
-  Eigen::Affine3d rx =
-      Eigen::Affine3d(Eigen::AngleAxisd(ax, Eigen::Vector3d(1, 0, 0)));
-  Eigen::Affine3d ry =
-      Eigen::Affine3d(Eigen::AngleAxisd(ay, Eigen::Vector3d(0, 1, 0)));
-  Eigen::Affine3d rz =
-      Eigen::Affine3d(Eigen::AngleAxisd(az, Eigen::Vector3d(0, 0, 1)));
-  return rz * rx * ry;
+	Eigen::Affine3d rx =
+		Eigen::Affine3d(Eigen::AngleAxisd(ax, Eigen::Vector3d(1, 0, 0)));
+	Eigen::Affine3d ry =
+		Eigen::Affine3d(Eigen::AngleAxisd(ay, Eigen::Vector3d(0, 1, 0)));
+	Eigen::Affine3d rz =
+		Eigen::Affine3d(Eigen::AngleAxisd(az, Eigen::Vector3d(0, 0, 1)));
+	return rz * rx * ry;
 }
 
 ftl::gui::Camera::Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src) : screen_(screen), src_(src) {
@@ -145,14 +148,17 @@ ftl::gui::Camera::Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src) : scr
 	posewin_->setTheme(screen->windowtheme);
 	posewin_->setVisible(false);
 
-	src->setCallback([this](int64_t ts, cv::Mat &rgb, cv::Mat &depth) {
+	src->setCallback([this](int64_t ts, cv::Mat &channel1, cv::Mat &channel2) {
 		UNIQUE_LOCK(mutex_, lk);
-		rgb_.create(rgb.size(), rgb.type());
-		depth_.create(depth.size(), depth.type());
-		cv::swap(rgb_,rgb);
-		cv::swap(depth_, depth);
-		cv::flip(rgb_,rgb_,0);
-		cv::flip(depth_,depth_,0);
+		im1_.create(channel1.size(), channel1.type());
+		im2_.create(channel2.size(), channel2.type());
+
+		//cv::swap(channel1, im1_);
+		//cv::swap(channel2, im2_);
+		
+		// OpenGL (0,0) bottom left
+		cv::flip(channel1, im1_, 0);
+		cv::flip(channel2, im2_, 0);
 	});
 }
 
@@ -231,7 +237,34 @@ void ftl::gui::Camera::showSettings() {
 
 }
 
+#ifdef HAVE_OPENVR
+bool ftl::gui::Camera::setVR(bool on) {
+	if (on == vr_mode_) {
+		LOG(WARNING) << "VR mode already enabled";
+		return on;
+	}
+
+	if (on) {
+		setChannel(Channel::Right);
+		vr_mode_ = true;
+	}
+	else {
+		vr_mode_ = false;
+		setChannel(Channel::Left); // reset to left channel
+	}
+	
+	return vr_mode_;
+}
+#endif
+
 void ftl::gui::Camera::setChannel(Channel c) {
+#ifdef HAVE_OPENVR
+	if (isVR()) {
+		LOG(ERROR) << "Changing channel in VR mode is not possible.";
+		return;
+	}
+#endif
+
 	channel_ = c;
 	switch (c) {
 	case Channel::Energy:
@@ -256,17 +289,6 @@ void ftl::gui::Camera::setChannel(Channel c) {
 	}
 }
 
-static Eigen::Matrix4d ConvertSteamVRMatrixToMatrix4( const vr::HmdMatrix34_t &matPose )
-{
-	Eigen::Matrix4d matrixObj;
-	matrixObj <<
-		matPose.m[0][0], matPose.m[1][0], matPose.m[2][0], 0.0,
-		matPose.m[0][1], matPose.m[1][1], matPose.m[2][1], 0.0,
-		matPose.m[0][2], matPose.m[1][2], matPose.m[2][2], 0.0,
-		matPose.m[0][3], matPose.m[1][3], matPose.m[2][3], 1.0f;
-	return matrixObj;
-}
-
 static void visualizeDepthMap(	const cv::Mat &depth, cv::Mat &out,
 								const float max_depth)
 {
@@ -308,11 +330,12 @@ static void drawEdges(	const cv::Mat &in, cv::Mat &out,
 bool ftl::gui::Camera::thumbnail(cv::Mat &thumb) {
 	UNIQUE_LOCK(mutex_, lk);
 	src_->grab(1,9);
-	if (rgb_.empty()) return false;
-	cv::resize(rgb_, thumb, cv::Size(320,180));
+	if (im1_.empty()) return false;
+	cv::resize(im1_, thumb, cv::Size(320,180));
+	cv::flip(thumb, thumb, 0);
 	return true;
 }
-
+#include <math.h>
 const GLTexture &ftl::gui::Camera::captureFrame() {
 	float now = (float)glfwGetTime();
 	delta_ = now - ftime_;
@@ -320,30 +343,49 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 
 	if (src_ && src_->isReady()) {
 		UNIQUE_LOCK(mutex_, lk);
-
-		if (screen_->hasVR()) {
+		
+		if (isVR()) {
 			#ifdef HAVE_OPENVR
-			src_->setChannel(Channel::Right);
-
 			vr::VRCompositor()->WaitGetPoses(rTrackedDevicePose_, vr::k_unMaxTrackedDeviceCount, NULL, 0 );
 
-			if ( rTrackedDevicePose_[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid )
+			if ((channel_ == Channel::Right) && rTrackedDevicePose_[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid )
 			{
-				auto pose = ConvertSteamVRMatrixToMatrix4( rTrackedDevicePose_[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking );
-				pose.inverse();
-
-				// Lerp the Eye
-				eye_[0] += (neye_[0] - eye_[0]) * lerpSpeed_ * delta_;
-				eye_[1] += (neye_[1] - eye_[1]) * lerpSpeed_ * delta_;
-				eye_[2] += (neye_[2] - eye_[2]) * lerpSpeed_ * delta_;
-
+				Eigen::Matrix4d eye_l = ConvertSteamVRMatrixToMatrix4(
+					vr::VRSystem()->GetEyeToHeadTransform(vr::Eye_Left));
+				
+				// assumes eye_l(3, 0) = -eye_r(3, 0)
+				// => baseline = abs(2*eye_l(3, 0))
+				float baseline_in = 2.0 * -eye_l(0, 3);
+				if (baseline_in != baseline_) {
+					baseline_ = baseline_in;
+					// TODO: update baseline on ftl-reconstruct
+				}
+				
+				Eigen::Matrix4d pose = ConvertSteamVRMatrixToMatrix4(rTrackedDevicePose_[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking);
+				
+				// translate to L eye
 				Eigen::Translation3d trans(eye_);
 				Eigen::Affine3d t(trans);
 				Eigen::Matrix4d viewPose = t.matrix() * pose;
 
+				// flip rotation (different coordinate axis on OpenGL/ftl)
+				// NOTE: image also flipped!
+
+				Eigen::Vector3d ea = viewPose.block<3, 3>(0, 0).eulerAngles(0, 1, 2);
+				//double rd = 180.0 / M_PI;
+				//LOG(INFO) << "Camera X: " << ea[0] *rd << ", Y: " << ea[1] * rd << ", Z: " << ea[2] * rd;
+
+				// NOTE: If modified, should be verified with VR headset!
+				Eigen::Matrix3d R;
+				R =		Eigen::AngleAxisd(-ea[0], Eigen::Vector3d::UnitX()) *
+						Eigen::AngleAxisd(ea[1], Eigen::Vector3d::UnitY()) *
+						Eigen::AngleAxisd(ea[2], Eigen::Vector3d::UnitZ()); 
+				viewPose.block<3, 3>(0, 0) = R;
+
 				if (src_->hasCapabilities(ftl::rgbd::kCapMovable)) src_->setPose(viewPose);
+				
 			} else {
-				LOG(ERROR) << "No VR Pose";
+				//LOG(ERROR) << "No VR Pose";
 			}
 			#endif
 		} else {
@@ -360,10 +402,11 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 		}
 
 		src_->grab();
-		//src_->getFrames(rgb, depth);
 
 		// When switching from right to depth, client may still receive
 		// right images from previous batch (depth.channels() == 1 check)
+
+		/* todo: does not work
 		if (channel_ == Channel::Deviation &&
 			depth_.rows > 0 && depth_.channels() == 1)
 		{
@@ -372,60 +415,68 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 			}
 			
 			stats_->update(depth_);
-		}
+		}*/
 
 		cv::Mat tmp;
 
 		switch(channel_) {
 			case Channel::Confidence:
-				if (depth_.rows == 0) { break; }
-				visualizeEnergy(depth_, tmp, 1.0);
-				texture_.update(tmp);
+				if (im2_.rows == 0) { break; }
+				visualizeEnergy(im2_, tmp, 1.0);
+				texture2_.update(tmp);
 				break;
+			
 			case Channel::Density:
 			case Channel::Energy:
-				if (depth_.rows == 0) { break; }
-				visualizeEnergy(depth_, tmp, 10.0);
-				texture_.update(tmp);
+				if (im2_.rows == 0) { break; }
+				visualizeEnergy(im2_, tmp, 10.0);
+				texture2_.update(tmp);
 				break;
+			
 			case Channel::Depth:
-				if (depth_.rows == 0) { break; }
-				visualizeDepthMap(depth_, tmp, 7.0);
-				if (screen_->root()->value("showEdgesInDepth", false)) drawEdges(rgb_, tmp);
-				texture_.update(tmp);
+				if (im2_.rows == 0) { break; }
+				visualizeDepthMap(im2_, tmp, 7.0);
+				if (screen_->root()->value("showEdgesInDepth", false)) drawEdges(im1_, tmp);
+				texture2_.update(tmp);
 				break;
 			
 			case Channel::Deviation:
-				if (depth_.rows == 0) { break; }
+				if (im2_.rows == 0) { break; }/*
 				//imageSize = Vector2f(depth.cols, depth.rows);
 				stats_->getStdDev(tmp);
 				tmp.convertTo(tmp, CV_8U, 1000.0);
 				applyColorMap(tmp, tmp, cv::COLORMAP_HOT);
-				texture_.update(tmp);
+				texture2_.update(tmp);*/
 				break;
 
 		case Channel::Flow:
 		case Channel::Normals:
 		case Channel::Right:
-				if (depth_.rows == 0 || depth_.type() != CV_8UC3) { break; }
-				texture_.update(depth_);
+				if (im2_.rows == 0 || im2_.type() != CV_8UC3) { break; }
+				texture2_.update(im2_);
 				break;
 
 			default:
-				if (rgb_.rows == 0) { break; }
+				break;
+				/*if (rgb_.rows == 0) { break; }
 				//imageSize = Vector2f(rgb.cols,rgb.rows);
 				texture_.update(rgb_);
-
 				#ifdef HAVE_OPENVR
 				if (screen_->hasVR() && depth_.channels() >= 3) {
 					LOG(INFO) << "DRAW RIGHT";
 					textureRight_.update(depth_);
 				}
 				#endif
+				*/
+		}
+
+		if (im1_.rows != 0) {
+			//imageSize = Vector2f(rgb.cols,rgb.rows);
+			texture1_.update(im1_);
 		}
 	}
 
-	return texture_;
+	return texture1_;
 }
 
 nlohmann::json ftl::gui::Camera::getMetaData() {
diff --git a/applications/gui/src/camera.hpp b/applications/gui/src/camera.hpp
index 1c5ebef55dbd9531d56d51659c2104ab614fece2..24dcbacfdc51d20166ffc49175f268140e4dcdf3 100644
--- a/applications/gui/src/camera.hpp
+++ b/applications/gui/src/camera.hpp
@@ -39,14 +39,15 @@ class Camera {
 	void showSettings();
 
 	void setChannel(ftl::codecs::Channel c);
-
+	const ftl::codecs::Channel getChannel() { return channel_; }
+	
 	void togglePause();
 	void isPaused();
 	const ftl::codecs::Channels &availableChannels();
 
 	const GLTexture &captureFrame();
-	const GLTexture &getLeft() const { return texture_; }
-	const GLTexture &getRight() const { return textureRight_; }
+	const GLTexture &getLeft() const { return texture1_; }
+	const GLTexture &getRight() const { return texture2_; }
 
 	bool thumbnail(cv::Mat &thumb);
 
@@ -54,12 +55,21 @@ class Camera {
 
 	StatisticsImage *stats_ = nullptr;
 
+
+#ifdef HAVE_OPENVR
+	bool isVR() { return vr_mode_; }
+	bool setVR(bool on);
+#else
+	bool isVR() { return false; }
+#endif
+
 	private:
 	Screen *screen_;
 	ftl::rgbd::Source *src_;
 	GLTexture thumb_;
-	GLTexture texture_;
-	GLTexture textureRight_;
+	GLTexture texture1_; // first channel (always left at the moment)
+	GLTexture texture2_; // second channel ("right")
+
 	ftl::gui::PoseWindow *posewin_;
 	nlohmann::json meta_;
 	Eigen::Vector4d neye_;
@@ -73,12 +83,15 @@ class Camera {
 	bool pause_;
 	ftl::codecs::Channel channel_;
 	ftl::codecs::Channels channels_;
-	cv::Mat rgb_;
-	cv::Mat depth_;
+	cv::Mat im1_; // first channel (left)
+	cv::Mat im2_; // second channel ("right")
+
 	MUTEX mutex_;
 
 	#ifdef HAVE_OPENVR
 	vr::TrackedDevicePose_t rTrackedDevicePose_[ vr::k_unMaxTrackedDeviceCount ];
+	bool vr_mode_;
+	float baseline_;
 	#endif
 };
 
diff --git a/applications/gui/src/ctrl_window.cpp b/applications/gui/src/ctrl_window.cpp
index 06543822027853969ca7fee277523896969a100f..67a3682bd2fcfcb55a68fb9c71b6dd55aad36491 100644
--- a/applications/gui/src/ctrl_window.cpp
+++ b/applications/gui/src/ctrl_window.cpp
@@ -26,8 +26,8 @@ ControlWindow::ControlWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
 	_updateDetails();
 
 	auto tools = new Widget(this);
-    tools->setLayout(new BoxLayout(Orientation::Horizontal,
-                                       Alignment::Middle, 0, 6));
+	tools->setLayout(new BoxLayout(	Orientation::Horizontal,
+									Alignment::Middle, 0, 6));
 
 	auto button = new Button(tools, "", ENTYPO_ICON_PLUS);
 	button->setCallback([this] {
@@ -35,6 +35,9 @@ ControlWindow::ControlWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
 		_addNode();
 	});
 	button->setTooltip("Add new node");
+	
+	// commented-out buttons not working/useful
+	/*
 	button = new Button(tools, "", ENTYPO_ICON_CYCLE);
 	button->setCallback([this] {
 		ctrl_->restart();
@@ -43,7 +46,7 @@ ControlWindow::ControlWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
 	button->setCallback([this] {
 		ctrl_->pause();
 	});
-	button->setTooltip("Pause all nodes");
+	button->setTooltip("Pause all nodes");*/
 
 	new Label(this, "Select Node","sans-bold");
 	auto select = new ComboBox(this, node_titles_);
@@ -55,14 +58,14 @@ ControlWindow::ControlWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
 	new Label(this, "Node Options","sans-bold");
 
 	tools = new Widget(this);
-    tools->setLayout(new BoxLayout(Orientation::Horizontal,
-                                       Alignment::Middle, 0, 6));
+	tools->setLayout(new BoxLayout(	Orientation::Horizontal,
+									Alignment::Middle, 0, 6));
 
-	button = new Button(tools, "", ENTYPO_ICON_INFO);
+	/*button = new Button(tools, "", ENTYPO_ICON_INFO);
 	button->setCallback([this] {
 		
 	});
-	button->setTooltip("Node status information");
+	button->setTooltip("Node status information");*/
 
 	button = new Button(tools, "", ENTYPO_ICON_COG);
 	button->setCallback([this,parent] {
@@ -71,17 +74,17 @@ ControlWindow::ControlWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
 	});
 	button->setTooltip("Edit node configuration");
 
-	button = new Button(tools, "", ENTYPO_ICON_CYCLE);
+	/*button = new Button(tools, "", ENTYPO_ICON_CYCLE);
 	button->setCallback([this] {
 		ctrl_->restart(_getActiveID());
 	});
-	button->setTooltip("Restart this node");
+	button->setTooltip("Restart this node");*/
 
-	button = new Button(tools, "", ENTYPO_ICON_CONTROLLER_PAUS);
+	/*button = new Button(tools, "", ENTYPO_ICON_CONTROLLER_PAUS);
 	button->setCallback([this] {
 		ctrl_->pause(_getActiveID());
 	});
-	button->setTooltip("Pause node processing");
+	button->setTooltip("Pause node processing");*/
 
 	ctrl->getNet()->onConnect([this,select](ftl::net::Peer *p) {
 		_updateDetails();
diff --git a/applications/gui/src/media_panel.cpp b/applications/gui/src/media_panel.cpp
index 826111c9800339eac124f4a59e532e950f7d15e6..b3ea1a6afbbc8f2a31bf294e85a85564fdffe1cb 100644
--- a/applications/gui/src/media_panel.cpp
+++ b/applications/gui/src/media_panel.cpp
@@ -19,6 +19,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 
 	paused_ = false;
 	writer_ = nullptr;
+	disable_switch_channels_ = false;
 
 	setLayout(new BoxLayout(Orientation::Horizontal,
 									Alignment::Middle, 5, 10));
@@ -77,6 +78,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 
 	//button = new Button(this, "", ENTYPO_ICON_CONTROLLER_RECORD);
 
+	/* Doesn't work at the moment
  #ifdef HAVE_LIBARCHIVE
 	auto button_snapshot = new Button(this, "", ENTYPO_ICON_IMAGES);
 	button_snapshot->setTooltip("Screen capture");
@@ -102,103 +104,133 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 	});
 #endif
 
-    auto popbutton = new PopupButton(this, "", ENTYPO_ICON_LAYERS);
-    popbutton->setSide(Popup::Side::Right);
-	popbutton->setChevronIcon(ENTYPO_ICON_CHEVRON_SMALL_RIGHT);
-    Popup *popup = popbutton->popup();
-    popup->setLayout(new GroupLayout());
+	
+	// not very useful (l/r)
+
+	auto button_dual = new Button(this, "", ENTYPO_ICON_MAP);
+	button_dual->setCallback([this]() {
+		screen_->setDualView(!screen_->getDualView());
+	});
+	*/
+
+#ifdef HAVE_OPENVR
+	if (this->screen_->hasVR()) {
+		auto button_vr = new Button(this, "VR");
+		button_vr->setFlags(Button::ToggleButton);
+		button_vr->setChangeCallback([this, button_vr](bool state) {
+			if (!screen_->useVR()) {
+				if (screen_->switchVR(true) == true) {
+					button_vr->setTextColor(nanogui::Color(0.5f,0.5f,1.0f,1.0f));
+					this->button_channels_->setEnabled(false);
+				}
+			}
+			else {
+				if (screen_->switchVR(false) == false) {
+					button_vr->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
+					this->button_channels_->setEnabled(true);
+				}
+			}
+		});
+	}
+#endif
+
+	button_channels_ = new PopupButton(this, "", ENTYPO_ICON_LAYERS);
+	button_channels_->setSide(Popup::Side::Right);
+	button_channels_->setChevronIcon(ENTYPO_ICON_CHEVRON_SMALL_RIGHT);
+	Popup *popup = button_channels_->popup();
+	popup->setLayout(new GroupLayout());
 	popup->setTheme(screen->toolbuttheme);
-    popup->setAnchorHeight(150);
-
-    button = new Button(popup, "Left");
-    button->setFlags(Button::RadioButton);
-    button->setPushed(true);
-    button->setCallback([this]() {
-        ftl::gui::Camera *cam = screen_->activeCamera();
-        if (cam) {
-            cam->setChannel(Channel::Left);
-        }
-    });
+	popup->setAnchorHeight(150);
+
+	button = new Button(popup, "Left");
+	button->setFlags(Button::RadioButton);
+	button->setPushed(true);
+	button->setCallback([this]() {
+		ftl::gui::Camera *cam = screen_->activeCamera();
+		if (cam) {
+			cam->setChannel(Channel::Left);
+		}
+	});
 
 	right_button_ = new Button(popup, "Right");
-    right_button_->setFlags(Button::RadioButton);
-    right_button_->setCallback([this]() {
-        ftl::gui::Camera *cam = screen_->activeCamera();
-        if (cam) {
-            cam->setChannel(Channel::Right);
-        }
-    });
-
-    depth_button_ = new Button(popup, "Depth");
-    depth_button_->setFlags(Button::RadioButton);
-    depth_button_->setCallback([this]() {
-        ftl::gui::Camera *cam = screen_->activeCamera();
-        if (cam) {
-            cam->setChannel(Channel::Depth);
-        }
-    });
-
-	popbutton = new PopupButton(popup, "More");
-    popbutton->setSide(Popup::Side::Right);
+	right_button_->setFlags(Button::RadioButton);
+	right_button_->setCallback([this]() {
+		ftl::gui::Camera *cam = screen_->activeCamera();
+		if (cam) {
+			cam->setChannel(Channel::Right);
+		}
+	});
+
+	depth_button_ = new Button(popup, "Depth");
+	depth_button_->setFlags(Button::RadioButton);
+	depth_button_->setCallback([this]() {
+		ftl::gui::Camera *cam = screen_->activeCamera();
+		if (cam) {
+			cam->setChannel(Channel::Depth);
+		}
+	});
+
+	auto *popbutton = new PopupButton(popup, "More");
+	popbutton->setSide(Popup::Side::Right);
 	popbutton->setChevronIcon(ENTYPO_ICON_CHEVRON_SMALL_RIGHT);
-    popup = popbutton->popup();
-    popup->setLayout(new GroupLayout());
+	popup = popbutton->popup();
+	popup->setLayout(new GroupLayout());
 	popup->setTheme(screen->toolbuttheme);
-    popup->setAnchorHeight(150);
+	popup->setAnchorHeight(150);
 
-    button = new Button(popup, "Deviation");
-    button->setFlags(Button::RadioButton);
-    button->setCallback([this]() {
-        ftl::gui::Camera *cam = screen_->activeCamera();
-        if (cam) {
-            cam->setChannel(Channel::Deviation);
-        }
-    });
+	button = new Button(popup, "Deviation");
+	button->setFlags(Button::RadioButton);
+	button->setCallback([this]() {
+		ftl::gui::Camera *cam = screen_->activeCamera();
+		if (cam) {
+			cam->setChannel(Channel::Deviation);
+		}
+	});
 
 	button = new Button(popup, "Normals");
-    button->setFlags(Button::RadioButton);
-    button->setCallback([this]() {
-        ftl::gui::Camera *cam = screen_->activeCamera();
-        if (cam) {
-            cam->setChannel(Channel::Normals);
-        }
-    });
+	button->setFlags(Button::RadioButton);
+	button->setCallback([this]() {
+		ftl::gui::Camera *cam = screen_->activeCamera();
+		if (cam) {
+			cam->setChannel(Channel::Normals);
+		}
+	});
 
 	button = new Button(popup, "Flow");
-    button->setFlags(Button::RadioButton);
-    button->setCallback([this]() {
-        ftl::gui::Camera *cam = screen_->activeCamera();
-        if (cam) {
-            cam->setChannel(Channel::Flow);
-        }
-    });
+	button->setFlags(Button::RadioButton);
+	button->setCallback([this]() {
+		ftl::gui::Camera *cam = screen_->activeCamera();
+		if (cam) {
+			cam->setChannel(Channel::Flow);
+		}
+	});
 
 	button = new Button(popup, "Confidence");
-    button->setFlags(Button::RadioButton);
-    button->setCallback([this]() {
-        ftl::gui::Camera *cam = screen_->activeCamera();
-        if (cam) {
-            cam->setChannel(Channel::Confidence);
-        }
-    });
-
-    button = new Button(popup, "Energy");
-    button->setFlags(Button::RadioButton);
-    button->setCallback([this]() {
-        ftl::gui::Camera *cam = screen_->activeCamera();
-        if (cam) {
-            cam->setChannel(Channel::Energy);
-        }
-    });
+	button->setFlags(Button::RadioButton);
+	button->setCallback([this]() {
+		ftl::gui::Camera *cam = screen_->activeCamera();
+		if (cam) {
+			cam->setChannel(Channel::Confidence);
+		}
+	});
+
+	button = new Button(popup, "Energy");
+	button->setFlags(Button::RadioButton);
+	button->setCallback([this]() {
+		ftl::gui::Camera *cam = screen_->activeCamera();
+		if (cam) {
+			cam->setChannel(Channel::Energy);
+		}
+	});
 
 	button = new Button(popup, "Density");
-    button->setFlags(Button::RadioButton);
-    button->setCallback([this]() {
-        ftl::gui::Camera *cam = screen_->activeCamera();
-        if (cam) {
-            cam->setChannel(Channel::Density);
-        }
-    });
+	button->setFlags(Button::RadioButton);
+	button->setCallback([this]() {
+		ftl::gui::Camera *cam = screen_->activeCamera();
+		if (cam) {
+			cam->setChannel(Channel::Density);
+		}
+	});
 
 }
 
@@ -208,12 +240,12 @@ MediaPanel::~MediaPanel() {
 
 // Update button enabled status
 void MediaPanel::cameraChanged() {
-    ftl::gui::Camera *cam = screen_->activeCamera();
-    if (cam) {
-        if (cam->source()->hasCapabilities(ftl::rgbd::kCapStereo)) {
-            right_button_->setEnabled(true);
-        } else {
-            right_button_->setEnabled(false);
-        }
-    }
+	ftl::gui::Camera *cam = screen_->activeCamera();
+	if (cam) {
+		if (cam->source()->hasCapabilities(ftl::rgbd::kCapStereo)) {
+			right_button_->setEnabled(true);
+		} else {
+			right_button_->setEnabled(false);
+		}
+	}
 }
diff --git a/applications/gui/src/media_panel.hpp b/applications/gui/src/media_panel.hpp
index 9e9154d860483a6bf6c88b8da856a4a89862de2f..0279cb3fadab41003ceefef538e8f42a20f00139 100644
--- a/applications/gui/src/media_panel.hpp
+++ b/applications/gui/src/media_panel.hpp
@@ -14,18 +14,22 @@ namespace gui {
 class Screen;
 
 class MediaPanel : public nanogui::Window {
-    public:
-    explicit MediaPanel(ftl::gui::Screen *);
-    ~MediaPanel();
-
-    void cameraChanged();
-
-    private:
-    ftl::gui::Screen *screen_;
-    bool paused_;
-    ftl::rgbd::SnapshotStreamWriter *writer_;
-    nanogui::Button *right_button_;
-    nanogui::Button *depth_button_;
+	public:
+	explicit MediaPanel(ftl::gui::Screen *);
+	~MediaPanel();
+
+	void cameraChanged();
+
+	private:
+	ftl::gui::Screen *screen_;
+
+	bool paused_;
+	bool disable_switch_channels_;
+
+	ftl::rgbd::SnapshotStreamWriter *writer_;
+	nanogui::PopupButton *button_channels_;
+	nanogui::Button *right_button_;
+	nanogui::Button *depth_button_;
 };
 
 }
diff --git a/applications/gui/src/screen.cpp b/applications/gui/src/screen.cpp
index 03d1847112fa86468d9661d5a9966b71a3d46b09..b0a74d37a2e39856901006504093c29c708e8189 100644
--- a/applications/gui/src/screen.cpp
+++ b/applications/gui/src/screen.cpp
@@ -20,6 +20,10 @@
 #include "camera.hpp"
 #include "media_panel.hpp"
 
+#ifdef HAVE_OPENVR
+#include "vr.hpp"
+#endif
+
 using ftl::gui::Screen;
 using ftl::gui::Camera;
 using std::string;
@@ -27,28 +31,28 @@ using ftl::rgbd::Source;
 using ftl::rgbd::isValidDepth;
 
 namespace {
-    constexpr char const *const defaultImageViewVertexShader =
-        R"(#version 330
-        uniform vec2 scaleFactor;
-        uniform vec2 position;
-        in vec2 vertex;
-        out vec2 uv;
-        void main() {
-            uv = vertex;
-            vec2 scaledVertex = (vertex * scaleFactor) + position;
-            gl_Position  = vec4(2.0*scaledVertex.x - 1.0,
-                                2.0*scaledVertex.y - 1.0,
-                                0.0, 1.0);
-        })";
-
-    constexpr char const *const defaultImageViewFragmentShader =
-        R"(#version 330
-        uniform sampler2D image;
-        out vec4 color;
-        in vec2 uv;
-        void main() {
-            color = texture(image, uv);
-        })";
+	constexpr char const *const defaultImageViewVertexShader =
+		R"(#version 330
+		uniform vec2 scaleFactor;
+		uniform vec2 position;
+		in vec2 vertex;
+		out vec2 uv;
+		void main() {
+			uv = vertex;
+			vec2 scaledVertex = (vertex * scaleFactor) + position;
+			gl_Position  = vec4(2.0*scaledVertex.x - 1.0,
+								2.0*scaledVertex.y - 1.0,
+								0.0, 1.0);
+		})";
+
+	constexpr char const *const defaultImageViewFragmentShader =
+		R"(#version 330
+		uniform sampler2D image;
+		out vec4 color;
+		in vec2 uv;
+		void main() {
+			color = texture(image, uv);
+		})";
 }
 
 ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl::ctrl::Master *controller) :
@@ -60,6 +64,11 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 	root_ = proot;
 	camera_ = nullptr;
 
+	#ifdef HAVE_OPENVR
+	HMD_ = nullptr;
+	has_vr_ = vr::VR_IsHmdPresent();
+	#endif
+
 	setSize(Vector2i(1280,720));
 
 	toolbuttheme = new Theme(*theme());
@@ -83,7 +92,7 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 	mediatheme->mButtonGradientTopFocused = nanogui::Color(80,230);
 	mediatheme->mButtonGradientBotFocused = nanogui::Color(80,230);
 	mediatheme->mIconColor = nanogui::Color(255,255);
-    mediatheme->mTextColor = nanogui::Color(1.0f,1.0f,1.0f,1.0f);
+	mediatheme->mTextColor = nanogui::Color(1.0f,1.0f,1.0f,1.0f);
 	mediatheme->mBorderDark = nanogui::Color(0,0);
 	mediatheme->mBorderMedium = nanogui::Color(0,0);
 	mediatheme->mBorderLight = nanogui::Color(0,0);
@@ -161,9 +170,9 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 	popbutton->setSide(Popup::Side::Right);
 	popbutton->setChevronIcon(0);
 	Popup *popup = popbutton->popup();
-    popup->setLayout(new GroupLayout());
+	popup->setLayout(new GroupLayout());
 	popup->setTheme(toolbuttheme);
-    //popup->setAnchorHeight(100);
+	//popup->setAnchorHeight(100);
 
 	auto itembutton = new Button(popup, "Add Camera", ENTYPO_ICON_CAMERA);
 	itembutton->setCallback([this,popup]() {
@@ -185,7 +194,7 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 	popbutton->setSide(Popup::Side::Right);
 	popbutton->setChevronIcon(0);
 	popup = popbutton->popup();
-    popup->setLayout(new GroupLayout());
+	popup->setLayout(new GroupLayout());
 	popup->setTheme(toolbuttheme);
 	//popbutton->setCallback([this]() {
 	//	cwindow_->setVisible(true);
@@ -244,30 +253,60 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 
 	setVisible(true);
 	performLayout();
+}
 
+#ifdef HAVE_OPENVR
+bool ftl::gui::Screen::initVR() {
+	if (!vr::VR_IsHmdPresent()) {
+		return false;
+	}
 
-	#ifdef HAVE_OPENVR
-	if (vr::VR_IsHmdPresent()) {
-		// Loading the SteamVR Runtime
-		vr::EVRInitError eError = vr::VRInitError_None;
-		HMD_ = vr::VR_Init( &eError, vr::VRApplication_Scene );
-
-		if ( eError != vr::VRInitError_None )
-		{
-			HMD_ = nullptr;
-			LOG(ERROR) << "Unable to init VR runtime: " << vr::VR_GetVRInitErrorAsEnglishDescription( eError );
-		}
-	} else {
+	vr::EVRInitError eError = vr::VRInitError_None;
+	HMD_ = vr::VR_Init( &eError, vr::VRApplication_Scene );
+	
+	if (eError != vr::VRInitError_None)
+	{
 		HMD_ = nullptr;
+		LOG(ERROR) << "Unable to init VR runtime: " << vr::VR_GetVRInitErrorAsEnglishDescription(eError);
 	}
-	#endif
+
+	uint32_t size_x, size_y;
+	HMD_->GetRecommendedRenderTargetSize(&size_x, &size_y);
+	LOG(INFO) << size_x << ", " << size_y;
+	LOG(INFO) << "\n" << getCameraMatrix(HMD_, vr::Eye_Left);
+	return true;
+}
+
+bool ftl::gui::Screen::useVR() {
+	auto *cam = activeCamera();
+	if (HMD_ == nullptr || cam == nullptr) { return false; }
+	return cam->isVR();
 }
 
+bool ftl::gui::Screen::switchVR(bool on) {
+	if (useVR() == on) { return on; }
+
+	if (on && (HMD_ == nullptr) && !initVR()) {
+		return false;
+	}
+
+	if (on) {
+		activeCamera()->setVR(true);
+	} else {
+		activeCamera()->setVR(false);
+	}
+	
+	return useVR();
+}
+#endif
+
 ftl::gui::Screen::~Screen() {
 	mShader.free();
 
 	#ifdef HAVE_OPENVR
-	vr::VR_Shutdown();
+	if (HMD_ != nullptr) {
+		vr::VR_Shutdown();
+	}
 	#endif
 }
 
@@ -361,13 +400,19 @@ void ftl::gui::Screen::draw(NVGcontext *ctx) {
 		leftEye_ = mImageID;
 		rightEye_ = camera_->getRight().texture();
 
+		if (camera_->getChannel() != ftl::codecs::Channel::Left) { mImageID = rightEye_; }
+
 		#ifdef HAVE_OPENVR
-		if (hasVR() && imageSize[0] > 0 && camera_->getLeft().isValid() && camera_->getRight().isValid()) {
+		if (useVR() && imageSize[0] > 0 && camera_->getLeft().isValid() && camera_->getRight().isValid()) {
+			
 			vr::Texture_t leftEyeTexture = {(void*)(uintptr_t)leftEye_, vr::TextureType_OpenGL, vr::ColorSpace_Gamma };
 			vr::VRCompositor()->Submit(vr::Eye_Left, &leftEyeTexture );
+
 			glBindTexture(GL_TEXTURE_2D, rightEye_);
 			vr::Texture_t rightEyeTexture = {(void*)(uintptr_t)rightEye_, vr::TextureType_OpenGL, vr::ColorSpace_Gamma };
 			vr::VRCompositor()->Submit(vr::Eye_Right, &rightEyeTexture );
+			
+			mImageID = leftEye_;
 		}
 		#endif
 
@@ -400,4 +445,4 @@ void ftl::gui::Screen::draw(NVGcontext *ctx) {
 	/* Draw the user interface */
 	screen()->performLayout(ctx);
 	nanogui::Screen::draw(ctx);
-}
\ No newline at end of file
+}
diff --git a/applications/gui/src/screen.hpp b/applications/gui/src/screen.hpp
index d51cec2bf2c34afc7c589b7e6114f503379afe30..b78ad8b690017d84950f0e135473731c631492bc 100644
--- a/applications/gui/src/screen.hpp
+++ b/applications/gui/src/screen.hpp
@@ -43,11 +43,27 @@ class Screen : public nanogui::Screen {
 	void setActiveCamera(ftl::gui::Camera*);
 	ftl::gui::Camera *activeCamera() { return camera_; }
 
-	#ifdef HAVE_OPENVR
-	bool hasVR() const { return HMD_ != nullptr; }
-	#else
+#ifdef HAVE_OPENVR
+	// initialize OpenVR
+	bool initVR();
+
+	// is VR available (HMD was found at initialization)
+	bool hasVR() const { return has_vr_; }
+
+	// is VR mode on/off
+	bool useVR();
+
+	// toggle VR on/off
+	bool switchVR(bool mode);
+
+	vr::IVRSystem* getVR() { return HMD_; }
+
+#else
 	bool hasVR() const { return false; }
-	#endif
+#endif
+
+	void setDualView(bool v) { show_two_images_ = v; LOG(INFO) << "CLICK"; }
+	bool getDualView() { return show_two_images_; }
 
 	nanogui::Theme *windowtheme;
 	nanogui::Theme *specialtheme;
@@ -58,10 +74,11 @@ class Screen : public nanogui::Screen {
 	ftl::gui::SourceWindow *swindow_;
 	ftl::gui::ControlWindow *cwindow_;
 	ftl::gui::MediaPanel *mwindow_;
+
 	//std::vector<SourceViews> sources_;
 	ftl::net::Universe *net_;
 	nanogui::GLShader mShader;
-    GLuint mImageID;
+	GLuint mImageID;
 	//Source *src_;
 	GLTexture texture_;
 	Eigen::Vector3f eye_;
@@ -82,7 +99,10 @@ class Screen : public nanogui::Screen {
 	GLuint leftEye_;
 	GLuint rightEye_;
 
+	bool show_two_images_ = false;
+
 	#ifdef HAVE_OPENVR
+	bool has_vr_;
 	vr::IVRSystem *HMD_;
 	#endif
 };
diff --git a/applications/gui/src/src_window.cpp b/applications/gui/src/src_window.cpp
index e7ffc4b1e0e44caccd67cce5d60d34b481ec58d7..8e087d1dd42e25b00afd0efaf7b5dd22d5139a0d 100644
--- a/applications/gui/src/src_window.cpp
+++ b/applications/gui/src/src_window.cpp
@@ -61,10 +61,9 @@ SourceWindow::SourceWindow(ftl::gui::Screen *screen)
 
 	screen->net()->onConnect([this](ftl::net::Peer *p) {
 		UNIQUE_LOCK(mutex_, lk);
-		//select->setItems(available_);
 		_updateCameras(screen_->net()->findAll<string>("list_streams"));
 	});
-
+	
 	UNIQUE_LOCK(mutex_, lk);
 
 	std::vector<ftl::rgbd::Source*> srcs = ftl::createArray<ftl::rgbd::Source>(screen_->root(), "sources", screen_->net());
diff --git a/applications/gui/src/vr.cpp b/applications/gui/src/vr.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b300a27b2484733786f5bd216f0a48aef3dc913c
--- /dev/null
+++ b/applications/gui/src/vr.cpp
@@ -0,0 +1,36 @@
+#include "loguru.hpp"
+#include "vr.hpp"
+
+Eigen::Matrix3d getCameraMatrix(const double tanx1,
+								const double tanx2,
+								const double tany1,
+								const double tany2,
+								const double size_x,
+								const double size_y) {
+	
+	Eigen::Matrix3d C = Eigen::Matrix3d::Identity();
+	
+	CHECK(tanx1 < 0 && tanx2 > 0 && tany1 < 0 && tany2 > 0);
+	CHECK(size_x > 0 && size_y > 0);
+
+	double fx = size_x / (-tanx1 + tanx2);
+	double fy = size_y / (-tany1 + tany2);
+	C(0,0) = fx;
+	C(1,1) = fy;
+	C(0,2) = tanx1 * fx;
+	C(1,2) = tany1 * fy;
+
+	// safe to remove
+	CHECK((int) (abs(tanx1 * fx) + abs(tanx2 * fx)) == (int) size_x);
+	CHECK((int) (abs(tany1 * fy) + abs(tany2 * fy)) == (int) size_y);
+
+	return C;
+}
+
+Eigen::Matrix3d getCameraMatrix(vr::IVRSystem *vr, const vr::Hmd_Eye &eye) {
+	float tanx1, tanx2, tany1, tany2;
+	uint32_t size_x, size_y;
+	vr->GetProjectionRaw(eye, &tanx1, &tanx2, &tany1, &tany2);
+	vr->GetRecommendedRenderTargetSize(&size_x, &size_y);
+	return getCameraMatrix(tanx1, tanx2, tany1, tany2, size_x, size_y);
+}
\ No newline at end of file
diff --git a/applications/gui/src/vr.hpp b/applications/gui/src/vr.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2a66dfd054c954cdb40b4b8f0159ea1286ff3cfd
--- /dev/null
+++ b/applications/gui/src/vr.hpp
@@ -0,0 +1,57 @@
+#include <openvr/openvr.h>
+#include <Eigen/Eigen>
+#include <openvr/openvr.h>
+
+/* @brief	Calculate (pinhole camera) intrinsic matrix from OpenVR parameters
+ * @param	Tangent of left half angle (negative) from center view axis
+ * @param	Tangent of right half angle from center view axis
+ * @param	Tangent of top half angle (negative) from center view axis
+ * @param	Tangent of bottom half angle from center view axis
+ * @param	Image width
+ * @param	Image height
+ * 
+ * Parameters are provided by IVRSystem::GetProjectionRaw and
+ * IVRSystem::GetRecommendedRenderTargetSize.
+ * 
+ * tanx1 = x1 / fx		(1)
+ * tanx2 = x2 / fy		(2)
+ * x1 + x2 = size_x		(3)
+ * 
+ * :. fx = size_x / (-tanx1 + tanx2)
+ * 
+ * fy can be calculated in same way
+ */
+Eigen::Matrix3d getCameraMatrix(const double tanx1,
+								const double tanx2,
+								const double tany1,
+								const double tany2,
+								const double size_x,
+								const double size_y);
+
+/*
+ * @brief	Same as above, but uses given IVRSystem and eye.
+ */
+Eigen::Matrix3d getCameraMatrix(vr::IVRSystem *vr, const vr::Hmd_Eye &eye);
+
+
+static inline Eigen::Matrix4d ConvertSteamVRMatrixToMatrix4( const vr::HmdMatrix34_t &matPose )
+{
+	Eigen::Matrix4d matrixObj;
+	matrixObj <<
+		matPose.m[0][0], matPose.m[0][1], matPose.m[0][2], 0.0,
+		matPose.m[1][0], matPose.m[1][1], matPose.m[1][2], 0.0,
+		matPose.m[2][0], matPose.m[2][1], matPose.m[2][2], 0.0,
+		matPose.m[3][0], matPose.m[3][1], matPose.m[3][2], 1.0f;
+	return matrixObj.transpose();
+}
+
+static inline Eigen::Matrix4d ConvertSteamVRMatrixToMatrix4( const vr::HmdMatrix44_t &matPose )
+{
+	Eigen::Matrix4d matrixObj;
+	matrixObj <<
+		matPose.m[0][0], matPose.m[0][1], matPose.m[0][2], matPose.m[0][3],
+		matPose.m[1][0], matPose.m[1][1], matPose.m[1][2], matPose.m[1][3],
+		matPose.m[2][0], matPose.m[2][1], matPose.m[2][2], matPose.m[2][3],
+		matPose.m[3][0], matPose.m[3][1], matPose.m[3][2], matPose.m[3][3];
+	return matrixObj.transpose();
+}
\ No newline at end of file
diff --git a/components/codecs/src/nvpipe_decoder.cpp b/components/codecs/src/nvpipe_decoder.cpp
index b5e358388f74ac9dfb7df4082a56ab8426cf9f4f..bd0273a2747ecd4501a8c755c4cccf1423ba7440 100644
--- a/components/codecs/src/nvpipe_decoder.cpp
+++ b/components/codecs/src/nvpipe_decoder.cpp
@@ -21,11 +21,36 @@ NvPipeDecoder::~NvPipeDecoder() {
 	}
 }
 
+void cropAndScaleUp(cv::Mat &in, cv::Mat &out) {
+	CHECK(in.type() == out.type());
+
+	auto isize = in.size();
+	auto osize = out.size();
+	cv::Mat tmp;
+	
+	if (isize != osize) {
+		double x_scale = ((double) isize.width) / osize.width;
+		double y_scale = ((double) isize.height) / osize.height;
+		double x_scalei = 1.0 / x_scale;
+		double y_scalei = 1.0 / y_scale;
+		cv::Size sz_crop;
+
+		// assume downscaled image
+		if (x_scalei > y_scalei) {
+			sz_crop = cv::Size(isize.width, isize.height * x_scale);
+		} else {
+			sz_crop = cv::Size(isize.width * y_scale, isize.height);
+		}
+
+		tmp = in(cv::Rect(cv::Point2i(0, 0), sz_crop));
+		cv::resize(tmp, out, osize);
+	}
+}
+
 bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::Mat &out) {
 	cudaSetDevice(0);
 	UNIQUE_LOCK(mutex_,lk);
 	if (pkt.codec != codec_t::HEVC) return false;
-
 	bool is_float_frame = out.type() == CV_32F;
 
 	// Is the previous decoder still valid for current resolution and type?
@@ -53,7 +78,7 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::Mat &out) {
 
 		seen_iframe_ = false;
 	}
-
+	
 	// TODO: (Nick) Move to member variable to prevent re-creation
 	cv::Mat tmp(cv::Size(ftl::codecs::getWidth(pkt.definition),ftl::codecs::getHeight(pkt.definition)), (is_float_frame) ? CV_16U : CV_8UC4);
 
@@ -72,8 +97,11 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::Mat &out) {
 		if (out.rows == ftl::codecs::getHeight(pkt.definition)) {
 			tmp.convertTo(out, CV_32FC1, 1.0f/1000.0f);
 		} else {
-			tmp.convertTo(tmp, CV_32FC1, 1.0f/1000.0f);
+			cv::cvtColor(tmp, tmp, cv::COLOR_BGRA2BGR);
 			cv::resize(tmp, out, out.size());
+			//cv::Mat tmp2;
+			//tmp.convertTo(tmp2, CV_32FC1, 1.0f/1000.0f);
+			//cropAndScaleUp(tmp2, out);
 		}
 	} else {
 		// Is the received frame the same size as requested output?
@@ -82,6 +110,9 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::Mat &out) {
 		} else {
 			cv::cvtColor(tmp, tmp, cv::COLOR_BGRA2BGR);
 			cv::resize(tmp, out, out.size());
+			//cv::Mat tmp2;
+			//cv::cvtColor(tmp, tmp2, cv::COLOR_BGRA2BGR);
+			//cropAndScaleUp(tmp2, out);
 		}
 	}
 
diff --git a/components/codecs/src/nvpipe_encoder.cpp b/components/codecs/src/nvpipe_encoder.cpp
index 1947170b92014da359f32c9e776e0fe890652aa2..1b91c13d083c2bb48a14805d480793a2b3215580 100644
--- a/components/codecs/src/nvpipe_encoder.cpp
+++ b/components/codecs/src/nvpipe_encoder.cpp
@@ -16,14 +16,14 @@ using ftl::codecs::Packet;
 
 NvPipeEncoder::NvPipeEncoder(definition_t maxdef,
 			definition_t mindef) : Encoder(maxdef, mindef, ftl::codecs::device_t::Hardware) {
-    nvenc_ = nullptr;
-    current_definition_ = definition_t::HD1080;
-    is_float_channel_ = false;
+	nvenc_ = nullptr;
+	current_definition_ = definition_t::HD1080;
+	is_float_channel_ = false;
 	was_reset_ = false;
 }
 
 NvPipeEncoder::~NvPipeEncoder() {
-    if (nvenc_) NvPipe_Destroy(nvenc_);
+	if (nvenc_) NvPipe_Destroy(nvenc_);
 }
 
 void NvPipeEncoder::reset() {
@@ -43,87 +43,119 @@ definition_t NvPipeEncoder::_verifiedDefinition(definition_t def, const cv::Mat
 	return def;
 }
 
+void scaleDownAndPad(cv::Mat &in, cv::Mat &out) {
+	const auto isize = in.size();
+	const auto osize = out.size();
+	cv::Mat tmp;
+	
+	if (isize != osize) {
+		double x_scale = ((double) isize.width) / osize.width;
+		double y_scale = ((double) isize.height) / osize.height;
+		double x_scalei = 1.0 / x_scale;
+		double y_scalei = 1.0 / y_scale;
+
+		if (x_scale > 1.0 || y_scale > 1.0) {
+			if (x_scale > y_scale) {
+				cv::resize(in, tmp, cv::Size(osize.width, osize.height * x_scalei));
+			} else {
+				cv::resize(in, tmp, cv::Size(osize.width * y_scalei, osize.height));
+			}
+		}
+		else { tmp = in; }
+		
+		if (tmp.size().width < osize.width || tmp.size().height < osize.height) {
+			tmp.copyTo(out(cv::Rect(cv::Point2i(0, 0), tmp.size())));
+		}
+		else { out = tmp; }
+	}
+}
+
 bool NvPipeEncoder::encode(const cv::Mat &in, definition_t odefinition, bitrate_t bitrate, const std::function<void(const ftl::codecs::Packet&)> &cb) {
-    cudaSetDevice(0);
+	cudaSetDevice(0);
 	auto definition = _verifiedDefinition(odefinition, in);
 
 	//LOG(INFO) << "Definition: " << ftl::codecs::getWidth(definition) << "x" << ftl::codecs::getHeight(definition);
 
-    if (in.empty()) {
-        LOG(ERROR) << "Missing data for Nvidia encoder";
-        return false;
-    }
-    if (!_createEncoder(in, definition, bitrate)) return false;
+	if (in.empty()) {
+		LOG(ERROR) << "Missing data for Nvidia encoder";
+		return false;
+	}
+	if (!_createEncoder(in, definition, bitrate)) return false;
 
-    //LOG(INFO) << "NvPipe Encode: " << int(definition) << " " << in.cols;
+	//LOG(INFO) << "NvPipe Encode: " << int(definition) << " " << in.cols;
 
-    cv::Mat tmp;
-    if (in.type() == CV_32F) {
+	cv::Mat tmp;
+	if (in.type() == CV_32F) {
 		in.convertTo(tmp, CV_16UC1, 1000);
-    } else if (in.type() == CV_8UC3) {
-        cv::cvtColor(in, tmp, cv::COLOR_BGR2BGRA);
+	} else if (in.type() == CV_8UC3) {
+		cv::cvtColor(in, tmp, cv::COLOR_BGR2BGRA);
 	} else {
-		tmp = in;
+		in.copyTo(tmp);
 	}
 
+	// scale/pad to fit output format
+	//cv::Mat tmp2 = cv::Mat::zeros(getHeight(odefinition), getWidth(odefinition), tmp.type());
+	//scaleDownAndPad(tmp, tmp2);
+	//std::swap(tmp, tmp2);
+
 	Packet pkt;
 	pkt.codec = codec_t::HEVC;
 	pkt.definition = definition;
 	pkt.block_total = 1;
 	pkt.block_number = 0;
 
-    pkt.data.resize(ftl::codecs::kVideoBufferSize);
-    uint64_t cs = NvPipe_Encode(
-        nvenc_,
-        tmp.data,
-        tmp.step,
-        pkt.data.data(),
-        ftl::codecs::kVideoBufferSize,
-        in.cols,
-        in.rows,
-        was_reset_		// Force IFrame!
-    );
-    pkt.data.resize(cs);
+	pkt.data.resize(ftl::codecs::kVideoBufferSize);
+	uint64_t cs = NvPipe_Encode(
+		nvenc_,
+		tmp.data,
+		tmp.step,
+		pkt.data.data(),
+		ftl::codecs::kVideoBufferSize,
+		tmp.cols,
+		tmp.rows,
+		was_reset_		// Force IFrame!
+	);
+	pkt.data.resize(cs);
 	was_reset_ = false;
 
-    if (cs == 0) {
-        LOG(ERROR) << "Could not encode video frame: " << NvPipe_GetError(nvenc_);
-        return false;
-    } else {
+	if (cs == 0) {
+		LOG(ERROR) << "Could not encode video frame: " << NvPipe_GetError(nvenc_);
+		return false;
+	} else {
 		cb(pkt);
-        return true;
-    }
+		return true;
+	}
 }
 
 bool NvPipeEncoder::_encoderMatch(const cv::Mat &in, definition_t def) {
-    return ((in.type() == CV_32F && is_float_channel_) ||
-        ((in.type() == CV_8UC3 || in.type() == CV_8UC4) && !is_float_channel_)) && current_definition_ == def;
+	return ((in.type() == CV_32F && is_float_channel_) ||
+		((in.type() == CV_8UC3 || in.type() == CV_8UC4) && !is_float_channel_)) && current_definition_ == def;
 }
 
 bool NvPipeEncoder::_createEncoder(const cv::Mat &in, definition_t def, bitrate_t rate) {
-    if (_encoderMatch(in, def) && nvenc_) return true;
-
-    if (in.type() == CV_32F) is_float_channel_ = true;
-    else is_float_channel_ = false;
-    current_definition_ = def;
-
-    if (nvenc_) NvPipe_Destroy(nvenc_);
-    const int fps = 1000/ftl::timer::getInterval();
-    nvenc_ = NvPipe_CreateEncoder(
-        (is_float_channel_) ? NVPIPE_UINT16 : NVPIPE_RGBA32,
-        NVPIPE_HEVC,
-        (is_float_channel_) ? NVPIPE_LOSSLESS : NVPIPE_LOSSY,
-        16*1000*1000,
-        fps,				// FPS
-        ftl::codecs::getWidth(def),	// Output Width
-        ftl::codecs::getHeight(def)	// Output Height
-    );
-
-    if (!nvenc_) {
-        LOG(ERROR) << "Could not create video encoder: " << NvPipe_GetError(NULL);
-        return false;
-    } else {
-        LOG(INFO) << "NvPipe encoder created";
-        return true;
-    }
+	if (_encoderMatch(in, def) && nvenc_) return true;
+
+	if (in.type() == CV_32F) is_float_channel_ = true;
+	else is_float_channel_ = false;
+	current_definition_ = def;
+
+	if (nvenc_) NvPipe_Destroy(nvenc_);
+	const int fps = 1000/ftl::timer::getInterval();
+	nvenc_ = NvPipe_CreateEncoder(
+		(is_float_channel_) ? NVPIPE_UINT16 : NVPIPE_RGBA32,
+		NVPIPE_HEVC,
+		(is_float_channel_) ? NVPIPE_LOSSLESS : NVPIPE_LOSSY,
+		16*1000*1000,
+		fps,				// FPS
+		ftl::codecs::getWidth(def),	// Output Width
+		ftl::codecs::getHeight(def)	// Output Height
+	);
+
+	if (!nvenc_) {
+		LOG(ERROR) << "Could not create video encoder: " << NvPipe_GetError(NULL);
+		return false;
+	} else {
+		LOG(INFO) << "NvPipe encoder created";
+		return true;
+	}
 }
diff --git a/components/renderers/cpp/src/splat_render.cpp b/components/renderers/cpp/src/splat_render.cpp
index bf259a94007af9b603b4928b31ccef5052472fa9..a7e796dc97d86d65c8f4082beadbd801f3cf9ca0 100644
--- a/components/renderers/cpp/src/splat_render.cpp
+++ b/components/renderers/cpp/src/splat_render.cpp
@@ -311,10 +311,10 @@ bool Splatter::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 	scene_->upload(Channel::Colour + Channel::Depth, stream_);
 
 	const auto &camera = src->parameters();
-
 	//cudaSafeCall(cudaSetDevice(scene_->getCUDADevice()));
 
 	// Create all the required channels
+	
 	out.create<GpuMat>(Channel::Depth, Format<float>(camera.width, camera.height));
 	out.create<GpuMat>(Channel::Colour, Format<uchar4>(camera.width, camera.height));
 
@@ -338,7 +338,6 @@ bool Splatter::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 	params.m_viewMatrix = MatrixConversion::toCUDA(src->getPose().cast<float>().inverse());
 	params.m_viewMatrixInverse = MatrixConversion::toCUDA(src->getPose().cast<float>());
 	params.camera = camera;
-
 	// Clear all channels to 0 or max depth
 
 	out.get<GpuMat>(Channel::Depth).setTo(cv::Scalar(1000.0f), cvstream);
@@ -384,7 +383,7 @@ bool Splatter::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 		}
 
 		if (!f.hasChannel(Channel::Normals)) {
-			Eigen::Matrix4f matrix =  s->getPose().cast<float>();
+			Eigen::Matrix4f matrix =  s->getPose().cast<float>().transpose();
 			auto pose = MatrixConversion::toCUDA(matrix);
 
 			auto &g = f.get<GpuMat>(Channel::Colour);
@@ -458,8 +457,14 @@ bool Splatter::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 	}
 	else if (chan == Channel::Right)
 	{
-		Eigen::Affine3f transform(Eigen::Translation3f(camera.baseline,0.0f,0.0f));
-		Eigen::Matrix4f matrix =  src->getPose().cast<float>() * transform.matrix();
+		float baseline = camera.baseline;
+		
+		Eigen::Translation3f translation(baseline, 0.0f, 0.0f);
+		LOG(INFO) << translation.vector();
+		Eigen::Affine3f transform(translation);
+		LOG(INFO) << "\n" << transform.matrix();
+		LOG(INFO) << "baseline " << baseline;
+		Eigen::Matrix4f matrix = transform.matrix() * src->getPose().cast<float>();
 		params.m_viewMatrix = MatrixConversion::toCUDA(matrix.inverse());
 		params.m_viewMatrixInverse = MatrixConversion::toCUDA(matrix);
 		
@@ -468,6 +473,18 @@ bool Splatter::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 
 		_dibr(stream_); // Need to re-dibr due to pose change
 		_renderChannel(out, Channel::Left, Channel::Right, stream_);
+		
+		// renderFrame() expects to render right frame from left as well; Should
+		// possibly add channel_in and channel_out parameters to renderFrame()?
+		// (l/r swap as temporary fix)
+		/*
+		auto &tmp = out.get<GpuMat>(Channel::Left);
+		swap(out.get<GpuMat>(Channel::Right), tmp);
+		_renderChannel(params, out, Channel::Left, stream_);
+		swap(tmp, out.get<GpuMat>(Channel::Right));
+		*/
+
+		_renderChannel(out, Channel::Left, Channel::Right, stream_);
 	} else if (chan != Channel::None) {
 		if (ftl::codecs::isFloatChannel(chan)) {
 			out.create<GpuMat>(chan, Format<float>(camera.width, camera.height));
diff --git a/components/rgbd-sources/src/sources/virtual/virtual.cpp b/components/rgbd-sources/src/sources/virtual/virtual.cpp
index f4bcb4a498973f0a39687f4f0c70a06807dc47ca..afea7e17c485b7a1a6ae050c02e61372bb5d4604 100644
--- a/components/rgbd-sources/src/sources/virtual/virtual.cpp
+++ b/components/rgbd-sources/src/sources/virtual/virtual.cpp
@@ -10,6 +10,15 @@ class VirtualImpl : public ftl::rgbd::detail::Source {
 		params_ = params;
 		capabilities_ = ftl::rgbd::kCapVideo | ftl::rgbd::kCapStereo;
 		if (!host->value("locked", false)) capabilities_ |= ftl::rgbd::kCapMovable;
+		host->on("baseline", [this](const ftl::config::Event&) {
+			params_.baseline = host_->value("baseline", 0.0f);
+		});
+		
+		host->on("focal", [this](const ftl::config::Event&) {
+			params_.fx = host_->value("focal", 700.0f);
+			params_.fy = params_.fx;
+		});
+
 	}
 
 	~VirtualImpl() {