diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 28e09eeb330a07a43e139e475f0350fcc6cfac9f..512a4d19087bd66c5c895f3f4c5595190122132c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -25,7 +25,7 @@ linux:
   script:
     - mkdir build
     - cd build
-    - cmake ..
+    - cmake .. -DWITH_OPTFLOW=TRUE -DBUILD_CALIBRATION=TRUE
     - make
     - ctest --output-on-failure
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 95530e23a154175eac8a303897f73a91fe6432f5..b0aecb25a357a5c978db35a80cb20487adb5d08d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -40,6 +40,19 @@ 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)
+endif()
+
+
 if (WITH_FIXSTARS)
 	find_package( LibSGM )
 	if (LibSGM_FOUND)
@@ -175,7 +188,7 @@ check_include_file_cxx("opencv2/cudastereo.hpp" HAVE_OPENCVCUDA)
 find_program(CPPCHECK_FOUND cppcheck)
 if (CPPCHECK_FOUND)
 	message(STATUS "Found cppcheck: will perform source checks")
-	set(CMAKE_CXX_CPPCHECK "cppcheck" "--enable=warning,performance,portability,style" "--inline-suppr" "--std=c++11" "--suppress=*:*catch.hpp" "--suppress=*:*elas*" "--suppress=*:*nanogui*" "--suppress=*:*json.hpp" "--quiet")
+	set(CMAKE_CXX_CPPCHECK "cppcheck" "-D__align__(A)" "-DCUDARTAPI" "--enable=warning,performance,style" "--inline-suppr" "--std=c++14" "--suppress=*:*catch.hpp" "--suppress=*:*elas*" "--suppress=*:*nanogui*" "--suppress=*:*json.hpp" "--quiet")
 endif()
 
 # include_directories(${PROJECT_SOURCE_DIR}/common/cpp/include)
diff --git a/applications/calibration-multi/src/main.cpp b/applications/calibration-multi/src/main.cpp
index f9c258d7665933b510e6dc5af08e6c499e16ec91..ea770f69b03981164c2b6c97f95aa8841f1d8ae7 100644
--- a/applications/calibration-multi/src/main.cpp
+++ b/applications/calibration-multi/src/main.cpp
@@ -37,6 +37,7 @@ using cv::Vec4d;
 
 using ftl::net::Universe;
 using ftl::rgbd::Source;
+using ftl::rgbd::Channel;
 
 Mat getCameraMatrix(const ftl::rgbd::Camera &parameters) {
 	Mat m = (cv::Mat_<double>(3,3) << parameters.fx, 0.0, -parameters.cx, 0.0, parameters.fy, -parameters.cy, 0.0, 0.0, 1.0);
@@ -97,7 +98,7 @@ bool loadRegistration(const string &ifile, map<string, Eigen::Matrix4d> &data) {
 
 //
 
-bool saveIntrinsics(const string &ofile, const vector<Mat> &M) {
+bool saveIntrinsics(const string &ofile, const vector<Mat> &M, const Size &size) {
 	vector<Mat> D;
 	{
 		cv::FileStorage fs(ofile, cv::FileStorage::READ);
@@ -107,6 +108,7 @@ bool saveIntrinsics(const string &ofile, const vector<Mat> &M) {
 	{
 		cv::FileStorage fs(ofile, cv::FileStorage::WRITE);
 		if (fs.isOpened()) {
+			fs << "resolution" << size;
 			fs << "K" << M << "D" << D;
 			fs.release();
 			return true;
@@ -195,6 +197,7 @@ struct CalibrationParams {
 	bool optimize_intrinsic = false;
 	int reference_camera = -1;
 	double alpha = 0.0;
+	Size size;
 };
 
 void calibrate(	MultiCameraCalibrationNew &calib, vector<string> &uri_cameras,
@@ -232,8 +235,8 @@ void calibrate(	MultiCameraCalibrationNew &calib, vector<string> &uri_cameras,
 		Mat R_c1c2, T_c1c2;
 
 		calculateTransform(R[c], t[c], R[c + 1], t[c + 1], R_c1c2, T_c1c2);
-		cv::stereoRectify(K1, D1, K2, D2, Size(1280, 720), R_c1c2, T_c1c2, R1, R2, P1, P2, Q, 0, params.alpha);
-		
+		cv::stereoRectify(K1, D1, K2, D2, params.size, R_c1c2, T_c1c2, R1, R2, P1, P2, Q, 0, params.alpha);
+
 		Mat _t = Mat(Size(1, 3), CV_64FC1, Scalar(0.0));
 		Rt_out[c] = getMat4x4(R[c], t[c]) * getMat4x4(R1, _t).inv();
 		Rt_out[c + 1] = getMat4x4(R[c + 1], t[c + 1]) * getMat4x4(R2, _t).inv();
@@ -243,15 +246,20 @@ void calibrate(	MultiCameraCalibrationNew &calib, vector<string> &uri_cameras,
 			size_t pos1 = uri_cameras[c/2].find("node");
 			size_t pos2 = uri_cameras[c/2].find("#", pos1);
 			node_name = uri_cameras[c/2].substr(pos1, pos2 - pos1);
-			//LOG(INFO) << c << ":" << calib.getCameraMatNormalized(c, 1280, 720);
-			//LOG(INFO) << c + 1 << ":" << calib.getCameraMatNormalized(c + 1, 1280, 720);
+			
 			if (params.save_extrinsic) {
+				// TODO:	only R and T required, rectification performed on vision node,
+				//			consider saving global extrinsic calibration?
 				saveExtrinsics(params.output_path + node_name + "-extrinsic.yml", R_c1c2, T_c1c2, R1, R2, P1, P2, Q);
 				LOG(INFO) << "Saved: " << params.output_path + node_name + "-extrinsic.yml";
 			}
 			if (params.save_intrinsic) {
-				saveIntrinsics(params.output_path + node_name + "-intrinsic.yml",
-					{calib.getCameraMatNormalized(c, 1280, 720), calib.getCameraMatNormalized(c + 1, 1280, 720)}
+				saveIntrinsics(
+					params.output_path + node_name + "-intrinsic.yml",
+					{calib.getCameraMat(c),
+					 calib.getCameraMat(c + 1)},
+					params.size
+
 				);
 				LOG(INFO) << "Saved: " << params.output_path + node_name + "-intrinsic.yml";
 			}
@@ -259,9 +267,9 @@ void calibrate(	MultiCameraCalibrationNew &calib, vector<string> &uri_cameras,
 
 		// for visualization
 		Size new_size;
-		cv::stereoRectify(K1, D1, K2, D2, Size(1280, 720), R_c1c2, T_c1c2, R1, R2, P1, P2, Q, 0, 1.0, new_size, &roi[c], &roi[c + 1]);
-		cv::initUndistortRectifyMap(K1, D1, R1, P1, Size(1280, 720), CV_16SC2, map1[c], map2[c]);
-		cv::initUndistortRectifyMap(K2, D2, R2, P2, Size(1280, 720), CV_16SC2, map1[c + 1], map2[c + 1]);
+		cv::stereoRectify(K1, D1, K2, D2, params.size, R_c1c2, T_c1c2, R1, R2, P1, P2, Q, 0, 1.0, new_size, &roi[c], &roi[c + 1]);
+		cv::initUndistortRectifyMap(K1, D1, R1, P1, params.size, CV_16SC2, map1[c], map2[c]);
+		cv::initUndistortRectifyMap(K2, D2, R2, P2, params.size, CV_16SC2, map1[c + 1], map2[c + 1]);
 	}
 
 	{
@@ -297,6 +305,7 @@ void calibrateFromPath(	const string &path,
 	vector<string> uri_cameras;
 	cv::FileStorage fs(path + filename, cv::FileStorage::READ);
 	fs["uri"] >> uri_cameras;
+	fs["resolution"] >> params.size;
 
 	//params.idx_cameras = {2, 3};//{0, 1, 4, 5, 6, 7, 8, 9, 10, 11};
 	params.idx_cameras.resize(uri_cameras.size() * 2);
@@ -361,11 +370,11 @@ void runCameraCalibration(	ftl::Configurable* root,
 	const size_t n_sources = sources.size();
 	const size_t n_cameras = n_sources * 2;
 	size_t reference_camera = 0;
-	Size resolution;
+	
 	{
-		auto params = sources[0]->parameters();
-		resolution = Size(params.width, params.height);
-		LOG(INFO) << "Camera resolution: " << resolution;
+		auto camera = sources[0]->parameters();
+		params.size = Size(camera.width, camera.height);
+		LOG(INFO) << "Camera resolution: " << params.size;
 	}
 
 	params.idx_cameras.resize(n_cameras);
@@ -373,29 +382,29 @@ void runCameraCalibration(	ftl::Configurable* root,
 
 	// TODO: parameter for calibration target type
 	auto calib = MultiCameraCalibrationNew(	n_cameras, reference_camera,
-											resolution, CalibrationTarget(0.250)
+											params.size, CalibrationTarget(0.250)
 	);
 
 	for (size_t i = 0; i < n_sources; i++) {
-		auto params_r = sources[i]->parameters(ftl::rgbd::kChanRight);
-		auto params_l = sources[i]->parameters(ftl::rgbd::kChanLeft);
+		auto camera_r = sources[i]->parameters(Channel::Right);
+		auto camera_l = sources[i]->parameters(Channel::Left);
 
-		CHECK(resolution == Size(params_r.width, params_r.height));
-		CHECK(resolution == Size(params_l.width, params_l.height));
+		CHECK(params.size == Size(camera_r.width, camera_r.height));
+		CHECK(params.size == Size(camera_l.width, camera_l.height));
 		
 		Mat K;
-		K = getCameraMatrix(params_r);
+		K = getCameraMatrix(camera_r);
 		LOG(INFO) << "K[" << 2 * i + 1 << "] = \n" << K;
 		calib.setCameraParameters(2 * i + 1, K);
 
-		K = getCameraMatrix(params_l);
+		K = getCameraMatrix(camera_l);
 		LOG(INFO) << "K[" << 2 * i << "] = \n" << K;
 		calib.setCameraParameters(2 * i, K);
 	}
 
 	ftl::rgbd::Group group;
 	for (Source* src : sources) {
-		src->setChannel(ftl::rgbd::kChanRight);
+		src->setChannel(Channel::Right);
 		group.addSource(src);
 	}
 
@@ -408,14 +417,14 @@ void runCameraCalibration(	ftl::Configurable* root,
 	group.sync([&mutex, &new_frames, &rgb_new](ftl::rgbd::FrameSet &frames) {
 		mutex.lock();
 		bool good = true;
-		for (size_t i = 0; i < frames.channel1.size(); i ++) {
-			if (frames.channel1[i].empty()) good = false;
-			if (frames.channel1[i].empty()) good = false;
-			if (frames.channel1[i].channels() != 3) good = false; // ASSERT
-			if (frames.channel2[i].channels() != 3) good = false;
+		for (size_t i = 0; i < frames.sources.size(); i ++) {
+			if (frames.frames[i].get<cv::Mat>(Channel::Left).empty()) good = false;
+			if (frames.frames[i].get<cv::Mat>(Channel::Right).empty()) good = false;
+			if (frames.frames[i].get<cv::Mat>(Channel::Left).channels() != 3) good = false; // ASSERT
+			if (frames.frames[i].get<cv::Mat>(Channel::Right).channels() != 3) good = false;
 			if (!good) break;
-			cv::swap(frames.channel1[i], rgb_new[2 * i]);
-			cv::swap(frames.channel2[i], rgb_new[2 * i + 1]);
+			cv::swap(frames.frames[i].get<cv::Mat>(Channel::Left), rgb_new[2 * i]);
+			cv::swap(frames.frames[i].get<cv::Mat>(Channel::Right), rgb_new[2 * i + 1]);
 		}
 
 		new_frames = good;
@@ -550,7 +559,8 @@ int main(int argc, char **argv) {
 	params.optimize_intrinsic = optimize_intrinsic;
 	params.output_path = output_directory;
 	params.registration_file = registration_file;
-
+	params.reference_camera = ref_camera;
+	
 	LOG(INFO)	<< "\n"
 				<< "\nIMPORTANT: Remeber to set \"use_intrinsics\" to false for nodes!"
 				<< "\n"
diff --git a/applications/calibration-multi/src/multicalibrate.cpp b/applications/calibration-multi/src/multicalibrate.cpp
index 2292d178d70f0f7c68b79cbbf1cb9172cdd173be..9d85d1af7ae938981bb445a852fd85a913c6e26e 100644
--- a/applications/calibration-multi/src/multicalibrate.cpp
+++ b/applications/calibration-multi/src/multicalibrate.cpp
@@ -651,6 +651,7 @@ double MultiCameraCalibrationNew::calibrateAll(int reference_camera) {
 		if (calibratePair(c1, c2, R, t) > 16.0) {
 			LOG(ERROR)	<< "Pairwise calibration failed, skipping cameras "
 						<< c1 << " and " << c2;
+			visibility_graph_.deleteEdge(c1, c2);
 			continue;
 		}
 
diff --git a/applications/calibration-multi/src/visibility.cpp b/applications/calibration-multi/src/visibility.cpp
index bd001f60371d02aa7e5b76a8f060950f05b42db3..a4c16165b47decadfa9ca18ccf641a32b1cb16c1 100644
--- a/applications/calibration-multi/src/visibility.cpp
+++ b/applications/calibration-multi/src/visibility.cpp
@@ -49,6 +49,12 @@ int Visibility::getOptimalCamera() {
 	return best_i;
 }
 
+void Visibility::deleteEdge(int camera1, int camera2)
+{
+	visibility_.at<int>(camera1, camera2) = 0;
+	visibility_.at<int>(camera2, camera1) = 0;
+}
+
 int Visibility::getMinVisibility() {
 	int min_i;
 	int min_count = INT_MAX;
diff --git a/applications/calibration-multi/src/visibility.hpp b/applications/calibration-multi/src/visibility.hpp
index dcf7e71ddb29304c5a0825e310e566ef3622cd55..3521435dd62e2cd6eeea417f926b02f8049eb560 100644
--- a/applications/calibration-multi/src/visibility.hpp
+++ b/applications/calibration-multi/src/visibility.hpp
@@ -27,6 +27,7 @@ public:
 	vector<vector<pair<int, int>>> findShortestPaths(int reference);
 
 	vector<int> getClosestCameras(int c);
+	void deleteEdge(int camera1, int camera2);
 	int getOptimalCamera();
 	int getMinVisibility();
 	int getViewsCount(int camera);
diff --git a/applications/calibration/src/common.cpp b/applications/calibration/src/common.cpp
index 0098bba58c5e6cbcbc0b75185f4286894447f16c..8a678e4b9ba808dccea146c2ce4c8c1c6942a7f0 100644
--- a/applications/calibration/src/common.cpp
+++ b/applications/calibration/src/common.cpp
@@ -62,20 +62,24 @@ bool saveExtrinsics(const string &ofile, Mat &R, Mat &T, Mat &R1, Mat &R2, Mat &
 	return false;
 }
 
-bool saveIntrinsics(const string &ofile, const vector<Mat> &M, const vector<Mat>& D) {
+bool saveIntrinsics(const string &ofile, const vector<Mat> &M, const vector<Mat>& D, const Size &size)
+{
 	cv::FileStorage fs(ofile, cv::FileStorage::WRITE);
-	if (fs.isOpened()) {
+	if (fs.isOpened())
+	{
+		fs << "resolution" << size;
 		fs << "K" << M << "D" << D;
 		fs.release();
 		return true;
 	}
-	else {
+	else
+	{
 		LOG(ERROR) << "Error: can not save the intrinsic parameters to '" << ofile << "'";
 	}
 	return false;
 }
 
-bool loadIntrinsics(const string &ifile, vector<Mat> &K1, vector<Mat> &D1) {
+bool loadIntrinsics(const string &ifile, vector<Mat> &K1, vector<Mat> &D1, Size &size) {
 	using namespace cv;
 
 	FileStorage fs;
@@ -89,9 +93,10 @@ bool loadIntrinsics(const string &ifile, vector<Mat> &K1, vector<Mat> &D1) {
 	
 	LOG(INFO) << "Intrinsics from: " << ifile;
 
+	fs["resolution"] >> size;
 	fs["K"] >> K1;
 	fs["D"] >> D1;
-
+	
 	return true;
 }
 
@@ -211,6 +216,23 @@ bool CalibrationChessboard::findPoints(Mat &img, vector<Vec2f> &points) {
 	return cv::findChessboardCornersSB(img, pattern_size_, points, chessboard_flags_);
 }
 
+
+void CalibrationChessboard::drawCorners(Mat &img, const vector<Vec2f> &points) {
+	using cv::Point2i;
+	vector<Point2i> corners(4);
+	corners[1] = Point2i(points[0]);
+	corners[0] = Point2i(points[pattern_size_.width - 1]);
+	corners[2] = Point2i(points[pattern_size_.width * (pattern_size_.height - 1)]);
+	corners[3] = Point2i(points.back());
+	
+	cv::Scalar color = cv::Scalar(200, 200, 200);
+	
+	for (int i = 0; i <= 4; i++)
+	{
+		cv::line(img, corners[i % 4], corners[(i + 1) % 4], color, 2);
+	}
+}
+
 void CalibrationChessboard::drawPoints(Mat &img, const vector<Vec2f> &points) {
 	cv::drawChessboardCorners(img, pattern_size_, points, true);
 }
diff --git a/applications/calibration/src/common.hpp b/applications/calibration/src/common.hpp
index 274698b32b51ae9b741b94e25b492ab059637e37..c84f25d249b49eee094e3e898090ffb9ff129f03 100644
--- a/applications/calibration/src/common.hpp
+++ b/applications/calibration/src/common.hpp
@@ -15,8 +15,8 @@ int getOptionInt(const std::map<std::string, std::string> &options, const std::s
 double getOptionDouble(const std::map<std::string, std::string> &options, const std::string &opt, double default_value);
 std::string getOptionString(const std::map<std::string, std::string> &options, const std::string &opt, std::string default_value);
 
-bool loadIntrinsics(const std::string &ifile, std::vector<cv::Mat> &K, std::vector<cv::Mat> &D);
-bool saveIntrinsics(const std::string &ofile, const std::vector<cv::Mat> &K, const std::vector<cv::Mat> &D);
+bool loadIntrinsics(const std::string &ifile, std::vector<cv::Mat> &K, std::vector<cv::Mat> &D, cv::Size &size);
+bool saveIntrinsics(const std::string &ofile, const std::vector<cv::Mat> &K, const std::vector<cv::Mat> &D, const cv::Size &size);
 
 // TODO loadExtrinsics()
 bool saveExtrinsics(const std::string &ofile, cv::Mat &R, cv::Mat &T, cv::Mat &R1, cv::Mat &R2, cv::Mat &P1, cv::Mat &P2, cv::Mat &Q);
@@ -90,10 +90,11 @@ public:
  */
 class CalibrationChessboard : Calibration {
 public:
-	CalibrationChessboard(const std::map<std::string, std::string> &opt);
+	explicit CalibrationChessboard(const std::map<std::string, std::string> &opt);
 	void objectPoints(std::vector<cv::Vec3f> &out);
 	bool findPoints(cv::Mat &in, std::vector<cv::Vec2f> &out);
 	void drawPoints(cv::Mat &img, const std::vector<cv::Vec2f> &points);
+	void drawCorners(cv::Mat &img, const std::vector<cv::Vec2f> &points);
 
 private:
 	int chessboard_flags_ = 0;
diff --git a/applications/calibration/src/lens.cpp b/applications/calibration/src/lens.cpp
index f793dbfc6907b79ac65dfcbcf4a921884e6ca5cc..b69b26cc6e4cfec9dab04d337d75b21721d2fbae 100644
--- a/applications/calibration/src/lens.cpp
+++ b/applications/calibration/src/lens.cpp
@@ -14,6 +14,8 @@
 #include <opencv2/highgui.hpp>
 
 #include <vector>
+#include <atomic>
+#include <thread>
 
 using std::map;
 using std::string;
@@ -30,8 +32,8 @@ void ftl::calibration::intrinsic(map<string, string> &opt) {
 	LOG(INFO) << "Begin intrinsic calibration";
 
 	// TODO PARAMETERS TO CONFIG FILE
-	const Size image_size = Size(	getOptionInt(opt, "width", 1280),
-							getOptionInt(opt, "height", 720));
+	const Size image_size = Size(	getOptionInt(opt, "width", 1920),
+							getOptionInt(opt, "height", 1080));
 	const int n_cameras = getOptionInt(opt, "n_cameras", 2);
 	const int iter = getOptionInt(opt, "iter", 40);
 	const int delay = getOptionInt(opt, "delay", 1000);
@@ -39,18 +41,21 @@ void ftl::calibration::intrinsic(map<string, string> &opt) {
 	const double aperture_height = getOptionDouble(opt, "aperture_height", 4.6);
 	const string filename_intrinsics = getOptionString(opt, "profile", FTL_LOCAL_CONFIG_ROOT "/intrinsics.yml");
 	CalibrationChessboard calib(opt);
-	const bool use_guess = getOptionInt(opt, "use_guess", 1);
+	bool use_guess = getOptionInt(opt, "use_guess", 1);
+	//bool use_guess_distortion = getOptionInt(opt, "use_guess_distortion", 0);
 
 	LOG(INFO) << "Intrinsic calibration parameters";
-	LOG(INFO) << "         profile: " << filename_intrinsics;
-	LOG(INFO) << "       n_cameras: " << n_cameras;
-	LOG(INFO) << "           width: " << image_size.width;
-	LOG(INFO) << "          height: " << image_size.height;
-	LOG(INFO) << "            iter: " << iter;
-	LOG(INFO) << "           delay: " << delay;
-	LOG(INFO) << "  aperture_width: " << aperture_width;
-	LOG(INFO) << " aperture_height: " << aperture_height;
-	LOG(INFO) << "       use_guess: " << use_guess;
+	LOG(INFO) << "               profile: " << filename_intrinsics;
+	LOG(INFO) << "             n_cameras: " << n_cameras;
+	LOG(INFO) << "                 width: " << image_size.width;
+	LOG(INFO) << "                height: " << image_size.height;
+	LOG(INFO) << "                  iter: " << iter;
+	LOG(INFO) << "                 delay: " << delay;
+	LOG(INFO) << "        aperture_width: " << aperture_width;
+	LOG(INFO) << "       aperture_height: " << aperture_height;
+	LOG(INFO) << "             use_guess: " << use_guess;
+	//LOG(INFO) << "  use_guess_distortion: " << use_guess_distortion;
+
 	LOG(INFO) << "-----------------------------------";
 
 	int calibrate_flags =	cv::CALIB_ZERO_TANGENT_DIST | cv::CALIB_FIX_ASPECT_RATIO;
@@ -61,18 +66,39 @@ void ftl::calibration::intrinsic(map<string, string> &opt) {
 
 	vector<Mat> camera_matrix(n_cameras), dist_coeffs(n_cameras);
 
-	if (use_guess) {
+	for (Mat &d : dist_coeffs)
+	{
+		d = Mat(Size(5, 1), CV_64FC1, cv::Scalar(0.0));
+	}
+
+	if (use_guess)
+	{
 		camera_matrix.clear();
 		vector<Mat> tmp;
-		//dist_coeffs.clear();
-		loadIntrinsics(filename_intrinsics, camera_matrix, tmp);
+		Size tmp_size;
+		
+		loadIntrinsics(filename_intrinsics, camera_matrix, tmp, tmp_size);
 		CHECK(camera_matrix.size() == n_cameras); // (camera_matrix.size() == dist_coeffs.size())
+		if ((tmp_size != image_size) && (!tmp_size.empty()))
+		{
+			Mat scale = Mat::eye(Size(3, 3), CV_64FC1);
+			scale.at<double>(0, 0) = ((double) image_size.width) / ((double) tmp_size.width);
+			scale.at<double>(1, 1) = ((double) image_size.height) / ((double) tmp_size.height);
+			for (Mat &K : camera_matrix) { K = scale * K; }
+		}
+
+		if (tmp_size.empty())
+		{
+			use_guess = false;
+			LOG(FATAL) << "No valid calibration found.";
+		}
 	}
 
 	vector<cv::VideoCapture> cameras;
 	cameras.reserve(n_cameras);
 	for (int c = 0; c < n_cameras; c++) { cameras.emplace_back(c); }
-	for (auto &camera : cameras) {
+	for (auto &camera : cameras)
+	{
 		if (!camera.isOpened()) {
 			LOG(ERROR) << "Could not open camera device";
 			return;
@@ -83,41 +109,102 @@ void ftl::calibration::intrinsic(map<string, string> &opt) {
 
 	vector<vector<vector<Vec2f>>> image_points(n_cameras);
 	vector<vector<vector<Vec3f>>> object_points(n_cameras);
+	
 	vector<Mat> img(n_cameras);
+	vector<Mat> img_display(n_cameras);
 	vector<int> count(n_cameras, 0);
+	Mat display(Size(image_size.width * n_cameras, image_size.height), CV_8UC3);
 
-	while (iter > *std::min_element(count.begin(), count.end())) {
+	for (int c = 0; c < n_cameras; c++)
+	{
+		img_display[c] = Mat(display, cv::Rect(c * image_size.width, 0, image_size.width, image_size.height));
+	}
 
-		for (auto &camera : cameras) { camera.grab(); }
+	std::mutex m;
+	std::atomic<bool> ready = false;
+	auto capture = std::thread([n_cameras, delay, &m, &ready, &count, &calib, &img, &image_points, &object_points]()
+	{
+		vector<Mat> tmp(n_cameras);
+		while(true)
+		{
+			if (!ready)
+			{
+				std::this_thread::sleep_for(std::chrono::milliseconds(delay));
+				continue;
+			}
 
-		for (int c = 0; c < n_cameras; c++) {
-			vector<Vec2f> points;
-			cameras[c].retrieve(img[c]);
-		
-			if (calib.findPoints(img[c], points)) {
-				calib.drawPoints(img[c], points);
-				count[c]++;
+			m.lock();
+			ready = false;
+			for (int c = 0; c < n_cameras; c++)
+			{
+				img[c].copyTo(tmp[c]);
+			}
+			m.unlock();
+			
+			for (int c = 0; c < n_cameras; c++)
+			{
+				vector<Vec2f> points;
+				if (calib.findPoints(tmp[c], points))
+				{
+					count[c]++;
+				}
+				else { continue; }
+
+				vector<Vec3f> points_ref;
+				calib.objectPoints(points_ref);
+				Mat camera_matrix, dist_coeffs;
+				image_points[c].push_back(points);
+				object_points[c].push_back(points_ref);
 			}
-			else { continue; }
-		
-			vector<Vec3f> points_ref;
-			calib.objectPoints(points_ref);
 
-			Mat camera_matrix, dist_coeffs;
-			vector<Mat> rvecs, tvecs;
-		
-			image_points[c].push_back(points);
-			object_points[c].push_back(points_ref);
+			std::this_thread::sleep_for(std::chrono::milliseconds(delay));
+		}
+	});
+
+	while (iter > *std::min_element(count.begin(), count.end()))
+	{
+		if (m.try_lock())
+		{
+			for (auto &camera : cameras) { camera.grab(); }
+
+			for (int c = 0; c < n_cameras; c++)
+			{
+				cameras[c].retrieve(img[c]);
+			}
+
+			ready = true;
+			m.unlock();
 		}
 		
-		for (int c = 0; c < n_cameras; c++) {
-			cv::imshow("Camera " + std::to_string(c), img[c]);
+		for (int c = 0; c < n_cameras; c++)
+		{
+			img[c].copyTo(img_display[c]);
+			m.lock();
+
+			if (image_points[c].size() > 0)
+			{
+				
+				for (auto &points : image_points[c])
+				{
+					calib.drawCorners(img_display[c], points);
+				}
+
+				calib.drawPoints(img_display[c], image_points[c].back());
+			}
+
+			m.unlock();
 		}
 
-		cv::waitKey(delay);
+		cv::namedWindow("Cameras", cv::WINDOW_KEEPRATIO | cv::WINDOW_NORMAL);
+		cv::imshow("Cameras", display);
+
+		cv::waitKey(10);
 	}
 
-	for (int c = 0; c < n_cameras; c++) {
+	cv::destroyAllWindows();
+
+	for (int c = 0; c < n_cameras; c++)
+	{
 		LOG(INFO) << "Calculating intrinsic paramters for camera " << std::to_string(c);
 		vector<Mat> rvecs, tvecs;
 		
@@ -148,7 +235,7 @@ void ftl::calibration::intrinsic(map<string, string> &opt) {
 		LOG(INFO) << "";
 	}
 
-	saveIntrinsics(filename_intrinsics, camera_matrix, dist_coeffs);
+	saveIntrinsics(filename_intrinsics, camera_matrix, dist_coeffs, image_size);
 	LOG(INFO) << "intrinsic paramaters saved to: " << filename_intrinsics;
 	
 	vector<Mat> map1(n_cameras), map2(n_cameras);
diff --git a/applications/calibration/src/stereo.cpp b/applications/calibration/src/stereo.cpp
index 0d2e1636996438ec9d241f227ff1aab003406f2a..964cf815300971cf31130e78fae94e1c21a172ad 100644
--- a/applications/calibration/src/stereo.cpp
+++ b/applications/calibration/src/stereo.cpp
@@ -93,11 +93,17 @@ void ftl::calibration::stereo(map<string, string> &opt) {
 	
 	vector<Mat> dist_coeffs(2);
 	vector<Mat> camera_matrices(2);
-
-	if (!loadIntrinsics(filename_intrinsics, camera_matrices, dist_coeffs)) {
+	Size intrinsic_resolution;
+	if (!loadIntrinsics(filename_intrinsics, camera_matrices, dist_coeffs, intrinsic_resolution))
+	{
 		LOG(FATAL) << "Failed to load intrinsic camera parameters from file.";
 	}
 	
+	if (intrinsic_resolution != image_size)
+	{
+		LOG(FATAL) << "Intrinsic resolution is not same as input resolution (TODO)";
+	}
+
 	Mat R, T, E, F, per_view_errors;
 	
 	// capture calibration patterns
diff --git a/applications/groupview/src/main.cpp b/applications/groupview/src/main.cpp
index 3a821a01323fbe51dbc8b6fdef98597f0bd20853..6b32221ef1a13ad05f9e5bb915c1186eca0aa52c 100644
--- a/applications/groupview/src/main.cpp
+++ b/applications/groupview/src/main.cpp
@@ -16,6 +16,7 @@ using std::string;
 using std::vector;
 using cv::Size;
 using cv::Mat;
+using ftl::rgbd::Channel;
 
 // TODO: remove code duplication (function from reconstruction)
 static void from_json(nlohmann::json &json, map<string, Matrix4d> &transformations) {
@@ -79,7 +80,7 @@ void modeLeftRight(ftl::Configurable *root) {
 	ftl::rgbd::Group group;
 
 	for (auto* src : sources) {
-		src->setChannel(ftl::rgbd::kChanRight);
+		src->setChannel(Channel::Right);
 		group.addSource(src);
 	}
 
@@ -90,15 +91,17 @@ void modeLeftRight(ftl::Configurable *root) {
 	group.sync([&mutex, &new_frames, &rgb_new](ftl::rgbd::FrameSet &frames) {
 		mutex.lock();
 		bool good = true;
-		for (size_t i = 0; i < frames.channel1.size(); i ++) {
-			if (frames.channel1[i].empty()) good = false;
-			if (frames.channel1[i].empty()) good = false;
-			if (frames.channel1[i].channels() != 3) good = false; // ASSERT
-			if (frames.channel2[i].channels() != 3) good = false;
+		for (size_t i = 0; i < frames.frames.size(); i ++) {
+			auto &chan1 = frames.frames[i].get<cv::Mat>(Channel::Colour);
+			auto &chan2 = frames.frames[i].get<cv::Mat>(frames.sources[i]->getChannel());
+			if (chan1.empty()) good = false;
+			if (chan2.empty()) good = false;
+			if (chan1.channels() != 3) good = false; // ASSERT
+			if (chan2.channels() != 3) good = false;
 			if (!good) break;
 			
-			frames.channel1[i].copyTo(rgb_new[2 * i]);
-			frames.channel2[i].copyTo(rgb_new[2 * i + 1]);
+			chan1.copyTo(rgb_new[2 * i]);
+			chan2.copyTo(rgb_new[2 * i + 1]);
 		}
 
 		new_frames = good;
@@ -107,11 +110,12 @@ void modeLeftRight(ftl::Configurable *root) {
 	});
 	
 	int idx = 0;
-	int key;
 
 	Mat show;
 	
 	while (ftl::running) {
+		int key;
+		
 		while (!new_frames) {
 			for (auto src : sources) { src->grab(30); }
 			key = cv::waitKey(10);
@@ -164,7 +168,7 @@ void modeFrame(ftl::Configurable *root, int frames=1) {
 		} else {
 			s->setPose(T->second);
 		}
-		s->setChannel(ftl::rgbd::kChanDepth);
+		s->setChannel(Channel::Depth);
 		group.addSource(s);
 	}
 
@@ -180,14 +184,19 @@ void modeFrame(ftl::Configurable *root, int frames=1) {
 		//LOG(INFO) << "Complete set: " << fs.timestamp;
 		if (!ftl::running) { return false; }
 		
+		std::vector<cv::Mat> frames;
 
 		for (size_t i=0; i<fs.sources.size(); ++i) {
-			if (fs.channel1[i].empty() || fs.channel2[i].empty()) return true;	
+			auto &chan1 = fs.frames[i].get<cv::Mat>(Channel::Colour);
+			auto &chan2 = fs.frames[i].get<cv::Mat>(fs.sources[i]->getChannel());
+			if (chan1.empty() || chan2.empty()) return true;
+
+			frames.push_back(chan1);
 		}
 
 		cv::Mat show;
 
-		stack(fs.channel1, show);
+		stack(frames, show);
 
 		cv::resize(show, show, cv::Size(1280,720));
 		cv::namedWindow("Cameras", cv::WINDOW_KEEPRATIO | cv::WINDOW_NORMAL);
@@ -206,9 +215,12 @@ void modeFrame(ftl::Configurable *root, int frames=1) {
 			auto writer = ftl::rgbd::SnapshotWriter(std::string(timestamp) + ".tar.gz");
 
 			for (size_t i=0; i<fs.sources.size(); ++i) {
+				auto &chan1 = fs.frames[i].get<cv::Mat>(Channel::Colour);
+				auto &chan2 = fs.frames[i].get<cv::Mat>(fs.sources[i]->getChannel());
+
 				writer.addSource(fs.sources[i]->getURI(), fs.sources[i]->parameters(), fs.sources[i]->getPose());
-				LOG(INFO) << "SAVE: " << fs.channel1[i].cols << ", " << fs.channel2[i].type();
-				writer.addRGBD(i, fs.channel1[i], fs.channel2[i]);
+				//LOG(INFO) << "SAVE: " << fs.channel1[i].cols << ", " << fs.channel2[i].type();
+				writer.addRGBD(i, chan1, chan2);
 			}
 		}
 #endif  // HAVE_LIBARCHIVE
@@ -248,7 +260,7 @@ void modeVideo(ftl::Configurable *root) {
 	auto sources = ftl::createArray<ftl::rgbd::Source>(root, "sources", net);
 	const string path = root->value<string>("save_to", "./");
 
-	for (auto* src : sources) { src->setChannel(ftl::rgbd::kChanDepth); }
+	for (auto* src : sources) { src->setChannel(Channel::Depth); }
 
 	cv::Mat show;
 	vector<cv::Mat> rgb(sources.size());
diff --git a/applications/gui/CMakeLists.txt b/applications/gui/CMakeLists.txt
index baffb9bdd645c85cbfc593d81e14687965b9bf4c..fbed0680dde997c0f0a76519ac272fb3e5c1c64f 100644
--- a/applications/gui/CMakeLists.txt
+++ b/applications/gui/CMakeLists.txt
@@ -27,6 +27,6 @@ target_include_directories(ftl-gui PUBLIC
 #endif()
 
 #target_include_directories(cv-node PUBLIC ${PROJECT_SOURCE_DIR}/include)
-target_link_libraries(ftl-gui ftlcommon ftlctrl ftlrgbd Threads::Threads ${OpenCV_LIBS} glog::glog ftlnet ftlrender nanogui GL)
+target_link_libraries(ftl-gui ftlcommon ftlctrl ftlrgbd Threads::Threads ${OpenCV_LIBS} ${OPENVR_LIBRARIES} glog::glog ftlnet nanogui GL)
 
 
diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index f44521b3a81d06a4087a56a8eacf6d2bf4d4700b..32d78a4a0ff8bbccfed60b13293b1277ab599792 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -7,6 +7,8 @@
 using ftl::rgbd::isValidDepth;
 using ftl::gui::GLTexture;
 using ftl::gui::PoseWindow;
+using ftl::rgbd::Channel;
+using ftl::rgbd::Channels;
 
 // TODO(Nick) MOVE
 class StatisticsImage {
@@ -16,7 +18,7 @@ private:
 	float n_;		// total number of samples
 
 public:
-	StatisticsImage(cv::Size size);
+	explicit StatisticsImage(cv::Size size);
 	StatisticsImage(cv::Size size, float max_f);
 
 	/* @brief reset all statistics to 0
@@ -133,10 +135,10 @@ ftl::gui::Camera::Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src) : scr
 	ftime_ = (float)glfwGetTime();
 	pause_ = false;
 
-	channel_ = ftl::rgbd::kChanLeft;
+	channel_ = Channel::Left;
 
-	channels_.push_back(ftl::rgbd::kChanLeft);
-	channels_.push_back(ftl::rgbd::kChanDepth);
+	channels_ += Channel::Left;
+	channels_ += Channel::Depth;
 
 	// Create pose window...
 	posewin_ = new PoseWindow(screen, src_->getURI());
@@ -149,6 +151,8 @@ ftl::gui::Camera::Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src) : scr
 		depth_.create(depth.size(), depth.type());
 		cv::swap(rgb_,rgb);
 		cv::swap(depth_, depth);
+		cv::flip(rgb_,rgb_,0);
+		cv::flip(depth_,depth_,0);
 	});
 }
 
@@ -227,30 +231,41 @@ void ftl::gui::Camera::showSettings() {
 
 }
 
-void ftl::gui::Camera::setChannel(ftl::rgbd::channel_t c) {
+void ftl::gui::Camera::setChannel(Channel c) {
 	channel_ = c;
 	switch (c) {
-	case ftl::rgbd::kChanEnergy:
-	case ftl::rgbd::kChanFlow:
-	case ftl::rgbd::kChanConfidence:
-	case ftl::rgbd::kChanNormals:
-	case ftl::rgbd::kChanRight:
+	case Channel::Energy:
+	case Channel::Flow:
+	case Channel::Confidence:
+	case Channel::Normals:
+	case Channel::Right:
 		src_->setChannel(c);
 		break;
 
-	case ftl::rgbd::kChanDeviation:
+	case Channel::Deviation:
 		if (stats_) { stats_->reset(); }
-		src_->setChannel(ftl::rgbd::kChanDepth);
+		src_->setChannel(Channel::Depth);
 		break;
 	
-	case ftl::rgbd::kChanDepth:
+	case Channel::Depth:
 		src_->setChannel(c);
 		break;
 	
-	default: src_->setChannel(ftl::rgbd::kChanNone);
+	default: src_->setChannel(Channel::None);
 	}
 }
 
+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)
 {
@@ -305,22 +320,50 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 	if (src_ && src_->isReady()) {
 		UNIQUE_LOCK(mutex_, lk);
 
-		// 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_;
+		if (screen_->hasVR()) {
+			#ifdef HAVE_OPENVR
+			src_->setChannel(Channel::Right);
+
+			vr::VRCompositor()->WaitGetPoses(rTrackedDevicePose_, vr::k_unMaxTrackedDeviceCount, NULL, 0 );
+
+			if ( rTrackedDevicePose_[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid )
+			{
+				auto pose = ConvertSteamVRMatrixToMatrix4( rTrackedDevicePose_[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking );
+				pose.inverse();
 
-		Eigen::Translation3d trans(eye_);
-		Eigen::Affine3d t(trans);
-		Eigen::Matrix4d viewPose = t.matrix() * rotmat_;
+				// 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::Translation3d trans(eye_);
+				Eigen::Affine3d t(trans);
+				Eigen::Matrix4d viewPose = t.matrix() * pose;
+
+				if (src_->hasCapabilities(ftl::rgbd::kCapMovable)) src_->setPose(viewPose);
+			} else {
+				LOG(ERROR) << "No VR Pose";
+			}
+			#endif
+		} else {
+			// 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::Translation3d trans(eye_);
+			Eigen::Affine3d t(trans);
+			Eigen::Matrix4d viewPose = t.matrix() * rotmat_;
+
+			if (src_->hasCapabilities(ftl::rgbd::kCapMovable)) src_->setPose(viewPose);
+		}
 
-		if (src_->hasCapabilities(ftl::rgbd::kCapMovable)) src_->setPose(viewPose);
 		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)
-		if (channel_ == ftl::rgbd::kChanDeviation &&
+		if (channel_ == Channel::Deviation &&
 			depth_.rows > 0 && depth_.channels() == 1)
 		{
 			if (!stats_) {
@@ -333,19 +376,19 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 		cv::Mat tmp;
 
 		switch(channel_) {
-			case ftl::rgbd::kChanEnergy:
+			case Channel::Energy:
 				if (depth_.rows == 0) { break; }
 				visualizeEnergy(depth_, tmp, 10.0);
 				texture_.update(tmp);
 				break;
-			case ftl::rgbd::kChanDepth:
+			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);
 				break;
 			
-			case ftl::rgbd::kChanDeviation:
+			case Channel::Deviation:
 				if (depth_.rows == 0) { break; }
 				//imageSize = Vector2f(depth.cols, depth.rows);
 				stats_->getStdDev(tmp);
@@ -354,10 +397,10 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 				texture_.update(tmp);
 				break;
 
-		case ftl::rgbd::kChanFlow:
-		case ftl::rgbd::kChanConfidence:
-		case ftl::rgbd::kChanNormals:
-			case ftl::rgbd::kChanRight:
+		case Channel::Flow:
+		case Channel::Confidence:
+		case Channel::Normals:
+		case Channel::Right:
 				if (depth_.rows == 0 || depth_.type() != CV_8UC3) { break; }
 				texture_.update(depth_);
 				break;
@@ -366,6 +409,13 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 				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
 		}
 	}
 
diff --git a/applications/gui/src/camera.hpp b/applications/gui/src/camera.hpp
index f412d9c43d1809374566b1ba24c427ecda792695..55042fe0d7b3baf5f24a26e390b1348084bcf320 100644
--- a/applications/gui/src/camera.hpp
+++ b/applications/gui/src/camera.hpp
@@ -6,6 +6,10 @@
 
 #include <string>
 
+#ifdef HAVE_OPENVR
+#include <openvr/openvr.h>
+#endif
+
 class StatisticsImage;
 
 namespace ftl {
@@ -19,6 +23,8 @@ class Camera {
 	Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src);
 	~Camera();
 
+	Camera(const Camera &)=delete;
+
 	ftl::rgbd::Source *source();
 
 	int width() { return (src_) ? src_->parameters().width : 0; }
@@ -32,13 +38,15 @@ class Camera {
 	void showPoseWindow();
 	void showSettings();
 
-	void setChannel(ftl::rgbd::channel_t c);
+	void setChannel(ftl::rgbd::Channel c);
 
 	void togglePause();
 	void isPaused();
-	const std::vector<ftl::rgbd::channel_t> &availableChannels();
+	const ftl::rgbd::Channels &availableChannels();
 
 	const GLTexture &captureFrame();
+	const GLTexture &getLeft() const { return texture_; }
+	const GLTexture &getRight() const { return textureRight_; }
 
 	bool thumbnail(cv::Mat &thumb);
 
@@ -51,6 +59,7 @@ class Camera {
 	ftl::rgbd::Source *src_;
 	GLTexture thumb_;
 	GLTexture texture_;
+	GLTexture textureRight_;
 	ftl::gui::PoseWindow *posewin_;
 	nlohmann::json meta_;
 	Eigen::Vector4d neye_;
@@ -62,11 +71,15 @@ class Camera {
 	float lerpSpeed_;
 	bool sdepth_;
 	bool pause_;
-	ftl::rgbd::channel_t channel_;
-	std::vector<ftl::rgbd::channel_t> channels_;
+	ftl::rgbd::Channel channel_;
+	ftl::rgbd::Channels channels_;
 	cv::Mat rgb_;
 	cv::Mat depth_;
 	MUTEX mutex_;
+
+	#ifdef HAVE_OPENVR
+	vr::TrackedDevicePose_t rTrackedDevicePose_[ vr::k_unMaxTrackedDeviceCount ];
+	#endif
 };
 
 }
diff --git a/applications/gui/src/media_panel.cpp b/applications/gui/src/media_panel.cpp
index 2ee06a69d09690e497bb8f88283a947b466aae46..cb44400bb1c850669332376e0134f138b9c460f3 100644
--- a/applications/gui/src/media_panel.cpp
+++ b/applications/gui/src/media_panel.cpp
@@ -12,6 +12,7 @@
 #endif
 
 using ftl::gui::MediaPanel;
+using ftl::rgbd::Channel;
 
 MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""), screen_(screen) {
 	using namespace nanogui;
@@ -115,7 +116,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
     button->setCallback([this]() {
         ftl::gui::Camera *cam = screen_->activeCamera();
         if (cam) {
-            cam->setChannel(ftl::rgbd::kChanLeft);
+            cam->setChannel(Channel::Left);
         }
     });
 
@@ -124,7 +125,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
     right_button_->setCallback([this]() {
         ftl::gui::Camera *cam = screen_->activeCamera();
         if (cam) {
-            cam->setChannel(ftl::rgbd::kChanRight);
+            cam->setChannel(Channel::Right);
         }
     });
 
@@ -133,7 +134,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
     depth_button_->setCallback([this]() {
         ftl::gui::Camera *cam = screen_->activeCamera();
         if (cam) {
-            cam->setChannel(ftl::rgbd::kChanDepth);
+            cam->setChannel(Channel::Depth);
         }
     });
 
@@ -150,7 +151,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
     button->setCallback([this]() {
         ftl::gui::Camera *cam = screen_->activeCamera();
         if (cam) {
-            cam->setChannel(ftl::rgbd::kChanDeviation);
+            cam->setChannel(Channel::Deviation);
         }
     });
 
@@ -159,7 +160,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
     button->setCallback([this]() {
         ftl::gui::Camera *cam = screen_->activeCamera();
         if (cam) {
-            cam->setChannel(ftl::rgbd::kChanNormals);
+            cam->setChannel(Channel::Normals);
         }
     });
 
@@ -168,7 +169,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
     button->setCallback([this]() {
         ftl::gui::Camera *cam = screen_->activeCamera();
         if (cam) {
-            cam->setChannel(ftl::rgbd::kChanFlow);
+            cam->setChannel(Channel::Flow);
         }
     });
 
@@ -177,7 +178,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
     button->setCallback([this]() {
         ftl::gui::Camera *cam = screen_->activeCamera();
         if (cam) {
-            cam->setChannel(ftl::rgbd::kChanConfidence);
+            cam->setChannel(Channel::Confidence);
         }
     });
 
@@ -186,7 +187,7 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
     button->setCallback([this]() {
         ftl::gui::Camera *cam = screen_->activeCamera();
         if (cam) {
-            cam->setChannel(ftl::rgbd::kChanEnergy);
+            cam->setChannel(Channel::Energy);
         }
     });
 
diff --git a/applications/gui/src/media_panel.hpp b/applications/gui/src/media_panel.hpp
index d7f9aa9938ee51418629ee42781e738b535c38d0..9e9154d860483a6bf6c88b8da856a4a89862de2f 100644
--- a/applications/gui/src/media_panel.hpp
+++ b/applications/gui/src/media_panel.hpp
@@ -15,7 +15,7 @@ class Screen;
 
 class MediaPanel : public nanogui::Window {
     public:
-    MediaPanel(ftl::gui::Screen *);
+    explicit MediaPanel(ftl::gui::Screen *);
     ~MediaPanel();
 
     void cameraChanged();
diff --git a/applications/gui/src/screen.cpp b/applications/gui/src/screen.cpp
index 56624ce61d9524c43e6dffe00948e807528ae595..03d1847112fa86468d9661d5a9966b71a3d46b09 100644
--- a/applications/gui/src/screen.cpp
+++ b/applications/gui/src/screen.cpp
@@ -37,7 +37,7 @@ namespace {
             uv = vertex;
             vec2 scaledVertex = (vertex * scaleFactor) + position;
             gl_Position  = vec4(2.0*scaledVertex.x - 1.0,
-                                1.0 - 2.0*scaledVertex.y,
+                                2.0*scaledVertex.y - 1.0,
                                 0.0, 1.0);
         })";
 
@@ -51,15 +51,15 @@ namespace {
         })";
 }
 
-ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl::ctrl::Master *controller) : nanogui::Screen(Eigen::Vector2i(1024, 768), "FT-Lab Remote Presence") {
+ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl::ctrl::Master *controller) :
+		nanogui::Screen(Eigen::Vector2i(1024, 768), "FT-Lab Remote Presence"),
+		status_("FT-Lab Remote Presence System") {
 	using namespace nanogui;
 	net_ = pnet;
 	ctrl_ = controller;
 	root_ = proot;
 	camera_ = nullptr;
 
-	status_ = "FT-Lab Remote Presence System";
-
 	setSize(Vector2i(1280,720));
 
 	toolbuttheme = new Theme(*theme());
@@ -244,10 +244,31 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 
 	setVisible(true);
 	performLayout();
+
+
+	#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 {
+		HMD_ = nullptr;
+	}
+	#endif
 }
 
 ftl::gui::Screen::~Screen() {
 	mShader.free();
+
+	#ifdef HAVE_OPENVR
+	vr::VR_Shutdown();
+	#endif
 }
 
 void ftl::gui::Screen::setActiveCamera(ftl::gui::Camera *cam) {
@@ -337,6 +358,18 @@ void ftl::gui::Screen::draw(NVGcontext *ctx) {
 		imageSize = {camera_->width(), camera_->height()};
 
 		mImageID = camera_->captureFrame().texture();
+		leftEye_ = mImageID;
+		rightEye_ = camera_->getRight().texture();
+
+		#ifdef HAVE_OPENVR
+		if (hasVR() && 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 );
+		}
+		#endif
 
 		if (mImageID < std::numeric_limits<unsigned int>::max() && imageSize[0] > 0) {
 			auto mScale = (screenSize.cwiseQuotient(imageSize).minCoeff());
diff --git a/applications/gui/src/screen.hpp b/applications/gui/src/screen.hpp
index 8bfce830a034e024ec42037dbc43539a62d50101..d51cec2bf2c34afc7c589b7e6114f503379afe30 100644
--- a/applications/gui/src/screen.hpp
+++ b/applications/gui/src/screen.hpp
@@ -11,6 +11,10 @@
 #include "src_window.hpp"
 #include "gltexture.hpp"
 
+#ifdef HAVE_OPENVR
+#include <openvr/openvr.h>
+#endif
+
 class StatisticsImageNSamples;
 
 namespace ftl {
@@ -39,6 +43,12 @@ 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
+	bool hasVR() const { return false; }
+	#endif
+
 	nanogui::Theme *windowtheme;
 	nanogui::Theme *specialtheme;
 	nanogui::Theme *mediatheme;
@@ -68,6 +78,13 @@ class Screen : public nanogui::Screen {
 	ftl::Configurable *root_;
 	std::string status_;
 	ftl::gui::Camera *camera_;
+
+	GLuint leftEye_;
+	GLuint rightEye_;
+
+	#ifdef HAVE_OPENVR
+	vr::IVRSystem *HMD_;
+	#endif
 };
 
 }
diff --git a/applications/gui/src/src_window.hpp b/applications/gui/src/src_window.hpp
index 7f28279c7fa3cd83bdb62a074e55d4d549799f73..b2fe8a9e0957f3345a719a0c37bac46bf473089c 100644
--- a/applications/gui/src/src_window.hpp
+++ b/applications/gui/src/src_window.hpp
@@ -22,7 +22,7 @@ class Camera;
 
 class SourceWindow : public nanogui::Window {
 	public:
-	SourceWindow(ftl::gui::Screen *screen);
+	explicit SourceWindow(ftl::gui::Screen *screen);
 	~SourceWindow();
 
 	const std::vector<ftl::gui::Camera*> &getCameras();
diff --git a/applications/reconstruct/CMakeLists.txt b/applications/reconstruct/CMakeLists.txt
index 1e55c671c87487b995b0dd772495ff8a612a8c78..dcee7afa0147378ffaabd91263e200f8fe284c98 100644
--- a/applications/reconstruct/CMakeLists.txt
+++ b/applications/reconstruct/CMakeLists.txt
@@ -4,24 +4,19 @@
 
 set(REPSRC
 	src/main.cpp
-	src/voxel_scene.cpp
-	src/scene_rep_hash_sdf.cu
-	src/compactors.cu
-	src/garbage.cu
-	src/integrators.cu
+	#src/voxel_scene.cpp
 	#src/ray_cast_sdf.cu
-	src/voxel_render.cu
 	src/camera_util.cu
-	src/voxel_hash.cu
-	src/voxel_hash.cpp
 	#src/ray_cast_sdf.cpp
 	src/registration.cpp
 	#src/virtual_source.cpp
-	src/splat_render.cpp
-	src/dibr.cu
-	src/mls.cu
-	src/depth_camera.cu
-	src/depth_camera.cpp
+	#src/splat_render.cpp
+	#src/dibr.cu
+	#src/mls.cu
+	#src/depth_camera.cu
+	#src/depth_camera.cpp
+	src/ilw.cpp
+	src/ilw.cu
 )
 
 add_executable(ftl-reconstruct ${REPSRC})
diff --git a/applications/reconstruct/include/ftl/depth_camera.hpp b/applications/reconstruct/include/ftl/depth_camera.hpp
index cff8ff71f8872a965b2f05071e96f26a4b0a604a..39e037abd90f64e6730577578e09a1f69998b43c 100644
--- a/applications/reconstruct/include/ftl/depth_camera.hpp
+++ b/applications/reconstruct/include/ftl/depth_camera.hpp
@@ -4,8 +4,8 @@
 
 //#include <cutil_inline.h>
 //#include <cutil_math.h>
-#include <vector_types.h>
-#include <cuda_runtime.h>
+//#include <vector_types.h>
+//#include <cuda_runtime.h>
 
 #include <ftl/cuda_matrix_util.hpp>
 #include <ftl/cuda_common.hpp>
diff --git a/applications/reconstruct/include/ftl/depth_camera_params.hpp b/applications/reconstruct/include/ftl/depth_camera_params.hpp
index 4864fccbdc9fa52647687e885f955a12132b0c04..c1c3b8e615577e2d4006ad15cfa975a27c5797fe 100644
--- a/applications/reconstruct/include/ftl/depth_camera_params.hpp
+++ b/applications/reconstruct/include/ftl/depth_camera_params.hpp
@@ -4,12 +4,13 @@
 
 //#include <cutil_inline.h>
 //#include <cutil_math.h>
-#include <vector_types.h>
-#include <cuda_runtime.h>
+//#include <cuda_runtime.h>
 
 #include <ftl/cuda_matrix_util.hpp>
 #include <ftl/rgbd/camera.hpp>
 
+//#include <vector_types.h>
+
 struct __align__(16) DepthCameraParams {
 	float fx;
 	float fy;
diff --git a/applications/reconstruct/include/ftl/voxel_hash.hpp b/applications/reconstruct/include/ftl/voxel_hash.hpp
deleted file mode 100644
index 98c2eca90530c309b5d2e46848493634aaaa053c..0000000000000000000000000000000000000000
--- a/applications/reconstruct/include/ftl/voxel_hash.hpp
+++ /dev/null
@@ -1,428 +0,0 @@
-// From: https://github.com/niessner/VoxelHashing/blob/master/DepthSensingCUDA/Source/VoxelUtilHashSDF.h
-
-#pragma once
-
-#ifndef sint
-typedef signed int sint;
-#endif
-
-#ifndef uint
-typedef unsigned int uint;
-#endif 
-
-#ifndef slong 
-typedef signed long slong;
-#endif
-
-#ifndef ulong
-typedef unsigned long ulong;
-#endif
-
-#ifndef uchar
-typedef unsigned char uchar;
-#endif
-
-#ifndef schar
-typedef signed char schar;
-#endif
-
-
-
-
-#include <ftl/cuda_util.hpp>
-
-#include <ftl/cuda_matrix_util.hpp>
-#include <ftl/voxel_hash_params.hpp>
-
-#include <ftl/depth_camera.hpp>
-
-#define SDF_BLOCK_SIZE 8
-#define SDF_BLOCK_SIZE_OLAP 8
-
-#ifndef MINF
-#define MINF __int_as_float(0xff800000)
-#endif
-
-#ifndef PINF
-#define PINF __int_as_float(0x7f800000)
-#endif
-
-extern  __constant__ ftl::voxhash::HashParams c_hashParams;
-extern "C" void updateConstantHashParams(const ftl::voxhash::HashParams& hashParams);
-
-namespace ftl {
-namespace voxhash {
-
-//status flags for hash entries
-static const int LOCK_ENTRY = -1;
-static const int FREE_ENTRY = -2147483648;
-static const int NO_OFFSET = 0;
-
-static const uint kFlagSurface = 0x00000001;
-
-struct __align__(16) HashEntryHead {
-	union {
-	short4 posXYZ;		// hash position (lower left corner of SDFBlock))
-	uint64_t pos;
-	};
-	int offset;	// offset for collisions
-	uint flags;
-};
-
-struct __align__(16) HashEntry 
-{
-	HashEntryHead head;
-	uint voxels[16];  // 512 bits, 1 bit per voxel
-	//uint validity[16];  // Is the voxel valid, 512 bit
-	
-	/*__device__ void operator=(const struct HashEntry& e) {
-		((long long*)this)[0] = ((const long long*)&e)[0];
-		((long long*)this)[1] = ((const long long*)&e)[1];
-		//((int*)this)[4] = ((const int*)&e)[4];
-		((long long*)this)[2] = ((const long long*)&e)[2];
-		((long long*)this)[2] = ((const long long*)&e)[3];
-		((long long*)this)[2] = ((const long long*)&e)[4];
-		((long long*)this)[2] = ((const long long*)&e)[5];
-		((long long*)this)[2] = ((const long long*)&e)[6];
-		((long long*)this)[2] = ((const long long*)&e)[7];
-		((long long*)this)[2] = ((const long long*)&e)[8];
-		((long long*)this)[2] = ((const long long*)&e)[9];
-		((long long*)this)[2] = ((const long long*)&e)[10];
-	}*/
-};
-
-struct __align__(8) Voxel {
-	float	sdf;		//signed distance function
-	uchar3	color;		//color 
-	uchar	weight;		//accumulated sdf weight
-
-	__device__ void operator=(const struct Voxel& v) {
-		((long long*)this)[0] = ((const long long*)&v)[0];
-	}
-
-};
- 
-/**
- * Voxel Hash Table structure and operations. Works on both CPU and GPU with
- * host <-> device transfer included.
- */
-struct HashData {
-
-	///////////////
-	// Host part //
-	///////////////
-
-	__device__ __host__
-	HashData() {
-		//d_heap = NULL;
-		//d_heapCounter = NULL;
-		d_hash = NULL;
-		d_hashDecision = NULL;
-		d_hashDecisionPrefix = NULL;
-		d_hashCompactified = NULL;
-		d_hashCompactifiedCounter = NULL;
-		//d_SDFBlocks = NULL;
-		d_hashBucketMutex = NULL;
-		m_bIsOnGPU = false;
-	}
-
-	/**
-	 * Create all the data structures, either on GPU or system memory.
-	 */
-	__host__ void allocate(const HashParams& params, bool dataOnGPU = true);
-
-	__host__ void updateParams(const HashParams& params);
-
-	__host__ void free();
-
-	/**
-	 * Download entire hash table from GPU to CPU memory.
-	 */
-	__host__ HashData download() const;
-
-	/**
-	 * Upload entire hash table from CPU to GPU memory.
-	 */
-	__host__ HashData upload() const;
-
-	__host__ size_t getAllocatedBlocks() const;
-
-	__host__ size_t getFreeBlocks() const;
-
-	__host__ size_t getCollisionCount() const;
-
-
-
-	/////////////////
-	// Device part //
-	/////////////////
-//#define __CUDACC__
-#ifdef __CUDACC__
-
-	__device__
-	const HashParams& params() const {
-		return c_hashParams;
-	}
-
-	//! see teschner et al. (but with correct prime values)
-	__device__ 
-	uint computeHashPos(const int3& virtualVoxelPos) const { 
-		const int p0 = 73856093;
-		const int p1 = 19349669;
-		const int p2 = 83492791;
-
-		int res = ((virtualVoxelPos.x * p0) ^ (virtualVoxelPos.y * p1) ^ (virtualVoxelPos.z * p2)) % params().m_hashNumBuckets;
-		if (res < 0) res += params().m_hashNumBuckets;
-		return (uint)res;
-	}
-
-	//merges two voxels (v0 the currently stored voxel, v1 is the input voxel)
-	__device__ 
-	void combineVoxel(const Voxel &v0, const Voxel& v1, Voxel &out) const 	{
-
-		//v.color = (10*v0.weight * v0.color + v1.weight * v1.color)/(10*v0.weight + v1.weight);	//give the currently observed color more weight
-		//v.color = (v0.weight * v0.color + v1.weight * v1.color)/(v0.weight + v1.weight);
-		//out.color = 0.5f * (v0.color + v1.color);	//exponential running average 
-		
-
-		float3 c0 = make_float3(v0.color.x, v0.color.y, v0.color.z);
-		float3 c1 = make_float3(v1.color.x, v1.color.y, v1.color.z);
-
-		//float3 res = (c0.x+c0.y+c0.z == 0) ? c1 : 0.5f*c0 + 0.5f*c1;
-		//float3 res = (c0+c1)/2;
-		float3 res = (c0 * (float)v0.weight + c1 * (float)v1.weight) / ((float)v0.weight + (float)v1.weight);
-		//float3 res = c1;
-
-		out.color.x = (uchar)(res.x+0.5f);	out.color.y = (uchar)(res.y+0.5f); out.color.z = (uchar)(res.z+0.5f);
-		
-		// Nick: reduces colour flicker but not ideal..
-		//out.color = v1.color;
-
-		// Option 3 (Nick): Use colour with minimum SDF since it should be closest to surface.
-		// Results in stable but pixelated output
-		//out.color = (v0.weight > 0 && (fabs(v0.sdf) < fabs(v1.sdf))) ? v0.color : v1.color;
-
-		// Option 4 (Nick): Merge colours based upon relative closeness
-		/*float3 c0 = make_float3(v0.color.x, v0.color.y, v0.color.z);
-		float3 c1 = make_float3(v1.color.x, v1.color.y, v1.color.z);
-		float factor = fabs(v0.sdf - v1.sdf) / 0.05f / 2.0f;
-		if (factor > 0.5f) factor = 0.5f;
-		float factor0 = (fabs(v0.sdf) < fabs(v1.sdf)) ? 1.0f - factor : factor;
-		float factor1 = 1.0f - factor0;
-		out.color.x = (v0.weight > 0) ? (uchar)(c0.x * factor0 + c1.x * factor1) : c1.x;
-		out.color.y = (v0.weight > 0) ? (uchar)(c0.y * factor0 + c1.y * factor1) : c1.y;
-		out.color.z = (v0.weight > 0) ? (uchar)(c0.z * factor0 + c1.z * factor1) : c1.z;*/
-
-		out.sdf = (v0.sdf * (float)v0.weight + v1.sdf * (float)v1.weight) / ((float)v0.weight + (float)v1.weight);
-		out.weight = min(params().m_integrationWeightMax, (unsigned int)v0.weight + (unsigned int)v1.weight);
-	}
-
-
-	//! returns the truncation of the SDF for a given distance value
-	__device__ 
-	float getTruncation(float z) const {
-		return params().m_truncation + params().m_truncScale * z;
-	}
-
-
-	__device__ 
-	float3 worldToVirtualVoxelPosFloat(const float3& pos) const	{
-		return pos / params().m_virtualVoxelSize;
-	}
-
-	__device__ 
-	int3 worldToVirtualVoxelPos(const float3& pos) const {
-		//const float3 p = pos*g_VirtualVoxelResolutionScalar;
-		const float3 p = pos / params().m_virtualVoxelSize;
-		return make_int3(p+make_float3(sign(p))*0.5f);
-	}
-
-	__device__ 
-	int3 virtualVoxelPosToSDFBlock(int3 virtualVoxelPos) const {
-		if (virtualVoxelPos.x < 0) virtualVoxelPos.x -= SDF_BLOCK_SIZE_OLAP-1;
-		if (virtualVoxelPos.y < 0) virtualVoxelPos.y -= SDF_BLOCK_SIZE_OLAP-1;
-		if (virtualVoxelPos.z < 0) virtualVoxelPos.z -= SDF_BLOCK_SIZE_OLAP-1;
-
-		return make_int3(
-			virtualVoxelPos.x/SDF_BLOCK_SIZE_OLAP,
-			virtualVoxelPos.y/SDF_BLOCK_SIZE_OLAP,
-			virtualVoxelPos.z/SDF_BLOCK_SIZE_OLAP);
-	}
-
-	// Computes virtual voxel position of corner sample position
-	__device__ 
-	int3 SDFBlockToVirtualVoxelPos(const int3& sdfBlock) const	{
-		return sdfBlock*SDF_BLOCK_SIZE_OLAP;
-	}
-
-	__device__ 
-	float3 virtualVoxelPosToWorld(const int3& pos) const	{
-		return make_float3(pos)*params().m_virtualVoxelSize;
-	}
-
-	__device__ 
-	float3 SDFBlockToWorld(const int3& sdfBlock) const	{
-		return virtualVoxelPosToWorld(SDFBlockToVirtualVoxelPos(sdfBlock));
-	}
-
-	__device__ 
-	int3 worldToSDFBlock(const float3& worldPos) const	{
-		return virtualVoxelPosToSDFBlock(worldToVirtualVoxelPos(worldPos));
-	}
-
-	__device__
-	bool isInBoundingBox(const HashParams &hashParams, const int3& sdfBlock) {
-		// NOTE (Nick): Changed, just assume all voxels are potentially in frustrum
-		//float3 posWorld = virtualVoxelPosToWorld(SDFBlockToVirtualVoxelPos(sdfBlock)) + hashParams.m_virtualVoxelSize * 0.5f * (SDF_BLOCK_SIZE - 1.0f);
-		//return camera.isInCameraFrustumApprox(hashParams.m_rigidTransformInverse, posWorld);
-		return !(hashParams.m_flags & ftl::voxhash::kFlagClipping) || sdfBlock.x > hashParams.m_minBounds.x && sdfBlock.x < hashParams.m_maxBounds.x &&
-			sdfBlock.y > hashParams.m_minBounds.y && sdfBlock.y < hashParams.m_maxBounds.y &&
-			sdfBlock.z > hashParams.m_minBounds.z && sdfBlock.z < hashParams.m_maxBounds.z;
-	}
-
-	//! computes the (local) virtual voxel pos of an index; idx in [0;511]
-	__device__ 
-	uint3 delinearizeVoxelIndex(uint idx) const	{
-		uint x = idx % SDF_BLOCK_SIZE;
-		uint y = (idx % (SDF_BLOCK_SIZE * SDF_BLOCK_SIZE)) / SDF_BLOCK_SIZE;
-		uint z = idx / (SDF_BLOCK_SIZE * SDF_BLOCK_SIZE);	
-		return make_uint3(x,y,z);
-	}
-
-	//! computes the linearized index of a local virtual voxel pos; pos in [0;7]^3
-	__device__ 
-	uint linearizeVoxelPos(const int3& virtualVoxelPos)	const {
-		return  
-			virtualVoxelPos.z * SDF_BLOCK_SIZE * SDF_BLOCK_SIZE +
-			virtualVoxelPos.y * SDF_BLOCK_SIZE +
-			virtualVoxelPos.x;
-	}
-
-	__device__ 
-	int virtualVoxelPosToLocalSDFBlockIndex(const int3& virtualVoxelPos) const	{
-		int3 localVoxelPos = make_int3(
-			virtualVoxelPos.x % SDF_BLOCK_SIZE,
-			virtualVoxelPos.y % SDF_BLOCK_SIZE,
-			virtualVoxelPos.z % SDF_BLOCK_SIZE);
-
-		if (localVoxelPos.x < 0) localVoxelPos.x += SDF_BLOCK_SIZE;
-		if (localVoxelPos.y < 0) localVoxelPos.y += SDF_BLOCK_SIZE;
-		if (localVoxelPos.z < 0) localVoxelPos.z += SDF_BLOCK_SIZE;
-
-		return linearizeVoxelPos(localVoxelPos);
-	}
-
-	__device__ 
-	int worldToLocalSDFBlockIndex(const float3& world) const	{
-		int3 virtualVoxelPos = worldToVirtualVoxelPos(world);
-		return virtualVoxelPosToLocalSDFBlockIndex(virtualVoxelPos);
-	}
-
-
-		//! returns the hash entry for a given worldPos; if there was no hash entry the returned entry will have a ptr with FREE_ENTRY set
-	__device__ 
-	int getHashEntry(const float3& worldPos) const	{
-		//int3 blockID = worldToSDFVirtualVoxelPos(worldPos)/SDF_BLOCK_SIZE;	//position of sdf block
-		int3 blockID = worldToSDFBlock(worldPos);
-		return getHashEntryForSDFBlockPos(blockID);
-	}
-
-
-	__device__ 
-		void deleteHashEntry(uint id) {
-			deleteHashEntry(d_hash[id]);
-	}
-
-	__device__ 
-		void deleteHashEntry(HashEntry& hashEntry) {
-			hashEntry.head.pos = 0;
-			hashEntry.head.offset = FREE_ENTRY;
-			for (int i=0; i<16; ++i) hashEntry.voxels[i] = 0;
-	}
-
-	__device__ 
-		bool voxelExists(const float3& worldPos) const	{
-			int hashEntry = getHashEntry(worldPos);
-			return (hashEntry != -1);
-	}
-
-	__device__  
-	void deleteVoxel(Voxel& v) const {
-		v.color = make_uchar3(0,0,0);
-		v.weight = 0;
-		v.sdf = 0.0f;
-	}
-
-
-	__device__ 
-	bool getVoxel(const float3& worldPos) const	{
-		int hashEntry = getHashEntry(worldPos);
-		if (hashEntry == -1) {
-			return false;		
-		} else {
-			int3 virtualVoxelPos = worldToVirtualVoxelPos(worldPos);
-			int ix = virtualVoxelPosToLocalSDFBlockIndex(virtualVoxelPos);
-			return d_hash[hashEntry].voxels[ix/32] & (0x1 << (ix % 32));
-		}
-	}
-
-	__device__ 
-	bool getVoxel(const int3& virtualVoxelPos) const	{
-		int hashEntry = getHashEntryForSDFBlockPos(virtualVoxelPosToSDFBlock(virtualVoxelPos));
-		if (hashEntry == -1) {
-			return false;		
-		} else {
-			int ix = virtualVoxelPosToLocalSDFBlockIndex(virtualVoxelPos);
-			return d_hash[hashEntry].voxels[ix >> 5] & (0x1 << (ix & 0x1F));
-		}
-	}
-	
-	/*__device__ 
-	void setVoxel(const int3& virtualVoxelPos, bool voxelInput) const {
-		int hashEntry = getHashEntryForSDFBlockPos(virtualVoxelPosToSDFBlock(virtualVoxelPos));
-		if (hashEntry == -1) {
-			d_SDFBlocks[hashEntry.ptr + virtualVoxelPosToLocalSDFBlockIndex(virtualVoxelPos)] = voxelInput;
-			int ix = virtualVoxelPosToLocalSDFBlockIndex(virtualVoxelPos);
-			d_hash[hashEntry].voxels[ix >> 5] |= (0x1 << (ix & 0x1F));
-		}
-	}*/
-
-	//! returns the hash entry for a given sdf block id; if there was no hash entry the returned entry will have a ptr with FREE_ENTRY set
-	__device__ 
-	int getHashEntryForSDFBlockPos(const int3& sdfBlock) const;
-
-	//for histogram (no collision traversal)
-	__device__ 
-	unsigned int getNumHashEntriesPerBucket(unsigned int bucketID);
-
-	//for histogram (collisions traversal only)
-	__device__ 
-	unsigned int getNumHashLinkedList(unsigned int bucketID);
-
-
-	//pos in SDF block coordinates
-	__device__
-	void allocBlock(const int3& pos);
-
-	//!inserts a hash entry without allocating any memory: used by streaming: TODO MATTHIAS check the atomics in this function
-	__device__
-	bool insertHashEntry(HashEntry entry);
-
-	//! deletes a hash entry position for a given sdfBlock index (returns true uppon successful deletion; otherwise returns false)
-	__device__
-	bool deleteHashEntryElement(const int3& sdfBlock);
-
-#endif	//CUDACC
-
-	int*		d_hashDecision;				//
-	int*		d_hashDecisionPrefix;		//
-	HashEntry*	d_hash;						//hash that stores pointers to sdf blocks
-	HashEntry**	d_hashCompactified;			//same as before except that only valid pointers are there
-	int*		d_hashCompactifiedCounter;	//atomic counter to add compactified entries atomically 
-	int*		d_hashBucketMutex;			//binary flag per hash bucket; used for allocation to atomically lock a bucket
-
-	bool		m_bIsOnGPU;					//the class be be used on both cpu and gpu
-};
-
-}  // namespace voxhash
-}  // namespace ftl
diff --git a/applications/reconstruct/include/ftl/voxel_hash_params.hpp b/applications/reconstruct/include/ftl/voxel_hash_params.hpp
index 5e13ec21c259eec02d9a2f49b25ad3a98c06d08c..ac5fcaa726febaff0b56426b6d481d48bae3e421 100644
--- a/applications/reconstruct/include/ftl/voxel_hash_params.hpp
+++ b/applications/reconstruct/include/ftl/voxel_hash_params.hpp
@@ -4,8 +4,8 @@
 
 //#include <cutil_inline.h>
 //#include <cutil_math.h>
-#include <vector_types.h>
-#include <cuda_runtime.h>
+//#include <vector_types.h>
+//#include <cuda_runtime.h>
 
 #include <ftl/cuda_matrix_util.hpp>
 
diff --git a/applications/reconstruct/include/ftl/voxel_scene.hpp b/applications/reconstruct/include/ftl/voxel_scene.hpp
index b1ee6f3bc1388a0c57398fc63971b7ccb163235a..4ea2d5eb3d98b0a3baac018f2c9ace9b5ad7a6f0 100644
--- a/applications/reconstruct/include/ftl/voxel_scene.hpp
+++ b/applications/reconstruct/include/ftl/voxel_scene.hpp
@@ -2,7 +2,7 @@
 
 #pragma once
 
-#include <cuda_runtime.h>
+//#include <cuda_runtime.h>
 
 #include <ftl/cuda_common.hpp>
 #include <ftl/rgbd/source.hpp>
diff --git a/applications/reconstruct/src/compactors.cu b/applications/reconstruct/src/compactors.cu
deleted file mode 100644
index b7cdd5028f0f5ec78de47d8bf6f9099c5448a494..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/compactors.cu
+++ /dev/null
@@ -1,236 +0,0 @@
-#include "compactors.hpp"
-
-using ftl::voxhash::HashData;
-using ftl::voxhash::HashParams;
-using ftl::voxhash::Voxel;
-using ftl::voxhash::HashEntry;
-using ftl::voxhash::FREE_ENTRY;
-
-#define COMPACTIFY_HASH_THREADS_PER_BLOCK 256
-//#define COMPACTIFY_HASH_SIMPLE
-
-
-/*__global__ void fillDecisionArrayKernel(HashData hashData, DepthCameraData depthCameraData) 
-{
-	const HashParams& hashParams = c_hashParams;
-	const unsigned int idx = blockIdx.x*blockDim.x + threadIdx.x;
-
-	if (idx < hashParams.m_hashNumBuckets * HASH_BUCKET_SIZE) {
-		hashData.d_hashDecision[idx] = 0;
-		if (hashData.d_hash[idx].ptr != FREE_ENTRY) {
-			if (hashData.isSDFBlockInCameraFrustumApprox(hashData.d_hash[idx].pos)) {
-				hashData.d_hashDecision[idx] = 1;	//yes
-			}
-		}
-	}
-}*/
-
-/*extern "C" void fillDecisionArrayCUDA(HashData& hashData, const HashParams& hashParams, const DepthCameraData& depthCameraData)
-{
-	const dim3 gridSize((HASH_BUCKET_SIZE * hashParams.m_hashNumBuckets + (T_PER_BLOCK*T_PER_BLOCK) - 1)/(T_PER_BLOCK*T_PER_BLOCK), 1);
-	const dim3 blockSize((T_PER_BLOCK*T_PER_BLOCK), 1);
-
-	fillDecisionArrayKernel<<<gridSize, blockSize>>>(hashData, depthCameraData);
-
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-	//cutilCheckMsg(__FUNCTION__);
-#endif
-
-}*/
-
-/*__global__ void compactifyHashKernel(HashData hashData) 
-{
-	const HashParams& hashParams = c_hashParams;
-	const unsigned int idx = blockIdx.x*blockDim.x + threadIdx.x;
-	if (idx < hashParams.m_hashNumBuckets * HASH_BUCKET_SIZE) {
-		if (hashData.d_hashDecision[idx] == 1) {
-			hashData.d_hashCompactified[hashData.d_hashDecisionPrefix[idx]-1] = hashData.d_hash[idx];
-		}
-	}
-}*/
-
-/*extern "C" void compactifyHashCUDA(HashData& hashData, const HashParams& hashParams) 
-{
-	const dim3 gridSize((HASH_BUCKET_SIZE * hashParams.m_hashNumBuckets + (T_PER_BLOCK*T_PER_BLOCK) - 1)/(T_PER_BLOCK*T_PER_BLOCK), 1);
-	const dim3 blockSize((T_PER_BLOCK*T_PER_BLOCK), 1);
-
-	compactifyHashKernel<<<gridSize, blockSize>>>(hashData);
-
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-	//cutilCheckMsg(__FUNCTION__);
-#endif
-}*/
-
-/*__global__ void compactifyVisibleKernel(HashData hashData, HashParams hashParams, DepthCameraParams camera)
-{
-	//const HashParams& hashParams = c_hashParams;
-	const unsigned int idx = blockIdx.x*blockDim.x + threadIdx.x;
-#ifdef COMPACTIFY_HASH_SIMPLE
-	if (idx < hashParams.m_hashNumBuckets) {
-		if (hashData.d_hash[idx].ptr != FREE_ENTRY) {
-			if (hashData.isSDFBlockInCameraFrustumApprox(hashParams, camera, hashData.d_hash[idx].pos))
-			{
-				int addr = atomicAdd(hashData.d_hashCompactifiedCounter, 1);
-				hashData.d_hashCompactified[addr] = hashData.d_hash[idx];
-			}
-		}
-	}
-#else	
-	__shared__ int localCounter;
-	if (threadIdx.x == 0) localCounter = 0;
-	__syncthreads();
-
-	int addrLocal = -1;
-	if (idx < hashParams.m_hashNumBuckets) {
-		if (hashData.d_hash[idx].ptr != FREE_ENTRY) {
-			if (hashData.isSDFBlockInCameraFrustumApprox(hashParams, camera, hashData.d_hash[idx].pos))
-			{
-				addrLocal = atomicAdd(&localCounter, 1);
-			}
-		}
-	}
-
-	__syncthreads();
-
-	__shared__ int addrGlobal;
-	if (threadIdx.x == 0 && localCounter > 0) {
-		addrGlobal = atomicAdd(hashData.d_hashCompactifiedCounter, localCounter);
-	}
-	__syncthreads();
-
-	if (addrLocal != -1) {
-		const unsigned int addr = addrGlobal + addrLocal;
-		hashData.d_hashCompactified[addr] = hashData.d_hash[idx];
-	}
-#endif
-}
-
-void ftl::cuda::compactifyVisible(HashData& hashData, const HashParams& hashParams, const DepthCameraParams &camera, cudaStream_t stream) {
-	const unsigned int threadsPerBlock = COMPACTIFY_HASH_THREADS_PER_BLOCK;
-	const dim3 gridSize((hashParams.m_hashNumBuckets + threadsPerBlock - 1) / threadsPerBlock, 1);
-	const dim3 blockSize(threadsPerBlock, 1);
-
-	cudaSafeCall(cudaMemsetAsync(hashData.d_hashCompactifiedCounter, 0, sizeof(int),stream));
-	compactifyVisibleKernel << <gridSize, blockSize, 0, stream >> >(hashData, hashParams, camera);
-	//unsigned int res = 0;
-	//cudaSafeCall(cudaMemcpyAsync(&res, hashData.d_hashCompactifiedCounter, sizeof(unsigned int), cudaMemcpyDeviceToHost, stream));
-
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-	//cutilCheckMsg(__FUNCTION__);
-#endif
-	//return res;
-}*/
-
-__global__ void compactifyAllocatedKernel(HashData hashData)
-{
-	const HashParams& hashParams = c_hashParams;
-	const unsigned int idx = blockIdx.x*blockDim.x + threadIdx.x;
-#ifdef COMPACTIFY_HASH_SIMPLE
-	if (idx < hashParams.m_hashNumBuckets) {
-		if (hashData.d_hash[idx].head.offset != FREE_ENTRY) {
-			int addr = atomicAdd(hashData.d_hashCompactifiedCounter, 1);
-			hashData.d_hashCompactified[addr] = &hashData.d_hash[idx];
-		}
-	}
-#else	
-	__shared__ int localCounter;
-	if (threadIdx.x == 0) localCounter = 0;
-	__syncthreads();
-
-	int addrLocal = -1;
-	if (idx < hashParams.m_hashNumBuckets) {
-		if (hashData.d_hash[idx].head.offset != FREE_ENTRY) {
-			addrLocal = atomicAdd(&localCounter, 1);
-		}
-	}
-
-	__syncthreads();
-
-	__shared__ int addrGlobal;
-	if (threadIdx.x == 0 && localCounter > 0) {
-		addrGlobal = atomicAdd(hashData.d_hashCompactifiedCounter, localCounter);
-	}
-	__syncthreads();
-
-	if (addrLocal != -1) {
-		const unsigned int addr = addrGlobal + addrLocal;
-		hashData.d_hashCompactified[addr] = &hashData.d_hash[idx];
-	}
-#endif
-}
-
-void ftl::cuda::compactifyAllocated(HashData& hashData, const HashParams& hashParams, cudaStream_t stream) {
-	const unsigned int threadsPerBlock = COMPACTIFY_HASH_THREADS_PER_BLOCK;
-	const dim3 gridSize((hashParams.m_hashNumBuckets + threadsPerBlock - 1) / threadsPerBlock, 1);
-	const dim3 blockSize(threadsPerBlock, 1);
-
-	cudaSafeCall(cudaMemsetAsync(hashData.d_hashCompactifiedCounter, 0, sizeof(int), stream));
-	compactifyAllocatedKernel << <gridSize, blockSize, 0, stream >> >(hashData);
-	//unsigned int res = 0;
-	//cudaSafeCall(cudaMemcpyAsync(&res, hashData.d_hashCompactifiedCounter, sizeof(unsigned int), cudaMemcpyDeviceToHost, stream));
-
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-	//cutilCheckMsg(__FUNCTION__);
-#endif
-	//return res;
-}
-
-
-__global__ void compactifyOccupiedKernel(HashData hashData)
-{
-	const HashParams& hashParams = c_hashParams;
-	const unsigned int idx = blockIdx.x*blockDim.x + threadIdx.x;
-#ifdef COMPACTIFY_HASH_SIMPLE
-	if (idx < hashParams.m_hashNumBuckets) {
-		if (hashData.d_hash[idx].head.offset != FREE_ENTRY && hashData.d_hash[idx].head.flags & ftl::voxhash::kFlagSurface) {
-			int addr = atomicAdd(hashData.d_hashCompactifiedCounter, 1);
-			hashData.d_hashCompactified[addr] = &hashData.d_hash[idx];
-		}
-	}
-#else	
-	__shared__ int localCounter;
-	if (threadIdx.x == 0) localCounter = 0;
-	__syncthreads();
-
-	int addrLocal = -1;
-	if (idx < hashParams.m_hashNumBuckets) {
-		if (hashData.d_hash[idx].head.offset != FREE_ENTRY && (hashData.d_hash[idx].head.flags & ftl::voxhash::kFlagSurface)) {  // TODO:(Nick) Check voxels for all 0 or all 1
-			addrLocal = atomicAdd(&localCounter, 1);
-		}
-	}
-
-	__syncthreads();
-
-	__shared__ int addrGlobal;
-	if (threadIdx.x == 0 && localCounter > 0) {
-		addrGlobal = atomicAdd(hashData.d_hashCompactifiedCounter, localCounter);
-	}
-	__syncthreads();
-
-	if (addrLocal != -1) {
-		const unsigned int addr = addrGlobal + addrLocal;
-		hashData.d_hashCompactified[addr] = &hashData.d_hash[idx];
-	}
-#endif
-}
-
-void ftl::cuda::compactifyOccupied(HashData& hashData, const HashParams& hashParams, cudaStream_t stream) {
-	const unsigned int threadsPerBlock = COMPACTIFY_HASH_THREADS_PER_BLOCK;
-	const dim3 gridSize((hashParams.m_hashNumBuckets + threadsPerBlock - 1) / threadsPerBlock, 1);
-	const dim3 blockSize(threadsPerBlock, 1);
-
-	cudaSafeCall(cudaMemsetAsync(hashData.d_hashCompactifiedCounter, 0, sizeof(int), stream));
-	compactifyAllocatedKernel << <gridSize, blockSize, 0, stream >> >(hashData);
-	//unsigned int res = 0;
-	//cudaSafeCall(cudaMemcpyAsync(&res, hashData.d_hashCompactifiedCounter, sizeof(unsigned int), cudaMemcpyDeviceToHost, stream));
-
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-	//cutilCheckMsg(__FUNCTION__);
-#endif
-	//return res;
-}
diff --git a/applications/reconstruct/src/compactors.hpp b/applications/reconstruct/src/compactors.hpp
deleted file mode 100644
index 6c61985eea8448a078b8abe3e821992519c9425f..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/compactors.hpp
+++ /dev/null
@@ -1,21 +0,0 @@
-#ifndef _FTL_RECONSTRUCT_COMPACTORS_HPP_
-#define _FTL_RECONSTRUCT_COMPACTORS_HPP_
-
-#include <ftl/voxel_hash.hpp>
-
-namespace ftl {
-namespace cuda {
-
-// Compact visible
-//void compactifyVisible(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, const DepthCameraParams &camera, cudaStream_t);
-
-// Compact allocated
-void compactifyAllocated(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, cudaStream_t);
-
-// Compact visible surfaces
-void compactifyOccupied(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, cudaStream_t stream);
-
-}
-}
-
-#endif  // _FTL_RECONSTRUCT_COMPACTORS_HPP_
diff --git a/applications/reconstruct/src/garbage.cu b/applications/reconstruct/src/garbage.cu
deleted file mode 100644
index b685e9e6b7d94434ff425eff268699a715261522..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/garbage.cu
+++ /dev/null
@@ -1,135 +0,0 @@
-#include <ftl/voxel_hash.hpp>
-#include "garbage.hpp"
-
-using namespace ftl::voxhash;
-
-#define T_PER_BLOCK 8
-#define NUM_CUDA_BLOCKS	10000
-
-/*__global__ void starveVoxelsKernel(HashData hashData) {
-	int ptr;
-
-	// Stride over all allocated blocks
-	for (int bi=blockIdx.x; bi<*hashData.d_hashCompactifiedCounter; bi+=NUM_CUDA_BLOCKS) {
-
-	ptr = hashData.d_hashCompactified[bi].ptr;
-	int weight = hashData.d_SDFBlocks[ptr + threadIdx.x].weight;
-	weight = max(0, weight-2);	
-	hashData.d_SDFBlocks[ptr + threadIdx.x].weight = weight;  //CHECK Remove to totally clear previous frame (Nick)
-
-	}
-}
-
-void ftl::cuda::starveVoxels(HashData& hashData, const HashParams& hashParams, cudaStream_t stream) {
-	const unsigned int threadsPerBlock = SDF_BLOCK_SIZE*SDF_BLOCK_SIZE*SDF_BLOCK_SIZE;
-	const dim3 gridSize(NUM_CUDA_BLOCKS, 1);
-	const dim3 blockSize(threadsPerBlock, 1);
-
-	//if (hashParams.m_numOccupiedBlocks > 0) {
-		starveVoxelsKernel << <gridSize, blockSize, 0, stream >> >(hashData);
-	//}
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-	//cutilCheckMsg(__FUNCTION__);
-#endif
-}*/
-
-#define ENTRIES_PER_BLOCK 4
-
-__global__ void clearVoxelsKernel(HashData hashData) {
-	const int lane = threadIdx.x % 16;
-	const int halfWarp = threadIdx.x / 16;
-
-	// Stride over all allocated blocks
-	for (int bi=blockIdx.x+halfWarp; bi<*hashData.d_hashCompactifiedCounter; bi+=NUM_CUDA_BLOCKS*ENTRIES_PER_BLOCK) {
-
-	HashEntry *entry = hashData.d_hashCompactified[bi];	
-	//hashData.d_SDFBlocks[entry.ptr + threadIdx.x].weight = 0;
-	entry->voxels[lane] = 0;
-
-	}
-}
-
-void ftl::cuda::clearVoxels(HashData& hashData, const HashParams& hashParams) {
-	const unsigned int threadsPerBlock = 16 * ENTRIES_PER_BLOCK;
-	const dim3 gridSize(NUM_CUDA_BLOCKS, 1);
-	const dim3 blockSize(threadsPerBlock, 1);
-
-	clearVoxelsKernel << <gridSize, blockSize >> >(hashData);
-}
-
-
-__global__ void garbageCollectIdentifyKernel(HashData hashData) {
-	const int lane = threadIdx.x % 16;
-	const int halfWarp = threadIdx.x / 16;
-
-	// Stride over all allocated blocks
-	for (int bi=blockIdx.x+halfWarp; bi<*hashData.d_hashCompactifiedCounter; bi+=NUM_CUDA_BLOCKS * ENTRIES_PER_BLOCK) {
-
-	const HashEntry *entry = hashData.d_hashCompactified[bi];
-
-	const uint v = entry->voxels[lane];
-	const uint mask = (halfWarp & 0x1) ? 0xFFFF0000 : 0x0000FFFF;
-	uint ballot_result = __ballot_sync(mask, v == 0 || v == 0xFFFFFFFF);
-
-	if (lane == 0) hashData.d_hashDecision[bi] = (ballot_result == mask) ? 1 : 0;
-
-	}
-}
- 
-void ftl::cuda::garbageCollectIdentify(HashData& hashData, const HashParams& hashParams, cudaStream_t stream) {
-	
-	const unsigned int threadsPerBlock = SDF_BLOCK_SIZE * SDF_BLOCK_SIZE * SDF_BLOCK_SIZE / 2;
-	const dim3 gridSize(NUM_CUDA_BLOCKS, 1);
-	const dim3 blockSize(threadsPerBlock, 1);
-
-	//if (hashParams.m_numOccupiedBlocks > 0) {
-		garbageCollectIdentifyKernel << <gridSize, blockSize, 0, stream >> >(hashData);
-	//}
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-	//cutilCheckMsg(__FUNCTION__);
-#endif
-}
-
-
-__global__ void garbageCollectFreeKernel(HashData hashData) {
-
-	// Stride over all allocated blocks
-	for (int bi=blockIdx.x*blockDim.x + threadIdx.x; bi<*hashData.d_hashCompactifiedCounter; bi+=NUM_CUDA_BLOCKS*blockDim.x) {
-
-	HashEntry *entry = hashData.d_hashCompactified[bi];
-
-	if ((entry->head.flags & ftl::voxhash::kFlagSurface) == 0) {	//decision to delete the hash entry
-
-		
-		//if (entry->head.offset == FREE_ENTRY) return; //should never happen since we did compactify before
-
-		int3 posI3 = make_int3(entry->head.posXYZ.x, entry->head.posXYZ.y, entry->head.posXYZ.z);
-
-		if (hashData.deleteHashEntryElement(posI3)) {	//delete hash entry from hash (and performs heap append)
-			//#pragma unroll
-			//for (uint i = 0; i < 16; i++) {	//clear sdf block: CHECK TODO another kernel?
-			//	entry->voxels[i] = 0;
-			//}
-		}
-	}
-
-	}
-}
-
-
-void ftl::cuda::garbageCollectFree(HashData& hashData, const HashParams& hashParams, cudaStream_t stream) {
-	
-	const unsigned int threadsPerBlock = T_PER_BLOCK*T_PER_BLOCK;
-	const dim3 gridSize(NUM_CUDA_BLOCKS, 1);  // (hashParams.m_numOccupiedBlocks + threadsPerBlock - 1) / threadsPerBlock
-	const dim3 blockSize(threadsPerBlock, 1);
-	
-	//if (hashParams.m_numOccupiedBlocks > 0) {
-		garbageCollectFreeKernel << <gridSize, blockSize, 0, stream >> >(hashData);
-	//}
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-	//cutilCheckMsg(__FUNCTION__);
-#endif
-}
diff --git a/applications/reconstruct/src/garbage.hpp b/applications/reconstruct/src/garbage.hpp
deleted file mode 100644
index 5d1d7574d252b40da18008da39f1bf89a7d667fb..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/garbage.hpp
+++ /dev/null
@@ -1,15 +0,0 @@
-#ifndef _FTL_RECONSTRUCTION_GARBAGE_HPP_
-#define _FTL_RECONSTRUCTION_GARBAGE_HPP_
-
-namespace ftl {
-namespace cuda {
-
-void clearVoxels(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams);
-void starveVoxels(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, cudaStream_t stream);
-void garbageCollectIdentify(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, cudaStream_t stream);
-void garbageCollectFree(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, cudaStream_t stream);
-
-}
-}
-
-#endif  // _FTL_RECONSTRUCTION_GARBAGE_HPP_
diff --git a/applications/reconstruct/src/ilw.cpp b/applications/reconstruct/src/ilw.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..86a4cca5e4f82047ed6591a137b694788da879eb
--- /dev/null
+++ b/applications/reconstruct/src/ilw.cpp
@@ -0,0 +1,120 @@
+#include "ilw.hpp"
+#include <ftl/utility/matrix_conversion.hpp>
+#include <ftl/rgbd/source.hpp>
+#include <ftl/cuda/points.hpp>
+#include <loguru.hpp>
+
+#include "ilw_cuda.hpp"
+
+using ftl::ILW;
+using ftl::detail::ILWData;
+using ftl::rgbd::Channel;
+using ftl::rgbd::Channels;
+using ftl::rgbd::Format;
+using cv::cuda::GpuMat;
+
+ILW::ILW(nlohmann::json &config) : ftl::Configurable(config) {
+
+}
+
+ILW::~ILW() {
+
+}
+
+bool ILW::process(ftl::rgbd::FrameSet &fs, cudaStream_t stream) {
+    _phase0(fs, stream);
+
+    //for (int i=0; i<2; ++i) {
+        _phase1(fs, stream);
+        //for (int j=0; j<3; ++j) {
+        //    _phase2(fs);
+        //}
+
+		// TODO: Break if no time left
+    //}
+
+    return true;
+}
+
+bool ILW::_phase0(ftl::rgbd::FrameSet &fs, cudaStream_t stream) {
+    // Make points channel...
+    for (size_t i=0; i<fs.frames.size(); ++i) {
+		auto &f = fs.frames[i];
+		auto *s = fs.sources[i];
+
+		if (f.empty(Channel::Depth + Channel::Colour)) {
+			LOG(ERROR) << "Missing required channel";
+			continue;
+		}
+			
+        auto &t = f.createTexture<float4>(Channel::Points, Format<float4>(f.get<GpuMat>(Channel::Colour).size()));
+        auto pose = MatrixConversion::toCUDA(s->getPose().cast<float>()); //.inverse());
+        ftl::cuda::point_cloud(t, f.createTexture<float>(Channel::Depth), s->parameters(), pose, stream);
+
+        // TODO: Create energy vector texture and clear it
+        // Create energy and clear it
+
+        // Convert colour from BGR to BGRA if needed
+		if (f.get<GpuMat>(Channel::Colour).type() == CV_8UC3) {
+			// Convert to 4 channel colour
+			auto &col = f.get<GpuMat>(Channel::Colour);
+			GpuMat tmp(col.size(), CV_8UC4);
+			cv::cuda::swap(col, tmp);
+			cv::cuda::cvtColor(tmp,col, cv::COLOR_BGR2BGRA);
+		}
+
+        f.createTexture<float4>(Channel::EnergyVector, Format<float4>(f.get<GpuMat>(Channel::Colour).size()));
+        f.createTexture<float>(Channel::Energy, Format<float>(f.get<GpuMat>(Channel::Colour).size()));
+        f.createTexture<uchar4>(Channel::Colour);
+    }
+
+    return true;
+}
+
+bool ILW::_phase1(ftl::rgbd::FrameSet &fs, cudaStream_t stream) {
+    // Run correspondence kernel to create an energy vector
+
+	// For each camera combination
+    for (size_t i=0; i<fs.frames.size(); ++i) {
+        for (size_t j=0; j<fs.frames.size(); ++j) {
+            if (i == j) continue;
+
+            LOG(INFO) << "Running phase1";
+
+            auto &f1 = fs.frames[i];
+            auto &f2 = fs.frames[j];
+            //auto s1 = fs.frames[i];
+            auto s2 = fs.sources[j];
+
+            auto pose = MatrixConversion::toCUDA(s2->getPose().cast<float>().inverse());
+
+            try {
+            //Calculate energy vector to best correspondence
+            ftl::cuda::correspondence_energy_vector(
+                f1.getTexture<float4>(Channel::Points),
+                f2.getTexture<float4>(Channel::Points),
+                f1.getTexture<uchar4>(Channel::Colour),
+                f2.getTexture<uchar4>(Channel::Colour),
+                // TODO: Add normals and other things...
+                f1.getTexture<float4>(Channel::EnergyVector),
+                f1.getTexture<float>(Channel::Energy),
+                pose,
+                s2->parameters(),
+                stream
+            );
+            } catch (ftl::exception &e) {
+                LOG(ERROR) << "Exception in correspondence: " << e.what();
+            }
+
+            LOG(INFO) << "Correspondences done... " << i;
+        }
+    }
+
+    return true;
+}
+
+bool ILW::_phase2(ftl::rgbd::FrameSet &fs) {
+    // Run energies and motion kernel
+
+    return true;
+}
diff --git a/applications/reconstruct/src/ilw.cu b/applications/reconstruct/src/ilw.cu
new file mode 100644
index 0000000000000000000000000000000000000000..90133a3a57800ee87a91fd50902deea5f701258a
--- /dev/null
+++ b/applications/reconstruct/src/ilw.cu
@@ -0,0 +1,86 @@
+#include "ilw_cuda.hpp"
+
+using ftl::cuda::TextureObject;
+using ftl::rgbd::Camera;
+
+#define WARP_SIZE 32
+#define T_PER_BLOCK 8
+#define FULL_MASK 0xffffffff
+
+__device__ inline float warpMax(float e) {
+	for (int i = WARP_SIZE/2; i > 0; i /= 2) {
+		const float other = __shfl_xor_sync(FULL_MASK, e, i, WARP_SIZE);
+		e = max(e, other);
+	}
+	return e;
+}
+
+__global__ void correspondence_energy_vector_kernel(
+        TextureObject<float4> p1,
+        TextureObject<float4> p2,
+        TextureObject<uchar4> c1,
+        TextureObject<uchar4> c2,
+        TextureObject<float4> vout,
+        TextureObject<float> eout,
+        float4x4 pose2,  // Inverse
+        Camera cam2) {
+
+    // Each warp picks point in p1
+    const int tid = (threadIdx.x + threadIdx.y * blockDim.x);
+	const int x = (blockIdx.x*blockDim.x + threadIdx.x) / WARP_SIZE;
+    const int y = blockIdx.y*blockDim.y + threadIdx.y;
+    
+    const float3 world1 = make_float3(p1.tex2D(x, y));
+    const float3 camPos2 = pose2 * world1;
+    const uint2 screen2 = cam2.camToScreen<uint2>(camPos2);
+
+    const int upsample = 8;
+
+    // Project to p2 using cam2
+    // Each thread takes a possible correspondence and calculates a weighting
+    const int lane = tid % WARP_SIZE;
+	for (int i=lane; i<upsample*upsample; i+=WARP_SIZE) {
+		const float u = (i % upsample) - (upsample / 2);
+        const float v = (i / upsample) - (upsample / 2);
+        
+        const float3 world2 = make_float3(p2.tex2D(screen2.x+u, screen2.y+v));
+
+        // Determine degree of correspondence
+        const float confidence = 1.0f / length(world1 - world2);
+
+        printf("conf %f\n", confidence);
+        const float maxconf = warpMax(confidence);
+
+        // This thread has best confidence value
+        if (maxconf == confidence) {
+            vout(x,y) = vout.tex2D(x, y) + make_float4(
+                (world1.x - world2.x) * maxconf,
+                (world1.y - world2.y) * maxconf,
+                (world1.z - world2.z) * maxconf,
+                maxconf);
+            eout(x,y) = eout.tex2D(x,y) + length(world1 - world2)*maxconf;
+        }
+    }
+}
+
+void ftl::cuda::correspondence_energy_vector(
+        TextureObject<float4> &p1,
+        TextureObject<float4> &p2,
+        TextureObject<uchar4> &c1,
+        TextureObject<uchar4> &c2,
+        TextureObject<float4> &vout,
+        TextureObject<float> &eout,
+        float4x4 &pose2,
+        const Camera &cam2,
+        cudaStream_t stream) {
+
+    const dim3 gridSize((p1.width() + 2 - 1)/2, (p1.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
+    const dim3 blockSize(2*WARP_SIZE, T_PER_BLOCK);
+
+    printf("COR SIZE %d,%d\n", p1.width(), p1.height());
+
+    correspondence_energy_vector_kernel<<<gridSize, blockSize, 0, stream>>>(
+        p1, p2, c1, c2, vout, eout, pose2, cam2
+    );
+    cudaSafeCall( cudaGetLastError() );
+}
diff --git a/applications/reconstruct/src/ilw.hpp b/applications/reconstruct/src/ilw.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0be45d015e976b540263a2c16cc5605376092a43
--- /dev/null
+++ b/applications/reconstruct/src/ilw.hpp
@@ -0,0 +1,66 @@
+#ifndef _FTL_RECONSTRUCT_ILW_HPP_
+#define _FTL_RECONSTRUCT_ILW_HPP_
+
+#include <ftl/cuda_common.hpp>
+#include <ftl/rgbd/frameset.hpp>
+#include <ftl/configurable.hpp>
+#include <vector>
+
+namespace ftl {
+
+namespace detail {
+struct ILWData{
+    // x,y,z + confidence
+    ftl::cuda::TextureObject<float4> correspondence;
+
+    ftl::cuda::TextureObject<float4> points;
+
+	// Residual potential energy
+	ftl::cuda::TextureObject<float> residual;
+
+	// Flow magnitude
+	ftl::cuda::TextureObject<float> flow;
+};
+}
+
+/**
+ * For a set of sources, perform Iterative Lattice Warping to correct the
+ * location of points between the cameras. The algorithm finds possible
+ * correspondences and warps the original pixel lattice of points in each
+ * camera towards the correspondences, iterating the process as many times as
+ * possible. The result is that both local and global adjustment is made to the
+ * point clouds to improve micro alignment that may have been incorrect due to
+ * either inaccurate camera pose estimation or noise/errors in the depth maps.
+ */
+class ILW : public ftl::Configurable {
+    public:
+    explicit ILW(nlohmann::json &config);
+    ~ILW();
+
+    /**
+     * Take a frameset and perform the iterative lattice warping.
+     */
+    bool process(ftl::rgbd::FrameSet &fs, cudaStream_t stream=0);
+
+    private:
+    /*
+     * Initialise data.
+     */
+    bool _phase0(ftl::rgbd::FrameSet &fs, cudaStream_t stream);
+
+    /*
+     * Find possible correspondences and a confidence value.
+     */
+    bool _phase1(ftl::rgbd::FrameSet &fs, cudaStream_t stream);
+
+    /*
+     * Calculate energies and move the points.
+     */
+    bool _phase2(ftl::rgbd::FrameSet &fs);
+
+    std::vector<detail::ILWData> data_;
+};
+
+}
+
+#endif  // _FTL_RECONSTRUCT_ILW_HPP_
diff --git a/applications/reconstruct/src/ilw_cuda.hpp b/applications/reconstruct/src/ilw_cuda.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a01af75149409fe033ba39ffb0170489ee926be9
--- /dev/null
+++ b/applications/reconstruct/src/ilw_cuda.hpp
@@ -0,0 +1,26 @@
+#ifndef _FTL_ILW_CUDA_HPP_
+#define _FTL_ILW_CUDA_HPP_
+
+#include <ftl/cuda_common.hpp>
+#include <ftl/rgbd/camera.hpp>
+#include <ftl/cuda_matrix_util.hpp>
+
+namespace ftl {
+namespace cuda {
+
+void correspondence_energy_vector(
+    ftl::cuda::TextureObject<float4> &p1,
+    ftl::cuda::TextureObject<float4> &p2,
+    ftl::cuda::TextureObject<uchar4> &c1,
+    ftl::cuda::TextureObject<uchar4> &c2,
+    ftl::cuda::TextureObject<float4> &vout,
+    ftl::cuda::TextureObject<float> &eout,
+    float4x4 &pose2,
+    const ftl::rgbd::Camera &cam2,
+    cudaStream_t stream
+);
+
+}
+}
+
+#endif  // _FTL_ILW_CUDA_HPP_
diff --git a/applications/reconstruct/src/integrators.cu b/applications/reconstruct/src/integrators.cu
deleted file mode 100644
index d23fada9982aed2ff49039aa91bfa8760f91fcd1..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/integrators.cu
+++ /dev/null
@@ -1,342 +0,0 @@
-#include "integrators.hpp"
-//#include <ftl/ray_cast_params.hpp>
-#include <vector_types.h>
-#include <cuda_runtime.h>
-#include <ftl/cuda_matrix_util.hpp>
-#include <ftl/cuda_util.hpp>
-#include <ftl/cuda_common.hpp>
-
-#define T_PER_BLOCK 8
-#define NUM_CUDA_BLOCKS		10000
-#define WARP_SIZE 32
-
-using ftl::voxhash::HashData;
-using ftl::voxhash::HashParams;
-using ftl::voxhash::Voxel;
-using ftl::voxhash::HashEntry;
-using ftl::voxhash::HashEntryHead;
-using ftl::voxhash::FREE_ENTRY;
-
-extern __constant__ ftl::voxhash::DepthCameraCUDA c_cameras[MAX_CAMERAS];
-extern __constant__ HashParams c_hashParams;
-
-__device__ float4 make_float4(uchar4 c) {
-	return make_float4(static_cast<float>(c.x), static_cast<float>(c.y), static_cast<float>(c.z), static_cast<float>(c.w));
-}
-
-__device__ float colourDistance(const uchar4 &c1, const uchar3 &c2) {
-	float x = c1.x-c2.x;
-	float y = c1.y-c2.y;
-	float z = c1.z-c2.z;
-	return x*x + y*y + z*z;
-}
-
-/*
- * Kim, K., Chalidabhongse, T. H., Harwood, D., & Davis, L. (2005).
- * Real-time foreground-background segmentation using codebook model.
- * Real-Time Imaging. https://doi.org/10.1016/j.rti.2004.12.004
- */
-__device__ bool colordiff(const uchar4 &pa, const uchar3 &pb, float epsilon) {
-	float x_2 = pb.x * pb.x + pb.y * pb.y + pb.z * pb.z;
-	float v_2 = pa.x * pa.x + pa.y * pa.y + pa.z * pa.z;
-	float xv_2 = powf(float(pb.x * pa.x + pb.y * pa.y + pb.z * pa.z), 2.0f);
-	float p_2 = xv_2 / v_2;
-	return sqrt(x_2 - p_2) < epsilon;
-}
-
-/*
- * Guennebaud, G.; Gross, M. Algebraic point set surfaces. ACMTransactions on Graphics Vol. 26, No. 3, Article No. 23, 2007.
- * Used in: FusionMLS: Highly dynamic 3D reconstruction with consumer-grade RGB-D cameras
- *     r = distance between points
- *     h = smoothing parameter in meters (default 4cm)
- */
-__device__ float spatialWeighting(float r) {
-	const float h = c_hashParams.m_spatialSmoothing;
-	if (r >= h) return 0.0f;
-	float rh = r / h;
-	rh = 1.0f - rh*rh;
-	return rh*rh*rh*rh;
-}
-
-__device__ float spatialWeighting(float r, float h) {
-	//const float h = c_hashParams.m_spatialSmoothing;
-	if (r >= h) return 0.0f;
-	float rh = r / h;
-	rh = 1.0f - rh*rh;
-	return rh*rh*rh*rh;
-}
-
-
-__global__ void integrateDepthMapsKernel(HashData hashData, HashParams hashParams, int numcams) {
-	__shared__ uint all_warp_ballot;
-	__shared__ uint voxels[16];
-
-	const uint i = threadIdx.x;	//inside of an SDF block
-	const int3 po = make_int3(hashData.delinearizeVoxelIndex(i));
-
-	// Stride over all allocated blocks
-	for (int bi=blockIdx.x; bi<*hashData.d_hashCompactifiedCounter; bi+=NUM_CUDA_BLOCKS) {
-
-	//TODO check if we should load this in shared memory
-	//HashEntryHead entry = hashData.d_hashCompactified[bi]->head;
-
-	int3 pi_base = hashData.SDFBlockToVirtualVoxelPos(make_int3(hashData.d_hashCompactified[bi]->head.posXYZ));
-
-	//uint idx = entry.offset + i;
-	int3 pi = pi_base + po;
-	float3 pfb = hashData.virtualVoxelPosToWorld(pi);
-	int count = 0;
-	//float camdepths[MAX_CAMERAS];
-
-	Voxel oldVoxel; // = hashData.d_SDFBlocks[idx];
-	hashData.deleteVoxel(oldVoxel);
-
-	for (uint cam=0; cam<numcams; ++cam) {
-		const ftl::voxhash::DepthCameraCUDA &camera = c_cameras[cam];
-	
-		float3 pf = camera.poseInverse * pfb;
-		uint2 screenPos = make_uint2(camera.params.cameraToKinectScreenInt(pf));
-
-		// For this voxel in hash, get its screen position and check it is on screen
-		if (screenPos.x < camera.params.m_imageWidth && screenPos.y < camera.params.m_imageHeight) {	//on screen
-
-			//float depth = g_InputDepth[screenPos];
-			float depth = tex2D<float>(camera.depth, screenPos.x, screenPos.y);
-			//if (depth > 20.0f) return;
-
-			//uchar4 color  = make_uchar4(0, 0, 0, 0);
-			//if (cameraData.d_colorData) {
-				//color = (cam == 0) ? make_uchar4(255,0,0,255) : make_uchar4(0,0,255,255);
-				//color = tex2D<uchar4>(camera.colour, screenPos.x, screenPos.y);
-				//color = bilinearFilterColor(cameraData.cameraToKinectScreenFloat(pf));
-			//}
-
-			//printf("screen pos %d\n", color.x);
-			//return;
-
-			// TODO:(Nick) Accumulate weighted positions
-			// TODO:(Nick) Accumulate weighted normals
-			// TODO:(Nick) Accumulate weights
-
-			// Depth is within accepted max distance from camera
-			if (depth > 0.01f && depth < hashParams.m_maxIntegrationDistance) { // valid depth and color (Nick: removed colour check)
-				//camdepths[count] = depth;
-				++count;
-
-				// Calculate SDF of this voxel wrt the depth map value
-				float sdf = depth - pf.z;
-				float truncation = hashData.getTruncation(depth);
-				float depthZeroOne = camera.params.cameraToKinectProjZ(depth);
-
-				// Is this voxel close enough to cam for depth map value
-				// CHECK Nick: If is too close then free space violation so remove?
-				if (sdf > -truncation) // && depthZeroOne >= 0.0f && depthZeroOne <= 1.0f) //check if in truncation range should already be made in depth map computation
-				{
-					float weightUpdate = max(hashParams.m_integrationWeightSample * 1.5f * (1.0f-depthZeroOne), 1.0f);
-
-					Voxel curr;	//construct current voxel
-					curr.sdf = sdf;
-					curr.weight = weightUpdate;
-					//curr.color = make_uchar3(color.x, color.y, color.z);
-
-
-					//if (entry.flags != cameraParams.flags & 0xFF) {
-					//	entry.flags = cameraParams.flags & 0xFF;
-						//hashData.d_SDFBlocks[idx].color = make_uchar3(0,0,0);
-					//}
-					
-					Voxel newVoxel;
-					//if (color.x == MINF) hashData.combineVoxelDepthOnly(hashData.d_SDFBlocks[idx], curr, newVoxel);
-					//else hashData.combineVoxel(hashData.d_SDFBlocks[idx], curr, newVoxel);
-					hashData.combineVoxel(oldVoxel, curr, newVoxel);
-
-					oldVoxel = newVoxel;
-
-					//Voxel prev = getVoxel(g_SDFBlocksSDFUAV, g_SDFBlocksRGBWUAV, idx);
-					//Voxel newVoxel = combineVoxel(curr, prev);
-					//setVoxel(g_SDFBlocksSDFUAV, g_SDFBlocksRGBWUAV, idx, newVoxel);
-				}
-			} else {
-				// Depth is invalid so what to do here?
-				// TODO(Nick) Use past voxel if available (set weight from 0 to 1)
-
-				// Naive: need to know if this is a foreground voxel
-				//bool coldist = colordiff(color, hashData.d_SDFBlocks[idx].color, 5.0f);
-				//if (!coldist) ++count;
-
-			}
-		}
-	}
-
-	// Calculate voxel sign values across a warp
-	int warpNum = i / WARP_SIZE;
-	//uint ballot_result = __ballot_sync(0xFFFFFFFF, (oldVoxel.sdf >= 0.0f) ? 0 : 1);
-	uint ballot_result = __ballot_sync(0xFFFFFFFF, (fabs(oldVoxel.sdf) <= hashParams.m_virtualVoxelSize && oldVoxel.weight > 0) ? 1 : 0);
-
-	// Aggregate each warp result into voxel mask
-	if (i % WARP_SIZE == 0) {
-		voxels[warpNum] = ballot_result;
-	}
-
-	__syncthreads();
-
-	// Work out if block is occupied or not and save voxel masks
-	// TODO:(Nick) Is it faster to do this in a separate garbage kernel?
-	if (i < 16) {
-		const uint v = voxels[i];
-		hashData.d_hashCompactified[bi]->voxels[i] = v;
-		const uint mask = 0x0000FFFF;
-		uint b1 = __ballot_sync(mask, v == 0xFFFFFFFF);
-		uint b2 = __ballot_sync(mask, v == 0);
-		if (i == 0) {
-			if (b1 != mask && b2 != mask) hashData.d_hashCompactified[bi]->head.flags |= ftl::voxhash::kFlagSurface;
-			else hashData.d_hashCompactified[bi]->head.flags &= ~ftl::voxhash::kFlagSurface;
-		}
-	}
-
-	}
-}
-
-#define WINDOW_RADIUS 1
-#define PATCH_SIZE 32
-
-__global__ void integrateMLSKernel(HashData hashData, HashParams hashParams, int numcams) {
-	__shared__ uint voxels[16];
-
-	const uint i = threadIdx.x;	//inside of an SDF block
-	const int3 po = make_int3(hashData.delinearizeVoxelIndex(i));
-	const int warpNum = i / WARP_SIZE;
-	const int lane = i % WARP_SIZE;
-
-	// Stride over all allocated blocks
-	for (int bi=blockIdx.x; bi<*hashData.d_hashCompactifiedCounter; bi+=NUM_CUDA_BLOCKS) {
-
-	//TODO check if we should load this in shared memory
-	//HashEntryHead entry = hashData.d_hashCompactified[bi]->head;
-
-	const int3 pi_base = hashData.SDFBlockToVirtualVoxelPos(make_int3(hashData.d_hashCompactified[bi]->head.posXYZ));
-
-	//uint idx = entry.offset + i;
-	const int3 pi = pi_base + po;
-	const float3 pfb = hashData.virtualVoxelPosToWorld(pi);
-	//int count = 0;
-	//float camdepths[MAX_CAMERAS];
-
-	//Voxel oldVoxel; // = hashData.d_SDFBlocks[idx];
-	//hashData.deleteVoxel(oldVoxel);
-
-	//float3 awpos = make_float3(0.0f);
-	//float3 awnorm = make_float3(0.0f);
-	//float aweights = 0.0f;
-	float sdf = 0.0f;
-	float weights = 0.0f;
-
-	// Preload depth values
-	// 1. Find min and max screen positions
-	// 2. Subtract/Add WINDOW_RADIUS to min/max
-	// ... check that the buffer is not too small to cover this
-	// ... if buffer not big enough then don't buffer at all.
-	// 3. Populate shared mem depth map buffer using all threads
-	// 4. Adjust window lookups to use shared mem buffer
-
-	//uint cam=0;
-	for (uint cam=0; cam<numcams; ++cam) {
-		const ftl::voxhash::DepthCameraCUDA &camera = c_cameras[cam];
-		const uint height = camera.params.m_imageHeight;
-		const uint width = camera.params.m_imageWidth;
-	
-		const float3 pf = camera.poseInverse * pfb;
-		const uint2 screenPos = make_uint2(camera.params.cameraToKinectScreenInt(pf));
-
-		//float3 wpos = make_float3(0.0f);
-		float3 wnorm = make_float3(0.0f);
-		
-
-		#pragma unroll
-		for (int v=-WINDOW_RADIUS; v<=WINDOW_RADIUS; ++v) {
-			for (int u=-WINDOW_RADIUS; u<=WINDOW_RADIUS; ++u) {
-				if (screenPos.x+u < width && screenPos.y+v < height) {	//on screen
-					float4 depth = tex2D<float4>(camera.points, screenPos.x+u, screenPos.y+v);
-					if (depth.z == MINF) continue;
-
-					//float4 normal = tex2D<float4>(camera.normal, screenPos.x+u, screenPos.y+v);
-					const float3 camPos = camera.poseInverse * make_float3(depth); //camera.pose * camera.params.kinectDepthToSkeleton(screenPos.x+u, screenPos.y+v, depth);
-					const float weight = spatialWeighting(length(pf - camPos));
-
-					//wpos += weight*worldPos;
-					sdf += weight*(camPos.z - pf.z);
-					//sdf += camPos.z - pf.z;
-					//wnorm += weight*make_float3(normal);
-					//weights += 1.0f;	
-					weights += weight;			
-				}
-			}
-		}
-
-		//awpos += wpos;
-		//aweights += weights;
-	}
-
-	//awpos /= aweights;
-	//wnorm /= weights;
-
-	sdf /= weights;
-
-	//float sdf = (aweights == 0.0f) ? MINF : length(pfb - awpos);
-	//float sdf = wnorm.x * (pfb.x - wpos.x) + wnorm.y * (pfb.y - wpos.y) + wnorm.z * (pfb.z - wpos.z);
-
-	//printf("WEIGHTS: %f\n", weights);
-
-	//if (weights < 0.00001f) sdf = 0.0f;
-
-	// Calculate voxel sign values across a warp
-	int warpNum = i / WARP_SIZE;
-
-	//uint solid_ballot = __ballot_sync(0xFFFFFFFF, (fabs(sdf) < hashParams.m_virtualVoxelSize && aweights >= 0.5f) ? 1 : 0);
-	//uint solid_ballot = __ballot_sync(0xFFFFFFFF, (fabs(sdf) <= hashParams.m_virtualVoxelSize) ? 1 : 0);
-	//uint solid_ballot = __ballot_sync(0xFFFFFFFF, (aweights >= 0.0f) ? 1 : 0);
-	uint solid_ballot = __ballot_sync(0xFFFFFFFF, (sdf < 0.0f ) ? 1 : 0);
-
-	// Aggregate each warp result into voxel mask
-	if (i % WARP_SIZE == 0) {
-		voxels[warpNum] = solid_ballot;
-		//valid[warpNum] = valid_ballot;
-	}
-
-	__syncthreads();
-
-	// Work out if block is occupied or not and save voxel masks
-	// TODO:(Nick) Is it faster to do this in a separate garbage kernel?
-	if (i < 16) {
-		const uint v = voxels[i];
-		hashData.d_hashCompactified[bi]->voxels[i] = v;
-		//hashData.d_hashCompactified[bi]->validity[i] = valid[i];
-		const uint mask = 0x0000FFFF;
-		uint b1 = __ballot_sync(mask, v == 0xFFFFFFFF);
-		uint b2 = __ballot_sync(mask, v == 0);
-		if (i == 0) {
-			if (b1 != mask && b2 != mask) hashData.d_hashCompactified[bi]->head.flags |= ftl::voxhash::kFlagSurface;
-			else hashData.d_hashCompactified[bi]->head.flags &= ~ftl::voxhash::kFlagSurface;
-		}
-	}
-
-	}
-}
-
-
-
-void ftl::cuda::integrateDepthMaps(HashData& hashData, const HashParams& hashParams, int numcams, cudaStream_t stream) {
-const unsigned int threadsPerBlock = SDF_BLOCK_SIZE*SDF_BLOCK_SIZE*SDF_BLOCK_SIZE;
-const dim3 gridSize(NUM_CUDA_BLOCKS, 1);
-const dim3 blockSize(threadsPerBlock, 1);
-
-//if (hashParams.m_numOccupiedBlocks > 0) {	//this guard is important if there is no depth in the current frame (i.e., no blocks were allocated)
-	integrateMLSKernel << <gridSize, blockSize, 0, stream >> >(hashData, hashParams, numcams);
-//}
-
-//cudaSafeCall( cudaGetLastError() );
-#ifdef _DEBUG
-cudaSafeCall(cudaDeviceSynchronize());
-//cutilCheckMsg(__FUNCTION__);
-#endif
-}
diff --git a/applications/reconstruct/src/integrators.hpp b/applications/reconstruct/src/integrators.hpp
deleted file mode 100644
index 789551dd1fa7347bf02c518c8c5a73f6ae4269b4..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/integrators.hpp
+++ /dev/null
@@ -1,22 +0,0 @@
-#ifndef _FTL_RECONSTRUCTION_INTEGRATORS_HPP_
-#define _FTL_RECONSTRUCTION_INTEGRATORS_HPP_
-
-#include <ftl/voxel_hash.hpp>
-#include <ftl/depth_camera.hpp>
-
-namespace ftl {
-namespace cuda {
-
-/*void integrateDepthMap(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams,
-		const DepthCameraData& depthCameraData, const DepthCameraParams& depthCameraParams, cudaStream_t stream);
-
-void integrateRegistration(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams,
-		const DepthCameraData& depthCameraData, const DepthCameraParams& depthCameraParams, cudaStream_t stream);
-*/
-
-void integrateDepthMaps(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, int numcams, cudaStream_t stream);
-
-}
-}
-
-#endif  // _FTL_RECONSTRUCTION_INTEGRATORS_HPP_
diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp
index 01745d2dd2e6dcaf902fa4260f781a3b7fb3dea0..6c1d8a8813dd5f5a39a1457003df31bc388ab642 100644
--- a/applications/reconstruct/src/main.cpp
+++ b/applications/reconstruct/src/main.cpp
@@ -9,14 +9,15 @@
 #include <ftl/config.h>
 #include <ftl/configuration.hpp>
 #include <ftl/depth_camera.hpp>
-#include <ftl/voxel_scene.hpp>
 #include <ftl/rgbd.hpp>
-#include <ftl/virtual_source.hpp>
+#include <ftl/rgbd/virtual.hpp>
 #include <ftl/rgbd/streamer.hpp>
 #include <ftl/slave.hpp>
 #include <ftl/rgbd/group.hpp>
+#include <ftl/threads.hpp>
 
-#include "splat_render.hpp"
+#include "ilw.hpp"
+#include <ftl/render/splat_render.hpp>
 
 #include <string>
 #include <vector>
@@ -37,6 +38,7 @@ using std::string;
 using std::vector;
 using ftl::rgbd::Source;
 using ftl::config::json_t;
+using ftl::rgbd::Channel;
 
 using json = nlohmann::json;
 using std::this_thread::sleep_for;
@@ -91,102 +93,78 @@ static void run(ftl::Configurable *root) {
 		}
 	}
 
-	ftl::voxhash::SceneRep *scene = ftl::create<ftl::voxhash::SceneRep>(root, "voxelhash");
+	ftl::rgbd::FrameSet scene_A;  // Output of align process
+	ftl::rgbd::FrameSet scene_B;  // Input of render process
+
+	//ftl::voxhash::SceneRep *scene = ftl::create<ftl::voxhash::SceneRep>(root, "voxelhash");
 	ftl::rgbd::Streamer *stream = ftl::create<ftl::rgbd::Streamer>(root, "stream", net);
-	ftl::rgbd::Source *virt = ftl::create<ftl::rgbd::Source>(root, "virtual", net);
-	ftl::render::Splatter *splat = new ftl::render::Splatter(scene);
+	ftl::rgbd::VirtualSource *virt = ftl::create<ftl::rgbd::VirtualSource>(root, "virtual");
+	ftl::render::Splatter *splat = ftl::create<ftl::render::Splatter>(root, "renderer", &scene_B);
 	ftl::rgbd::Group group;
+	ftl::ILW *align = ftl::create<ftl::ILW>(root, "merge");
 
-	//auto virtimpl = new ftl::rgbd::VirtualSource(virt);
-	//virt->customImplementation(virtimpl);
-	//virtimpl->setScene(scene);
+	// Generate virtual camera render when requested by streamer
+	virt->onRender([splat,virt,&scene_B](ftl::rgbd::Frame &out) {
+		virt->setTimestamp(scene_B.timestamp);
+		splat->render(virt, out);
+	});
 	stream->add(virt);
 
 	for (size_t i=0; i<sources.size(); i++) {
 		Source *in = sources[i];
-		in->setChannel(ftl::rgbd::kChanDepth);
-		//stream->add(in);
-		scene->addSource(in);
+		in->setChannel(Channel::Depth);
 		group.addSource(in);
 	}
 
+	stream->setLatency(4);  // FIXME: This depends on source!?
 	stream->run();
 
 	bool busy = false;
 
+	group.setLatency(4);
 	group.setName("ReconGroup");
-	group.sync([scene,splat,virt,&busy,&slave](ftl::rgbd::FrameSet &fs) -> bool {
-		cudaSetDevice(scene->getCUDADevice());
+	group.sync([splat,virt,&busy,&slave,&scene_A,&scene_B,&align](ftl::rgbd::FrameSet &fs) -> bool {
+		//cudaSetDevice(scene->getCUDADevice());
+
+		if (slave.isPaused()) return true;
 		
 		if (busy) {
 			LOG(INFO) << "Group frameset dropped: " << fs.timestamp;
 			return true;
 		}
 		busy = true;
-		scene->nextFrame();
 
-		// Send all frames to GPU, block until done?
-		// TODO: Allow non-block and keep frameset locked until later
-		if (!slave.isPaused()) scene->upload(fs);
+		// Swap the entire frameset to allow rapid return
+		fs.swapTo(scene_A);
 
-		int64_t ts = fs.timestamp;
-
-		ftl::pool.push([scene,splat,virt,&busy,ts,&slave](int id) {
-			cudaSetDevice(scene->getCUDADevice());
+		ftl::pool.push([&scene_B,&scene_A,&busy,&slave,&align](int id) {
+			//cudaSetDevice(scene->getCUDADevice());
 			// TODO: Release frameset here...
-			cudaSafeCall(cudaStreamSynchronize(scene->getIntegrationStream()));
+			//cudaSafeCall(cudaStreamSynchronize(scene->getIntegrationStream()));
 
-			if (!slave.isPaused()) {
-				scene->integrate();
-				scene->garbage();
-			}
+			UNIQUE_LOCK(scene_A.mtx, lk);
 
-			// Don't render here... but update timestamp.
-			splat->render(ts, virt, scene->getIntegrationStream());
+			// Send all frames to GPU, block until done?
+			scene_A.upload(Channel::Colour + Channel::Depth);  // TODO: (Nick) Add scene stream.
+			//align->process(scene_A);
+
+			// TODO: To use second GPU, could do a download, swap, device change,
+			// then upload to other device. Or some direct device-2-device copy.
+			scene_A.swapTo(scene_B);
+			LOG(INFO) << "Align complete... " << scene_A.timestamp;
 			busy = false;
 		});
 		return true;
 	});
 
-
-	/*int active = sources.size();
-	while (ftl::running) {
-		if (active == 0) {
-			LOG(INFO) << "Waiting for sources...";
-			sleep_for(milliseconds(1000));
-		}
-
-		active = 0;
-
-		if (!slave.isPaused()) {
-			// Mark voxels as cleared
-			scene->nextFrame();
-		
-			// Grab, upload frames and allocate voxel blocks
-			active = scene->upload();
-
-			// Make sure previous virtual camera frame has finished rendering
-			//stream->wait();
-			cudaSafeCall(cudaStreamSynchronize(scene->getIntegrationStream()));
-
-
-			// Merge new frames into the voxel structure
-			scene->integrate();
-
-			//LOG(INFO) << "Allocated: " << scene->getOccupiedCount();
-
-			// Remove any redundant voxels
-			scene->garbage();
-
-		} else {
-			active = 1;
-		}
-
-		splat->render(virt, scene->getIntegrationStream());
-
-		// Start virtual camera rendering and previous frame compression
-		stream->poll();
-	}*/
+	ftl::timer::stop();
+	slave.stop();
+	net->shutdown();
+	delete align;
+	delete splat;
+	delete stream;
+	delete virt;
+	delete net;
 }
 
 int main(int argc, char **argv) {
diff --git a/applications/reconstruct/src/ray_cast_sdf.cu b/applications/reconstruct/src/ray_cast_sdf.cu
index a43b608429b1fad1ac160b188c9be6b084274154..10fd3e0b7b84ca694432abc7dc68fc513ad483eb 100644
--- a/applications/reconstruct/src/ray_cast_sdf.cu
+++ b/applications/reconstruct/src/ray_cast_sdf.cu
@@ -1,5 +1,5 @@
 
-#include <cuda_runtime.h>
+//#include <cuda_runtime.h>
 
 #include <ftl/cuda_matrix_util.hpp>
 
diff --git a/applications/reconstruct/src/scene_rep_hash_sdf.cu b/applications/reconstruct/src/scene_rep_hash_sdf.cu
index 247247b6cc5279186f9f9bdc81bbe627ab0621ea..4750d3e7ed4f7aeb68875e0b5050d76efed5b715 100644
--- a/applications/reconstruct/src/scene_rep_hash_sdf.cu
+++ b/applications/reconstruct/src/scene_rep_hash_sdf.cu
@@ -2,8 +2,8 @@
 
 //#include <cutil_inline.h>
 //#include <cutil_math.h>
-#include <vector_types.h>
-#include <cuda_runtime.h>
+//#include <vector_types.h>
+//#include <cuda_runtime.h>
 
 #include <ftl/cuda_matrix_util.hpp>
 
diff --git a/applications/reconstruct/src/splat_render.cpp b/applications/reconstruct/src/splat_render.cpp
deleted file mode 100644
index cc52bb7bf33d8688a9fc1da9f3b0d07474aff811..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/splat_render.cpp
+++ /dev/null
@@ -1,137 +0,0 @@
-#include "splat_render.hpp"
-#include "splat_render_cuda.hpp"
-#include "compactors.hpp"
-#include "depth_camera_cuda.hpp"
-
-using ftl::render::Splatter;
-
-Splatter::Splatter(ftl::voxhash::SceneRep *scene) : scene_(scene) {
-
-}
-
-Splatter::~Splatter() {
-
-}
-
-void Splatter::render(int64_t ts, ftl::rgbd::Source *src, cudaStream_t stream) {
-	if (!src->isReady()) return;
-
-	const auto &camera = src->parameters();
-
-	cudaSafeCall(cudaSetDevice(scene_->getCUDADevice()));
-
-	// Create buffers if they don't exists
-	if ((unsigned int)depth1_.width() != camera.width || (unsigned int)depth1_.height() != camera.height) {
-		depth1_ = ftl::cuda::TextureObject<int>(camera.width, camera.height);
-	}
-	if ((unsigned int)depth3_.width() != camera.width || (unsigned int)depth3_.height() != camera.height) {
-		depth3_ = ftl::cuda::TextureObject<int>(camera.width, camera.height);
-	}
-	if ((unsigned int)colour1_.width() != camera.width || (unsigned int)colour1_.height() != camera.height) {
-		colour1_ = ftl::cuda::TextureObject<uchar4>(camera.width, camera.height);
-	}
-	if ((unsigned int)colour_tmp_.width() != camera.width || (unsigned int)colour_tmp_.height() != camera.height) {
-		colour_tmp_ = ftl::cuda::TextureObject<float4>(camera.width, camera.height);
-	}
-	if ((unsigned int)normal1_.width() != camera.width || (unsigned int)normal1_.height() != camera.height) {
-		normal1_ = ftl::cuda::TextureObject<float4>(camera.width, camera.height);
-	}
-	if ((unsigned int)depth2_.width() != camera.width || (unsigned int)depth2_.height() != camera.height) {
-		depth2_ = ftl::cuda::TextureObject<float>(camera.width, camera.height);
-	}
-	if ((unsigned int)colour2_.width() != camera.width || (unsigned int)colour2_.height() != camera.height) {
-		colour2_ = ftl::cuda::TextureObject<uchar4>(camera.width, camera.height);
-	}
-
-	// Parameters object to pass to CUDA describing the camera
-	SplatParams params;
-	params.m_flags = 0;
-	if (src->value("splatting", true) == false) params.m_flags |= ftl::render::kNoSplatting;
-	if (src->value("upsampling", true) == false) params.m_flags |= ftl::render::kNoUpsampling;
-	if (src->value("texturing", true) == false) params.m_flags |= ftl::render::kNoTexturing;
-
-	params.m_viewMatrix = MatrixConversion::toCUDA(src->getPose().cast<float>().inverse());
-	params.m_viewMatrixInverse = MatrixConversion::toCUDA(src->getPose().cast<float>());
-	params.voxelSize = scene_->getHashParams().m_virtualVoxelSize;
-	params.camera.flags = 0;
-	params.camera.fx = camera.fx;
-	params.camera.fy = camera.fy;
-	params.camera.mx = -camera.cx;
-	params.camera.my = -camera.cy;
-	params.camera.m_imageWidth = camera.width;
-	params.camera.m_imageHeight = camera.height;
-	params.camera.m_sensorDepthWorldMax = camera.maxDepth;
-	params.camera.m_sensorDepthWorldMin = camera.minDepth;
-
-	//ftl::cuda::compactifyAllocated(scene_->getHashData(), scene_->getHashParams(), stream);
-	//LOG(INFO) << "Occupied: " << scene_->getOccupiedCount();
-
-	if (scene_->value("voxels", false)) {
-		// TODO:(Nick) Stereo for voxel version
-		ftl::cuda::isosurface_point_image(scene_->getHashData(), depth1_, params, stream);
-		//ftl::cuda::splat_points(depth1_, depth2_, params, stream);
-		//ftl::cuda::dibr(depth2_, colour1_, scene_->cameraCount(), params, stream);
-		src->writeFrames(ts, colour1_, depth2_, stream);
-	} else {
-		ftl::cuda::clear_depth(depth1_, stream);
-		ftl::cuda::clear_depth(depth3_, stream);
-		ftl::cuda::clear_depth(depth2_, stream);
-		ftl::cuda::clear_colour(colour2_, stream);
-		ftl::cuda::dibr(depth1_, colour1_, normal1_, depth2_, colour_tmp_, depth3_, scene_->cameraCount(), params, stream);
-
-		// Step 1: Put all points into virtual view to gather them
-		//ftl::cuda::dibr_raw(depth1_, scene_->cameraCount(), params, stream);
-
-		// Step 2: For each point, use a warp to do MLS and up sample
-		//ftl::cuda::mls_render_depth(depth1_, depth3_, params, scene_->cameraCount(), stream);
-
-		if (src->getChannel() == ftl::rgbd::kChanDepth) {
-			//ftl::cuda::int_to_float(depth1_, depth2_, 1.0f / 1000.0f, stream);
-			if (src->value("splatting",  false)) {
-				//ftl::cuda::splat_points(depth1_, colour1_, normal1_, depth2_, colour2_, params, stream);
-				ftl::cuda::int_to_float(depth1_, depth2_, 1.0f / 1000.0f, stream);
-				src->writeFrames(ts, colour1_, depth2_, stream);
-			} else {
-				ftl::cuda::int_to_float(depth1_, depth2_, 1.0f / 1000.0f, stream);
-				src->writeFrames(ts, colour1_, depth2_, stream);
-			}
-		} else if (src->getChannel() == ftl::rgbd::kChanEnergy) {
-			//ftl::cuda::int_to_float(depth1_, depth2_, 1.0f / 1000.0f, stream);
-			//if (src->value("splatting",  false)) {
-				//ftl::cuda::splat_points(depth1_, colour1_, normal1_, depth2_, colour2_, params, stream);
-				//ftl::cuda::int_to_float(depth1_, depth2_, 1.0f / 1000.0f, stream);
-				src->writeFrames(ts, colour1_, depth2_, stream);
-			//} else {
-				//ftl::cuda::int_to_float(depth1_, depth2_, 1.0f / 1000.0f, stream);
-			//	src->writeFrames(colour1_, depth2_, stream);
-			//}
-		} else if (src->getChannel() == ftl::rgbd::kChanRight) {
-			// Adjust pose to right eye position
-			Eigen::Affine3f transform(Eigen::Translation3f(camera.baseline,0.0f,0.0f));
-			Eigen::Matrix4f matrix =  src->getPose().cast<float>() * transform.matrix();
-			params.m_viewMatrix = MatrixConversion::toCUDA(matrix.inverse());
-			params.m_viewMatrixInverse = MatrixConversion::toCUDA(matrix);
-
-			ftl::cuda::clear_depth(depth1_, stream);
-			ftl::cuda::dibr(depth1_, colour1_, normal1_, depth2_, colour_tmp_, depth3_, scene_->cameraCount(), params, stream);
-			src->writeFrames(ts, colour1_, colour2_, stream);
-		} else {
-			if (src->value("splatting",  false)) {
-				//ftl::cuda::splat_points(depth1_, colour1_, normal1_, depth2_, colour2_, params, stream);
-				src->writeFrames(ts, colour1_, depth2_, stream);
-			} else {
-				ftl::cuda::int_to_float(depth1_, depth2_, 1.0f / 1000.0f, stream);
-				src->writeFrames(ts, colour1_, depth2_, stream);
-			}
-		}
-	}
-
-	//ftl::cuda::median_filter(depth1_, depth2_, stream);
-	//ftl::cuda::splat_points(depth1_, depth2_, params, stream);
-
-	// TODO: Second pass
-}
-
-void Splatter::setOutputDevice(int device) {
-	device_ = device;
-}
diff --git a/applications/reconstruct/src/voxel_hash.cpp b/applications/reconstruct/src/voxel_hash.cpp
deleted file mode 100644
index 6f929c746d66cae5c382bf15b2d25e26f6b46702..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/voxel_hash.cpp
+++ /dev/null
@@ -1,95 +0,0 @@
-#include <ftl/voxel_hash.hpp>
-#include <loguru.hpp>
-
-using ftl::voxhash::HashData;
-using ftl::voxhash::HashParams;
-
-void HashData::allocate(const HashParams& params, bool dataOnGPU) {
-	m_bIsOnGPU = dataOnGPU;
-	if (m_bIsOnGPU) {
-		cudaSafeCall(cudaMalloc(&d_hash, sizeof(HashEntry)* params.m_hashNumBuckets));
-		cudaSafeCall(cudaMalloc(&d_hashDecision, sizeof(int)* params.m_hashNumBuckets));
-		cudaSafeCall(cudaMalloc(&d_hashDecisionPrefix, sizeof(int)* params.m_hashNumBuckets));
-		cudaSafeCall(cudaMalloc(&d_hashCompactified, sizeof(HashEntry*)* params.m_hashNumBuckets));
-		cudaSafeCall(cudaMalloc(&d_hashCompactifiedCounter, sizeof(int)));
-		cudaSafeCall(cudaMalloc(&d_hashBucketMutex, sizeof(int)* params.m_hashNumBuckets));
-	} else {
-		d_hash = new HashEntry[params.m_hashNumBuckets];
-		d_hashDecision = new int[params.m_hashNumBuckets];
-		d_hashDecisionPrefix = new int[params.m_hashNumBuckets];
-		d_hashCompactified = new HashEntry*[params.m_hashNumBuckets];
-		d_hashCompactifiedCounter = new int[1];
-		d_hashBucketMutex = new int[params.m_hashNumBuckets];
-	}
-
-	updateParams(params);
-}
-
-void HashData::updateParams(const HashParams& params) {
-	if (m_bIsOnGPU) {
-		updateConstantHashParams(params);
-	} 
-}
-
-void HashData::free() {
-	if (m_bIsOnGPU) {
-		cudaSafeCall(cudaFree(d_hash));
-		cudaSafeCall(cudaFree(d_hashDecision));
-		cudaSafeCall(cudaFree(d_hashDecisionPrefix));
-		cudaSafeCall(cudaFree(d_hashCompactified));
-		cudaSafeCall(cudaFree(d_hashCompactifiedCounter));
-		cudaSafeCall(cudaFree(d_hashBucketMutex));
-	} else {
-		if (d_hash) delete[] d_hash;
-		if (d_hashDecision) delete[] d_hashDecision;
-		if (d_hashDecisionPrefix) delete[] d_hashDecisionPrefix;
-		if (d_hashCompactified) delete[] d_hashCompactified;
-		if (d_hashCompactifiedCounter) delete[] d_hashCompactifiedCounter;
-		if (d_hashBucketMutex) delete[] d_hashBucketMutex;
-	}
-
-	d_hash = NULL;
-	d_hashDecision = NULL;
-	d_hashDecisionPrefix = NULL;
-	d_hashCompactified = NULL;
-	d_hashCompactifiedCounter = NULL;
-	d_hashBucketMutex = NULL;
-}
-
-HashData HashData::download() const {
-	if (!m_bIsOnGPU) return *this;
-	HashParams params;
-	
-	HashData hashData;
-	hashData.allocate(params, false);	//allocate the data on the CPU
-	cudaSafeCall(cudaMemcpy(hashData.d_hash, d_hash, sizeof(HashEntry)* params.m_hashNumBuckets, cudaMemcpyDeviceToHost));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashDecision, d_hashDecision, sizeof(int)*params.m_hashNumBuckets, cudaMemcpyDeviceToHost));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashDecisionPrefix, d_hashDecisionPrefix, sizeof(int)*params.m_hashNumBuckets, cudaMemcpyDeviceToHost));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashCompactified, d_hashCompactified, sizeof(HashEntry*)* params.m_hashNumBuckets, cudaMemcpyDeviceToHost));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashCompactifiedCounter, d_hashCompactifiedCounter, sizeof(unsigned int), cudaMemcpyDeviceToHost));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashBucketMutex, d_hashBucketMutex, sizeof(int)* params.m_hashNumBuckets, cudaMemcpyDeviceToHost));
-	
-	return hashData;
-}
-
-HashData HashData::upload() const {
-	if (m_bIsOnGPU) return *this;
-	HashParams params;
-	
-	HashData hashData;
-	hashData.allocate(params, false);	//allocate the data on the CPU
-	cudaSafeCall(cudaMemcpy(hashData.d_hash, d_hash, sizeof(HashEntry)* params.m_hashNumBuckets, cudaMemcpyHostToDevice));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashDecision, d_hashDecision, sizeof(int)*params.m_hashNumBuckets, cudaMemcpyHostToDevice));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashDecisionPrefix, d_hashDecisionPrefix, sizeof(int)*params.m_hashNumBuckets, cudaMemcpyHostToDevice));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashCompactified, d_hashCompactified, sizeof(HashEntry)* params.m_hashNumBuckets, cudaMemcpyHostToDevice));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashCompactifiedCounter, d_hashCompactifiedCounter, sizeof(unsigned int), cudaMemcpyHostToDevice));
-	cudaSafeCall(cudaMemcpy(hashData.d_hashBucketMutex, d_hashBucketMutex, sizeof(int)* params.m_hashNumBuckets, cudaMemcpyHostToDevice));
-	
-	return hashData;
-}
-
-/*size_t HashData::getAllocatedBlocks() const {
-	unsigned int count;
-	cudaSafeCall(cudaMemcpy(d_heapCounter, &count, sizeof(unsigned int), cudaMemcpyDeviceToHost));
-	return count;
-}*/
diff --git a/applications/reconstruct/src/voxel_hash.cu b/applications/reconstruct/src/voxel_hash.cu
deleted file mode 100644
index c2d07c391a6e48d2b45cc23dbf32b00878ffd5c9..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/voxel_hash.cu
+++ /dev/null
@@ -1,257 +0,0 @@
-#include <ftl/voxel_hash.hpp>
-
-using namespace ftl::voxhash;
-
-#define COLLISION_LIST_SIZE 6
-
-__device__ inline uint64_t compactPosition(const int3 &pos) {
-	union __align__(8) {
-	short4 posXYZ;
-	uint64_t pos64;
-	};
-	posXYZ.x = pos.x; posXYZ.y = pos.y; posXYZ.z = pos.z; posXYZ.w = 0;
-	return pos64;
-}
-
-//! returns the hash entry for a given sdf block id; if there was no hash entry the returned entry will have a ptr with FREE_ENTRY set
-__device__ 
-int HashData::getHashEntryForSDFBlockPos(const int3& sdfBlock) const {
-	uint h = computeHashPos(sdfBlock); //hash
-	uint64_t pos = compactPosition(sdfBlock);
-
-	HashEntryHead curr;
-
-	int i = h;
-	unsigned int maxIter = 0;
-
-	#pragma unroll 2
-	while (maxIter < COLLISION_LIST_SIZE) {
-		curr = d_hash[i].head;
-
-		if (curr.pos == pos && curr.offset != FREE_ENTRY) return i;
-		if (curr.offset == 0 || curr.offset == FREE_ENTRY) break;
-
-		i +=  curr.offset;  //go to next element in the list
-		i %= (params().m_hashNumBuckets);  //check for overflow
-		++maxIter;
-	}
-
-	// Could not find
-	return -1;
-}
-
-//for histogram (collisions traversal only)
-__device__ 
-unsigned int HashData::getNumHashLinkedList(unsigned int bucketID) {
-	unsigned int listLen = 0;
-
-	unsigned int i = bucketID;	//start with the last entry of the current bucket
-	HashEntryHead curr;	curr.offset = 0;
-
-	unsigned int maxIter = 0;
-
-	#pragma unroll 2 
-	while (maxIter < COLLISION_LIST_SIZE) {
-		curr = d_hash[i].head;
-
-		if (curr.offset == 0 || curr.offset == FREE_ENTRY) break;
-
-		i += curr.offset;		//go to next element in the list
-		i %= (params().m_hashNumBuckets);	//check for overflow
-		++listLen;
-		++maxIter;
-	}
-	
-	return listLen;
-}
-
-//pos in SDF block coordinates
-__device__
-void HashData::allocBlock(const int3& pos) {
-	uint h = computeHashPos(pos);				//hash bucket
-	uint i = h;
-	HashEntryHead curr;	//curr.offset = 0;
-	const uint64_t pos64 = compactPosition(pos);
-
-	unsigned int maxIter = 0;
-	#pragma  unroll 2
-	while (maxIter < COLLISION_LIST_SIZE) {
-		//offset = curr.offset;
-		curr = d_hash[i].head;	//TODO MATTHIAS do by reference
-		if (curr.pos == pos64 && curr.offset != FREE_ENTRY) return;
-		if (curr.offset == 0 || curr.offset == FREE_ENTRY) break;
-
-		i += curr.offset;		//go to next element in the list
-		i %= (params().m_hashNumBuckets);	//check for overflow
-		++maxIter;
-	}
-
-	// Limit reached...
-	//if (maxIter == COLLISION_LIST_SIZE) return;
-
-	int j = i;
-	while (maxIter < COLLISION_LIST_SIZE) {
-		//offset = curr.offset;
-
-		if (curr.offset == FREE_ENTRY) {
-			int prevValue = atomicExch(&d_hashBucketMutex[i], LOCK_ENTRY);
-			if (prevValue != LOCK_ENTRY) {
-				if (i == j) {
-					HashEntryHead& entry = d_hash[j].head;
-					entry.pos = pos64;
-					entry.offset = 0;
-					entry.flags = 0;
-				} else {
-					//InterlockedExchange(g_HashBucketMutex[h], LOCK_ENTRY, prevValue);	//lock the hash bucket where we have found a free entry
-					prevValue = atomicExch(&d_hashBucketMutex[j], LOCK_ENTRY);
-					if (prevValue != LOCK_ENTRY) {	//only proceed if the bucket has been locked
-						HashEntryHead& entry = d_hash[j].head;
-						entry.pos = pos64;
-						entry.offset = 0;
-						entry.flags = 0;  // Flag block as valid in this frame (Nick)		
-						//entry.ptr = consumeHeap() * SDF_BLOCK_SIZE*SDF_BLOCK_SIZE*SDF_BLOCK_SIZE;	//memory alloc
-						d_hash[i].head.offset = j-i;
-						//setHashEntry(g_Hash, idxLastEntryInBucket, lastEntryInBucket);
-					}
-				}
-			} 
-			return;	//bucket was already locked
-		}
-
-		++j;
-		j %= (params().m_hashNumBuckets);	//check for overflow
-		curr = d_hash[j].head;	//TODO MATTHIAS do by reference
-		++maxIter;
-	}
-}
-
-
-//!inserts a hash entry without allocating any memory: used by streaming: TODO MATTHIAS check the atomics in this function
-/*__device__
-bool HashData::insertHashEntry(HashEntry entry)
-{
-	uint h = computeHashPos(entry.pos);
-	uint hp = h * HASH_BUCKET_SIZE;
-
-	for (uint j = 0; j < HASH_BUCKET_SIZE; j++) {
-		uint i = j + hp;		
-		//const HashEntry& curr = d_hash[i];
-		int prevWeight = 0;
-		//InterlockedCompareExchange(hash[3*i+2], FREE_ENTRY, LOCK_ENTRY, prevWeight);
-		prevWeight = atomicCAS(&d_hash[i].ptr, FREE_ENTRY, LOCK_ENTRY);
-		if (prevWeight == FREE_ENTRY) {
-			d_hash[i] = entry;
-			//setHashEntry(hash, i, entry);
-			return true;
-		}
-	}
-
-#ifdef HANDLE_COLLISIONS
-	//updated variables as after the loop
-	const uint idxLastEntryInBucket = (h+1)*HASH_BUCKET_SIZE - 1;	//get last index of bucket
-
-	uint i = idxLastEntryInBucket;											//start with the last entry of the current bucket
-	HashEntry curr;
-
-	unsigned int maxIter = 0;
-	//[allow_uav_condition]
-	uint g_MaxLoopIterCount = params().m_hashMaxCollisionLinkedListSize;
-	#pragma  unroll 1 
-	while (maxIter < g_MaxLoopIterCount) {									//traverse list until end // why find the end? we you are inserting at the start !!!
-		//curr = getHashEntry(hash, i);
-		curr = d_hash[i];	//TODO MATTHIAS do by reference
-		if (curr.offset == 0) break;									//we have found the end of the list
-		i = idxLastEntryInBucket + curr.offset;							//go to next element in the list
-		i %= (HASH_BUCKET_SIZE * params().m_hashNumBuckets);	//check for overflow
-
-		maxIter++;
-	}
-
-	maxIter = 0;
-	int offset = 0;
-	#pragma  unroll 1 
-	while (maxIter < g_MaxLoopIterCount) {													//linear search for free entry
-		offset++;
-		uint i = (idxLastEntryInBucket + offset) % (HASH_BUCKET_SIZE * params().m_hashNumBuckets);	//go to next hash element
-		if ((offset % HASH_BUCKET_SIZE) == 0) continue;										//cannot insert into a last bucket element (would conflict with other linked lists)
-
-		int prevWeight = 0;
-		//InterlockedCompareExchange(hash[3*i+2], FREE_ENTRY, LOCK_ENTRY, prevWeight);		//check for a free entry
-		uint* d_hashUI = (uint*)d_hash;
-		prevWeight = prevWeight = atomicCAS(&d_hashUI[3*idxLastEntryInBucket+1], (uint)FREE_ENTRY, (uint)LOCK_ENTRY);
-		if (prevWeight == FREE_ENTRY) {														//if free entry found set prev->next = curr & curr->next = prev->next
-			//[allow_uav_condition]
-			//while(hash[3*idxLastEntryInBucket+2] == LOCK_ENTRY); // expects setHashEntry to set the ptr last, required because pos.z is packed into the same value -> prev->next = curr -> might corrput pos.z
-
-			HashEntry lastEntryInBucket = d_hash[idxLastEntryInBucket];			//get prev (= lastEntry in Bucket)
-
-			int newOffsetPrev = (offset << 16) | (lastEntryInBucket.pos.z & 0x0000ffff);	//prev->next = curr (maintain old z-pos)
-			int oldOffsetPrev = 0;
-			//InterlockedExchange(hash[3*idxLastEntryInBucket+1], newOffsetPrev, oldOffsetPrev);	//set prev offset atomically
-			uint* d_hashUI = (uint*)d_hash;
-			oldOffsetPrev = prevWeight = atomicExch(&d_hashUI[3*idxLastEntryInBucket+1], newOffsetPrev);
-			entry.offset = oldOffsetPrev >> 16;													//remove prev z-pos from old offset
-
-			//setHashEntry(hash, i, entry);														//sets the current hashEntry with: curr->next = prev->next
-			d_hash[i] = entry;
-			return true;
-		}
-
-		maxIter++;
-	} 
-#endif
-
-	return false;
-}*/
-
-
-
-//! deletes a hash entry position for a given sdfBlock index (returns true uppon successful deletion; otherwise returns false)
-__device__
-bool HashData::deleteHashEntryElement(const int3& sdfBlock) {
-	uint h = computeHashPos(sdfBlock);	//hash bucket
-	const uint64_t pos = compactPosition(sdfBlock);
-
-	int i = h;
-	int prev = -1;
-	HashEntryHead curr;
-	unsigned int maxIter = 0;
-
-	#pragma  unroll 2 
-	while (maxIter < COLLISION_LIST_SIZE) {
-		curr = d_hash[i].head;
-	
-		//found that dude that we need/want to delete
-		if (curr.pos == pos && curr.offset != FREE_ENTRY) {
-			//int prevValue = 0;
-			//InterlockedExchange(bucketMutex[h], LOCK_ENTRY, prevValue);	//lock the hash bucket
-			int prevValue = atomicExch(&d_hashBucketMutex[i], LOCK_ENTRY);
-			if (prevValue == LOCK_ENTRY)	return false;
-			if (prevValue != LOCK_ENTRY) {
-				prevValue = (prev >= 0) ? atomicExch(&d_hashBucketMutex[prev], LOCK_ENTRY) : 0;
-				if (prevValue == LOCK_ENTRY)	return false;
-				if (prevValue != LOCK_ENTRY) {
-					//const uint linBlockSize = SDF_BLOCK_SIZE * SDF_BLOCK_SIZE * SDF_BLOCK_SIZE;
-					//appendHeap(curr.ptr / linBlockSize);
-					deleteHashEntry(i);
-
-					if (prev >= 0) {
-						d_hash[prev].head.offset = curr.offset;
-					}
-					return true;
-				}
-			}
-		}
-
-		if (curr.offset == 0 || curr.offset == FREE_ENTRY) {	//we have found the end of the list
-			return false;	//should actually never happen because we need to find that guy before
-		}
-		prev = i;
-		i += curr.offset;		//go to next element in the list
-		i %= (params().m_hashNumBuckets);	//check for overflow
-
-		++maxIter;
-	}
-
-	return false;
-}
\ No newline at end of file
diff --git a/applications/reconstruct/src/voxel_render.cu b/applications/reconstruct/src/voxel_render.cu
deleted file mode 100644
index 46eb6f57c031b0541add1dcd982a9c6d1e690140..0000000000000000000000000000000000000000
--- a/applications/reconstruct/src/voxel_render.cu
+++ /dev/null
@@ -1,261 +0,0 @@
-#include "splat_render_cuda.hpp"
-#include <cuda_runtime.h>
-
-#include <ftl/cuda_matrix_util.hpp>
-
-#include "splat_params.hpp"
-
-#define T_PER_BLOCK 8
-#define NUM_GROUPS_X 1024
-
-#define NUM_CUDA_BLOCKS  10000
-
-using ftl::cuda::TextureObject;
-using ftl::render::SplatParams;
-
-__global__ void clearDepthKernel(ftl::voxhash::HashData hashData, TextureObject<int> depth) {
-	const unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;
-	const unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;
-
-	if (x < depth.width() && y < depth.height()) {
-		depth(x,y) = 0x7f800000; //PINF;
-		//colour(x,y) = make_uchar4(76,76,82,0);
-	}
-}
-
-#define SDF_BLOCK_SIZE_PAD 8
-#define SDF_BLOCK_BUFFER 512  // > 8x8x8
-#define SDF_DX 1
-#define SDF_DY SDF_BLOCK_SIZE_PAD
-#define SDF_DZ (SDF_BLOCK_SIZE_PAD*SDF_BLOCK_SIZE_PAD)
-
-#define LOCKED 0x7FFFFFFF
-
-//! computes the (local) virtual voxel pos of an index; idx in [0;511]
-__device__ 
-int3 pdelinVoxelIndex(uint idx)	{
-	int x = idx % SDF_BLOCK_SIZE_PAD;
-	int y = (idx % (SDF_BLOCK_SIZE_PAD * SDF_BLOCK_SIZE_PAD)) / SDF_BLOCK_SIZE_PAD;
-	int z = idx / (SDF_BLOCK_SIZE_PAD * SDF_BLOCK_SIZE_PAD);	
-	return make_int3(x,y,z);
-}
-
-//! computes the linearized index of a local virtual voxel pos; pos in [0;7]^3
-__device__ 
-uint plinVoxelPos(const int3& virtualVoxelPos) {
-	return  
-		virtualVoxelPos.z * SDF_BLOCK_SIZE_PAD * SDF_BLOCK_SIZE_PAD +
-		virtualVoxelPos.y * SDF_BLOCK_SIZE_PAD +
-		virtualVoxelPos.x;
-}
-
-//! computes the linearized index of a local virtual voxel pos; pos in [0;7]^3
-__device__ 
-uint plinVoxelPos(int x, int y, int z) {
-	return  
-		z * SDF_BLOCK_SIZE_PAD * SDF_BLOCK_SIZE_PAD +
-		y * SDF_BLOCK_SIZE_PAD + x;
-}
-
-__device__  
-void deleteVoxel(ftl::voxhash::Voxel& v) {
-	v.color = make_uchar3(0,0,0);
-	v.weight = 0;
-	v.sdf = PINF;
-}
-
-__device__ inline int3 blockDelinear(const int3 &base, uint i) {
-	return make_int3(base.x + (i & 0x1), base.y + (i & 0x2), base.z + (i & 0x4));
-}
-
-__device__ inline uint blockLinear(int x, int y, int z) {
-	return x + (y << 1) + (z << 2);
-}
-
-__device__ inline bool getVoxel(uint *voxels, int ix) {
-	return voxels[ix/32] & (0x1 << (ix % 32));
-}
-
-__global__ void occupied_image_kernel(ftl::voxhash::HashData hashData, TextureObject<int> depth, SplatParams params) {
-	__shared__ uint voxels[16];
-	__shared__ ftl::voxhash::HashEntryHead block;
-
-	// Stride over all allocated blocks
-	for (int bi=blockIdx.x; bi<*hashData.d_hashCompactifiedCounter; bi+=NUM_CUDA_BLOCKS) {
-	__syncthreads();
-
-	const uint i = threadIdx.x;	//inside of an SDF block
-
-	if (i == 0) block = hashData.d_hashCompactified[bi]->head;
-	if (i < 16) {
-		voxels[i] = hashData.d_hashCompactified[bi]->voxels[i];
-		//valid[i] = hashData.d_hashCompactified[bi]->validity[i];
-	}
-
-	// Make sure all hash entries are cached
-	__syncthreads();
-
-	const int3 pi_base = hashData.SDFBlockToVirtualVoxelPos(make_int3(block.posXYZ));
-	const int3 vp = make_int3(hashData.delinearizeVoxelIndex(i));
-	const int3 pi = pi_base + vp;
-	const float3 worldPos = hashData.virtualVoxelPosToWorld(pi);
-
-	const bool v = getVoxel(voxels, i);
-
-	uchar4 color = make_uchar4(255,0,0,255);
-	bool is_surface = v; //((params.m_flags & ftl::render::kShowBlockBorders) && edgeX + edgeY + edgeZ >= 2);
-
-
-	// Only for surface voxels, work out screen coordinates
-	if (!is_surface) continue;
-
-	// TODO: For each original camera, render a new depth map
-
-	const float3 camPos = params.m_viewMatrix * worldPos;
-	const float2 screenPosf = params.camera.cameraToKinectScreenFloat(camPos);
-	const uint2 screenPos = make_uint2(make_int2(screenPosf)); //  + make_float2(0.5f, 0.5f)
-
-	//printf("Worldpos: %f,%f,%f\n", camPos.x, camPos.y, camPos.z);
-
-	if (camPos.z < params.camera.m_sensorDepthWorldMin) continue;
-
-	const unsigned int x = screenPos.x;
-	const unsigned int y = screenPos.y;
-	const int idepth = static_cast<int>(camPos.z * 1000.0f);
-
-	// See: Gunther et al. 2013. A GPGPU-based Pipeline for Accelerated Rendering of Point Clouds
-	if (x < depth.width() && y < depth.height()) {
-		atomicMin(&depth(x,y), idepth);
-	}
-
-	}  // Stride
-}
-
-__global__ void isosurface_image_kernel(ftl::voxhash::HashData hashData, TextureObject<int> depth, SplatParams params) {
-	// TODO:(Nick) Reduce bank conflicts by aligning these
-	__shared__ uint voxels[16];
-	//__shared__ uint valid[16];
-	__shared__ ftl::voxhash::HashEntryHead block;
-
-	// Stride over all allocated blocks
-	for (int bi=blockIdx.x; bi<*hashData.d_hashCompactifiedCounter; bi+=NUM_CUDA_BLOCKS) {
-	__syncthreads();
-
-	const uint i = threadIdx.x;	//inside of an SDF block
-
-	if (i == 0) block = hashData.d_hashCompactified[bi]->head;
-	if (i < 16) {
-		voxels[i] = hashData.d_hashCompactified[bi]->voxels[i];
-		//valid[i] = hashData.d_hashCompactified[bi]->validity[i];
-	}
-
-	// Make sure all hash entries are cached
-	__syncthreads();
-
-	const int3 pi_base = hashData.SDFBlockToVirtualVoxelPos(make_int3(block.posXYZ));
-	const int3 vp = make_int3(hashData.delinearizeVoxelIndex(i));
-	const int3 pi = pi_base + vp;
-	//const uint j = plinVoxelPos(vp);  // Padded linear index
-	const float3 worldPos = hashData.virtualVoxelPosToWorld(pi);
-
-	// Load distances and colours into shared memory + padding
-	//const ftl::voxhash::Voxel &v = hashData.d_SDFBlocks[block.ptr + i];
-	//voxels[j] = v;
-	const bool v = getVoxel(voxels, i);
-
-	//__syncthreads();
-
-	//if (voxels[j].weight == 0) continue;
-	if (vp.x == 7 || vp.y == 7 || vp.z == 7) continue;
-
-
-	int edgeX = (vp.x == 0 ) ? 1 : 0;
-	int edgeY = (vp.y == 0 ) ? 1 : 0;
-	int edgeZ = (vp.z == 0 ) ? 1 : 0;
-
-	uchar4 color = make_uchar4(255,0,0,255);
-	bool is_surface = v; //((params.m_flags & ftl::render::kShowBlockBorders) && edgeX + edgeY + edgeZ >= 2);
-	//if (is_surface) color = make_uchar4(255,(vp.x == 0 && vp.y == 0 && vp.z == 0) ? 255 : 0,0,255);
-
-	if (v) continue;  // !getVoxel(valid, i)
-
-	//if (vp.z == 7) voxels[j].color = make_uchar3(0,255,(voxels[j].sdf < 0.0f) ? 255 : 0);
-
-	// Identify surfaces through sign change. Since we only check in one direction
-	// it is fine to check for any sign change?
-
-
-#pragma unroll
-	for (int u=0; u<=1; u++) {
-		for (int v=0; v<=1; v++) {
-			for (int w=0; w<=1; w++) {
-				const int3 uvi = make_int3(vp.x+u,vp.y+v,vp.z+w);
-
-				// Skip these cases since we didn't load voxels properly
-				//if (uvi.x == 8 || uvi.z == 8 || uvi.y == 8) continue;
-
-				const bool vox = getVoxel(voxels, hashData.linearizeVoxelPos(uvi));
-				if (vox) { //getVoxel(valid, hashData.linearizeVoxelPos(uvi))) {
-					is_surface = true;
-					// Should break but is slower?
-				}
-			}
-		}
-	}
-
-	// Only for surface voxels, work out screen coordinates
-	if (!is_surface) continue;
-
-	// TODO: For each original camera, render a new depth map
-
-	const float3 camPos = params.m_viewMatrix * worldPos;
-	const float2 screenPosf = params.camera.cameraToKinectScreenFloat(camPos);
-	const uint2 screenPos = make_uint2(make_int2(screenPosf)); //  + make_float2(0.5f, 0.5f)
-
-	//printf("Worldpos: %f,%f,%f\n", camPos.x, camPos.y, camPos.z);
-
-	if (camPos.z < params.camera.m_sensorDepthWorldMin) continue;
-
-	// For this voxel in hash, get its screen position and check it is on screen
-	// Convert depth map to int by x1000 and use atomicMin
-	//const int pixsize = static_cast<int>((c_hashParams.m_virtualVoxelSize*params.camera.fx/(camPos.z*0.8f)))+1;  // Magic number increase voxel to ensure coverage
-
-	const unsigned int x = screenPos.x;
-	const unsigned int y = screenPos.y;
-	const int idepth = static_cast<int>(camPos.z * 1000.0f);
-
-	// See: Gunther et al. 2013. A GPGPU-based Pipeline for Accelerated Rendering of Point Clouds
-	if (x < depth.width() && y < depth.height()) {
-		atomicMin(&depth(x,y), idepth);
-	}
-
-	}  // Stride
-}
-
-void ftl::cuda::isosurface_point_image(const ftl::voxhash::HashData& hashData,
-			const TextureObject<int> &depth,
-			const SplatParams &params, cudaStream_t stream) {
-
-	const dim3 clear_gridSize((depth.width() + T_PER_BLOCK - 1)/T_PER_BLOCK, (depth.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
-	const dim3 clear_blockSize(T_PER_BLOCK, T_PER_BLOCK);
-
-	clearDepthKernel<<<clear_gridSize, clear_blockSize, 0, stream>>>(hashData, depth);
-
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-#endif
-
-	const unsigned int threadsPerBlock = SDF_BLOCK_SIZE*SDF_BLOCK_SIZE*SDF_BLOCK_SIZE;
-	const dim3 gridSize(NUM_CUDA_BLOCKS, 1);
-	const dim3 blockSize(threadsPerBlock, 1);
-
-	occupied_image_kernel<<<gridSize, blockSize, 0, stream>>>(hashData, depth, params);
-
-	cudaSafeCall( cudaGetLastError() );
-
-#ifdef _DEBUG
-	cudaSafeCall(cudaDeviceSynchronize());
-#endif
-}
-
-
diff --git a/applications/reconstruct/src/voxel_scene.cpp b/applications/reconstruct/src/voxel_scene.cpp
index cba6e61e9e9134845e59d71dd6e69d4ad7ae8beb..09067b1f2334e0190e27022b64d374e31532f8c2 100644
--- a/applications/reconstruct/src/voxel_scene.cpp
+++ b/applications/reconstruct/src/voxel_scene.cpp
@@ -1,7 +1,4 @@
 #include <ftl/voxel_scene.hpp>
-#include "compactors.hpp"
-#include "garbage.hpp"
-#include "integrators.hpp"
 #include "depth_camera_cuda.hpp"
 
 #include <opencv2/core/cuda_stream_accessor.hpp>
@@ -10,15 +7,16 @@
 
 using namespace ftl::voxhash;
 using ftl::rgbd::Source;
+using ftl::rgbd::Channel;
 using ftl::Configurable;
 using cv::Mat;
 using std::vector;
 
 #define 	SAFE_DELETE_ARRAY(a)   { delete [] (a); (a) = NULL; }
 
-extern "C" void resetCUDA(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams);
-extern "C" void resetHashBucketMutexCUDA(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, cudaStream_t);
-extern "C" void allocCUDA(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, int camid, const DepthCameraParams &depthCameraParams, cudaStream_t);
+//extern "C" void resetCUDA(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams);
+//extern "C" void resetHashBucketMutexCUDA(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, cudaStream_t);
+//extern "C" void allocCUDA(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, int camid, const DepthCameraParams &depthCameraParams, cudaStream_t);
 //extern "C" void fillDecisionArrayCUDA(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams, const DepthCameraData& depthCameraData);
 //extern "C" void compactifyHashCUDA(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams);
 //extern "C" unsigned int compactifyHashAllInOneCUDA(ftl::voxhash::HashData& hashData, const ftl::voxhash::HashParams& hashParams);
@@ -98,7 +96,7 @@ void SceneRep::addSource(ftl::rgbd::Source *src) {
 	auto &cam = cameras_.emplace_back();
 	cam.source = src;
 	cam.params.m_imageWidth = 0;
-	src->setChannel(ftl::rgbd::kChanDepth);
+	src->setChannel(Channel::Depth);
 }
 
 extern "C" void updateCUDACameraConstant(ftl::voxhash::DepthCameraCUDA *data, int count);
@@ -184,7 +182,7 @@ int SceneRep::upload() {
 		//if (i > 0) cudaSafeCall(cudaStreamSynchronize(cv::cuda::StreamAccessor::getStream(cameras_[i-1].stream)));
 
 		//allocate all hash blocks which are corresponding to depth map entries
-		if (value("voxels", false)) _alloc(i, cv::cuda::StreamAccessor::getStream(cam.stream));
+		//if (value("voxels", false)) _alloc(i, cv::cuda::StreamAccessor::getStream(cam.stream));
 
 		// Calculate normals
 	}
@@ -233,6 +231,10 @@ int SceneRep::upload(ftl::rgbd::FrameSet &fs) {
 
 	for (size_t i=0; i<cameras_.size(); ++i) {
 		auto &cam = cameras_[i];
+		auto &chan1 = fs.frames[i].get<cv::Mat>(Channel::Colour);
+		auto &chan2 = fs.frames[i].get<cv::Mat>(fs.sources[i]->getChannel());
+
+		auto test = fs.frames[i].createTexture<uchar4>(Channel::Flow, ftl::rgbd::Format<uchar4>(100,100));
 
 		// Get the RGB-Depth frame from input
 		Source *input = cam.source;
@@ -247,12 +249,12 @@ int SceneRep::upload(ftl::rgbd::FrameSet &fs) {
 
 		// Must be in RGBA for GPU
 		Mat rgbt, rgba;
-		cv::cvtColor(fs.channel1[i],rgbt, cv::COLOR_BGR2Lab);
+		cv::cvtColor(chan1,rgbt, cv::COLOR_BGR2Lab);
 		cv::cvtColor(rgbt,rgba, cv::COLOR_BGR2BGRA);
 
 		// Send to GPU and merge view into scene
 		//cam.gpu.updateParams(cam.params);
-		cam.gpu.updateData(fs.channel2[i], rgba, cam.stream);
+		cam.gpu.updateData(chan2, rgba, cam.stream);
 
 		//setLastRigidTransform(input->getPose().cast<float>());
 
@@ -262,7 +264,7 @@ int SceneRep::upload(ftl::rgbd::FrameSet &fs) {
 		//if (i > 0) cudaSafeCall(cudaStreamSynchronize(cv::cuda::StreamAccessor::getStream(cameras_[i-1].stream)));
 
 		//allocate all hash blocks which are corresponding to depth map entries
-		if (value("voxels", false)) _alloc(i, cv::cuda::StreamAccessor::getStream(cam.stream));
+		//if (value("voxels", false)) _alloc(i, cv::cuda::StreamAccessor::getStream(cam.stream));
 
 		// Calculate normals
 	}
@@ -297,7 +299,7 @@ void SceneRep::integrate() {
 
 void SceneRep::garbage() {
 	//_compactifyAllocated();
-	if (value("voxels", false)) _garbageCollect();
+	//if (value("voxels", false)) _garbageCollect();
 
 	//cudaSafeCall(cudaStreamSynchronize(integ_stream_));
 }
@@ -417,19 +419,19 @@ void SceneRep::_alloc(int camid, cudaStream_t stream) {
 	}
 	else {*/
 		//this version is faster, but it doesn't guarantee that all blocks are allocated (staggers alloc to the next frame)
-		resetHashBucketMutexCUDA(m_hashData, m_hashParams, stream);
-		allocCUDA(m_hashData, m_hashParams, camid, cameras_[camid].params, stream);
+		//resetHashBucketMutexCUDA(m_hashData, m_hashParams, stream);
+		//allocCUDA(m_hashData, m_hashParams, camid, cameras_[camid].params, stream);
 	//}
 }
 
 
 void SceneRep::_compactifyVisible(const DepthCameraParams &camera) { //const DepthCameraData& depthCameraData) {
-	ftl::cuda::compactifyOccupied(m_hashData, m_hashParams, integ_stream_);		//this version uses atomics over prefix sums, which has a much better performance
+	//ftl::cuda::compactifyOccupied(m_hashData, m_hashParams, integ_stream_);		//this version uses atomics over prefix sums, which has a much better performance
 	//m_hashData.updateParams(m_hashParams);	//make sure numOccupiedBlocks is updated on the GPU
 }
 
 void SceneRep::_compactifyAllocated() {
-	ftl::cuda::compactifyAllocated(m_hashData, m_hashParams, integ_stream_);		//this version uses atomics over prefix sums, which has a much better performance
+	//ftl::cuda::compactifyAllocated(m_hashData, m_hashParams, integ_stream_);		//this version uses atomics over prefix sums, which has a much better performance
 	//std::cout << "Occ blocks = " << m_hashParams.m_numOccupiedBlocks << std::endl;
 	//m_hashData.updateParams(m_hashParams);	//make sure numOccupiedBlocks is updated on the GPU
 }
@@ -439,7 +441,7 @@ void SceneRep::_compactifyAllocated() {
 	else ftl::cuda::integrateRegistration(m_hashData, m_hashParams, depthCameraData, depthCameraParams, integ_stream_);
 }*/
 
-extern "C" void bilateralFilterFloatMap(float* d_output, float* d_input, float sigmaD, float sigmaR, unsigned int width, unsigned int height);
+//extern "C" void bilateralFilterFloatMap(float* d_output, float* d_input, float sigmaD, float sigmaR, unsigned int width, unsigned int height);
 
 void SceneRep::_integrateDepthMaps() {
 	//cudaSafeCall(cudaDeviceSynchronize());
@@ -454,11 +456,11 @@ void SceneRep::_integrateDepthMaps() {
 		//ftl::cuda::hole_fill(*(cameras_[i].gpu.depth2_tex_), *(cameras_[i].gpu.depth_tex_), cameras_[i].params, integ_stream_);
 		//bilateralFilterFloatMap(cameras_[i].gpu.depth_tex_->devicePtr(), cameras_[i].gpu.depth3_tex_->devicePtr(), 3, 7, cameras_[i].gpu.depth_tex_->width(), cameras_[i].gpu.depth_tex_->height());
 	}
-	if (value("voxels", false)) ftl::cuda::integrateDepthMaps(m_hashData, m_hashParams, cameras_.size(), integ_stream_);
+	//if (value("voxels", false)) ftl::cuda::integrateDepthMaps(m_hashData, m_hashParams, cameras_.size(), integ_stream_);
 }
 
 void SceneRep::_garbageCollect() {
 	//ftl::cuda::garbageCollectIdentify(m_hashData, m_hashParams, integ_stream_);
-	resetHashBucketMutexCUDA(m_hashData, m_hashParams, integ_stream_);	//needed if linked lists are enabled -> for memeory deletion
-	ftl::cuda::garbageCollectFree(m_hashData, m_hashParams, integ_stream_);
+	//resetHashBucketMutexCUDA(m_hashData, m_hashParams, integ_stream_);	//needed if linked lists are enabled -> for memeory deletion
+	//ftl::cuda::garbageCollectFree(m_hashData, m_hashParams, integ_stream_);
 }
diff --git a/applications/vision/CMakeLists.txt b/applications/vision/CMakeLists.txt
index 684b195b5a2a8605e9bc3e04e629d21a35dcd545..1753488f3407b74518efad555ecdd87be54ba1e9 100644
--- a/applications/vision/CMakeLists.txt
+++ b/applications/vision/CMakeLists.txt
@@ -21,6 +21,6 @@ set_property(TARGET ftl-vision PROPERTY CUDA_SEPARABLE_COMPILATION OFF)
 endif()
 
 #target_include_directories(cv-node PUBLIC ${PROJECT_SOURCE_DIR}/include)
-target_link_libraries(ftl-vision ftlrgbd ftlcommon ftlctrl ftlrender ${OpenCV_LIBS} ${LIBSGM_LIBRARIES} ${CUDA_LIBRARIES} ftlnet)
+target_link_libraries(ftl-vision ftlrgbd ftlcommon ftlctrl ${OpenCV_LIBS} ${LIBSGM_LIBRARIES} ${CUDA_LIBRARIES} ftlnet)
 
 
diff --git a/applications/vision/src/main.cpp b/applications/vision/src/main.cpp
index 1ce6cb162c322d82a51988fe63a9256505283b12..ba07e90424db91c5cc36e18d0aa40a68109bddbd 100644
--- a/applications/vision/src/main.cpp
+++ b/applications/vision/src/main.cpp
@@ -19,7 +19,7 @@
 #include <opencv2/opencv.hpp>
 #include <ftl/rgbd.hpp>
 #include <ftl/middlebury.hpp>
-#include <ftl/display.hpp>
+//#include <ftl/display.hpp>
 #include <ftl/rgbd/streamer.hpp>
 #include <ftl/net/universe.hpp>
 #include <ftl/slave.hpp>
@@ -36,7 +36,7 @@
 
 using ftl::rgbd::Source;
 using ftl::rgbd::Camera;
-using ftl::Display;
+//using ftl::Display;
 using ftl::rgbd::Streamer;
 using ftl::net::Universe;
 using std::string;
@@ -87,7 +87,7 @@ static void run(ftl::Configurable *root) {
 
 	if (file != "") source->set("uri", file);
 	
-	Display *display = ftl::create<Display>(root, "display", "local");
+	//Display *display = ftl::create<Display>(root, "display", "local");
 	
 	Streamer *stream = ftl::create<Streamer>(root, "stream", net);
 	stream->add(source);
@@ -95,7 +95,7 @@ static void run(ftl::Configurable *root) {
 	net->start();
 
 	LOG(INFO) << "Running...";
-	if (display->hasDisplays()) {
+	/*if (display->hasDisplays()) {
 		stream->run();
 		while (ftl::running && display->active()) {
 			cv::Mat rgb, depth;
@@ -103,9 +103,9 @@ static void run(ftl::Configurable *root) {
 			if (!rgb.empty()) display->render(rgb, depth, source->parameters());
 			display->wait(10);
 		}
-	} else {
+	} else {*/
 		stream->run(true);
-	}
+	//}
 
 	LOG(INFO) << "Stopping...";
 	slave.stop();
@@ -115,7 +115,7 @@ static void run(ftl::Configurable *root) {
 	ftl::pool.stop();
 
 	delete stream;
-	delete display;
+	//delete display;
 	//delete source;  // TODO(Nick) Add ftl::destroy
 	delete net;
 }
diff --git a/components/codecs/include/ftl/codecs/encoder.hpp b/components/codecs/include/ftl/codecs/encoder.hpp
index 4adeb4b0d625661306321073fad9f84383ed2a42..560810b84060b3b062b426614da40836634fdee5 100644
--- a/components/codecs/include/ftl/codecs/encoder.hpp
+++ b/components/codecs/include/ftl/codecs/encoder.hpp
@@ -1,6 +1,7 @@
 #ifndef _FTL_CODECS_ENCODER_HPP_
 #define _FTL_CODECS_ENCODER_HPP_
 
+#include <ftl/cuda_util.hpp>
 #include <opencv2/opencv.hpp>
 #include <opencv2/core/cuda.hpp>
 
diff --git a/components/codecs/src/nvpipe_decoder.cpp b/components/codecs/src/nvpipe_decoder.cpp
index cbeced55e0bd07c1347523c5c682767a769055c0..0dda5884cc7bf156e7cb134795e6f0ff2c180130 100644
--- a/components/codecs/src/nvpipe_decoder.cpp
+++ b/components/codecs/src/nvpipe_decoder.cpp
@@ -2,7 +2,8 @@
 
 #include <loguru.hpp>
 
-#include <cuda_runtime.h>
+#include <ftl/cuda_util.hpp>
+//#include <cuda_runtime.h>
 
 using ftl::codecs::NvPipeDecoder;
 
diff --git a/components/codecs/src/nvpipe_encoder.cpp b/components/codecs/src/nvpipe_encoder.cpp
index 1060f0f2a581966cb7fe1b537999d3ab914f506b..42374bedecf95a749e842a3a2ed22b5226d6d390 100644
--- a/components/codecs/src/nvpipe_encoder.cpp
+++ b/components/codecs/src/nvpipe_encoder.cpp
@@ -2,7 +2,7 @@
 #include <loguru.hpp>
 #include <ftl/timer.hpp>
 #include <ftl/codecs/bitrates.hpp>
-#include <cuda_runtime.h>
+#include <ftl/cuda_util.hpp>
 
 using ftl::codecs::NvPipeEncoder;
 using ftl::codecs::bitrate_t;
@@ -30,8 +30,6 @@ void NvPipeEncoder::reset() {
 
 /* Check preset resolution is not better than actual resolution. */
 definition_t NvPipeEncoder::_verifiedDefinition(definition_t def, const cv::Mat &in) {
-	bool is_float = in.type() == CV_32F;
-
 	int height = ftl::codecs::getHeight(def);
 
 	// FIXME: Make sure this can't go forever
@@ -93,7 +91,7 @@ bool NvPipeEncoder::encode(const cv::Mat &in, definition_t odefinition, bitrate_
 
 bool NvPipeEncoder::_encoderMatch(const cv::Mat &in, definition_t def) {
     return ((in.type() == CV_32F && is_float_channel_) ||
-        (in.type() == CV_8UC3 && !is_float_channel_)) && current_definition_ == def;
+        ((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) {
diff --git a/components/codecs/src/opencv_encoder.cpp b/components/codecs/src/opencv_encoder.cpp
index 5dafabf29a64c14969504960da8af2bc2e27fd2a..028395b9e32865adb7a907d148308c9085588fa3 100644
--- a/components/codecs/src/opencv_encoder.cpp
+++ b/components/codecs/src/opencv_encoder.cpp
@@ -37,9 +37,8 @@ bool OpenCVEncoder::encode(const cv::Mat &in, definition_t definition, bitrate_t
 		tmp.convertTo(tmp, CV_16UC1, 1000);
 	}
 
-	// TODO: Choose these base upon resolution
-	chunk_count_ = 16;
-	chunk_dim_ = 4;
+	chunk_dim_ = (definition == definition_t::LD360) ? 1 : 4;
+	chunk_count_ = chunk_dim_ * chunk_dim_;
 	jobs_ = chunk_count_;
 
 	for (int i=0; i<chunk_count_; ++i) {
diff --git a/components/common/cpp/CMakeLists.txt b/components/common/cpp/CMakeLists.txt
index 8759e81039afede246d4f7e783531da98a1bc473..de0f7707c504447a16967625c2f01ab76e1218bb 100644
--- a/components/common/cpp/CMakeLists.txt
+++ b/components/common/cpp/CMakeLists.txt
@@ -21,7 +21,7 @@ target_include_directories(ftlcommon PUBLIC
 	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
 	$<INSTALL_INTERFACE:include>
 	PRIVATE src)
-target_link_libraries(ftlcommon Threads::Threads ${OS_LIBS} ${OpenCV_LIBS} ${PCL_LIBRARIES} ${URIPARSER_LIBRARIES})
+target_link_libraries(ftlcommon Threads::Threads ${OS_LIBS} ${OpenCV_LIBS} ${PCL_LIBRARIES} ${URIPARSER_LIBRARIES} ${CUDA_LIBRARIES})
 
 add_subdirectory(test)
 
diff --git a/components/common/cpp/include/ftl/config.h.in b/components/common/cpp/include/ftl/config.h.in
index 121abb0605ae912de254f91a4a5be665e7867fff..540b332035a2ad5f671a51ed80f0de31d60f1ce7 100644
--- a/components/common/cpp/include/ftl/config.h.in
+++ b/components/common/cpp/include/ftl/config.h.in
@@ -23,6 +23,7 @@
 #cmakedefine HAVE_REALSENSE
 #cmakedefine HAVE_NANOGUI
 #cmakedefine HAVE_LIBARCHIVE
+#cmakedefine HAVE_OPENVR
 #cmakedefine HAVE_NVPIPE
 
 extern const char *FTL_BRANCH;
diff --git a/components/common/cpp/include/ftl/cuda_common.hpp b/components/common/cpp/include/ftl/cuda_common.hpp
index 3f004d0c8ffefaf692b84106dea3810e223d70f3..70a6a4ad6d4dc0def715979f9eaf348e621d103e 100644
--- a/components/common/cpp/include/ftl/cuda_common.hpp
+++ b/components/common/cpp/include/ftl/cuda_common.hpp
@@ -2,12 +2,19 @@
 #define _FTL_CUDA_COMMON_HPP_
 
 #include <ftl/config.h>
+#include <ftl/traits.hpp>
 
 #if defined HAVE_CUDA
 
+#include <ftl/cuda_util.hpp>
 #include <opencv2/core/cuda.hpp>
 #include <opencv2/core/cuda/common.hpp>
 
+#ifndef __CUDACC__
+#include <loguru.hpp>
+#include <exception>
+#endif
+
 /* Grid stride loop macros */
 #define STRIDE_Y(I,N) int I = blockIdx.y * blockDim.y + threadIdx.y; I < N; I += blockDim.y * gridDim.y
 #define STRIDE_X(I,N) int I = blockIdx.x * blockDim.x + threadIdx.x; I < N; I += blockDim.x * gridDim.x
@@ -15,69 +22,151 @@
 namespace ftl {
 namespace cuda {
 
-/*template <typename T>
-class HisteresisTexture {
+bool initialise();
+
+bool hasCompute(int major, int minor);
+
+int deviceCount();
+
+/**
+ * Represent a CUDA texture object. Instances of this class can be used on both
+ * host and device. A texture object base cannot be constructed directly, it
+ * must be constructed via a template TextureObject class.
+ */
+class TextureObjectBase {
 	public:
-	HisteresisTexture();
-	~HisteresisTexture();
+	__host__ __device__ TextureObjectBase()
+			: texobj_(0), pitch_(0), pitch2_(0), width_(0), height_(0),
+			  ptr_(nullptr), needsfree_(false), needsdestroy_(false),
+			  cvType_(-1) {};
+	~TextureObjectBase();
+
+	// Remove ability to copy object directly, instead must use
+	// templated derivative TextureObject.
+	TextureObjectBase(const TextureObjectBase &)=delete;
+	TextureObjectBase &operator=(const TextureObjectBase &)=delete;
+
+	TextureObjectBase(TextureObjectBase &&);
+	TextureObjectBase &operator=(TextureObjectBase &&);
+
+	inline size_t pitch() const { return pitch_; }
+	inline size_t pixelPitch() const { return pitch2_; }
+	inline uchar *devicePtr() const { return ptr_; };
+	__host__ __device__ inline uchar *devicePtr(int v) const { return &ptr_[v*pitch_]; }
+	__host__ __device__ inline int width() const { return width_; }
+	__host__ __device__ inline int height() const { return height_; }
+	__host__ __device__ inline cudaTextureObject_t cudaTexture() const { return texobj_; }
+
+	void upload(const cv::Mat &, cudaStream_t stream=0);
+	void download(cv::Mat &, cudaStream_t stream=0) const;
 	
-	HisteresisTexture<T> &operator=(TextureObject<T> &t);
-};*/
+	__host__ void free();
 
+	inline int cvType() const { return cvType_; }
+	
+	protected:
+	cudaTextureObject_t texobj_;
+	size_t pitch_;
+	size_t pitch2_; 		// in T units
+	int width_;
+	int height_;
+	uchar *ptr_;			// Device memory pointer
+	bool needsfree_;		// We manage memory, so free it
+	bool needsdestroy_;		// The texture object needs to be destroyed
+	int cvType_;				// Used to validate casting
+};
+
+/**
+ * Create and manage CUDA texture objects with a particular pixel data type.
+ * Note: it is not possible to create texture objects for certain types,
+ * specificially for 3 channel types.
+ */
 template <typename T>
-class TextureObject {
+class TextureObject : public TextureObjectBase {
 	public:
-	__host__ __device__ TextureObject()
-			: texobj_(0), pitch_(0), pitch2_(0), width_(0), height_(0), ptr_(nullptr), needsfree_(false) {};
-	TextureObject(const cv::cuda::PtrStepSz<T> &d);
+	typedef T type;
+
+	static_assert((16u % sizeof(T)) == 0, "Channel format must be aligned with 16 bytes");
+
+	__host__ __device__ TextureObject() : TextureObjectBase() {};
+	explicit TextureObject(const cv::cuda::GpuMat &d);
+	explicit TextureObject(const cv::cuda::PtrStepSz<T> &d);
 	TextureObject(T *ptr, int pitch, int width, int height);
 	TextureObject(size_t width, size_t height);
 	TextureObject(const TextureObject<T> &t);
 	__host__ __device__ TextureObject(TextureObject<T> &&);
 	~TextureObject();
 
-	__host__ TextureObject<T> &operator=(const TextureObject<T> &);
+	TextureObject<T> &operator=(const TextureObject<T> &);
 	__host__ __device__ TextureObject<T> &operator=(TextureObject<T> &&);
-	
-	size_t pitch() const { return pitch_; }
-	size_t pixelPitch() const { return pitch2_; }
-	T *devicePtr() const { return ptr_; };
-	__host__ __device__ T *devicePtr(int v) const { return &ptr_[v*pitch2_]; }
-	__host__ __device__ int width() const { return width_; }
-	__host__ __device__ int height() const { return height_; }
-	__host__ __device__ cudaTextureObject_t cudaTexture() const { return texobj_; }
+
+	operator cv::cuda::GpuMat();
+
+	__host__ __device__ T *devicePtr() const { return (T*)(ptr_); };
+	__host__ __device__ T *devicePtr(int v) const { return &(T*)(ptr_)[v*pitch2_]; }
 
 	#ifdef __CUDACC__
 	__device__ inline T tex2D(int u, int v) const { return ::tex2D<T>(texobj_, u, v); }
 	__device__ inline T tex2D(float u, float v) const { return ::tex2D<T>(texobj_, u, v); }
 	#endif
 
-	__host__ __device__ inline const T &operator()(int u, int v) const { return ptr_[u+v*pitch2_]; }
-	__host__ __device__ inline T &operator()(int u, int v) { return ptr_[u+v*pitch2_]; }
+	__host__ __device__ inline const T &operator()(int u, int v) const { return reinterpret_cast<T*>(ptr_)[u+v*pitch2_]; }
+	__host__ __device__ inline T &operator()(int u, int v) { return reinterpret_cast<T*>(ptr_)[u+v*pitch2_]; }
 
-	void upload(const cv::Mat &, cudaStream_t stream=0);
-	void download(cv::Mat &, cudaStream_t stream=0) const;
-	
-	__host__ void free() {
-		if (needsfree_) {
-			if (texobj_ != 0) cudaSafeCall( cudaDestroyTextureObject (texobj_) );
-			if (ptr_) cudaFree(ptr_);
-			ptr_ = nullptr;
-			texobj_ = 0;
-		}
-	}
-	
-	private:
-	cudaTextureObject_t texobj_;
-	size_t pitch_;
-	size_t pitch2_; // in T units
-	int width_;
-	int height_;
-	T *ptr_;
-	bool needsfree_;
-	//bool needsdestroy_;
+	/**
+	 * Cast a base texture object to this type of texture object. If the
+	 * underlying pixel types do not match then a bad_cast exception is thrown.
+	 */
+	static TextureObject<T> &cast(TextureObjectBase &);
 };
 
+#ifndef __CUDACC__
+template <typename T>
+TextureObject<T> &TextureObject<T>::cast(TextureObjectBase &b) {
+	if (b.cvType() != ftl::traits::OpenCVType<T>::value) {
+		LOG(ERROR) << "Bad cast of texture object";
+		throw std::bad_cast();
+	}
+	return reinterpret_cast<TextureObject<T>&>(b);
+}
+
+/**
+ * Create a 2D array texture from an OpenCV GpuMat object.
+ */
+template <typename T>
+TextureObject<T>::TextureObject(const cv::cuda::GpuMat &d) {
+	// GpuMat must have correct data type
+	CHECK(d.type() == ftl::traits::OpenCVType<T>::value);
+
+	cudaResourceDesc resDesc;
+	memset(&resDesc, 0, sizeof(resDesc));
+	resDesc.resType = cudaResourceTypePitch2D;
+	resDesc.res.pitch2D.devPtr = d.data;
+	resDesc.res.pitch2D.pitchInBytes = d.step;
+	resDesc.res.pitch2D.desc = cudaCreateChannelDesc<T>();
+	resDesc.res.pitch2D.width = d.cols;
+	resDesc.res.pitch2D.height = d.rows;
+
+	cudaTextureDesc texDesc;
+	// cppcheck-suppress memsetClassFloat
+	memset(&texDesc, 0, sizeof(texDesc));
+	texDesc.readMode = cudaReadModeElementType;
+
+	cudaTextureObject_t tex = 0;
+	cudaSafeCall(cudaCreateTextureObject(&tex, &resDesc, &texDesc, NULL));
+	texobj_ = tex;
+	pitch_ = d.step;
+	pitch2_ = pitch_ / sizeof(T);
+	ptr_ = d.data;
+	width_ = d.cols;
+	height_ = d.rows;
+	needsfree_ = false;
+	cvType_ = ftl::traits::OpenCVType<T>::value;
+	//needsdestroy_ = true;
+}
+
+#endif  // __CUDACC__
+
 /**
  * Create a 2D array texture from an OpenCV GpuMat object.
  */
@@ -93,6 +182,7 @@ TextureObject<T>::TextureObject(const cv::cuda::PtrStepSz<T> &d) {
 	resDesc.res.pitch2D.height = d.rows;
 
 	cudaTextureDesc texDesc;
+	// cppcheck-suppress memsetClassFloat
 	memset(&texDesc, 0, sizeof(texDesc));
 	texDesc.readMode = cudaReadModeElementType;
 
@@ -105,6 +195,7 @@ TextureObject<T>::TextureObject(const cv::cuda::PtrStepSz<T> &d) {
 	width_ = d.cols;
 	height_ = d.rows;
 	needsfree_ = false;
+	cvType_ = ftl::traits::OpenCVType<T>::value;
 	//needsdestroy_ = true;
 }
 
@@ -124,6 +215,7 @@ TextureObject<T>::TextureObject(T *ptr, int pitch, int width, int height) {
 	resDesc.res.pitch2D.height = height;
 
 	cudaTextureDesc texDesc;
+	// cppcheck-suppress memsetClassFloat
 	memset(&texDesc, 0, sizeof(texDesc));
 	texDesc.readMode = cudaReadModeElementType;
 
@@ -136,6 +228,7 @@ TextureObject<T>::TextureObject(T *ptr, int pitch, int width, int height) {
 	width_ = width;
 	height_ = height;
 	needsfree_ = false;
+	cvType_ = ftl::traits::OpenCVType<T>::value;
 	//needsdestroy_ = true;
 }
 
@@ -156,6 +249,7 @@ TextureObject<T>::TextureObject(size_t width, size_t height) {
 		resDesc.res.pitch2D.height = height;
 
 		cudaTextureDesc texDesc;
+		// cppcheck-suppress memsetClassFloat
 		memset(&texDesc, 0, sizeof(texDesc));
 		texDesc.readMode = cudaReadModeElementType;
 		cudaCreateTextureObject(&tex, &resDesc, &texDesc, NULL);
@@ -166,6 +260,7 @@ TextureObject<T>::TextureObject(size_t width, size_t height) {
 	height_ = (int)height;
 	needsfree_ = true;
 	pitch2_ = pitch_ / sizeof(T);
+	cvType_ = ftl::traits::OpenCVType<T>::value;
 	//needsdestroy_ = true;
 }
 
@@ -177,6 +272,7 @@ TextureObject<T>::TextureObject(const TextureObject<T> &p) {
 	height_ = p.height_;
 	pitch_ = p.pitch_;
 	pitch2_ = pitch_ / sizeof(T);
+	cvType_ = ftl::traits::OpenCVType<T>::value;
 	needsfree_ = false;
 }
 
@@ -192,6 +288,7 @@ TextureObject<T>::TextureObject(TextureObject<T> &&p) {
 	p.texobj_ = 0;
 	p.needsfree_ = false;
 	p.ptr_ = nullptr;
+	cvType_ = ftl::traits::OpenCVType<T>::value;
 }
 
 template <typename T>
@@ -202,6 +299,7 @@ TextureObject<T> &TextureObject<T>::operator=(const TextureObject<T> &p) {
 	height_ = p.height_;
 	pitch_ = p.pitch_;
 	pitch2_ = pitch_ / sizeof(T);
+	cvType_ = ftl::traits::OpenCVType<T>::value;
 	needsfree_ = false;
 	return *this;
 }
@@ -218,6 +316,7 @@ TextureObject<T> &TextureObject<T>::operator=(TextureObject<T> &&p) {
 	p.texobj_ = 0;
 	p.needsfree_ = false;
 	p.ptr_ = nullptr;
+	cvType_ = ftl::traits::OpenCVType<T>::value;
 	return *this;
 }
 
@@ -228,7 +327,7 @@ TextureObject<T>::~TextureObject() {
 	free();
 }
 
-template <>
+/*template <>
 void TextureObject<uchar4>::upload(const cv::Mat &m, cudaStream_t stream);
 
 template <>
@@ -257,7 +356,7 @@ template <>
 void TextureObject<float4>::download(cv::Mat &m, cudaStream_t stream) const;
 
 template <>
-void TextureObject<uchar>::download(cv::Mat &m, cudaStream_t stream) const;
+void TextureObject<uchar>::download(cv::Mat &m, cudaStream_t stream) const;*/
 
 }
 }
diff --git a/applications/reconstruct/include/ftl/cuda_matrix_util.hpp b/components/common/cpp/include/ftl/cuda_matrix_util.hpp
similarity index 99%
rename from applications/reconstruct/include/ftl/cuda_matrix_util.hpp
rename to components/common/cpp/include/ftl/cuda_matrix_util.hpp
index e8d803e6fdf7b7c62d52d456ddce288bf554b233..4dd1db7fa8a58c84a40f9d93abd8cc0a492b2792 100644
--- a/applications/reconstruct/include/ftl/cuda_matrix_util.hpp
+++ b/components/common/cpp/include/ftl/cuda_matrix_util.hpp
@@ -1606,6 +1606,7 @@ inline __device__ __host__ matNxM<2, 2> matNxM<2, 2>::getInverse() const
 
 // To Matrix from floatNxN
 template<>
+// cppcheck-suppress syntaxError
 template<>
 inline __device__ __host__  matNxM<1, 1>::matNxM(const float& other)
 {
diff --git a/applications/reconstruct/include/ftl/cuda_operators.hpp b/components/common/cpp/include/ftl/cuda_operators.hpp
similarity index 99%
rename from applications/reconstruct/include/ftl/cuda_operators.hpp
rename to components/common/cpp/include/ftl/cuda_operators.hpp
index ae85cfbd785fb72f79225ae28278bf4862b680db..5fc84fbcb158bc599b8bca55e38035757a857648 100644
--- a/applications/reconstruct/include/ftl/cuda_operators.hpp
+++ b/components/common/cpp/include/ftl/cuda_operators.hpp
@@ -19,7 +19,8 @@
 #ifndef _FTL_CUDA_OPERATORS_HPP_
 #define _FTL_CUDA_OPERATORS_HPP_
 
-#include <cuda_runtime.h>
+//#include <cuda_runtime.h>
+#include <ftl/cuda_util.hpp>
 
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/applications/reconstruct/include/ftl/cuda_util.hpp b/components/common/cpp/include/ftl/cuda_util.hpp
similarity index 86%
rename from applications/reconstruct/include/ftl/cuda_util.hpp
rename to components/common/cpp/include/ftl/cuda_util.hpp
index bf018f07919fe52c71a1104a6bf029ce52f17b8e..e55c1430c1c013780e4b5d9fc0381492da281e49 100644
--- a/applications/reconstruct/include/ftl/cuda_util.hpp
+++ b/components/common/cpp/include/ftl/cuda_util.hpp
@@ -6,7 +6,12 @@
 #undef max
 #undef min
 
+#ifdef CPPCHECK
+#define __align__(A)
+#endif
+
 #include <cuda_runtime.h>
+#include <vector_types.h>
 #include <ftl/cuda_operators.hpp>
 
 // Enable run time assertion checking in kernel code
diff --git a/components/common/cpp/include/ftl/exception.hpp b/components/common/cpp/include/ftl/exception.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6719158bba5f5f53abfcdeb0fb39a5b5dda0d5f2
--- /dev/null
+++ b/components/common/cpp/include/ftl/exception.hpp
@@ -0,0 +1,19 @@
+#ifndef _FTL_EXCEPTION_HPP_
+#define _FTL_EXCEPTION_HPP_
+
+namespace ftl {
+class exception : public std::exception
+{
+	public:
+	explicit exception(const char *msg) : msg_(msg) {};
+
+	const char * what () const throw () {
+    	return msg_;
+    }
+
+	private:
+	const char *msg_;
+};
+}
+
+#endif  // _FTL_EXCEPTION_HPP_
diff --git a/components/common/cpp/include/ftl/timer.hpp b/components/common/cpp/include/ftl/timer.hpp
index dad98a704dd127c23b961bd7429da14253399db3..97776c2c3ee9bcbb62b8d81909d87a7dc43ecd28 100644
--- a/components/common/cpp/include/ftl/timer.hpp
+++ b/components/common/cpp/include/ftl/timer.hpp
@@ -29,7 +29,9 @@ enum timerlevel_t {
  * a destructor, for example.
  */
 struct TimerHandle {
-	const int id = -1;
+	TimerHandle() : id_(-1) {}
+	explicit TimerHandle(int i) : id_(i) {}
+	TimerHandle(const TimerHandle &t) : id_(t.id()) {}
 
 	/**
 	 * Cancel the timer job. If currently executing it will block and wait for
@@ -52,7 +54,12 @@ struct TimerHandle {
 	/**
 	 * Allow copy assignment.
 	 */
-	TimerHandle &operator=(const TimerHandle &h) { const_cast<int&>(id) = h.id; return *this; }
+	TimerHandle &operator=(const TimerHandle &h) { id_ = h.id(); return *this; }
+
+	inline int id() const { return id_; }
+
+	private:
+	int id_;
 };
 
 int64_t get_time();
diff --git a/components/common/cpp/include/ftl/traits.hpp b/components/common/cpp/include/ftl/traits.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6ac54e8e66f6a12aa13e39e1228f5cf17ca69312
--- /dev/null
+++ b/components/common/cpp/include/ftl/traits.hpp
@@ -0,0 +1,44 @@
+#ifndef _FTL_TRAITS_HPP_
+#define _FTL_TRAITS_HPP_
+
+#include <opencv2/core.hpp>
+#include <ftl/cuda_util.hpp>
+
+namespace ftl {
+namespace traits {
+
+template <typename T>
+struct AlwaysFalse : std::false_type {};
+
+template <typename T> struct OpenCVType {
+	static_assert(AlwaysFalse<T>::value, "Not a valid format type");
+};
+template <> struct OpenCVType<uchar> { static const int value = CV_8UC1; };
+template <> struct OpenCVType<uchar2> { static const int value = CV_8UC2; };
+template <> struct OpenCVType<uchar3> { static const int value = CV_8UC3; };
+template <> struct OpenCVType<uchar4> { static const int value = CV_8UC4; };
+template <> struct OpenCVType<char> { static const int value = CV_8SC1; };
+template <> struct OpenCVType<char2> { static const int value = CV_8SC2; };
+template <> struct OpenCVType<char3> { static const int value = CV_8SC3; };
+template <> struct OpenCVType<char4> { static const int value = CV_8SC4; };
+template <> struct OpenCVType<ushort> { static const int value = CV_16UC1; };
+template <> struct OpenCVType<ushort2> { static const int value = CV_16UC2; };
+template <> struct OpenCVType<ushort3> { static const int value = CV_16UC3; };
+template <> struct OpenCVType<ushort4> { static const int value = CV_16UC4; };
+template <> struct OpenCVType<short> { static const int value = CV_16SC1; };
+template <> struct OpenCVType<short2> { static const int value = CV_16SC2; };
+template <> struct OpenCVType<short3> { static const int value = CV_16SC3; };
+template <> struct OpenCVType<short4> { static const int value = CV_16SC4; };
+template <> struct OpenCVType<int> { static const int value = CV_32SC1; };
+template <> struct OpenCVType<int2> { static const int value = CV_32SC2; };
+template <> struct OpenCVType<int3> { static const int value = CV_32SC3; };
+template <> struct OpenCVType<int4> { static const int value = CV_32SC4; };
+template <> struct OpenCVType<float> { static const int value = CV_32FC1; };
+template <> struct OpenCVType<float2> { static const int value = CV_32FC2; };
+template <> struct OpenCVType<float3> { static const int value = CV_32FC3; };
+template <> struct OpenCVType<float4> { static const int value = CV_32FC4; };
+
+}
+}
+
+#endif  // _FTL_TRAITS_HPP_
diff --git a/components/common/cpp/src/configuration.cpp b/components/common/cpp/src/configuration.cpp
index ef33ab93a1c93dc5c74ef6787cb47bceab49f71e..8a3849e8ee8799119f3d15700dc277e1dece7654 100644
--- a/components/common/cpp/src/configuration.cpp
+++ b/components/common/cpp/src/configuration.cpp
@@ -20,6 +20,7 @@
 #include <ftl/uri.hpp>
 #include <ftl/threads.hpp>
 #include <ftl/timer.hpp>
+#include <ftl/cuda_common.hpp>
 
 #include <fstream>
 #include <string>
@@ -457,7 +458,7 @@ Configurable *ftl::config::configure(ftl::config::json_t &cfg) {
 	loguru::g_preamble_uptime = false;
 	loguru::g_preamble_thread = false;
 	int argc = 1;
-	const char *argv[] = {"d",0};
+	const char *argv[]{"d",0};
 	loguru::init(argc, const_cast<char**>(argv), "--verbosity");
 
 	config_index.clear();
@@ -519,6 +520,9 @@ Configurable *ftl::config::configure(int argc, char **argv, const std::string &r
 	// Some global settings
 	ftl::timer::setInterval(1000 / rootcfg->value("fps",20));
 
+	// Check CUDA
+	ftl::cuda::initialise();
+
 	int pool_size = rootcfg->value("thread_pool_factor", 2.0f)*std::thread::hardware_concurrency();
 	if (pool_size != ftl::pool.size()) ftl::pool.resize(pool_size);
 
diff --git a/components/common/cpp/src/cuda_common.cpp b/components/common/cpp/src/cuda_common.cpp
index 571d6816b63413163471ef9006ae1d28031511fd..b29c1df08ba14d61a17d8b7afce290b353c25ce6 100644
--- a/components/common/cpp/src/cuda_common.cpp
+++ b/components/common/cpp/src/cuda_common.cpp
@@ -1,8 +1,106 @@
 #include <ftl/cuda_common.hpp>
 
-using ftl::cuda::TextureObject;
+using ftl::cuda::TextureObjectBase;
 
-template <>
+static int dev_count = 0;
+static std::vector<cudaDeviceProp> properties;
+
+bool ftl::cuda::initialise() {
+	// Do an initial CUDA check
+	cudaSafeCall(cudaGetDeviceCount(&dev_count));
+	CHECK_GE(dev_count, 1) << "No CUDA devices found";
+
+	LOG(INFO) << "CUDA Devices (" << dev_count << "):";
+
+	properties.resize(dev_count);
+	for (int i=0; i<dev_count; i++) {
+		cudaSafeCall(cudaGetDeviceProperties(&properties[i], i));
+		LOG(INFO) << " - " << properties[i].name;
+	}
+
+	return true;
+}
+
+bool ftl::cuda::hasCompute(int major, int minor) {
+	int dev = -1;
+	cudaSafeCall(cudaGetDevice(&dev));
+
+	if (dev > 0) {
+		return properties[dev].major > major ||
+			(properties[dev].major == major && properties[dev].minor >= minor);
+	}
+	return false;
+}
+
+int ftl::cuda::deviceCount() {
+	return dev_count;
+}
+
+TextureObjectBase::~TextureObjectBase() {
+	free();
+}
+
+TextureObjectBase::TextureObjectBase(TextureObjectBase &&o) {
+	needsfree_ = o.needsfree_;
+	needsdestroy_ = o.needsdestroy_;
+	ptr_ = o.ptr_;
+	texobj_ = o.texobj_;
+	cvType_ = o.cvType_;
+	width_ = o.width_;
+	height_ = o.height_;
+	pitch_ = o.pitch_;
+	pitch2_ = o.pitch2_;
+
+	o.ptr_ = nullptr;
+	o.needsfree_ = false;
+	o.texobj_ = 0;
+	o.needsdestroy_ = false;
+}
+
+TextureObjectBase &TextureObjectBase::operator=(TextureObjectBase &&o) {
+	free();
+
+	needsfree_ = o.needsfree_;
+	needsdestroy_ = o.needsdestroy_;
+	ptr_ = o.ptr_;
+	texobj_ = o.texobj_;
+	cvType_ = o.cvType_;
+	width_ = o.width_;
+	height_ = o.height_;
+	pitch_ = o.pitch_;
+	pitch2_ = o.pitch2_;
+
+	o.ptr_ = nullptr;
+	o.needsfree_ = false;
+	o.texobj_ = 0;
+	o.needsdestroy_ = false;
+	return *this;
+}
+
+void TextureObjectBase::free() {
+	if (needsfree_) {
+		if (texobj_ != 0) cudaSafeCall( cudaDestroyTextureObject (texobj_) );
+		if (ptr_) cudaFree(ptr_);
+		ptr_ = nullptr;
+		texobj_ = 0;
+		cvType_ = -1;
+	} else if (needsdestroy_) {
+		if (texobj_ != 0) cudaSafeCall( cudaDestroyTextureObject (texobj_) );
+		texobj_ = 0;
+		cvType_ = -1;
+	}
+}
+
+void TextureObjectBase::upload(const cv::Mat &m, cudaStream_t stream) {
+    cudaSafeCall(cudaMemcpy2DAsync(devicePtr(), pitch(), m.data, m.step, m.cols * m.elemSize(), m.rows, cudaMemcpyHostToDevice, stream));
+}
+
+void TextureObjectBase::download(cv::Mat &m, cudaStream_t stream) const {
+	m.create(height(), width(), cvType_);
+	cudaSafeCall(cudaMemcpy2DAsync(m.data, m.step, devicePtr(), pitch(), m.cols * m.elemSize(), m.rows, cudaMemcpyDeviceToHost, stream));
+}
+
+/*template <>
 void TextureObject<uchar4>::upload(const cv::Mat &m, cudaStream_t stream) {
     cudaSafeCall(cudaMemcpy2DAsync(devicePtr(), pitch(), m.data, m.step, m.cols * sizeof(uchar4), m.rows, cudaMemcpyHostToDevice, stream));
 }
@@ -56,4 +154,4 @@ template <>
 void TextureObject<uchar>::download(cv::Mat &m, cudaStream_t stream) const {
 	m.create(height(), width(), CV_8UC1);
 	cudaSafeCall(cudaMemcpy2DAsync(m.data, m.step, devicePtr(), pitch(), m.cols * sizeof(uchar), m.rows, cudaMemcpyDeviceToHost, stream));
-}
+}*/
diff --git a/components/common/cpp/src/timer.cpp b/components/common/cpp/src/timer.cpp
index d6d8441c65e401a021ceb68dc3fbf0eea0cfc4df..328006ab82041d9613ab9ef7847d74727e3aa7f2 100644
--- a/components/common/cpp/src/timer.cpp
+++ b/components/common/cpp/src/timer.cpp
@@ -30,9 +30,10 @@ struct TimerJob {
 	int id;
 	function<bool(int64_t)> job;
 	volatile bool active;
-	bool paused;
-	int multiplier;
-	int countdown;
+	// TODO: (Nick) Implement richer forms of timer
+	//bool paused;
+	//int multiplier;
+	//int countdown;
 	std::string name;
 };
 
@@ -105,12 +106,12 @@ void ftl::timer::setClockAdjustment(int64_t ms) {
 }
 
 const TimerHandle ftl::timer::add(timerlevel_t l, const std::function<bool(int64_t ts)> &f) {
-	if (l < 0 || l >= kTimerMAXLEVEL) return {-1};
+	if (l < 0 || l >= kTimerMAXLEVEL) return {};
 
 	UNIQUE_LOCK(mtx, lk);
 	int newid = last_id++;
-	jobs[l].push_back({newid, f, false, false, 0, 0, "NoName"});
-	return {newid};
+	jobs[l].push_back({newid, f, false, "NoName"});
+	return TimerHandle(newid);
 }
 
 static void removeJob(int id) {
@@ -217,7 +218,7 @@ void ftl::timer::reset() {
 // ===== TimerHandle ===========================================================
 
 void ftl::timer::TimerHandle::cancel() const {
-	removeJob(id);
+	removeJob(id());
 }
 
 void ftl::timer::TimerHandle::pause() const {
diff --git a/components/common/cpp/src/uri.cpp b/components/common/cpp/src/uri.cpp
index 0db7a4af505f28bd162d81eed763241ed6f5ff9d..90fb33522c1c2d701fac47b0a6d43a5a6afee3a7 100644
--- a/components/common/cpp/src/uri.cpp
+++ b/components/common/cpp/src/uri.cpp
@@ -41,8 +41,9 @@ void URI::_parse(uri_t puri) {
 	// NOTE: Non-standard additions to allow for Unix style relative file names.
 	if (suri[0] == '.') {
 		char cwdbuf[1024];
-		getcwd(cwdbuf, 1024);
-		suri = string("file://") + string(cwdbuf) + suri.substr(1);
+		if (getcwd(cwdbuf, 1024)) {
+			suri = string("file://") + string(cwdbuf) + suri.substr(1);
+		}
 	} else if (suri[0] == '~') {
 #ifdef WIN32
 		suri = string("file://") + string(std::getenv("HOMEDRIVE")) + string(std::getenv("HOMEPATH")) + suri.substr(1);
diff --git a/components/common/cpp/test/CMakeLists.txt b/components/common/cpp/test/CMakeLists.txt
index 2ef8e4338ae4e5bb3b39f4eaf8c88ec343fb1b95..f9c1773b92718fc79eb1e26c1d63c7668f12fa53 100644
--- a/components/common/cpp/test/CMakeLists.txt
+++ b/components/common/cpp/test/CMakeLists.txt
@@ -7,12 +7,13 @@ add_executable(configurable_unit
 	../src/configuration.cpp
 	../src/loguru.cpp
 	../src/ctpl_stl.cpp
+	../src/cuda_common.cpp
 	./configurable_unit.cpp
 )
 target_include_directories(configurable_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
 target_link_libraries(configurable_unit
 	${URIPARSER_LIBRARIES}
-	Threads::Threads ${OS_LIBS})
+	Threads::Threads ${OS_LIBS} ${OpenCV_LIBS} ${CUDA_LIBRARIES})
 
 ### URI ########################################################################
 add_executable(uri_unit
diff --git a/components/common/cpp/test/timer_unit.cpp b/components/common/cpp/test/timer_unit.cpp
index c1d6776299969bb55fe3d9b9c9a91bd3d35430d6..6cdea157e9228b920ce2220c0122c8dcad9cf76e 100644
--- a/components/common/cpp/test/timer_unit.cpp
+++ b/components/common/cpp/test/timer_unit.cpp
@@ -22,7 +22,7 @@ TEST_CASE( "Timer::add() High Precision Accuracy" ) {
 			return true;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::start(true);
 		REQUIRE( didrun == true );
@@ -40,7 +40,7 @@ TEST_CASE( "Timer::add() High Precision Accuracy" ) {
 			return true;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::start(true);
 		REQUIRE( didrun == true );
@@ -57,7 +57,7 @@ TEST_CASE( "Timer::add() High Precision Accuracy" ) {
 			return true;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::add(ftl::timer::kTimerHighPrecision, [&didrun](int64_t ts) {
 			didrun[1] = true;
@@ -90,7 +90,7 @@ TEST_CASE( "Timer::add() Idle10 job" ) {
 			return true;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::start(true);
 		REQUIRE( didrun == true );
@@ -108,7 +108,7 @@ TEST_CASE( "Timer::add() Idle10 job" ) {
 			return true;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::start(true);
 		REQUIRE( didrun == true );
@@ -125,7 +125,7 @@ TEST_CASE( "Timer::add() Idle10 job" ) {
 			return false;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::start(true);
 		REQUIRE( didrun == true );
@@ -145,7 +145,7 @@ TEST_CASE( "Timer::add() Main job" ) {
 			return true;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::start(true);
 		REQUIRE( didrun == true );
@@ -163,7 +163,7 @@ TEST_CASE( "Timer::add() Main job" ) {
 			return true;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::start(true);
 		REQUIRE( didrun == true );
@@ -182,7 +182,7 @@ TEST_CASE( "Timer::add() Main job" ) {
 			return true;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::add(ftl::timer::kTimerMain, [&job2](int64_t ts) {
 			job2++;
@@ -204,7 +204,7 @@ TEST_CASE( "Timer::add() Main job" ) {
 			return false;
 		});
 
-		REQUIRE( (rc.id >= 0) );
+		REQUIRE( (rc.id() >= 0) );
 
 		ftl::timer::start(true);
 		REQUIRE( didrun == true );
@@ -224,7 +224,7 @@ TEST_CASE( "TimerHandle::cancel()" ) {
 		});
 
 		// Fake Handle
-		ftl::timer::TimerHandle h = {44};
+		ftl::timer::TimerHandle h(44);
 		h.cancel();
 		ftl::timer::start(true);
 		REQUIRE( didjob );
diff --git a/components/net/cpp/include/ftl/net/peer.hpp b/components/net/cpp/include/ftl/net/peer.hpp
index 2c3e1fd636edd16772b3bf44ecb0e15fefa3b16b..976155ac94e323b112306f4fa675512ff3d23aca 100644
--- a/components/net/cpp/include/ftl/net/peer.hpp
+++ b/components/net/cpp/include/ftl/net/peer.hpp
@@ -6,6 +6,7 @@
 #endif
 
 #include <ftl/net/common.hpp>
+#include <ftl/exception.hpp>
 
 //#define GLOG_NO_ABBREVIATED_SEVERITIES
 #include <loguru.hpp>
@@ -343,7 +344,8 @@ R Peer::call(const std::string &name, ARGS... args) {
 	
 	if (!hasreturned) {
 		cancelCall(id);
-		throw 1;
+		LOG(ERROR) << "RPC Timeout: " << name;
+		throw ftl::exception("RPC failed with timeout");
 	}
 	
 	return result;
diff --git a/components/net/cpp/include/ftl/net/universe.hpp b/components/net/cpp/include/ftl/net/universe.hpp
index b4419b1f7fc713259b0d9f1a14f00ff12332dd6b..29680c601c19ba37ff20771659ef379ce3511631 100644
--- a/components/net/cpp/include/ftl/net/universe.hpp
+++ b/components/net/cpp/include/ftl/net/universe.hpp
@@ -378,7 +378,7 @@ R Universe::call(const ftl::UUID &pid, const std::string &name, ARGS... args) {
 	if (p == nullptr || !p->isConnected()) {
 		if (p == nullptr) DLOG(WARNING) << "Attempting to call an unknown peer : " << pid.to_string();
 		else DLOG(WARNING) << "Attempting to call an disconnected peer : " << pid.to_string();
-		throw -1;
+		throw ftl::exception("Calling disconnected peer");
 	}
 	return p->call<R>(name, args...);
 }
diff --git a/components/net/cpp/src/dispatcher.cpp b/components/net/cpp/src/dispatcher.cpp
index 3231b8ddc4604c7929a04c5c33a36385e1bd94ea..7a5df5091235dc081c0c9e51c199dcca5e7f0f31 100644
--- a/components/net/cpp/src/dispatcher.cpp
+++ b/components/net/cpp/src/dispatcher.cpp
@@ -2,6 +2,7 @@
 #include <loguru.hpp>
 #include <ftl/net/dispatcher.hpp>
 #include <ftl/net/peer.hpp>
+#include <ftl/exception.hpp>
 #include <iostream>
 
 using ftl::net::Peer;
@@ -88,13 +89,6 @@ void ftl::net::Dispatcher::dispatch_call(Peer &s, const msgpack::object &msg) {
 				std::stringstream buf;
 				msgpack::pack(buf, res_obj);			
 				s.send("__return__", buf.str());*/
-			} catch (int e) {
-				//throw;
-				LOG(ERROR) << "Exception when attempting to call RPC (" << e << ")";
-		        /*response_t res_obj = std::make_tuple(1,id,msgpack::object(e),msgpack::object());
-				std::stringstream buf;
-				msgpack::pack(buf, res_obj);			
-				s.send("__return__", buf.str());*/
 			}
 		} else {
 			LOG(WARNING) << "No binding found for " << name;
@@ -150,7 +144,7 @@ void ftl::net::Dispatcher::enforce_arg_count(std::string const &func, std::size_
                                    std::size_t expected) {
     if (found != expected) {
     	LOG(FATAL) << "RPC argument missmatch for '" << func << "' - " << found << " != " << expected;
-        throw -1;
+        throw ftl::exception("RPC argument missmatch");
     }
 }
 
@@ -158,7 +152,7 @@ void ftl::net::Dispatcher::enforce_unique_name(std::string const &func) {
     auto pos = funcs_.find(func);
     if (pos != end(funcs_)) {
     	LOG(FATAL) << "RPC non unique binding for '" << func << "'";
-        throw -1;
+        throw ftl::exception("RPC binding not unique");
     }
 }
 
diff --git a/components/net/cpp/src/peer.cpp b/components/net/cpp/src/peer.cpp
index 25059b5473c154d31ea6b31950a3e5e5eff02de8..0335ca67f3a284294b4973c83aea62100d56a5c2 100644
--- a/components/net/cpp/src/peer.cpp
+++ b/components/net/cpp/src/peer.cpp
@@ -424,50 +424,44 @@ void Peer::data() {
 		//LOG(INFO) << "Pool size: " << ftl::pool.q_size();
 
 		int rc=0;
-		int c=0;
 
-		//do {
-			recv_buf_.reserve_buffer(kMaxMessage);
+		recv_buf_.reserve_buffer(kMaxMessage);
 
-			if (recv_buf_.buffer_capacity() < (kMaxMessage / 10)) {
-				LOG(WARNING) << "Net buffer at capacity";
-				return;
-			}
-
-			int cap = recv_buf_.buffer_capacity();
-			auto buf = recv_buf_.buffer();
-			lk.unlock();
+		if (recv_buf_.buffer_capacity() < (kMaxMessage / 10)) {
+			LOG(WARNING) << "Net buffer at capacity";
+			return;
+		}
 
-			/*#ifndef WIN32
-			int n;
-			unsigned int m = sizeof(n);
-			getsockopt(sock_,SOL_SOCKET,SO_RCVBUF,(void *)&n, &m);
+		int cap = recv_buf_.buffer_capacity();
+		auto buf = recv_buf_.buffer();
+		lk.unlock();
 
-			int pending;
-			ioctl(sock_, SIOCINQ, &pending);
-			if (pending > 100000) LOG(INFO) << "Buffer usage: " << float(pending) / float(n);
-			#endif*/
-			rc = ftl::net::internal::recv(sock_, buf, cap, 0);
+		/*#ifndef WIN32
+		int n;
+		unsigned int m = sizeof(n);
+		getsockopt(sock_,SOL_SOCKET,SO_RCVBUF,(void *)&n, &m);
 
-			if (rc >= cap-1) {
-				LOG(WARNING) << "More than buffers worth of data received"; 
-			}
-			if (cap < (kMaxMessage / 10)) LOG(WARNING) << "NO BUFFER";
-
-			if (rc == 0) {
-				close();
-				return;
-			} else if (rc < 0 && c == 0) {
-				socketError();
-				return;
-			}
+		int pending;
+		ioctl(sock_, SIOCINQ, &pending);
+		if (pending > 100000) LOG(INFO) << "Buffer usage: " << float(pending) / float(n);
+		#endif*/
+		rc = ftl::net::internal::recv(sock_, buf, cap, 0);
 
-			//if (rc == -1) break;
-			++c;
-			
-			lk.lock();
-			recv_buf_.buffer_consumed(rc);
-		//} while (rc > 0);
+		if (rc >= cap-1) {
+			LOG(WARNING) << "More than buffers worth of data received"; 
+		}
+		if (cap < (kMaxMessage / 10)) LOG(WARNING) << "NO BUFFER";
+
+		if (rc == 0) {
+			close();
+			return;
+		} else if (rc < 0) {
+			socketError();
+			return;
+		}
+		
+		lk.lock();
+		recv_buf_.buffer_consumed(rc);
 
 		//auto end = std::chrono::high_resolution_clock::now();
 		//int64_t endts = std::chrono::time_point_cast<std::chrono::milliseconds>(end).time_since_epoch().count();
diff --git a/components/renderers/cpp/CMakeLists.txt b/components/renderers/cpp/CMakeLists.txt
index 33f910ca0342096bb551b430374776a86de89b2f..b575721587262e2e468d6cb48cf8c44c6771e6fc 100644
--- a/components/renderers/cpp/CMakeLists.txt
+++ b/components/renderers/cpp/CMakeLists.txt
@@ -1,6 +1,7 @@
 add_library(ftlrender
-	src/display.cpp
-	src/rgbd_display.cpp
+	src/splat_render.cpp
+	src/splatter.cu
+	src/points.cu
 )
 
 # These cause errors in CI build and are being removed from PCL in newer versions
@@ -11,6 +12,6 @@ target_include_directories(ftlrender PUBLIC
 	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
 	$<INSTALL_INTERFACE:include>
 	PRIVATE src)
-target_link_libraries(ftlrender ftlrgbd ftlcommon ftlnet Eigen3::Eigen Threads::Threads glog::glog ${OpenCV_LIBS} ${PCL_LIBRARIES})
+target_link_libraries(ftlrender ftlrgbd ftlcommon Eigen3::Eigen Threads::Threads ${OpenCV_LIBS})
 
 #ADD_SUBDIRECTORY(test)
diff --git a/applications/reconstruct/src/splat_render_cuda.hpp b/components/renderers/cpp/include/ftl/cuda/intersections.hpp
similarity index 56%
rename from applications/reconstruct/src/splat_render_cuda.hpp
rename to components/renderers/cpp/include/ftl/cuda/intersections.hpp
index e60fc8c27c0d39ef8798803a526891c5da2fca62..9cfdbc2544d9c1bd32c9f5e12a0a161f45c50d54 100644
--- a/applications/reconstruct/src/splat_render_cuda.hpp
+++ b/components/renderers/cpp/include/ftl/cuda/intersections.hpp
@@ -1,11 +1,9 @@
-#ifndef _FTL_RECONSTRUCTION_SPLAT_CUDA_HPP_
-#define _FTL_RECONSTRUCTION_SPLAT_CUDA_HPP_
+#ifndef _FTL_CUDA_INTERSECTIONS_HPP_
+#define _FTL_CUDA_INTERSECTIONS_HPP_
 
-#include <ftl/depth_camera.hpp>
-#include <ftl/voxel_hash.hpp>
-//#include <ftl/ray_cast_util.hpp>
-
-#include "splat_params.hpp"
+#ifndef PINF
+#define PINF __int_as_float(0x7f800000)
+#endif
 
 namespace ftl {
 namespace cuda {
@@ -84,45 +82,7 @@ __device__ inline float intersectDistance(const float3 &n, const float3 &p0, con
      return PINF; 
 }
 
-/**
- * NOTE: Not strictly isosurface currently since it includes the internals
- * of objects up to at most truncation depth.
- */
-void isosurface_point_image(const ftl::voxhash::HashData& hashData,
-			const ftl::cuda::TextureObject<int> &depth,
-			const ftl::render::SplatParams &params, cudaStream_t stream);
-
-//void isosurface_point_image_stereo(const ftl::voxhash::HashData& hashData,
-//		const ftl::voxhash::HashParams& hashParams,
-//		const RayCastData &rayCastData, const RayCastParams &params,
-//		cudaStream_t stream);
-
-// TODO: isosurface_point_cloud
-
-void splat_points(const ftl::cuda::TextureObject<int> &depth_in,
-		const ftl::cuda::TextureObject<uchar4> &colour_in,
-		const ftl::cuda::TextureObject<float4> &normal_in,
-		const ftl::cuda::TextureObject<float> &depth_out,
-		const ftl::cuda::TextureObject<uchar4> &colour_out, const ftl::render::SplatParams &params, cudaStream_t stream);
-
-void dibr(const ftl::cuda::TextureObject<int> &depth_out,
-		const ftl::cuda::TextureObject<uchar4> &colour_out,
-		const ftl::cuda::TextureObject<float4> &normal_out,
-        const ftl::cuda::TextureObject<float> &confidence_out,
-        const ftl::cuda::TextureObject<float4> &tmp_colour,
-        const ftl::cuda::TextureObject<int> &tmp_depth, int numcams,
-		const ftl::render::SplatParams &params, cudaStream_t stream);
-
-/**
- * Directly render input depth maps to virtual view with clipping.
- */
-void dibr_raw(const ftl::cuda::TextureObject<int> &depth_out, int numcams,
-		const ftl::render::SplatParams &params, cudaStream_t stream);
-
-void dibr(const ftl::cuda::TextureObject<float> &depth_out,
-    const ftl::cuda::TextureObject<uchar4> &colour_out, int numcams, const ftl::render::SplatParams &params, cudaStream_t stream);
-
 }
 }
 
-#endif  // _FTL_RECONSTRUCTION_SPLAT_CUDA_HPP_
+#endif  // _FTL_CUDA_INTERSECTIONS_HPP_
diff --git a/components/renderers/cpp/include/ftl/cuda/points.hpp b/components/renderers/cpp/include/ftl/cuda/points.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..deffe32777789e2b58a96aef2106975ad37e0cdd
--- /dev/null
+++ b/components/renderers/cpp/include/ftl/cuda/points.hpp
@@ -0,0 +1,16 @@
+#ifndef _FTL_CUDA_POINTS_HPP_
+#define _FTL_CUDA_POINTS_HPP_
+
+#include <ftl/cuda_common.hpp>
+#include <ftl/rgbd/camera.hpp>
+#include <ftl/cuda_matrix_util.hpp>
+
+namespace ftl {
+namespace cuda {
+
+void point_cloud(ftl::cuda::TextureObject<float4> &output, ftl::cuda::TextureObject<float> &depth, const ftl::rgbd::Camera &params, const float4x4 &pose, cudaStream_t stream);
+
+}
+}
+
+#endif  // _FTL_CUDA_POINTS_HPP_
diff --git a/components/renderers/cpp/include/ftl/cuda/weighting.hpp b/components/renderers/cpp/include/ftl/cuda/weighting.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9498d0508605087306db2658b2a1ae1943cde536
--- /dev/null
+++ b/components/renderers/cpp/include/ftl/cuda/weighting.hpp
@@ -0,0 +1,23 @@
+#ifndef _FTL_CUDA_WEIGHTING_HPP_
+#define _FTL_CUDA_WEIGHTING_HPP_
+
+namespace ftl {
+namespace cuda {
+
+/*
+ * Guennebaud, G.; Gross, M. Algebraic point set surfaces. ACMTransactions on Graphics Vol. 26, No. 3, Article No. 23, 2007.
+ * Used in: FusionMLS: Highly dynamic 3D reconstruction with consumer-grade RGB-D cameras
+ *     r = distance between points
+ *     h = smoothing parameter in meters (default 4cm)
+ */
+__device__ inline float spatialWeighting(float r, float h) {
+	if (r >= h) return 0.0f;
+	float rh = r / h;
+	rh = 1.0f - rh*rh;
+	return rh*rh*rh*rh;
+}
+
+}
+}
+
+#endif  // _FTL_CUDA_WEIGHTING_HPP_ 
diff --git a/components/renderers/cpp/include/ftl/display.hpp b/components/renderers/cpp/include/ftl/display.hpp
deleted file mode 100644
index 05ae0bf11e5ebc3a5b8d54339bc85166effbb4a9..0000000000000000000000000000000000000000
--- a/components/renderers/cpp/include/ftl/display.hpp
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2019 Nicolas Pope
- */
-
-#ifndef _FTL_DISPLAY_HPP_
-#define _FTL_DISPLAY_HPP_
-
-#include <ftl/config.h>
-#include <ftl/configurable.hpp>
-#include "../../../rgbd-sources/include/ftl/rgbd/camera.hpp"
-
-#include <nlohmann/json.hpp>
-#include <opencv2/opencv.hpp>
-#include "opencv2/highgui.hpp"
-
-#if defined HAVE_PCL
-#include <pcl/common/common_headers.h>
-#include <pcl/visualization/pcl_visualizer.h>
-#endif  // HAVE_PCL
-
-namespace ftl {
-
-/**
- * Multiple local display options for disparity or point clouds.
- */
-class Display : public ftl::Configurable {
-	private:
-		std::string name_;
-	public:
-	enum style_t {
-		STYLE_NORMAL, STYLE_DISPARITY, STYLE_DEPTH
-	};
-
-	public:
-	explicit Display(nlohmann::json &config, std::string name);
-	~Display();
-	
-	bool render(const cv::Mat &rgb, const cv::Mat &depth, const ftl::rgbd::Camera &p);
-
-#if defined HAVE_PCL
-	bool render(pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr);
-#endif  // HAVE_PCL
-	bool render(const cv::Mat &img, style_t s=STYLE_NORMAL);
-
-	bool active() const;
-
-	bool hasDisplays();
-	
-	void wait(int ms);
-
-	void onKey(const std::function<void(int)> &h) { key_handlers_.push_back(h); }
-
-	private:
-#if defined HAVE_VIZ
-	cv::viz::Viz3d *window_;
-#endif  // HAVE_VIZ
-
-#if defined HAVE_PCL
-	pcl::visualization::PCLVisualizer::Ptr pclviz_;
-#endif  // HAVE_PCL
-
-	bool active_;
-	std::vector<std::function<void(int)>> key_handlers_;
-};
-};
-
-#endif  // _FTL_DISPLAY_HPP_
-
diff --git a/components/renderers/cpp/include/ftl/render/renderer.hpp b/components/renderers/cpp/include/ftl/render/renderer.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1871b9f9f2a8e1fda0766e1c2e74d2169f47f3fa
--- /dev/null
+++ b/components/renderers/cpp/include/ftl/render/renderer.hpp
@@ -0,0 +1,35 @@
+#ifndef _FTL_RENDER_RENDERER_HPP_
+#define _FTL_RENDER_RENDERER_HPP_
+
+#include <ftl/configurable.hpp>
+#include <ftl/rgbd/virtual.hpp>
+#include <ftl/cuda_common.hpp>
+
+namespace ftl {
+namespace render {
+
+/**
+ * Abstract class for all renderers. A renderer takes some 3D scene and
+ * generates a virtual camera perspective of that scene. The scene might be
+ * based upon a point cloud, or an entirely virtual mesh or procedural scene.
+ * It is intended that multiple scenes can be rendered into a single virtual
+ * view using a compositing renderer, such a renderer accepting any kind of
+ * renderer for compositing and hence relying on this base class.
+ */
+class Renderer : public ftl::Configurable {
+    public:
+    explicit Renderer(nlohmann::json &config) : Configurable(config) {};
+    virtual ~Renderer() {};
+
+    /**
+     * Generate a single virtual camera frame. The frame takes its pose from
+     * the virtual camera object passed, and writes the result into the
+     * virtual camera.
+     */
+    virtual bool render(ftl::rgbd::VirtualSource *, ftl::rgbd::Frame &, cudaStream_t)=0;
+};
+
+}
+}
+
+#endif  // _FTL_RENDER_RENDERER_HPP_
diff --git a/applications/reconstruct/src/splat_params.hpp b/components/renderers/cpp/include/ftl/render/splat_params.hpp
similarity index 86%
rename from applications/reconstruct/src/splat_params.hpp
rename to components/renderers/cpp/include/ftl/render/splat_params.hpp
index 8ae5bf345e4e0d348c414cc1ce7bb52190d5ffff..4f9c8882b161d7774388e8d9fff7337cb1d6e685 100644
--- a/applications/reconstruct/src/splat_params.hpp
+++ b/components/renderers/cpp/include/ftl/render/splat_params.hpp
@@ -3,7 +3,7 @@
 
 #include <ftl/cuda_util.hpp>
 #include <ftl/cuda_matrix_util.hpp>
-#include <ftl/depth_camera_params.hpp>
+#include <ftl/rgbd/camera.hpp>
 
 namespace ftl {
 namespace render {
@@ -18,10 +18,10 @@ struct __align__(16) SplatParams {
 	float4x4 m_viewMatrixInverse;
 
 	uint m_flags;
-	float voxelSize;
+	//float voxelSize;
 	float depthThreshold;
 
-	DepthCameraParams camera;
+	ftl::rgbd::Camera camera;
 };
 
 }
diff --git a/applications/reconstruct/src/splat_render.hpp b/components/renderers/cpp/include/ftl/render/splat_render.hpp
similarity index 57%
rename from applications/reconstruct/src/splat_render.hpp
rename to components/renderers/cpp/include/ftl/render/splat_render.hpp
index 828db11fad6bbb5e3aeeae0899bc15549fab1708..2cbb82a8183a3a6269a3888134a57144c93050ca 100644
--- a/applications/reconstruct/src/splat_render.hpp
+++ b/components/renderers/cpp/include/ftl/render/splat_render.hpp
@@ -1,14 +1,9 @@
 #ifndef _FTL_RECONSTRUCTION_SPLAT_HPP_
 #define _FTL_RECONSTRUCTION_SPLAT_HPP_
 
-#include <ftl/configurable.hpp>
-#include <ftl/rgbd/source.hpp>
-#include <ftl/depth_camera.hpp>
-#include <ftl/voxel_scene.hpp>
-//#include <ftl/ray_cast_util.hpp>
-#include <ftl/cuda_common.hpp>
-
-#include "splat_params.hpp"
+#include <ftl/render/renderer.hpp>
+#include <ftl/rgbd/frameset.hpp>
+#include <ftl/render/splat_params.hpp>
 
 namespace ftl {
 namespace render {
@@ -21,26 +16,30 @@ namespace render {
  * on a separate machine or at a later time, the advantage being to save local
  * processing resources and that the first pass result may compress better.
  */
-class Splatter {
+class Splatter : public ftl::render::Renderer {
 	public:
-	explicit Splatter(ftl::voxhash::SceneRep *scene);
+	explicit Splatter(nlohmann::json &config, ftl::rgbd::FrameSet *fs);
 	~Splatter();
 
-	void render(int64_t ts, ftl::rgbd::Source *src, cudaStream_t stream=0);
+	bool render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out, cudaStream_t stream=0) override;
+	//void setOutputDevice(int);
 
-	void setOutputDevice(int);
+	protected:
+	void renderChannel(ftl::render::SplatParams &params, ftl::rgbd::Frame &out, const ftl::rgbd::Channel &channel, cudaStream_t stream);
 
 	private:
 	int device_;
-	ftl::cuda::TextureObject<int> depth1_;
+	/*ftl::cuda::TextureObject<int> depth1_;
 	ftl::cuda::TextureObject<int> depth3_;
 	ftl::cuda::TextureObject<uchar4> colour1_;
 	ftl::cuda::TextureObject<float4> colour_tmp_;
 	ftl::cuda::TextureObject<float> depth2_;
 	ftl::cuda::TextureObject<uchar4> colour2_;
-	ftl::cuda::TextureObject<float4> normal1_;
-	SplatParams params_;
-	ftl::voxhash::SceneRep *scene_;
+	ftl::cuda::TextureObject<float4> normal1_;*/
+	//SplatParams params_;
+
+	ftl::rgbd::Frame temp_;
+	ftl::rgbd::FrameSet *scene_;
 };
 
 }
diff --git a/components/renderers/cpp/include/ftl/rgbd_display.hpp b/components/renderers/cpp/include/ftl/rgbd_display.hpp
deleted file mode 100644
index 9c1be76fb5f38a584d3032d50b6ef046f0d0b605..0000000000000000000000000000000000000000
--- a/components/renderers/cpp/include/ftl/rgbd_display.hpp
+++ /dev/null
@@ -1,47 +0,0 @@
-#ifndef _FTL_RGBD_DISPLAY_HPP_
-#define _FTL_RGBD_DISPLAY_HPP_
-
-#include <nlohmann/json.hpp>
-#include <ftl/rgbd/source.hpp>
-
-using MouseAction = std::function<void(int, int, int, int)>;
-
-namespace ftl {
-namespace rgbd {
-
-class Display : public ftl::Configurable {
-	public:
-	explicit Display(nlohmann::json &);
-	Display(nlohmann::json &, Source *);
-	~Display();
-
-	void setSource(Source *src) { source_ = src; }
-	void update();
-
-	bool active() const { return active_; }
-
-	void onKey(const std::function<void(int)> &h) { key_handlers_.push_back(h); }
-
-	void wait(int ms);
-
-	private:
-	Source *source_;
-	std::string name_;
-	std::vector<std::function<void(int)>> key_handlers_;
-	Eigen::Vector3d eye_;
-	Eigen::Vector3d centre_;
-	Eigen::Vector3d up_;
-	Eigen::Vector3d lookPoint_;
-	float lerpSpeed_;
-	bool active_;
-	MouseAction mouseaction_;
-
-	static int viewcount__;
-
-	void init();
-};
-
-}
-}
-
-#endif  // _FTL_RGBD_DISPLAY_HPP_
diff --git a/applications/reconstruct/include/ftl/matrix_conversion.hpp b/components/renderers/cpp/include/ftl/utility/matrix_conversion.hpp
similarity index 100%
rename from applications/reconstruct/include/ftl/matrix_conversion.hpp
rename to components/renderers/cpp/include/ftl/utility/matrix_conversion.hpp
diff --git a/applications/reconstruct/src/dibr.cu b/components/renderers/cpp/src/dibr.cu
similarity index 98%
rename from applications/reconstruct/src/dibr.cu
rename to components/renderers/cpp/src/dibr.cu
index a7ba0e9df91637c687eda13d1048f323f72a30f4..66428ce3086e42b6a3e81c667ae126314d910c98 100644
--- a/applications/reconstruct/src/dibr.cu
+++ b/components/renderers/cpp/src/dibr.cu
@@ -1,6 +1,6 @@
 #include "splat_render_cuda.hpp"
 #include "depth_camera_cuda.hpp"
-#include <cuda_runtime.h>
+//#include <cuda_runtime.h>
 
 #include <ftl/cuda_matrix_util.hpp>
 
@@ -158,8 +158,7 @@ __global__ void OLD_dibr_visibility_kernel(TextureObject<int> depth, int cam, Sp
 	const int upsample = min(UPSAMPLE_MAX, int((r) * params.camera.fx / camPos.z));
 
 	// Not on screen so stop now...
-	if (screenPos.x + upsample < 0 || screenPos.y + upsample < 0 ||
-            screenPos.x - upsample >= depth.width() || screenPos.y - upsample >= depth.height()) return;
+	if (screenPos.x - upsample >= depth.width() || screenPos.y - upsample >= depth.height()) return;
             
     // TODO:(Nick) Check depth buffer and don't do anything if already hidden?
 
@@ -181,7 +180,7 @@ __global__ void OLD_dibr_visibility_kernel(TextureObject<int> depth, int cam, Sp
 		// and depth remains within the bounds.
 		// How to find min and max depths?
 
-        float ld = nearest.z;
+        //float ld = nearest.z;
 
 		// TODO: (Nick) Estimate depth using points plane, but needs better normals.
 		//float t;
@@ -241,7 +240,7 @@ __global__ void OLD_dibr_visibility_kernel(TextureObject<int> depth, int cam, Sp
                 }*/
 
                 //nearest = params.camera.kinectDepthToSkeleton(screenPos.x+u,screenPos.y+v,d);  // ld + (d - ld)*0.8f
-                ld = d;
+                //ld = d;
 			}
 		//}
 	}
@@ -287,8 +286,7 @@ __global__ void OLD_dibr_visibility_kernel(TextureObject<int> depth, int cam, Sp
 	const int upsample = min(UPSAMPLE_MAX, int((4.0f*r) * params.camera.fx / camPos.z));
 
 	// Not on screen so stop now...
-	if (screenPos.x + upsample < 0 || screenPos.y + upsample < 0 ||
-            screenPos.x - upsample >= depth.width() || screenPos.y - upsample >= depth.height()) return;
+	if (screenPos.x - upsample >= depth.width() || screenPos.y - upsample >= depth.height()) return;
             
 	// TODO:(Nick) Check depth buffer and don't do anything if already hidden?
 	
@@ -575,7 +573,7 @@ __global__ void dibr_attribute_contrib_kernel(
 	const ftl::voxhash::DepthCameraCUDA &camera = c_cameras[cam];
 
 	const int tid = (threadIdx.x + threadIdx.y * blockDim.x);
-	const int warp = tid / WARP_SIZE;
+	//const int warp = tid / WARP_SIZE;
 	const int x = (blockIdx.x*blockDim.x + threadIdx.x) / WARP_SIZE;
 	const int y = blockIdx.y*blockDim.y + threadIdx.y;
 
@@ -592,8 +590,7 @@ __global__ void dibr_attribute_contrib_kernel(
     const int upsample = 8; //min(UPSAMPLE_MAX, int((5.0f*r) * params.camera.fx / camPos.z));
 
 	// Not on screen so stop now...
-	if (screenPos.x < 0 || screenPos.y < 0 ||
-            screenPos.x >= depth_in.width() || screenPos.y >= depth_in.height()) return;
+	if (screenPos.x >= depth_in.width() || screenPos.y >= depth_in.height()) return;
             
     // Is this point near the actual surface and therefore a contributor?
     const float d = ((float)depth_in.tex2D((int)screenPos.x, (int)screenPos.y)/1000.0f);
@@ -722,7 +719,7 @@ void ftl::cuda::dibr(const TextureObject<int> &depth_out,
 	cudaSafeCall(cudaDeviceSynchronize());
 #endif
 
-	int i=3;
+	//int i=3;
 
 	bool noSplatting = params.m_flags & ftl::render::kNoSplatting;
 
diff --git a/components/renderers/cpp/src/display.cpp b/components/renderers/cpp/src/display.cpp
deleted file mode 100644
index 0f838df751d0c0a315f4d0acca9798d2d7741066..0000000000000000000000000000000000000000
--- a/components/renderers/cpp/src/display.cpp
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright 2019 Nicolas Pope
- */
-
-#include <loguru.hpp>
-
-#include <ftl/display.hpp>
-#include <ftl/utility/opencv_to_pcl.hpp>
-
-using ftl::Display;
-using cv::Mat;
-using cv::Vec3f;
-
-Display::Display(nlohmann::json &config, std::string name) : ftl::Configurable(config) {
-	name_ = name;
-#if defined HAVE_VIZ
-	window_ = new cv::viz::Viz3d("FTL: " + name);
-	window_->setBackgroundColor(cv::viz::Color::white());
-#endif  // HAVE_VIZ
-
-	//cv::namedWindow("Image", cv::WINDOW_KEEPRATIO);
-
-#if defined HAVE_PCL
-	if (value("points", false)) {
-		pclviz_ = pcl::visualization::PCLVisualizer::Ptr(new pcl::visualization::PCLVisualizer ("FTL Cloud: " + name));
-		pclviz_->setBackgroundColor (255, 255, 255);
-		pclviz_->addCoordinateSystem (1.0);
-		pclviz_->setShowFPS(true);
-		pclviz_->initCameraParameters ();
-
-		pclviz_->registerPointPickingCallback(
-			[](const pcl::visualization::PointPickingEvent& event, void* viewer_void) {
-				if (event.getPointIndex () == -1) return;
-				float x, y, z;
-				event.getPoint(x, y, z);
-				LOG(INFO) << "( " << x << ", " << y << ", " << z << ")";
-			}, (void*) &pclviz_);
-		
-		pclviz_->registerKeyboardCallback (
-			[](const pcl::visualization::KeyboardEvent &event, void* viewer_void) {
-				auto viewer = *static_cast<pcl::visualization::PCLVisualizer::Ptr*>(viewer_void);
-				pcl::visualization::Camera cam;
-				viewer->getCameraParameters(cam);
-
-				Eigen::Vector3f pos(cam.pos[0], cam.pos[1], cam.pos[2]);
-				Eigen::Vector3f focal(cam.focal[0], cam.focal[1], cam.focal[2]);
-				Eigen::Vector3f dir = focal - pos; //.normalize();
-				dir.normalize();
-
-				const float speed = 40.0f;
-
-				if (event.getKeySym() == "Up") {
-					pos += speed*dir;
-					focal += speed*dir;
-				} else if (event.getKeySym() == "Down") {
-					pos -= speed*dir;
-					focal -= speed*dir;
-				} else if (event.getKeySym() == "Left") {
-					Eigen::Matrix3f m = Eigen::AngleAxisf(-0.5f*M_PI, Eigen::Vector3f::UnitY()).toRotationMatrix();
-					dir = m*dir;
-					pos += speed*dir;
-					focal += speed*dir;
-				} else if (event.getKeySym() == "Right") {
-					Eigen::Matrix3f m = Eigen::AngleAxisf(0.5f*M_PI, Eigen::Vector3f::UnitY()).toRotationMatrix();
-					dir = m*dir;
-					pos += speed*dir;
-					focal += speed*dir;
-				}
-
-
-				cam.pos[0] = pos[0];
-				cam.pos[1] = pos[1];
-				cam.pos[2] = pos[2];
-				cam.focal[0] = focal[0];
-				cam.focal[1] = focal[1];
-				cam.focal[2] = focal[2];
-				viewer->setCameraParameters(cam);
-
-			}, (void*)&pclviz_);
-	}
-#endif  // HAVE_PCL
-
-	active_ = true;
-}
-
-Display::~Display() {
-	#if defined HAVE_VIZ
-	delete window_;
-	#endif  // HAVE_VIZ
-}
-
-#ifdef HAVE_PCL
-/**
- * Convert an OpenCV RGB and Depth Mats to a PCL XYZRGB point cloud.
- */
-static pcl::PointCloud<pcl::PointXYZRGB>::Ptr rgbdToPointXYZ(const cv::Mat &rgb, const cv::Mat &depth, const ftl::rgbd::Camera &p) {
-	const double CX = p.cx;
-	const double CY = p.cy;
-	const double FX = p.fx;
-	const double FY = p.fy;
-
-	pcl::PointCloud<pcl::PointXYZRGB>::Ptr point_cloud_ptr(new pcl::PointCloud<pcl::PointXYZRGB>);
-	point_cloud_ptr->width = rgb.cols * rgb.rows;
-	point_cloud_ptr->height = 1;
-
-	for(int i=0;i<rgb.rows;i++) {
-		const float *sptr = depth.ptr<float>(i);
-		for(int j=0;j<rgb.cols;j++) {
-			float d = sptr[j] * 1000.0f;
-
-			pcl::PointXYZRGB point;
-			point.x = (((double)j + CX) / FX) * d;
-			point.y = (((double)i + CY) / FY) * d;
-			point.z = d;
-
-			if (point.x == INFINITY || point.y == INFINITY || point.z > 20000.0f || point.z < 0.04f) {
-				point.x = 0.0f; point.y = 0.0f; point.z = 0.0f;
-			}
-
-			cv::Point3_<uchar> prgb = rgb.at<cv::Point3_<uchar>>(i, j);
-			uint32_t rgb = (static_cast<uint32_t>(prgb.z) << 16 | static_cast<uint32_t>(prgb.y) << 8 | static_cast<uint32_t>(prgb.x));
-			point.rgb = *reinterpret_cast<float*>(&rgb);
-
-			point_cloud_ptr -> points.push_back(point);
-		}
-	}
-
-	return point_cloud_ptr;
-}
-#endif  // HAVE_PCL
-
-bool Display::render(const cv::Mat &rgb, const cv::Mat &depth, const ftl::rgbd::Camera &p) {
-	Mat idepth;
-
-	if (value("points", false) && rgb.rows != 0) {
-#if defined HAVE_PCL
-		auto pc = rgbdToPointXYZ(rgb, depth, p);
-
-		pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(pc);
-		if (!pclviz_->updatePointCloud<pcl::PointXYZRGB> (pc, rgb, "reconstruction")) {
-			pclviz_->addPointCloud<pcl::PointXYZRGB> (pc, rgb, "reconstruction");
-			pclviz_->setCameraPosition(-878.0, -71.0, -2315.0, -0.1, -0.99, 0.068, 0.0, -1.0, 0.0);
-			pclviz_->setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "reconstruction");
-		}
-#elif defined HAVE_VIZ
-		//cv::Mat Q_32F;
-		//calibrate_.getQ().convertTo(Q_32F, CV_32F);
-		/*cv::Mat_<cv::Vec3f> XYZ(depth.rows, depth.cols);   // Output point cloud
-		reprojectImageTo3D(depth+20.0f, XYZ, q, true);
-
-		// Remove all invalid pixels from point cloud
-		XYZ.setTo(Vec3f(NAN, NAN, NAN), depth == 0.0f);
-
-		cv::viz::WCloud cloud_widget = cv::viz::WCloud(XYZ, rgb);
-		cloud_widget.setRenderingProperty(cv::viz::POINT_SIZE, 2);
-
-		window_->showWidget("coosys", cv::viz::WCoordinateSystem());
-		window_->showWidget("Depth", cloud_widget);
-
-		//window_->spinOnce(40, true);*/
-
-#else  // HAVE_VIZ
-
-		LOG(ERROR) << "Need OpenCV Viz module to display points";
-
-#endif  // HAVE_VIZ
-	}
-
-	if (value("left", false)) {
-		if (value("crosshair", false)) {
-			cv::line(rgb, cv::Point(0, rgb.rows/2), cv::Point(rgb.cols-1, rgb.rows/2), cv::Scalar(0,0,255), 1);
-            cv::line(rgb, cv::Point(rgb.cols/2, 0), cv::Point(rgb.cols/2, rgb.rows-1), cv::Scalar(0,0,255), 1);
-		}
-		cv::namedWindow("Left: " + name_, cv::WINDOW_KEEPRATIO);
-		cv::imshow("Left: " + name_, rgb);
-	}
-	if (value("right", false)) {
-		/*if (config_["crosshair"]) {
-			cv::line(rgbr, cv::Point(0, rgbr.rows/2), cv::Point(rgbr.cols-1, rgbr.rows/2), cv::Scalar(0,0,255), 1);
-            cv::line(rgbr, cv::Point(rgbr.cols/2, 0), cv::Point(rgbr.cols/2, rgbr.rows-1), cv::Scalar(0,0,255), 1);
-		}
-		cv::namedWindow("Right: " + name_, cv::WINDOW_KEEPRATIO);
-		cv::imshow("Right: " + name_, rgbr);*/
-	}
-
-	if (value("disparity", false)) {
-		/*Mat depth32F = (focal * (float)l.cols * base_line) / depth;
-		normalize(depth32F, depth32F, 0, 255, NORM_MINMAX, CV_8U);
-		cv::imshow("Depth", depth32F);
-		if(cv::waitKey(10) == 27){
-	        //exit if ESC is pressed
-	       	active_ = false;
-	    }*/
-    } else if (value("depth", false)) {
-		if (value("flip_vert", false)) {
-			cv::flip(depth, idepth, 0);
-		} else {
-			idepth = depth;
-		}
-
-    	idepth.convertTo(idepth, CV_8U, 255.0f / 10.0f);  // TODO(nick)
-
-    	applyColorMap(idepth, idepth, cv::COLORMAP_JET);
-		cv::imshow("Depth: " + name_, idepth);
-		//if(cv::waitKey(40) == 27) {
-	        // exit if ESC is pressed
-	    //    active_ = false;
-	    //}
-    }
-
-	return true;
-}
-
-#if defined HAVE_PCL
-bool Display::render(pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr pc) {	
-	pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(pc);
-	if (pclviz_ && !pclviz_->updatePointCloud<pcl::PointXYZRGB> (pc, rgb, "reconstruction")) {
-		pclviz_->addPointCloud<pcl::PointXYZRGB> (pc, rgb, "reconstruction");
-		pclviz_->setCameraPosition(-878.0, -71.0, -2315.0, -0.1, -0.99, 0.068, 0.0, -1.0, 0.0);
-		pclviz_->setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "reconstruction");
-	}
-	return true;
-}
-#endif  // HAVE_PCL
-bool Display::render(const cv::Mat &img, style_t s) {
-	if (s == STYLE_NORMAL) {
-		cv::imshow("Image", img);
-	} else if (s == STYLE_DISPARITY) {
-		Mat idepth;
-
-		if (value("flip_vert", false)) {
-			cv::flip(img, idepth, 0);
-		} else {
-			idepth = img;
-		}
-
-    	idepth.convertTo(idepth, CV_8U, 255.0f / 256.0f);
-
-    	applyColorMap(idepth, idepth, cv::COLORMAP_JET);
-		cv::imshow("Disparity", idepth);
-	}
-
-	return true;
-}
-
-bool Display::hasDisplays() {
-	return value("depth", false) || value("left", false) || value("right", false) || value("points", false);
-}
-
-void Display::wait(int ms) {
-	if (value("points", false)) {
-		#if defined HAVE_PCL
-		if (pclviz_) pclviz_->spinOnce(20);
-		#elif defined HAVE_VIZ
-		window_->spinOnce(1, true);
-		#endif  // HAVE_VIZ
-	}
-	
-	if (value("depth", false) || value("left", false) || value("right", false)) {
-		while (true) {
-			int key = cv::waitKey(ms);
-
-			if(key == 27) {
-				// exit if ESC is pressed
-				active_ = false;
-			} else if (key == -1) {
-				return;
-			} else {
-				ms = 1;
-				for (auto &h : key_handlers_) {
-					h(key);
-				}
-			}
-		}
-	}
-}
-
-bool Display::active() const {
-	#if defined HAVE_PCL
-	return active_ && (!pclviz_ || !pclviz_->wasStopped());
-	#elif defined HAVE_VIZ
-	return active_ && !window_->wasStopped();
-	#else
-	return active_;
-	#endif
-}
-
diff --git a/applications/reconstruct/src/mls_cuda.hpp b/components/renderers/cpp/src/mls_cuda.hpp
similarity index 100%
rename from applications/reconstruct/src/mls_cuda.hpp
rename to components/renderers/cpp/src/mls_cuda.hpp
diff --git a/components/renderers/cpp/src/points.cu b/components/renderers/cpp/src/points.cu
new file mode 100644
index 0000000000000000000000000000000000000000..39764e4c8aba523caf2758262d9f41f8782ac9dc
--- /dev/null
+++ b/components/renderers/cpp/src/points.cu
@@ -0,0 +1,28 @@
+#include <ftl/cuda/points.hpp>
+
+#define T_PER_BLOCK 8
+
+__global__ void point_cloud_kernel(ftl::cuda::TextureObject<float4> output, ftl::cuda::TextureObject<float> depth, ftl::rgbd::Camera params, float4x4 pose)
+{
+	const unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;
+	const unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+	if (x < params.width && y < params.height) {
+		float d = depth.tex2D((int)x, (int)y);
+
+		output(x,y) = (d >= params.minDepth && d <= params.maxDepth) ?
+			make_float4(pose * params.screenToCam(x, y, d), 0.0f) :
+			make_float4(MINF, MINF, MINF, MINF);
+	}
+}
+
+void ftl::cuda::point_cloud(ftl::cuda::TextureObject<float4> &output, ftl::cuda::TextureObject<float> &depth, const ftl::rgbd::Camera &params, const float4x4 &pose, cudaStream_t stream) {
+	const dim3 gridSize((params.width + T_PER_BLOCK - 1)/T_PER_BLOCK, (params.height + T_PER_BLOCK - 1)/T_PER_BLOCK);
+	const dim3 blockSize(T_PER_BLOCK, T_PER_BLOCK);
+
+	point_cloud_kernel<<<gridSize, blockSize, 0, stream>>>(output, depth, params, pose);
+
+#ifdef _DEBUG
+	cudaSafeCall(cudaDeviceSynchronize());
+#endif
+}
diff --git a/components/renderers/cpp/src/rgbd_display.cpp b/components/renderers/cpp/src/rgbd_display.cpp
deleted file mode 100644
index a0d79f8aeeb42b0a0a1fd281c5f3e9065b43780c..0000000000000000000000000000000000000000
--- a/components/renderers/cpp/src/rgbd_display.cpp
+++ /dev/null
@@ -1,129 +0,0 @@
-#include <ftl/rgbd_display.hpp>
-#include <opencv2/opencv.hpp>
-
-using ftl::rgbd::Source;
-using ftl::rgbd::Display;
-using std::string;
-using cv::Mat;
-
-int Display::viewcount__ = 0;
-
-template<class T>
-Eigen::Matrix<T,4,4> lookAt
-(
-	Eigen::Matrix<T,3,1> const & eye,
-	Eigen::Matrix<T,3,1> const & center,
-	Eigen::Matrix<T,3,1> const & up
-)
-{
-	typedef Eigen::Matrix<T,4,4> Matrix4;
-	typedef Eigen::Matrix<T,3,1> Vector3;
-
-	Vector3 f = (center - eye).normalized();
-	Vector3 u = up.normalized();
-	Vector3 s = f.cross(u).normalized();
-	u = s.cross(f);
-
-	Matrix4 res;
-	res <<	s.x(),s.y(),s.z(),-s.dot(eye),
-			u.x(),u.y(),u.z(),-u.dot(eye),
-			-f.x(),-f.y(),-f.z(),f.dot(eye),
-			0,0,0,1;
-
-	return res;
-}
-
-static void setMouseAction(const std::string& winName, const MouseAction &action)
-{
-  cv::setMouseCallback(winName,
-                       [] (int event, int x, int y, int flags, void* userdata) {
-    (*(MouseAction*)userdata)(event, x, y, flags);
-  }, (void*)&action);
-}
-
-Display::Display(nlohmann::json &config) : ftl::Configurable(config) {
-	name_ = value("name", string("View [")+std::to_string(viewcount__)+string("]"));
-	viewcount__++;
-
-	init();
-}
-
-Display::Display(nlohmann::json &config, Source *source)
-		: ftl::Configurable(config) {
-	name_ = value("name", string("View [")+std::to_string(viewcount__)+string("]"));
-	viewcount__++;
-	init();
-}
-
-Display::~Display() {
-
-}
-
-void Display::init() {
-	active_ = true;
-	source_ = nullptr;
-	cv::namedWindow(name_, cv::WINDOW_KEEPRATIO);
-
-	eye_ = Eigen::Vector3d(0.0, 0.0, 0.0);
-	centre_ = Eigen::Vector3d(0.0, 0.0, -4.0);
-	up_ = Eigen::Vector3d(0,1.0,0);
-	lookPoint_ = Eigen::Vector3d(0.0,0.0,-4.0);
-	lerpSpeed_ = 0.4f;
-
-	// Keyboard camera controls
-	onKey([this](int key) {
-		//LOG(INFO) << "Key = " << key;
-		if (key == 81 || key == 83) {
-			Eigen::Quaternion<double> q;  q = Eigen::AngleAxis<double>((key == 81) ? 0.01 : -0.01, up_);
-			eye_ = (q * (eye_ - centre_)) + centre_;
-		} else if (key == 84 || key == 82) {
-			double scalar = (key == 84) ? 0.99 : 1.01;
-			eye_ = ((eye_ - centre_) * scalar) + centre_;
-		}
-	});
-
-	// TODO(Nick) Calculate "camera" properties of viewport.
-	mouseaction_ = [this]( int event, int ux, int uy, int) {
-		//LOG(INFO) << "Mouse " << ux << "," << uy;
-		if (event == 1 && source_) {   // click
-			Eigen::Vector4d camPos = source_->point(ux,uy);
-			camPos *= -1.0f;
-			Eigen::Vector4d worldPos =  source_->getPose() * camPos;
-			lookPoint_ = Eigen::Vector3d(worldPos[0],worldPos[1],worldPos[2]);
-			LOG(INFO) << "Depth at click = " << -camPos[2];
-		}
-	};
-	::setMouseAction(name_, mouseaction_);
-}
-
-void Display::wait(int ms) {
-	while (true) {
-		int key = cv::waitKey(ms);
-
-		if(key == 27) {
-			// exit if ESC is pressed
-			active_ = false;
-		} else if (key == -1) {
-			return;
-		} else {
-			ms = 1;
-			for (auto &h : key_handlers_) {
-				h(key);
-			}
-		}
-	}
-}
-
-void Display::update() {
-	if (!source_) return;
-
-	centre_ += (lookPoint_ - centre_) * (lerpSpeed_ * 0.1f);
-	Eigen::Matrix4d viewPose = lookAt<double>(eye_,centre_,up_).inverse();
-	source_->setPose(viewPose);
-
-	Mat rgb, depth;
-	source_->grab();
-	source_->getFrames(rgb, depth);
-	if (rgb.rows > 0) cv::imshow(name_, rgb);
-	wait(1);
-}
diff --git a/components/renderers/cpp/src/splat_render.cpp b/components/renderers/cpp/src/splat_render.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1b39ccebf69e589372ab2944cb907bb03d1dbdd8
--- /dev/null
+++ b/components/renderers/cpp/src/splat_render.cpp
@@ -0,0 +1,221 @@
+#include <ftl/render/splat_render.hpp>
+#include <ftl/utility/matrix_conversion.hpp>
+#include "splatter_cuda.hpp"
+#include <ftl/cuda/points.hpp>
+
+#include <opencv2/core/cuda_stream_accessor.hpp>
+
+using ftl::render::Splatter;
+using ftl::rgbd::Channel;
+using ftl::rgbd::Channels;
+using ftl::rgbd::Format;
+using cv::cuda::GpuMat;
+
+Splatter::Splatter(nlohmann::json &config, ftl::rgbd::FrameSet *fs) : ftl::render::Renderer(config), scene_(fs) {
+
+}
+
+Splatter::~Splatter() {
+
+}
+
+void Splatter::renderChannel(
+					ftl::render::SplatParams &params, ftl::rgbd::Frame &out,
+					const Channel &channel, cudaStream_t stream)
+{
+	cv::cuda::Stream cvstream = cv::cuda::StreamAccessor::wrapStream(stream);
+	temp_.get<GpuMat>(Channel::Depth).setTo(cv::Scalar(0x7FFFFFFF), cvstream);
+	temp_.get<GpuMat>(Channel::Depth2).setTo(cv::Scalar(0x7FFFFFFF), cvstream);
+	temp_.get<GpuMat>(Channel::Colour).setTo(cv::Scalar(0.0f,0.0f,0.0f,0.0f), cvstream);
+	temp_.get<GpuMat>(Channel::Contribution).setTo(cv::Scalar(0.0f), cvstream);
+
+	bool is_float = ftl::rgbd::isFloatChannel(channel);
+	
+	// Render each camera into virtual view
+	for (size_t i=0; i < scene_->frames.size(); ++i) {
+		auto &f = scene_->frames[i];
+		auto *s = scene_->sources[i];
+
+		if (f.empty(Channel::Depth + Channel::Colour)) {
+			LOG(ERROR) << "Missing required channel";
+			continue;
+		}
+
+		// Needs to create points channel first?
+		if (!f.hasChannel(Channel::Points)) {
+			//LOG(INFO) << "Creating points... " << s->parameters().width;
+			
+			auto &t = f.createTexture<float4>(Channel::Points, Format<float4>(f.get<GpuMat>(Channel::Colour).size()));
+			auto pose = MatrixConversion::toCUDA(s->getPose().cast<float>()); //.inverse());
+			ftl::cuda::point_cloud(t, f.createTexture<float>(Channel::Depth), s->parameters(), pose, stream);
+
+			//LOG(INFO) << "POINTS Added";
+		}
+
+		ftl::cuda::dibr_merge(
+			f.createTexture<float4>(Channel::Points),
+			temp_.getTexture<int>(Channel::Depth),
+			params, stream
+		);
+
+		//LOG(INFO) << "DIBR DONE";
+	}
+
+	// TODO: Add the depth splatting step..
+
+	temp_.createTexture<float4>(Channel::Colour);
+	temp_.createTexture<float>(Channel::Contribution);
+
+	// Accumulate attribute contributions for each pixel
+	for (auto &f : scene_->frames) {
+		// Convert colour from BGR to BGRA if needed
+		if (f.get<GpuMat>(Channel::Colour).type() == CV_8UC3) {
+			// Convert to 4 channel colour
+			auto &col = f.get<GpuMat>(Channel::Colour);
+			GpuMat tmp(col.size(), CV_8UC4);
+			cv::cuda::swap(col, tmp);
+			cv::cuda::cvtColor(tmp,col, cv::COLOR_BGR2BGRA);
+		}
+	
+		if (is_float) {
+			ftl::cuda::dibr_attribute(
+				f.createTexture<float>(channel),
+				f.createTexture<float4>(Channel::Points),
+				temp_.getTexture<int>(Channel::Depth),
+				temp_.getTexture<float4>(Channel::Colour),
+				temp_.getTexture<float>(Channel::Contribution),
+				params, stream
+			);
+		} else if (channel == Channel::Colour || channel == Channel::Right) {
+			ftl::cuda::dibr_attribute(
+				f.createTexture<uchar4>(Channel::Colour),
+				f.createTexture<float4>(Channel::Points),
+				temp_.getTexture<int>(Channel::Depth),
+				temp_.getTexture<float4>(Channel::Colour),
+				temp_.getTexture<float>(Channel::Contribution),
+				params, stream
+			);
+		} else {
+			ftl::cuda::dibr_attribute(
+				f.createTexture<uchar4>(channel),
+				f.createTexture<float4>(Channel::Points),
+				temp_.getTexture<int>(Channel::Depth),
+				temp_.getTexture<float4>(Channel::Colour),
+				temp_.getTexture<float>(Channel::Contribution),
+				params, stream
+			);
+		}
+	}
+
+	if (is_float) {
+		// Normalise attribute contributions
+		ftl::cuda::dibr_normalise(
+			temp_.createTexture<float4>(Channel::Colour),
+			out.createTexture<float>(channel),
+			temp_.createTexture<float>(Channel::Contribution),
+			stream
+		);
+	} else {
+		// Normalise attribute contributions
+		ftl::cuda::dibr_normalise(
+			temp_.createTexture<float4>(Channel::Colour),
+			out.createTexture<uchar4>(channel),
+			temp_.createTexture<float>(Channel::Contribution),
+			stream
+		);
+	}
+}
+
+bool Splatter::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out, cudaStream_t stream) {
+	SHARED_LOCK(scene_->mtx, lk);
+	if (!src->isReady()) return false;
+
+	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));
+
+	// FIXME: Use source resolutions, not virtual resolution
+	temp_.create<GpuMat>(Channel::Colour, Format<float4>(camera.width, camera.height));
+	temp_.create<GpuMat>(Channel::Colour2, Format<uchar4>(camera.width, camera.height));
+	temp_.create<GpuMat>(Channel::Contribution, Format<float>(camera.width, camera.height));
+	temp_.create<GpuMat>(Channel::Depth, Format<int>(camera.width, camera.height));
+	temp_.create<GpuMat>(Channel::Depth2, Format<int>(camera.width, camera.height));
+	temp_.create<GpuMat>(Channel::Normals, Format<float4>(camera.width, camera.height));
+
+	cv::cuda::Stream cvstream = cv::cuda::StreamAccessor::wrapStream(stream);
+
+	// Create buffers if they don't exist
+	/*if ((unsigned int)depth1_.width() != camera.width || (unsigned int)depth1_.height() != camera.height) {
+		depth1_ = ftl::cuda::TextureObject<int>(camera.width, camera.height);
+	}
+	if ((unsigned int)depth3_.width() != camera.width || (unsigned int)depth3_.height() != camera.height) {
+		depth3_ = ftl::cuda::TextureObject<int>(camera.width, camera.height);
+	}
+	if ((unsigned int)colour1_.width() != camera.width || (unsigned int)colour1_.height() != camera.height) {
+		colour1_ = ftl::cuda::TextureObject<uchar4>(camera.width, camera.height);
+	}
+	if ((unsigned int)colour_tmp_.width() != camera.width || (unsigned int)colour_tmp_.height() != camera.height) {
+		colour_tmp_ = ftl::cuda::TextureObject<float4>(camera.width, camera.height);
+	}
+	if ((unsigned int)normal1_.width() != camera.width || (unsigned int)normal1_.height() != camera.height) {
+		normal1_ = ftl::cuda::TextureObject<float4>(camera.width, camera.height);
+	}
+	if ((unsigned int)depth2_.width() != camera.width || (unsigned int)depth2_.height() != camera.height) {
+		depth2_ = ftl::cuda::TextureObject<float>(camera.width, camera.height);
+	}
+	if ((unsigned int)colour2_.width() != camera.width || (unsigned int)colour2_.height() != camera.height) {
+		colour2_ = ftl::cuda::TextureObject<uchar4>(camera.width, camera.height);
+	}*/
+
+	// Parameters object to pass to CUDA describing the camera
+	SplatParams params;
+	params.m_flags = 0;
+	if (src->value("splatting", true) == false) params.m_flags |= ftl::render::kNoSplatting;
+	if (src->value("upsampling", true) == false) params.m_flags |= ftl::render::kNoUpsampling;
+	if (src->value("texturing", true) == false) params.m_flags |= ftl::render::kNoTexturing;
+	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);
+	out.get<GpuMat>(Channel::Colour).setTo(cv::Scalar(76,76,76), cvstream);
+
+	//LOG(INFO) << "Render ready: " << camera.width << "," << camera.height;
+
+	temp_.createTexture<int>(Channel::Depth);
+
+	renderChannel(params, out, Channel::Colour, stream);
+	
+	Channel chan = src->getChannel();
+	if (chan == Channel::Depth)
+	{
+		temp_.get<GpuMat>(Channel::Depth).convertTo(out.get<GpuMat>(Channel::Depth), CV_32F, 1.0f / 1000.0f, cvstream);
+	}
+	else if (chan == Channel::Contribution)
+	{
+		cv::cuda::swap(temp_.get<GpuMat>(Channel::Contribution), out.create<GpuMat>(Channel::Contribution));
+	}
+	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();
+		params.m_viewMatrix = MatrixConversion::toCUDA(matrix.inverse());
+		params.m_viewMatrixInverse = MatrixConversion::toCUDA(matrix);
+		
+		out.create<GpuMat>(Channel::Right, Format<uchar4>(camera.width, camera.height));
+		out.get<GpuMat>(Channel::Right).setTo(cv::Scalar(76,76,76), cvstream);
+		renderChannel(params, out, Channel::Right, stream);
+	}
+
+	return true;
+}
+
+//void Splatter::setOutputDevice(int device) {
+//	device_ = device;
+//}
diff --git a/components/renderers/cpp/src/splatter.cu b/components/renderers/cpp/src/splatter.cu
new file mode 100644
index 0000000000000000000000000000000000000000..3b1ae4b47ef0fe6b29b15d1fa20fdc9a0fd0b9bb
--- /dev/null
+++ b/components/renderers/cpp/src/splatter.cu
@@ -0,0 +1,304 @@
+#include <ftl/render/splat_params.hpp>
+#include "splatter_cuda.hpp"
+#include <ftl/rgbd/camera.hpp>
+#include <ftl/cuda_common.hpp>
+
+#include <ftl/cuda/weighting.hpp>
+
+#define T_PER_BLOCK 8
+#define UPSAMPLE_FACTOR 1.8f
+#define WARP_SIZE 32
+#define DEPTH_THRESHOLD 0.05f
+#define UPSAMPLE_MAX 60
+#define MAX_ITERATIONS 32  // Note: Must be multiple of 32
+#define SPATIAL_SMOOTHING 0.005f
+
+using ftl::cuda::TextureObject;
+using ftl::render::SplatParams;
+
+/*
+ * Pass 1: Directly render each camera into virtual view but with no upsampling
+ * for sparse points.
+ */
+ __global__ void dibr_merge_kernel(TextureObject<float4> points, TextureObject<int> depth, SplatParams params) {
+	const int x = blockIdx.x*blockDim.x + threadIdx.x;
+	const int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+	const float3 worldPos = make_float3(points.tex2D(x, y));
+	if (worldPos.x == MINF) return;
+
+    // Find the virtual screen position of current point
+	const float3 camPos = params.m_viewMatrix * worldPos;
+	if (camPos.z < params.camera.minDepth) return;
+	if (camPos.z > params.camera.maxDepth) return;
+
+	const float d = camPos.z;
+
+	const uint2 screenPos = params.camera.camToScreen<uint2>(camPos);
+	const unsigned int cx = screenPos.x;
+	const unsigned int cy = screenPos.y;
+	if (d > params.camera.minDepth && d < params.camera.maxDepth && cx < depth.width() && cy < depth.height()) {
+		// Transform estimated point to virtual cam space and output z
+		atomicMin(&depth(cx,cy), d * 1000.0f);
+	}
+}
+
+void ftl::cuda::dibr_merge(TextureObject<float4> &points, TextureObject<int> &depth, SplatParams params, cudaStream_t stream) {
+    const dim3 gridSize((depth.width() + T_PER_BLOCK - 1)/T_PER_BLOCK, (depth.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
+    const dim3 blockSize(T_PER_BLOCK, T_PER_BLOCK);
+
+    dibr_merge_kernel<<<gridSize, blockSize, 0, stream>>>(points, depth, params);
+    cudaSafeCall( cudaGetLastError() );
+}
+
+//==============================================================================
+
+__device__ inline float4 make_float4(const uchar4 &c) {
+    return make_float4(c.x,c.y,c.z,c.w);
+}
+
+
+#define ENERGY_THRESHOLD 0.1f
+#define SMOOTHING_MULTIPLIER_A 10.0f	// For surface search
+#define SMOOTHING_MULTIPLIER_B 4.0f		// For z contribution
+#define SMOOTHING_MULTIPLIER_C 4.0f		// For colour contribution
+
+/*
+ * Pass 2: Accumulate attribute contributions if the points pass a visibility test.
+ */
+__global__ void dibr_attribute_contrib_kernel(
+        TextureObject<uchar4> colour_in,    // Original colour image
+        TextureObject<float4> points,       // Original 3D points
+        TextureObject<int> depth_in,        // Virtual depth map
+        TextureObject<float4> colour_out,   // Accumulated output
+        //TextureObject<float4> normal_out,
+        TextureObject<float> contrib_out,
+        SplatParams params) {
+        
+	//const ftl::voxhash::DepthCameraCUDA &camera = c_cameras[cam];
+
+	const int tid = (threadIdx.x + threadIdx.y * blockDim.x);
+	//const int warp = tid / WARP_SIZE;
+	const int x = (blockIdx.x*blockDim.x + threadIdx.x) / WARP_SIZE;
+	const int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+	const float3 worldPos = make_float3(points.tex2D(x, y));
+	//const float3 normal = make_float3(tex2D<float4>(camera.normal, x, y));
+	if (worldPos.x == MINF) return;
+    //const float r = (camera.poseInverse * worldPos).z / camera.params.fx;
+
+	const float3 camPos = params.m_viewMatrix * worldPos;
+	if (camPos.z < params.camera.minDepth) return;
+	if (camPos.z > params.camera.maxDepth) return;
+	const uint2 screenPos = params.camera.camToScreen<uint2>(camPos);
+
+    const int upsample = 8; //min(UPSAMPLE_MAX, int((5.0f*r) * params.camera.fx / camPos.z));
+
+	// Not on screen so stop now...
+	if (screenPos.x >= depth_in.width() || screenPos.y >= depth_in.height()) return;
+            
+    // Is this point near the actual surface and therefore a contributor?
+    const float d = ((float)depth_in.tex2D((int)screenPos.x, (int)screenPos.y)/1000.0f);
+    //if (abs(d - camPos.z) > DEPTH_THRESHOLD) return;
+
+    // TODO:(Nick) Should just one thread load these to shared mem?
+    const float4 colour = make_float4(colour_in.tex2D(x, y));
+    //const float4 normal = tex2D<float4>(camera.normal, x, y);
+
+	// Each thread in warp takes an upsample point and updates corresponding depth buffer.
+	const int lane = tid % WARP_SIZE;
+	for (int i=lane; i<upsample*upsample; i+=WARP_SIZE) {
+		const float u = (i % upsample) - (upsample / 2);
+		const float v = (i / upsample) - (upsample / 2);
+
+        // Use the depth buffer to determine this pixels 3D position in camera space
+        const float d = ((float)depth_in.tex2D(screenPos.x+u, screenPos.y+v)/1000.0f);
+		const float3 nearest = params.camera.screenToCam((int)(screenPos.x+u),(int)(screenPos.y+v),d);
+
+        // What is contribution of our current point at this pixel?
+        const float weight = ftl::cuda::spatialWeighting(length(nearest - camPos), SMOOTHING_MULTIPLIER_C*(nearest.z/params.camera.fx));
+        if (screenPos.x+u < colour_out.width() && screenPos.y+v < colour_out.height() && weight > 0.0f) {  // TODO: Use confidence threshold here
+            const float4 wcolour = colour * weight;
+			//const float4 wnormal = normal * weight;
+			
+			//printf("Z %f\n", d);
+
+            // Add this points contribution to the pixel buffer
+            atomicAdd((float*)&colour_out(screenPos.x+u, screenPos.y+v), wcolour.x);
+            atomicAdd((float*)&colour_out(screenPos.x+u, screenPos.y+v)+1, wcolour.y);
+            atomicAdd((float*)&colour_out(screenPos.x+u, screenPos.y+v)+2, wcolour.z);
+            atomicAdd((float*)&colour_out(screenPos.x+u, screenPos.y+v)+3, wcolour.w);
+            //atomicAdd((float*)&normal_out(screenPos.x+u, screenPos.y+v), wnormal.x);
+            //atomicAdd((float*)&normal_out(screenPos.x+u, screenPos.y+v)+1, wnormal.y);
+            //atomicAdd((float*)&normal_out(screenPos.x+u, screenPos.y+v)+2, wnormal.z);
+            //atomicAdd((float*)&normal_out(screenPos.x+u, screenPos.y+v)+3, wnormal.w);
+            atomicAdd(&contrib_out(screenPos.x+u, screenPos.y+v), weight);
+        }
+	}
+}
+
+__global__ void dibr_attribute_contrib_kernel(
+    TextureObject<float> colour_in,    // Original colour image
+    TextureObject<float4> points,       // Original 3D points
+    TextureObject<int> depth_in,        // Virtual depth map
+    TextureObject<float4> colour_out,   // Accumulated output
+    //TextureObject<float4> normal_out,
+    TextureObject<float> contrib_out,
+    SplatParams params) {
+    
+    //const ftl::voxhash::DepthCameraCUDA &camera = c_cameras[cam];
+
+    const int tid = (threadIdx.x + threadIdx.y * blockDim.x);
+    //const int warp = tid / WARP_SIZE;
+    const int x = (blockIdx.x*blockDim.x + threadIdx.x) / WARP_SIZE;
+    const int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+    const float3 worldPos = make_float3(points.tex2D(x, y));
+    //const float3 normal = make_float3(tex2D<float4>(camera.normal, x, y));
+    if (worldPos.x == MINF) return;
+    //const float r = (camera.poseInverse * worldPos).z / camera.params.fx;
+
+    const float3 camPos = params.m_viewMatrix * worldPos;
+    if (camPos.z < params.camera.minDepth) return;
+    if (camPos.z > params.camera.maxDepth) return;
+    const uint2 screenPos = params.camera.camToScreen<uint2>(camPos);
+
+    const int upsample = 8; //min(UPSAMPLE_MAX, int((5.0f*r) * params.camera.fx / camPos.z));
+
+    // Not on screen so stop now...
+    if (screenPos.x >= depth_in.width() || screenPos.y >= depth_in.height()) return;
+            
+    // Is this point near the actual surface and therefore a contributor?
+    const float d = ((float)depth_in.tex2D((int)screenPos.x, (int)screenPos.y)/1000.0f);
+    //if (abs(d - camPos.z) > DEPTH_THRESHOLD) return;
+
+    // TODO:(Nick) Should just one thread load these to shared mem?
+    const float colour = (colour_in.tex2D(x, y));
+    //const float4 normal = tex2D<float4>(camera.normal, x, y);
+
+    // Each thread in warp takes an upsample point and updates corresponding depth buffer.
+    const int lane = tid % WARP_SIZE;
+    for (int i=lane; i<upsample*upsample; i+=WARP_SIZE) {
+        const float u = (i % upsample) - (upsample / 2);
+        const float v = (i / upsample) - (upsample / 2);
+
+        // Use the depth buffer to determine this pixels 3D position in camera space
+        const float d = ((float)depth_in.tex2D(screenPos.x+u, screenPos.y+v)/1000.0f);
+        const float3 nearest = params.camera.screenToCam((int)(screenPos.x+u),(int)(screenPos.y+v),d);
+
+        // What is contribution of our current point at this pixel?
+        const float weight = ftl::cuda::spatialWeighting(length(nearest - camPos), SMOOTHING_MULTIPLIER_C*(nearest.z/params.camera.fx));
+        if (screenPos.x+u < colour_out.width() && screenPos.y+v < colour_out.height() && weight > 0.0f) {  // TODO: Use confidence threshold here
+            const float wcolour = colour * weight;
+            //const float4 wnormal = normal * weight;
+            
+            //printf("Z %f\n", d);
+
+            // Add this points contribution to the pixel buffer
+            atomicAdd((float*)&colour_out(screenPos.x+u, screenPos.y+v), wcolour);
+            atomicAdd(&contrib_out(screenPos.x+u, screenPos.y+v), weight);
+        }
+    }
+}
+
+void ftl::cuda::dibr_attribute(
+        TextureObject<uchar4> &colour_in,    // Original colour image
+        TextureObject<float4> &points,       // Original 3D points
+        TextureObject<int> &depth_in,        // Virtual depth map
+        TextureObject<float4> &colour_out,   // Accumulated output
+        //TextureObject<float4> normal_out,
+        TextureObject<float> &contrib_out,
+        SplatParams &params, cudaStream_t stream) {
+    const dim3 gridSize((depth_in.width() + 2 - 1)/2, (depth_in.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
+	const dim3 blockSize(2*WARP_SIZE, T_PER_BLOCK);
+
+    dibr_attribute_contrib_kernel<<<gridSize, blockSize, 0, stream>>>(
+        colour_in,
+        points,
+        depth_in,
+        colour_out,
+        contrib_out,
+        params
+    );
+    cudaSafeCall( cudaGetLastError() );
+}
+
+void ftl::cuda::dibr_attribute(
+        TextureObject<float> &colour_in,    // Original colour image
+        TextureObject<float4> &points,       // Original 3D points
+        TextureObject<int> &depth_in,        // Virtual depth map
+        TextureObject<float4> &colour_out,   // Accumulated output
+        //TextureObject<float4> normal_out,
+        TextureObject<float> &contrib_out,
+        SplatParams &params, cudaStream_t stream) {
+    const dim3 gridSize((depth_in.width() + 2 - 1)/2, (depth_in.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
+    const dim3 blockSize(2*WARP_SIZE, T_PER_BLOCK);
+
+    dibr_attribute_contrib_kernel<<<gridSize, blockSize, 0, stream>>>(
+        colour_in,
+        points,
+        depth_in,
+        colour_out,
+        contrib_out,
+        params
+    );
+    cudaSafeCall( cudaGetLastError() );
+}
+
+//==============================================================================
+
+__global__ void dibr_normalise_kernel(
+        TextureObject<float4> colour_in,
+        TextureObject<uchar4> colour_out,
+        //TextureObject<float4> normals,
+        TextureObject<float> contribs) {
+    const unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;
+    const unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+    if (x < colour_in.width() && y < colour_in.height()) {
+        const float4 colour = colour_in.tex2D((int)x,(int)y);
+        //const float4 normal = normals.tex2D((int)x,(int)y);
+        const float contrib = contribs.tex2D((int)x,(int)y);
+
+        if (contrib > 0.0f) {
+            colour_out(x,y) = make_uchar4(colour.x / contrib, colour.y / contrib, colour.z / contrib, 0);
+            //normals(x,y) = normal / contrib;
+        }
+    }
+}
+
+__global__ void dibr_normalise_kernel(
+        TextureObject<float4> colour_in,
+        TextureObject<float> colour_out,
+        //TextureObject<float4> normals,
+        TextureObject<float> contribs) {
+    const unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;
+    const unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+    if (x < colour_in.width() && y < colour_in.height()) {
+        const float4 colour = colour_in.tex2D((int)x,(int)y);
+        //const float4 normal = normals.tex2D((int)x,(int)y);
+        const float contrib = contribs.tex2D((int)x,(int)y);
+
+        if (contrib > 0.0f) {
+            colour_out(x,y) = colour.x / contrib;
+            //normals(x,y) = normal / contrib;
+        }
+    }
+}
+
+void ftl::cuda::dibr_normalise(TextureObject<float4> &colour_in, TextureObject<uchar4> &colour_out, TextureObject<float> &contribs, cudaStream_t stream) {
+    const dim3 gridSize((colour_in.width() + T_PER_BLOCK - 1)/T_PER_BLOCK, (colour_in.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
+    const dim3 blockSize(T_PER_BLOCK, T_PER_BLOCK);
+
+    dibr_normalise_kernel<<<gridSize, blockSize, 0, stream>>>(colour_in, colour_out, contribs);
+    cudaSafeCall( cudaGetLastError() );
+}
+
+void ftl::cuda::dibr_normalise(TextureObject<float4> &colour_in, TextureObject<float> &colour_out, TextureObject<float> &contribs, cudaStream_t stream) {
+    const dim3 gridSize((colour_in.width() + T_PER_BLOCK - 1)/T_PER_BLOCK, (colour_in.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
+    const dim3 blockSize(T_PER_BLOCK, T_PER_BLOCK);
+
+    dibr_normalise_kernel<<<gridSize, blockSize, 0, stream>>>(colour_in, colour_out, contribs);
+    cudaSafeCall( cudaGetLastError() );
+}
diff --git a/components/renderers/cpp/src/splatter_cuda.hpp b/components/renderers/cpp/src/splatter_cuda.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8c57d58486c04aff5960f215c6a6308719e6af7b
--- /dev/null
+++ b/components/renderers/cpp/src/splatter_cuda.hpp
@@ -0,0 +1,47 @@
+#ifndef _FTL_RECONSTRUCTION_SPLAT_CUDA_HPP_
+#define _FTL_RECONSTRUCTION_SPLAT_CUDA_HPP_
+
+#include <ftl/cuda_common.hpp>
+#include <ftl/render/splat_params.hpp>
+
+namespace ftl {
+namespace cuda {
+	void dibr_merge(
+		ftl::cuda::TextureObject<float4> &points,
+		ftl::cuda::TextureObject<int> &depth,
+		ftl::render::SplatParams params,
+		cudaStream_t stream);
+
+	void dibr_attribute(
+		ftl::cuda::TextureObject<uchar4> &in,	// Original colour image
+		ftl::cuda::TextureObject<float4> &points,		// Original 3D points
+		ftl::cuda::TextureObject<int> &depth_in,		// Virtual depth map
+		ftl::cuda::TextureObject<float4> &out,	// Accumulated output
+		//TextureObject<float4> normal_out,
+		ftl::cuda::TextureObject<float> &contrib_out,
+		ftl::render::SplatParams &params, cudaStream_t stream);
+
+	void dibr_attribute(
+		ftl::cuda::TextureObject<float> &in,	// Original colour image
+		ftl::cuda::TextureObject<float4> &points,		// Original 3D points
+		ftl::cuda::TextureObject<int> &depth_in,		// Virtual depth map
+		ftl::cuda::TextureObject<float4> &out,	// Accumulated output
+		//TextureObject<float4> normal_out,
+		ftl::cuda::TextureObject<float> &contrib_out,
+		ftl::render::SplatParams &params, cudaStream_t stream);
+
+	void dibr_normalise(
+		ftl::cuda::TextureObject<float4> &in,
+		ftl::cuda::TextureObject<uchar4> &out,
+		ftl::cuda::TextureObject<float> &contribs,
+		cudaStream_t stream);
+
+	void dibr_normalise(
+		ftl::cuda::TextureObject<float4> &in,
+		ftl::cuda::TextureObject<float> &out,
+		ftl::cuda::TextureObject<float> &contribs,
+		cudaStream_t stream);
+}
+}
+
+#endif  // _FTL_RECONSTRUCTION_SPLAT_CUDA_HPP_
diff --git a/components/rgbd-sources/CMakeLists.txt b/components/rgbd-sources/CMakeLists.txt
index ff7d290981c1a84c5ee211219ae0651f85c58c77..2b056d009a73dd7ee82adaedbf03bb98194d636f 100644
--- a/components/rgbd-sources/CMakeLists.txt
+++ b/components/rgbd-sources/CMakeLists.txt
@@ -4,6 +4,7 @@ set(RGBDSRC
 	src/disparity.cpp
 	src/source.cpp
 	src/frame.cpp
+	src/frameset.cpp
 	src/stereovideo.cpp
 	src/middlebury_source.cpp
 	src/net.cpp
@@ -17,6 +18,7 @@ set(RGBDSRC
 	src/cb_segmentation.cpp
 	src/abr.cpp
 	src/offilter.cpp
+	src/virtual.cpp
 )
 
 if (HAVE_REALSENSE)
@@ -37,6 +39,7 @@ endif (LIBSGM_FOUND)
 if (CUDA_FOUND)
 	list(APPEND RGBDSRC
 		src/algorithms/disp2depth.cu
+		src/algorithms/offilter.cu
 #		"src/algorithms/opencv_cuda_bm.cpp"
 #		"src/algorithms/opencv_cuda_bp.cpp"
 #		"src/algorithms/rtcensus.cu"
diff --git a/components/rgbd-sources/include/ftl/offilter.hpp b/components/rgbd-sources/include/ftl/offilter.hpp
index 4c4fbbb9837ef39e80f9aad68c62ae5d82d4671e..6aece39aab241dfc7605dfb208d7d30ba6135509 100644
--- a/components/rgbd-sources/include/ftl/offilter.hpp
+++ b/components/rgbd-sources/include/ftl/offilter.hpp
@@ -1,8 +1,10 @@
 #pragma once
 
 #include <ftl/config.h>
+#include <ftl/rgbd/frame.hpp>
 
 #ifdef HAVE_OPTFLOW
+#include <ftl/cuda_util.hpp>
 #include <opencv2/core.hpp>
 #include <opencv2/core/cuda.hpp>
 #include <opencv2/cudaoptflow.hpp>
@@ -12,23 +14,15 @@ namespace rgbd {
 
 class OFDisparityFilter {
 public:
-	OFDisparityFilter() : n_max_(0), threshold_(0.0), size_(0, 0) {} // TODO: invalid state
+	OFDisparityFilter() : n_max_(0), threshold_(0.0) {}
 	OFDisparityFilter(cv::Size size, int n_frames, float threshold);
-	void filter(cv::Mat &disp, const cv::Mat &rgb);
+	void filter(ftl::rgbd::Frame &frame, cv::cuda::Stream &stream);
 
 private:
-	int n_;
 	int n_max_;
 	float threshold_;
-	cv::Size size_;
 
-	cv::Mat disp_;
-	cv::Mat gray_;
-
-	cv::Mat flowxy_;
-	cv::Mat flowxy_up_;
-
-	cv::Ptr<cv::cuda::NvidiaOpticalFlow_1_0> nvof_;
+	cv::cuda::GpuMat disp_old_;
 };
 
 }
diff --git a/components/rgbd-sources/include/ftl/rgbd/camera.hpp b/components/rgbd-sources/include/ftl/rgbd/camera.hpp
index 8acad9c41d6c4950398cde7d9f44ab8ae0bc08b6..e245be1ea44f3e7e8503f585bf2c387fef821a38 100644
--- a/components/rgbd-sources/include/ftl/rgbd/camera.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/camera.hpp
@@ -2,23 +2,69 @@
 #ifndef _FTL_RGBD_CAMERA_PARAMS_HPP_
 #define _FTL_RGBD_CAMERA_PARAMS_HPP_
 
+#include <vector_types.h>
+#include <cuda_runtime.h>
+#include <ftl/cuda_util.hpp>
+
 namespace ftl{
 namespace rgbd {
 
-struct Camera {
-	double fx;
-	double fy;
-	double cx;
-	double cy;
-	unsigned int width;
-	unsigned int height;
-	double minDepth;
-	double maxDepth;
-	double baseline;
-	double doffs;
+/**
+ * All properties associated with cameras. This structure is designed to
+ * operate on CPU and GPU.
+ */
+struct __align__(16) Camera {
+	double fx;				// Focal length X
+	double fy;				// Focal length Y (usually same as fx)
+	double cx;				// Principle point Y
+	double cy;				// Principle point Y
+	unsigned int width;		// Pixel width
+	unsigned int height;	// Pixel height
+	double minDepth;		// Near clip in meters
+	double maxDepth;		// Far clip in meters
+	double baseline;		// For stereo pair
+	double doffs;			// Disparity offset
+
+	/**
+	 * Convert camera coordinates into screen coordinates.
+	 */
+	template <typename T> __device__ T camToScreen(const float3 &pos) const;
+
+	/**
+	 * Convert screen plus depth into camera coordinates.
+	 */
+	__device__ float3 screenToCam(uint ux, uint uy, float depth) const; 
 };
 
 };
 };
 
+// ---- IMPLEMENTATIONS --------------------------------------------------------
+
+template <> __device__
+inline float2 ftl::rgbd::Camera::camToScreen<float2>(const float3 &pos) const {
+	return make_float2(
+			pos.x*fx/pos.z - cx,			
+			pos.y*fy/pos.z - cy);
+}
+
+template <> __device__
+inline int2 ftl::rgbd::Camera::camToScreen<int2>(const float3 &pos) const {
+	float2 pImage = camToScreen<float2>(pos);
+	return make_int2(pImage + make_float2(0.5f, 0.5f));
+}
+
+template <> __device__
+inline uint2 ftl::rgbd::Camera::camToScreen<uint2>(const float3 &pos) const {
+	int2 p = camToScreen<int2>(pos);
+	return make_uint2(p.x, p.y);
+}
+
+__device__
+inline float3 ftl::rgbd::Camera::screenToCam(uint ux, uint uy, float depth) const {
+	const float x = ((float)ux+cx) / fx;
+	const float y = ((float)uy+cy) / fy;
+	return make_float3(depth*x, depth*y, depth);
+}
+
 #endif
diff --git a/components/rgbd-sources/include/ftl/rgbd/channels.hpp b/components/rgbd-sources/include/ftl/rgbd/channels.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9bf731a5319fa47c501a91e09f1e2acc48c5a4a8
--- /dev/null
+++ b/components/rgbd-sources/include/ftl/rgbd/channels.hpp
@@ -0,0 +1,124 @@
+#ifndef _FTL_RGBD_CHANNELS_HPP_
+#define _FTL_RGBD_CHANNELS_HPP_
+
+#include <bitset>
+#include <msgpack.hpp>
+
+namespace ftl {
+namespace rgbd {
+
+enum struct Channel : int {
+    None = -1,
+    Colour = 0,         // 8UC3 or 8UC4
+    Left = 0,
+    Depth = 1,          // 32S or 32F
+    Right = 2,          // 8UC3 or 8UC4
+    Colour2 = 2,
+    Disparity = 3,
+    Depth2 = 3,
+    Deviation = 4,
+    Normals = 5,        // 32FC4
+    Points = 6,         // 32FC4
+    Confidence = 7,     // 32F
+    Contribution = 7,   // 32F
+    EnergyVector,       // 32FC4
+    Flow,               // 32F
+    Energy,             // 32F
+    LeftGray,
+    RightGray,
+    Overlay1
+};
+
+class Channels {
+    public:
+
+	class iterator {
+		public:
+		iterator(const Channels &c, unsigned int ix) : channels_(c), ix_(ix) { }
+		iterator operator++();
+		iterator operator++(int junk);
+		inline ftl::rgbd::Channel operator*() { return static_cast<Channel>(static_cast<int>(ix_)); }
+		//ftl::rgbd::Channel operator->() { return ptr_; }
+		inline bool operator==(const iterator& rhs) { return ix_ == rhs.ix_; }
+		inline bool operator!=(const iterator& rhs) { return ix_ != rhs.ix_; }
+		private:
+		const Channels &channels_;
+		unsigned int ix_;
+	};
+
+    inline Channels() { mask = 0; }
+    inline explicit Channels(unsigned int m) { mask = m; }
+    inline explicit Channels(Channel c) { mask = (c == Channel::None) ? 0 : 0x1 << static_cast<unsigned int>(c); }
+    inline Channels &operator=(Channel c) { mask = (c == Channel::None) ? 0 : 0x1 << static_cast<unsigned int>(c); return *this; }
+    inline Channels operator|(Channel c) const { return (c == Channel::None) ? Channels(mask) : Channels(mask | (0x1 << static_cast<unsigned int>(c))); }
+    inline Channels operator+(Channel c) const { return (c == Channel::None) ? Channels(mask) : Channels(mask | (0x1 << static_cast<unsigned int>(c))); }
+    inline Channels &operator|=(Channel c) { mask |= (c == Channel::None) ? 0 : (0x1 << static_cast<unsigned int>(c)); return *this; }
+    inline Channels &operator+=(Channel c) { mask |= (c == Channel::None) ? 0 : (0x1 << static_cast<unsigned int>(c)); return *this; }
+    inline Channels &operator-=(Channel c) { mask &= ~((c == Channel::None) ? 0 : (0x1 << static_cast<unsigned int>(c))); return *this; }
+    inline Channels &operator+=(unsigned int c) { mask |= (0x1 << c); return *this; }
+    inline Channels &operator-=(unsigned int c) { mask &= ~(0x1 << c); return *this; }
+
+    inline bool has(Channel c) const {
+        return (c == Channel::None) ? true : mask & (0x1 << static_cast<unsigned int>(c));
+    }
+
+    inline bool has(unsigned int c) const {
+        return mask & (0x1 << c);
+    }
+
+	inline iterator begin() { return iterator(*this, 0); }
+	inline iterator end() { return iterator(*this, 32); }
+
+    inline operator unsigned int() { return mask; }
+    inline operator bool() { return mask > 0; }
+    inline operator Channel() {
+        if (mask == 0) return Channel::None;
+        int ix = 0;
+        int tmask = mask;
+        while (!(tmask & 0x1) && ++ix < 32) tmask >>= 1;
+        return static_cast<Channel>(ix);
+    }
+    
+    inline size_t count() { return std::bitset<32>(mask).count(); }
+    inline void clear() { mask = 0; }
+
+    static const size_t kMax = 32;
+
+	static Channels All();
+
+    private:
+    unsigned int mask;
+};
+
+inline Channels::iterator Channels::iterator::operator++() { Channels::iterator i = *this; while (++ix_ < 32 && !channels_.has(ix_)); return i; }
+inline Channels::iterator Channels::iterator::operator++(int junk) { while (++ix_ < 32 && !channels_.has(ix_)); return *this; }
+
+inline Channels Channels::All() {
+	return Channels(0xFFFFFFFFu);
+}
+
+static const Channels kNoChannels;
+static const Channels kAllChannels(0xFFFFFFFFu);
+
+inline bool isFloatChannel(ftl::rgbd::Channel chan) {
+	switch (chan) {
+	case Channel::Depth		:
+	case Channel::Energy	: return true;
+	default					: return false;
+	}
+}
+
+}
+}
+
+MSGPACK_ADD_ENUM(ftl::rgbd::Channel);
+
+inline ftl::rgbd::Channels operator|(ftl::rgbd::Channel a, ftl::rgbd::Channel b) {
+    return ftl::rgbd::Channels(a) | b;
+}
+
+inline ftl::rgbd::Channels operator+(ftl::rgbd::Channel a, ftl::rgbd::Channel b) {
+    return ftl::rgbd::Channels(a) | b;
+}
+
+#endif  // _FTL_RGBD_CHANNELS_HPP_
diff --git a/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp b/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp
index 29edf722fe4a3eaac36c76f7c861797f8ff71a49..e98ff38aacd4cf0731ef96b67ecc85732d4c0c7f 100644
--- a/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp
@@ -2,6 +2,7 @@
 #define _FTL_RGBD_DETAIL_SOURCE_HPP_
 
 #include <Eigen/Eigen>
+#include <ftl/cuda_util.hpp>
 #include <opencv2/opencv.hpp>
 #include <ftl/rgbd/camera.hpp>
 #include <ftl/rgbd/frame.hpp>
@@ -55,7 +56,7 @@ class Source {
 	virtual bool isReady() { return false; };
 	virtual void setPose(const Eigen::Matrix4d &pose) { };
 
-	virtual Camera parameters(channel_t) { return params_; };
+	virtual Camera parameters(ftl::rgbd::Channel) { return params_; };
 
 	protected:
 	capability_t capabilities_;
diff --git a/components/rgbd-sources/include/ftl/rgbd/format.hpp b/components/rgbd-sources/include/ftl/rgbd/format.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2e86aaf96a5f64d13065385ddee360fd80ca9f8c
--- /dev/null
+++ b/components/rgbd-sources/include/ftl/rgbd/format.hpp
@@ -0,0 +1,52 @@
+#ifndef _FTL_RGBD_FORMAT_HPP_
+#define _FTL_RGBD_FORMAT_HPP_
+
+#include <opencv2/core.hpp>
+#include <opencv2/core/cuda.hpp>
+#include <ftl/cuda_util.hpp>
+#include <ftl/codecs/bitrates.hpp>
+#include <ftl/traits.hpp>
+
+namespace ftl {
+namespace rgbd {
+
+struct FormatBase {
+	FormatBase(size_t w, size_t h, int t) : width(w), height(h), cvType(t) {}
+
+	size_t width;		// Width in pixels
+	size_t height;		// Height in pixels
+	int cvType;			// OpenCV Mat type
+
+	inline bool empty() const { return width == 0 || height == 0; }
+	inline cv::Size size() const { return cv::Size(width, height); }
+};
+
+template <typename T>
+struct Format : public ftl::rgbd::FormatBase {
+	Format() : FormatBase(0,0,0) {}
+
+	Format(size_t w, size_t h) : FormatBase(
+			w, h, ftl::traits::OpenCVType<T>::value) {}
+
+	explicit Format(ftl::codecs::definition_t d) : FormatBase(
+			ftl::codecs::getWidth(d),
+			ftl::codecs::getHeight(d),
+			ftl::traits::OpenCVType<T>::value) {}
+
+	explicit Format(const cv::Size &s) : FormatBase(
+			s.width,
+			s.height,
+			ftl::traits::OpenCVType<T>::value) {}
+
+	explicit Format(const cv::InputArray &a) : FormatBase(
+			a.cols,
+			a.rows,
+			ftl::traits::OpenCVType<T>::value) {
+		CHECK(cvType == a.type());
+	}
+};
+
+}
+}
+
+#endif  // _FTL_RGBD_FORMAT_HPP_
diff --git a/components/rgbd-sources/include/ftl/rgbd/frame.hpp b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
index 227913e5de056f15380390201da96b66ec23e27f..252ff271de36336938d51d616cdd5a9e87d52187 100644
--- a/components/rgbd-sources/include/ftl/rgbd/frame.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
@@ -3,133 +3,253 @@
 #define _FTL_RGBD_FRAME_HPP_
 
 #include <ftl/configuration.hpp>
+#include <ftl/exception.hpp>
 #include <opencv2/core.hpp>
 #include <opencv2/core/cuda.hpp>
+#include <opencv2/core/cuda_stream_accessor.hpp>
 
-namespace ftl {
-namespace rgbd {
-
-typedef unsigned int channel_t;
-
-static const channel_t kChanNone = 0;
-static const channel_t kChanLeft = 0x0001;		// CV_8UC3
-static const channel_t kChanDepth = 0x0002;		// CV_32FC1
-static const channel_t kChanRight = 0x0004;		// CV_8UC3
-static const channel_t kChanDisparity = 0x0008; // CV_32FC1
-static const channel_t kChanDeviation = 0x0010;
-static const channel_t kChanNormals = 0x0020;
-static const channel_t kChanConfidence = 0x0040;
-static const channel_t kChanFlow = 0x0080;		// CV_16SC2 (format 10.5) from NVOF
-static const channel_t kChanEnergy = 0x0100;
+#include <ftl/rgbd/channels.hpp>
+#include <ftl/rgbd/format.hpp>
+#include <ftl/codecs/bitrates.hpp>
 
-// should l/r gray be removed (not that expensive to re-calculate if needed)?
-static const channel_t kChanLeftGray = 0x0200;	// CV_8UC1
-static const channel_t kChanRightGray = 0x0400;	// CV_8UC1
+#include <ftl/cuda_common.hpp>
 
-static const channel_t kChanOverlay1 = 0x1000;
+#include <type_traits>
+#include <array>
 
-// maximum number of available channels
-static const unsigned int n_channels = 13;
-
-inline bool isFloatChannel(ftl::rgbd::channel_t chan) {
-	return (chan == ftl::rgbd::kChanDepth || chan == ftl::rgbd::kChanEnergy);
-}
+namespace ftl {
+namespace rgbd {
 
 // TODO:	interpolation for scaling depends on channel type;
 //			NN for depth/disparity/optflow, linear/cubic/etc. for RGB
 
 class Frame;
+class Source;
 
+/**
+ * Manage a set of image channels corresponding to a single camera frame.
+ */
 class Frame {
 public:
-	Frame() :	channels_host_(n_channels),
-				channels_gpu_(n_channels),
-				available_(n_channels, 0)
-	{}
+	Frame() : src_(nullptr) {}
+	explicit Frame(ftl::rgbd::Source *src) : src_(src) {}
+
+	inline ftl::rgbd::Source *source() const { return src_; }
+
+	// Prevent frame copy, instead use a move.
+	//Frame(const Frame &)=delete;
+	//Frame &operator=(const Frame &)=delete;
+
+	void download(ftl::rgbd::Channel c, cv::cuda::Stream stream);
+	void upload(ftl::rgbd::Channel c, cv::cuda::Stream stream);
+	void download(ftl::rgbd::Channels c, cv::cuda::Stream stream);
+	void upload(ftl::rgbd::Channels c, cv::cuda::Stream stream);
+
+	inline void download(ftl::rgbd::Channel c, cudaStream_t stream=0) { download(c, cv::cuda::StreamAccessor::wrapStream(stream)); };
+	inline void upload(ftl::rgbd::Channel c, cudaStream_t stream=0) { upload(c, cv::cuda::StreamAccessor::wrapStream(stream)); };
+	inline void download(ftl::rgbd::Channels c, cudaStream_t stream=0) { download(c, cv::cuda::StreamAccessor::wrapStream(stream)); };
+	inline void upload(ftl::rgbd::Channels c, cudaStream_t stream=0) { upload(c, cv::cuda::StreamAccessor::wrapStream(stream)); };
+
+	/**
+	 * Perform a buffer swap of the selected channels. This is intended to be
+	 * a copy from `this` to the passed frame object but by buffer swap
+	 * instead of memory copy, meaning `this` may become invalid afterwards.
+	 */
+	void swapTo(ftl::rgbd::Channels, Frame &);
+
+	/**
+	 * Create a channel with a given format. This will discard any existing
+	 * data associated with the channel and ensure all data structures and
+	 * memory allocations match the new format.
+	 */
+	template <typename T> T &create(ftl::rgbd::Channel c, const ftl::rgbd::FormatBase &f);
 
-	/* @brief	Reset all channels without releasing memory.
+	/**
+	 * Create a channel but without any format.
 	 */
-	void reset()
-	{
-		std::fill(available_.begin(), available_.end(), 0);
+	template <typename T> T &create(ftl::rgbd::Channel c);
+
+	/**
+	 * Create a CUDA texture object for a channel. This version takes a format
+	 * argument to also create (or recreate) the associated GpuMat.
+	 */
+	template <typename T>
+	ftl::cuda::TextureObject<T> &createTexture(ftl::rgbd::Channel c, const ftl::rgbd::Format<T> &f);
+
+	/**
+	 * Create a CUDA texture object for a channel. With this version the GpuMat
+	 * must already exist and be of the correct type.
+	 */
+	template <typename T>
+	ftl::cuda::TextureObject<T> &createTexture(ftl::rgbd::Channel c);
+
+	/**
+	 * Reset all channels without releasing memory.
+	 */
+	void reset();
+
+	bool empty(ftl::rgbd::Channels c);
+
+	inline bool empty(ftl::rgbd::Channel c) {
+		auto &m = _get(c);
+		return !hasChannel(c) || (m.host.empty() && m.gpu.empty());
 	}
 
-	/* @brief	Is there valid data in channel (either host or gpu).
+	/**
+	 * Is there valid data in channel (either host or gpu).
 	 */
-	bool hasChannel(const ftl::rgbd::channel_t& channel)
-	{
-		return available_[_channelIdx(channel)];
+	inline bool hasChannel(ftl::rgbd::Channel channel) const {
+		return channels_.has(channel);
 	}
 
-	/* @brief	Method to get reference to the channel content
+	inline ftl::rgbd::Channels getChannels() const { return channels_; }
+
+	/**
+	 * Is the channel data currently located on GPU. This also returns false if
+	 * the channel does not exist.
+	 */
+	inline bool isGPU(ftl::rgbd::Channel channel) const {
+		return channels_.has(channel) && gpu_.has(channel);
+	}
+
+	/**
+	 * Is the channel data currently located on CPU memory. This also returns
+	 * false if the channel does not exist.
+	 */
+	inline bool isCPU(ftl::rgbd::Channel channel) const {
+		return channels_.has(channel) && !gpu_.has(channel);
+	}
+
+	/**
+	 * Method to get reference to the channel content.
 	 * @param	Channel type
-	 * @param	CUDA stream
-	 * @returns	Const reference to channel data
+	 * @return	Const reference to channel data
 	 * 
 	 * Result is valid only if hasChannel() is true. Host/Gpu transfer is
-	 * performed, if necessary, but only once unless channel contents is
-	 * changed by calling setChannel(). Return value valid only if
-	 * hasChannel(channel) is true.
+	 * performed, if necessary, but with a warning since an explicit upload or
+	 * download should be used.
 	 */
-	template <typename T> const T& getChannel(const ftl::rgbd::channel_t& channel, cv::cuda::Stream& stream);
-	template <typename T> const T& getChannel(const ftl::rgbd::channel_t& channel);
+	template <typename T> const T& get(ftl::rgbd::Channel channel) const;
 
-	/* @brief	Method to set/modify channel content
+	/**
+	 * Method to get reference to the channel content.
 	 * @param	Channel type
-	 * @returns	Reference to channel data
-	 * 
-	 * Returns non-const reference to channel memory. Invalidates other copies
-	 * of the data (host/gpu) for the specified channel, next time getChannel()
-	 * is called a memory transfer may occur.
+	 * @return	Reference to channel data
 	 * 
-	 * NOTE:	If user of setChannel<T>() wants to modify contents instead of
-	 * 			replacing them, getChannel<T>() needs to be called first to
-	 * 			ensure there is valid contents in the returned reference!
-	 * 			(TODO: interface could be improved)
+	 * Result is valid only if hasChannel() is true. Host/Gpu transfer is
+	 * performed, if necessary, but with a warning since an explicit upload or
+	 * download should be used.
 	 */
-	template <typename T> T& setChannel(const ftl::rgbd::channel_t& channel);
+	template <typename T> T& get(ftl::rgbd::Channel channel);
+
+	template <typename T> const ftl::cuda::TextureObject<T> &getTexture(ftl::rgbd::Channel) const;
+	template <typename T> ftl::cuda::TextureObject<T> &getTexture(ftl::rgbd::Channel);
 
 private:
+	struct ChannelData {
+		cv::Mat host;
+		cv::cuda::GpuMat gpu;
+		ftl::cuda::TextureObjectBase tex;
+	};
 
-	static size_t _channelIdx(const ftl::rgbd::channel_t& channel)
-	{
-		switch(channel)
-		{
-			case kChanNone:				return 0;
-			case kChanLeft:				return 1;
-			case kChanDepth:			return 2;
-			case kChanRight:			return 3;
-			case kChanDisparity:		return 4;
-			case kChanDeviation:		return 5;
-			case kChanNormals:			return 6;
-			case kChanConfidence:		return 7;
-			case kChanFlow:				return 8;
-			case kChanEnergy:			return 9;
-			case kChanLeftGray:			return 11;
-			case kChanRightGray:		return 12;
-			// should not happen (error); returned index is kChanNone
-			default:					return 0;
-		}
-	}
+	std::array<ChannelData, Channels::kMax> data_;
 
-	std::vector<cv::Mat> channels_host_;
-	std::vector<cv::cuda::GpuMat> channels_gpu_;
+	ftl::rgbd::Channels channels_;	// Does it have a channel
+	ftl::rgbd::Channels gpu_;		// Is the channel on a GPU
 
-	// bitmasks for each channel stored in available_
-	static const uint mask_host = 1;
-	static const uint mask_gpu = 2;
+	ftl::rgbd::Source *src_;
 
-	std::vector<uint> available_;
+	inline ChannelData &_get(ftl::rgbd::Channel c) { return data_[static_cast<unsigned int>(c)]; }
+	inline const ChannelData &_get(ftl::rgbd::Channel c) const { return data_[static_cast<unsigned int>(c)]; }
 };
 
-template<> const cv::Mat& Frame::getChannel(const ftl::rgbd::channel_t& channel, cv::cuda::Stream& stream);
-template<> const cv::cuda::GpuMat& Frame::getChannel(const ftl::rgbd::channel_t& channel, cv::cuda::Stream& stream);
+// Specialisations
+
+template<> const cv::Mat& Frame::get(ftl::rgbd::Channel channel) const;
+template<> const cv::cuda::GpuMat& Frame::get(ftl::rgbd::Channel channel) const;
+template<> cv::Mat& Frame::get(ftl::rgbd::Channel channel);
+template<> cv::cuda::GpuMat& Frame::get(ftl::rgbd::Channel channel);
+
+template <> cv::Mat &Frame::create(ftl::rgbd::Channel c, const ftl::rgbd::FormatBase &);
+template <> cv::cuda::GpuMat &Frame::create(ftl::rgbd::Channel c, const ftl::rgbd::FormatBase &);
+template <> cv::Mat &Frame::create(ftl::rgbd::Channel c);
+template <> cv::cuda::GpuMat &Frame::create(ftl::rgbd::Channel c);
+
+template <typename T>
+ftl::cuda::TextureObject<T> &Frame::getTexture(ftl::rgbd::Channel c) {
+	if (!channels_.has(c)) throw ftl::exception("Texture channel does not exist");
+	if (!gpu_.has(c)) throw ftl::exception("Texture channel is not on GPU");
+
+	auto &m = _get(c);
+
+	if (m.tex.cvType() != ftl::traits::OpenCVType<T>::value || m.tex.width() != m.gpu.cols || m.tex.height() != m.gpu.rows || m.gpu.type() != m.tex.cvType()) {
+		throw ftl::exception("Texture has not been created properly for this channel");
+	}
+
+	return ftl::cuda::TextureObject<T>::cast(m.tex);
+}
+
+template <typename T>
+ftl::cuda::TextureObject<T> &Frame::createTexture(ftl::rgbd::Channel c, const ftl::rgbd::Format<T> &f) {
+	if (!channels_.has(c)) channels_ += c;
+	if (!gpu_.has(c)) gpu_ += c;
 
-template<> const cv::Mat& Frame::getChannel(const ftl::rgbd::channel_t& channel);
-template<> const cv::cuda::GpuMat& Frame::getChannel(const ftl::rgbd::channel_t& channel);
+	auto &m = _get(c);
+
+	if (f.empty()) {
+		throw ftl::exception("createTexture needs a non-empty format");
+	} else {
+		m.gpu.create(f.size(), f.cvType);
+	}
+
+	if (m.gpu.type() != ftl::traits::OpenCVType<T>::value) {
+		throw ftl::exception("Texture type does not match underlying data type");
+	}
 
-template<> cv::Mat& Frame::setChannel(const ftl::rgbd::channel_t& channel);
-template<> cv::cuda::GpuMat& Frame::setChannel(const ftl::rgbd::channel_t& channel);
+	// TODO: Check tex cvType
+
+	if (m.tex.devicePtr() == nullptr) {
+		LOG(INFO) << "Creating texture object";
+		m.tex = ftl::cuda::TextureObject<T>(m.gpu);
+	} else if (m.tex.cvType() != ftl::traits::OpenCVType<T>::value || m.tex.width() != m.gpu.cols || m.tex.height() != m.gpu.rows) {
+		LOG(INFO) << "Recreating texture object";
+		m.tex.free();
+		m.tex = ftl::cuda::TextureObject<T>(m.gpu);
+	}
+
+	return ftl::cuda::TextureObject<T>::cast(m.tex);
+}
+
+template <typename T>
+ftl::cuda::TextureObject<T> &Frame::createTexture(ftl::rgbd::Channel c) {
+	if (!channels_.has(c)) throw ftl::exception("createTexture needs a format if the channel does not exist");
+
+	auto &m = _get(c);
+
+	if (isCPU(c) && !m.host.empty()) {
+		m.gpu.create(m.host.size(), m.host.type());
+		// TODO: Should this upload to GPU or not?
+		//gpu_ += c;
+	} else if (isCPU(c) || (isGPU(c) && m.gpu.empty())) {
+		throw ftl::exception("createTexture needs a format if no memory is allocated");
+	}
+
+	if (m.gpu.type() != ftl::traits::OpenCVType<T>::value) {
+		throw ftl::exception("Texture type does not match underlying data type");
+	}
+
+	// TODO: Check tex cvType
+
+	if (m.tex.devicePtr() == nullptr) {
+		LOG(INFO) << "Creating texture object";
+		m.tex = ftl::cuda::TextureObject<T>(m.gpu);
+	} else if (m.tex.cvType() != ftl::traits::OpenCVType<T>::value || m.tex.width() != m.gpu.cols || m.tex.height() != m.gpu.rows || m.tex.devicePtr() != m.gpu.data) {
+		m.tex.free();
+		m.tex = ftl::cuda::TextureObject<T>(m.gpu);
+	}
+
+	return ftl::cuda::TextureObject<T>::cast(m.tex);
+}
 
 }
 }
diff --git a/components/rgbd-sources/include/ftl/rgbd/frameset.hpp b/components/rgbd-sources/include/ftl/rgbd/frameset.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2fa39e2eacf19339860e98fa98df44f687ac64c7
--- /dev/null
+++ b/components/rgbd-sources/include/ftl/rgbd/frameset.hpp
@@ -0,0 +1,37 @@
+#ifndef _FTL_RGBD_FRAMESET_HPP_
+#define _FTL_RGBD_FRAMESET_HPP_
+
+#include <ftl/threads.hpp>
+#include <ftl/rgbd/frame.hpp>
+
+#include <opencv2/opencv.hpp>
+#include <vector>
+
+namespace ftl {
+namespace rgbd {
+
+class Source;
+
+/**
+ * Represents a set of synchronised frames, each with two channels. This is
+ * used to collect all frames from multiple computers that have the same
+ * timestamp.
+ */
+struct FrameSet {
+	int64_t timestamp;				// Millisecond timestamp of all frames
+	std::vector<Source*> sources;	// All source objects involved.
+	std::vector<ftl::rgbd::Frame> frames;
+	std::atomic<int> count;				// Number of valid frames
+	std::atomic<unsigned int> mask;		// Mask of all sources that contributed
+	bool stale;						// True if buffers have been invalidated
+	SHARED_MUTEX mtx;
+
+	void upload(ftl::rgbd::Channels, cudaStream_t stream=0);
+	void download(ftl::rgbd::Channels, cudaStream_t stream=0);
+	void swapTo(ftl::rgbd::FrameSet &);
+};
+
+}
+}
+
+#endif  // _FTL_RGBD_FRAMESET_HPP_
diff --git a/components/rgbd-sources/include/ftl/rgbd/group.hpp b/components/rgbd-sources/include/ftl/rgbd/group.hpp
index 000eea0ae60303d816e09298613ddc30f0a8146a..0ded29e80b7d2fa01ad656c2fbb3b8865b726a70 100644
--- a/components/rgbd-sources/include/ftl/rgbd/group.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/group.hpp
@@ -1,8 +1,11 @@
 #ifndef _FTL_RGBD_GROUP_HPP_
 #define _FTL_RGBD_GROUP_HPP_
 
+#include <ftl/cuda_util.hpp>
 #include <ftl/threads.hpp>
 #include <ftl/timer.hpp>
+#include <ftl/rgbd/frame.hpp>
+#include <ftl/rgbd/frameset.hpp>
 
 #include <opencv2/opencv.hpp>
 #include <vector>
@@ -12,22 +15,6 @@ namespace rgbd {
 
 class Source;
 
-/**
- * Represents a set of synchronised frames, each with two channels. This is
- * used to collect all frames from multiple computers that have the same
- * timestamp.
- */
-struct FrameSet {
-	int64_t timestamp;				// Millisecond timestamp of all frames
-	std::vector<Source*> sources;	// All source objects involved.
-	std::vector<cv::Mat> channel1;	// RGB
-	std::vector<cv::Mat> channel2;	// Depth (usually)
-	std::atomic<int> count;				// Number of valid frames
-	std::atomic<unsigned int> mask;		// Mask of all sources that contributed
-	bool stale;						// True if buffers have been invalidated
-	SHARED_MUTEX mtx;
-};
-
 // Allows a latency of 20 frames maximum
 static const size_t kFrameBufferSize = 20;
 
diff --git a/components/rgbd-sources/include/ftl/rgbd/source.hpp b/components/rgbd-sources/include/ftl/rgbd/source.hpp
index 1106d76220e676a3e7e36c5db18db9596a0f91d9..0ee163add0009023ec24e6df6bd18a1da927af1e 100644
--- a/components/rgbd-sources/include/ftl/rgbd/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/source.hpp
@@ -1,10 +1,11 @@
 #ifndef _FTL_RGBD_SOURCE_HPP_
 #define _FTL_RGBD_SOURCE_HPP_
 
+#include <ftl/cuda_util.hpp>
 #include <ftl/configuration.hpp>
 #include <ftl/rgbd/camera.hpp>
 #include <ftl/threads.hpp>
-//#include <ftl/net/universe.hpp>
+#include <ftl/net/universe.hpp>
 #include <ftl/uri.hpp>
 #include <ftl/rgbd/detail/source.hpp>
 #include <opencv2/opencv.hpp>
@@ -25,6 +26,7 @@ namespace rgbd {
 static inline bool isValidDepth(float d) { return (d > 0.01f) && (d < 39.99f); }
 
 class SnapshotReader;
+class VirtualSource;
 
 /**
  * RGBD Generic data source configurable entity. This class hides the
@@ -39,6 +41,7 @@ class Source : public ftl::Configurable {
 	public:
 	template <typename T, typename... ARGS>
 	friend T *ftl::config::create(ftl::config::json_t &, ARGS ...);
+	friend class VirtualSource;
 
 	//template <typename T, typename... ARGS>
 	//friend T *ftl::config::create(ftl::Configurable *, const std::string &, ARGS ...);
@@ -50,11 +53,11 @@ class Source : public ftl::Configurable {
 	Source(const Source&)=delete;
 	Source &operator=(const Source&) =delete;
 
-	private:
+	protected:
 	explicit Source(ftl::config::json_t &cfg);
 	Source(ftl::config::json_t &cfg, ftl::rgbd::SnapshotReader *);
 	Source(ftl::config::json_t &cfg, ftl::net::Universe *net);
-	~Source();
+	virtual ~Source();
 
 	public:
 	/**
@@ -65,9 +68,9 @@ class Source : public ftl::Configurable {
 	/**
 	 * Change the second channel source.
 	 */
-	bool setChannel(channel_t c);
+	bool setChannel(ftl::rgbd::Channel c);
 
-	channel_t getChannel() const { return channel_; }
+	ftl::rgbd::Channel getChannel() const { return channel_; }
 
 	/**
 	 * Perform the hardware or virtual frame grab operation. This should be
@@ -120,17 +123,6 @@ class Source : public ftl::Configurable {
 	 */
 	void getDepth(cv::Mat &d);
 
-	/**
-	 * Write frames into source buffers from an external renderer. Virtual
-	 * sources do not have an internal generator of frames but instead have
-	 * their data provided from an external rendering class. This function only
-	 * works when there is no internal generator.
-	 */
-	void writeFrames(int64_t ts, const cv::Mat &rgb, const cv::Mat &depth);
-	void writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uint> &depth, cudaStream_t stream);
-	void writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<float> &depth, cudaStream_t stream);
-	void writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uchar4> &rgb2, cudaStream_t stream);
-
 	int64_t timestamp() const { return timestamp_; }
 
 	/**
@@ -151,7 +143,7 @@ class Source : public ftl::Configurable {
 		else return params_;
 	}
 
-	const Camera parameters(channel_t) const;
+	const Camera parameters(ftl::rgbd::Channel) const;
 
 	cv::Mat cameraMatrix() const;
 
@@ -213,7 +205,7 @@ class Source : public ftl::Configurable {
 	void removeCallback() { callback_ = nullptr; }
 
 
-	private:
+	protected:
 	detail::Source *impl_;
 	cv::Mat rgb_;
 	cv::Mat depth_;
@@ -224,7 +216,7 @@ class Source : public ftl::Configurable {
 	SHARED_MUTEX mutex_;
 	bool paused_;
 	bool bullet_;
-	channel_t channel_;
+	ftl::rgbd::Channel channel_;
 	cudaStream_t stream_;
 	int64_t timestamp_;
 	std::function<void(int64_t, cv::Mat &, cv::Mat &)> callback_;
diff --git a/components/rgbd-sources/include/ftl/rgbd/streamer.hpp b/components/rgbd-sources/include/ftl/rgbd/streamer.hpp
index 3f6f1c4fa35bfcb4a97877a2d46d53230e2acbec..7c6e6f479afe022cdacefbabf9098e276e0c9f79 100644
--- a/components/rgbd-sources/include/ftl/rgbd/streamer.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/streamer.hpp
@@ -117,6 +117,8 @@ class Streamer : public ftl::Configurable {
 
 	void wait();
 
+	void setLatency(int l) { group_.setLatency(l); }
+
 	/**
 	 * Alternative to calling run(), it will operate a single frame capture,
 	 * compress and stream cycle.
diff --git a/components/rgbd-sources/include/ftl/rgbd/virtual.hpp b/components/rgbd-sources/include/ftl/rgbd/virtual.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..ed3530258d64b14427802f8e56375635ab26a24a
--- /dev/null
+++ b/components/rgbd-sources/include/ftl/rgbd/virtual.hpp
@@ -0,0 +1,30 @@
+#ifndef _FTL_RGBD_VIRTUAL_HPP_
+#define _FTL_RGBD_VIRTUAL_HPP_
+
+#include <ftl/rgbd/source.hpp>
+
+namespace ftl {
+namespace rgbd {
+
+class VirtualSource : public ftl::rgbd::Source {
+    public:
+    explicit VirtualSource(ftl::config::json_t &cfg);
+	~VirtualSource();
+
+	void onRender(const std::function<void(ftl::rgbd::Frame &)> &);
+
+	void setTimestamp(int64_t ts) { timestamp_ = ts; }
+
+    /**
+	 * Write frames into source buffers from an external renderer. Virtual
+	 * sources do not have an internal generator of frames but instead have
+	 * their data provided from an external rendering class. This function only
+	 * works when there is no internal generator.
+	 */
+    //void write(int64_t ts, ftl::rgbd::Frame &frame, cudaStream_t stream=0);
+};
+
+}
+}
+
+#endif  // _FTL_RGBD_VIRTUAL_HPP_
diff --git a/components/rgbd-sources/include/qsort.h b/components/rgbd-sources/include/qsort.h
new file mode 100644
index 0000000000000000000000000000000000000000..62a76b836c1b13861fc8a5a12ff0fc0eb7936f8a
--- /dev/null
+++ b/components/rgbd-sources/include/qsort.h
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2013, 2017 Alexey Tourbin
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/*
+ * This is a traditional Quicksort implementation which mostly follows
+ * [Sedgewick 1978].  Sorting is performed entirely on array indices,
+ * while actual access to the array elements is abstracted out with the
+ * user-defined `LESS` and `SWAP` primitives.
+ *
+ * Synopsis:
+ *	QSORT(N, LESS, SWAP);
+ * where
+ *	N - the number of elements in A[];
+ *	LESS(i, j) - compares A[i] to A[j];
+ *	SWAP(i, j) - exchanges A[i] with A[j].
+ */
+
+#ifndef QSORT_H
+#define QSORT_H
+
+/* Sort 3 elements. */
+#define Q_SORT3(q_a1, q_a2, q_a3, Q_LESS, Q_SWAP) \
+do {					\
+    if (Q_LESS(q_a2, q_a1)) {		\
+	if (Q_LESS(q_a3, q_a2))		\
+	    Q_SWAP(q_a1, q_a3);		\
+	else {				\
+	    Q_SWAP(q_a1, q_a2);		\
+	    if (Q_LESS(q_a3, q_a2))	\
+		Q_SWAP(q_a2, q_a3);	\
+	}				\
+    }					\
+    else if (Q_LESS(q_a3, q_a2)) {	\
+	Q_SWAP(q_a2, q_a3);		\
+	if (Q_LESS(q_a2, q_a1))		\
+	    Q_SWAP(q_a1, q_a2);		\
+    }					\
+} while (0)
+
+/* Partition [q_l,q_r] around a pivot.  After partitioning,
+ * [q_l,q_j] are the elements that are less than or equal to the pivot,
+ * while [q_i,q_r] are the elements greater than or equal to the pivot. */
+#define Q_PARTITION(q_l, q_r, q_i, q_j, Q_UINT, Q_LESS, Q_SWAP)		\
+do {									\
+    /* The middle element, not to be confused with the median. */	\
+    Q_UINT q_m = q_l + ((q_r - q_l) >> 1);				\
+    /* Reorder the second, the middle, and the last items.		\
+     * As [Edelkamp Weiss 2016] explain, using the second element	\
+     * instead of the first one helps avoid bad behaviour for		\
+     * decreasingly sorted arrays.  This method is used in recent	\
+     * versions of gcc's std::sort, see gcc bug 58437#c13, although	\
+     * the details are somewhat different (cf. #c14). */		\
+    Q_SORT3(q_l + 1, q_m, q_r, Q_LESS, Q_SWAP);				\
+    /* Place the median at the beginning. */				\
+    Q_SWAP(q_l, q_m);							\
+    /* Partition [q_l+2, q_r-1] around the median which is in q_l.	\
+     * q_i and q_j are initially off by one, they get decremented	\
+     * in the do-while loops. */					\
+    q_i = q_l + 1; q_j = q_r;						\
+    while (1) {								\
+	do q_i++; while (Q_LESS(q_i, q_l));				\
+	do q_j--; while (Q_LESS(q_l, q_j));				\
+	if (q_i >= q_j) break; /* Sedgewick says "until j < i" */	\
+	Q_SWAP(q_i, q_j);						\
+    }									\
+    /* Compensate for the i==j case. */					\
+    q_i = q_j + 1;							\
+    /* Put the median to its final place. */				\
+    Q_SWAP(q_l, q_j);							\
+    /* The median is not part of the left subfile. */			\
+    q_j--;								\
+} while (0)
+
+/* Insertion sort is applied to small subfiles - this is contrary to
+ * Sedgewick's suggestion to run a separate insertion sort pass after
+ * the partitioning is done.  The reason I don't like a separate pass
+ * is that it triggers extra comparisons, because it can't see that the
+ * medians are already in their final positions and need not be rechecked.
+ * Since I do not assume that comparisons are cheap, I also do not try
+ * to eliminate the (q_j > q_l) boundary check. */
+#define Q_INSERTION_SORT(q_l, q_r, Q_UINT, Q_LESS, Q_SWAP)		\
+do {									\
+    Q_UINT q_i, q_j;							\
+    /* For each item starting with the second... */			\
+    for (q_i = q_l + 1; q_i <= q_r; q_i++)				\
+    /* move it down the array so that the first part is sorted. */	\
+    for (q_j = q_i; q_j > q_l && (Q_LESS(q_j, q_j - 1)); q_j--)		\
+	Q_SWAP(q_j, q_j - 1);						\
+} while (0)
+
+/* When the size of [q_l,q_r], i.e. q_r-q_l+1, is greater than or equal to
+ * Q_THRESH, the algorithm performs recursive partitioning.  When the size
+ * drops below Q_THRESH, the algorithm switches to insertion sort.
+ * The minimum valid value is probably 5 (with 5 items, the second and
+ * the middle items, the middle itself being rounded down, are distinct). */
+#define Q_THRESH 16
+
+/* The main loop. */
+#define Q_LOOP(Q_UINT, Q_N, Q_LESS, Q_SWAP)				\
+do {									\
+    Q_UINT q_l = 0;							\
+    Q_UINT q_r = (Q_N) - 1;						\
+    Q_UINT q_sp = 0; /* the number of frames pushed to the stack */	\
+    struct { Q_UINT q_l, q_r; }						\
+	/* On 32-bit platforms, to sort a "char[3GB+]" array,		\
+	 * it may take full 32 stack frames.  On 64-bit CPUs,		\
+	 * though, the address space is limited to 48 bits.		\
+	 * The usage is further reduced if Q_N has a 32-bit type. */	\
+	q_st[sizeof(Q_UINT) > 4 && sizeof(Q_N) > 4 ? 48 : 32];		\
+    while (1) {								\
+	if (q_r - q_l + 1 >= Q_THRESH) {				\
+	    Q_UINT q_i, q_j;						\
+	    Q_PARTITION(q_l, q_r, q_i, q_j, Q_UINT, Q_LESS, Q_SWAP);	\
+	    /* Now have two subfiles: [q_l,q_j] and [q_i,q_r].		\
+	     * Dealing with them depends on which one is bigger. */	\
+	    if (q_j - q_l >= q_r - q_i)					\
+		Q_SUBFILES(q_l, q_j, q_i, q_r);				\
+	    else							\
+		Q_SUBFILES(q_i, q_r, q_l, q_j);				\
+	}								\
+	else {								\
+	    Q_INSERTION_SORT(q_l, q_r, Q_UINT, Q_LESS, Q_SWAP);		\
+	    /* Pop subfiles from the stack, until it gets empty. */	\
+	    if (q_sp == 0) break;					\
+	    q_sp--;							\
+	    q_l = q_st[q_sp].q_l;					\
+	    q_r = q_st[q_sp].q_r;					\
+	}								\
+    }									\
+} while (0)
+
+/* The missing part: dealing with subfiles.
+ * Assumes that the first subfile is not smaller than the second. */
+#define Q_SUBFILES(q_l1, q_r1, q_l2, q_r2)				\
+do {									\
+    /* If the second subfile is only a single element, it needs		\
+     * no further processing.  The first subfile will be processed	\
+     * on the next iteration (both subfiles cannot be only a single	\
+     * element, due to Q_THRESH). */					\
+    if (q_l2 == q_r2) {							\
+	q_l = q_l1;							\
+	q_r = q_r1;							\
+    }									\
+    else {								\
+	/* Otherwise, both subfiles need processing.			\
+	 * Push the larger subfile onto the stack. */			\
+	q_st[q_sp].q_l = q_l1;						\
+	q_st[q_sp].q_r = q_r1;						\
+	q_sp++;								\
+	/* Process the smaller subfile on the next iteration. */	\
+	q_l = q_l2;							\
+	q_r = q_r2;							\
+    }									\
+} while (0)
+
+/* And now, ladies and gentlemen, may I proudly present to you... */
+#define QSORT(Q_N, Q_LESS, Q_SWAP)					\
+do {									\
+    if ((Q_N) > 1)							\
+	/* We could check sizeof(Q_N) and use "unsigned", but at least	\
+	 * on x86_64, this has the performance penalty of up to 5%. */	\
+	Q_LOOP(unsigned long, Q_N, Q_LESS, Q_SWAP);			\
+} while (0)
+
+#endif
+
+/* ex:set ts=8 sts=4 sw=4 noet: */
\ No newline at end of file
diff --git a/components/rgbd-sources/src/algorithms/fixstars_sgm.cpp b/components/rgbd-sources/src/algorithms/fixstars_sgm.cpp
index 782338fc2be41d56a4d6fcd4f4920f6d920e663f..5f8921bda0e562bcc26b454d750a35294ed75537 100644
--- a/components/rgbd-sources/src/algorithms/fixstars_sgm.cpp
+++ b/components/rgbd-sources/src/algorithms/fixstars_sgm.cpp
@@ -7,6 +7,8 @@
 using ftl::algorithms::FixstarsSGM;
 using cv::Mat;
 using cv::cuda::GpuMat;
+using ftl::rgbd::Channel;
+using ftl::rgbd::Format;
 
 //static ftl::Disparity::Register fixstarssgm("libsgm", FixstarsSGM::create);
 
@@ -78,14 +80,9 @@ void FixstarsSGM::compute(ftl::rgbd::Frame &frame, cv::cuda::Stream &stream)
 		cv::cuda::cvtColor(rgb, gray, cv::COLOR_BGR2GRAY, 0, stream);
 	}*/
 
-	const auto &l = frame.getChannel<GpuMat>(ftl::rgbd::kChanLeft, stream);
-	const auto &r = frame.getChannel<GpuMat>(ftl::rgbd::kChanRight, stream);
-	auto &disp = frame.setChannel<GpuMat>(ftl::rgbd::kChanDisparity);
-
-	if (disp.size() != l.size())
-	{
-		disp = GpuMat(l.size(), CV_32FC1);
-	}
+	const auto &l = frame.get<GpuMat>(Channel::Left);
+	const auto &r = frame.get<GpuMat>(Channel::Right);
+	auto &disp = frame.create<GpuMat>(Channel::Disparity, Format<float>(l.size()));
 
 	GpuMat l_scaled;
 	if (l.size() != size_)
@@ -131,11 +128,7 @@ void FixstarsSGM::compute(ftl::rgbd::Frame &frame, cv::cuda::Stream &stream)
 	dispt_scaled.convertTo(disp, CV_32F, 1.0f / 16.0f, stream);
 
 #ifdef HAVE_OPTFLOW
-	if (use_off_)
-	{
-		frame.getChannel<Mat>(ftl::rgbd::kChanDisparity);
-		off_.filter(frame.setChannel<Mat>(ftl::rgbd::kChanDisparity), Mat(lbw_));
-	}
+	if (use_off_) { off_.filter(frame, stream); }
 #endif
 }
 
diff --git a/components/rgbd-sources/src/algorithms/fixstars_sgm.hpp b/components/rgbd-sources/src/algorithms/fixstars_sgm.hpp
index 039db0c874d8d816b91d447a6254c79fd5a43304..d2c0c1d2a8fd459695bb02c9f66f17e423dc9fa5 100644
--- a/components/rgbd-sources/src/algorithms/fixstars_sgm.hpp
+++ b/components/rgbd-sources/src/algorithms/fixstars_sgm.hpp
@@ -5,6 +5,7 @@
 #ifndef _FTL_ALGORITHMS_FIXSTARS_SGM_HPP_
 #define _FTL_ALGORITHMS_FIXSTARS_SGM_HPP_
 
+#include <ftl/cuda_util.hpp>
 #include <opencv2/core.hpp>
 #include <opencv2/opencv.hpp>
 #include <opencv2/cudastereo.hpp>
diff --git a/components/rgbd-sources/src/algorithms/offilter.cu b/components/rgbd-sources/src/algorithms/offilter.cu
new file mode 100644
index 0000000000000000000000000000000000000000..6feee5ae1daf9fc8b4b165370dbb8646b1d7b8a1
--- /dev/null
+++ b/components/rgbd-sources/src/algorithms/offilter.cu
@@ -0,0 +1,91 @@
+#include <ftl/cuda_common.hpp>
+#include <ftl/rgbd/camera.hpp>
+#include <opencv2/core/cuda_stream_accessor.hpp>
+#include <qsort.h>
+
+__device__ void quicksort(float A[], size_t n)
+{
+	float tmp;
+	#define LESS(i, j) A[i] < A[j]
+	#define SWAP(i, j) tmp = A[i], A[i] = A[j], A[j] = tmp
+	QSORT(n, LESS, SWAP);
+}
+
+template<typename T>
+__device__  static bool inline isValidDisparity(T d) { return (0.0 < d) && (d < 256.0); } // TODO
+
+static const int max_history = 32; // TODO dynamic shared memory
+
+__global__ void temporal_median_filter_kernel(
+	cv::cuda::PtrStepSz<float> disp,
+	cv::cuda::PtrStepSz<int16_t> optflow,
+	cv::cuda::PtrStepSz<float> history,
+	int n_max,
+	int16_t threshold, // fixed point 10.5
+	float granularity  // 4 for Turing
+)
+{
+	float sorted[max_history]; // TODO: dynamic shared memory
+	for (STRIDE_Y(y, disp.rows)) {
+	for (STRIDE_X(x, disp.cols)) {
+
+		int flowx = optflow(round(y / granularity), 2 * round(x / granularity));
+		int flowy = optflow(round(y / granularity), 2 * round(x / granularity) + 1);
+
+		if ((abs(flowx) + abs(flowy)) > threshold)
+		{
+			// last element in history[x][y][t]
+			history(y, (x + 1) * n_max - 1) = 0.0;
+			return;
+		}
+
+		int count = history(y, (x + 1) * n_max - 1);
+		int n = count % (n_max - 1);
+
+		if (isValidDisparity(disp(y, x)))
+		{
+			history(y, (x + 1) * n_max - 1) += 1.0;
+			count++;
+			history(y, x * n_max + n) = disp(y, x);
+		}
+
+		int n_end = count;
+		if (n_end >= n_max)	{ n_end = n_max - 1; }
+
+		if (n_end != 0)
+		{
+			for (size_t i = 0; i < n_end; i++)
+			{
+				sorted[i] = history(y, x * n_max + i);
+			}
+
+			quicksort(sorted, n_end);
+			disp(y, x) = sorted[n_end / 2];
+		}
+	}}
+}
+
+namespace ftl {
+namespace cuda {
+	
+void optflow_filter(cv::cuda::GpuMat &disp, const cv::cuda::GpuMat &optflow,
+					cv::cuda::GpuMat &history, int n, float threshold,
+					cv::cuda::Stream &stream)
+{
+	dim3 grid(1, 1, 1);
+	dim3 threads(128, 1, 1);
+	grid.x = cv::cuda::device::divUp(disp.cols, 128);
+	grid.y = cv::cuda::device::divUp(disp.rows, 1);
+
+	// TODO: dynamic shared memory
+	temporal_median_filter_kernel<<<grid, threads, 0, cv::cuda::StreamAccessor::getStream(stream)>>>
+		(	disp, optflow, history, n,
+			round(threshold * (1 << 5)),	// TODO: documentation; 10.5 format
+			4								// TODO: (4 pixels granularity for Turing)
+		);
+
+	cudaSafeCall(cudaGetLastError());
+}
+
+}
+}
\ No newline at end of file
diff --git a/components/rgbd-sources/src/calibrate.cpp b/components/rgbd-sources/src/calibrate.cpp
index df7fac455b68d8ee326957053ae99da946aa941f..3940520d2374661449c376020cb98c5802e2c674 100644
--- a/components/rgbd-sources/src/calibrate.cpp
+++ b/components/rgbd-sources/src/calibrate.cpp
@@ -78,21 +78,24 @@ bool Calibrate::_loadCalibration(cv::Size img_size, std::pair<Mat, Mat> &map1, s
 		return false;
 	}
 
+
+	cv::Size calib_size;
 	{
 		vector<Mat> K, D;
 		fs["K"] >> K;
 		fs["D"] >> D;
+		fs["resolution"] >> calib_size;
 
-		K[0].copyTo(M1_);
-		K[1].copyTo(M2_);
+		K[0].copyTo(K1_);
+		K[1].copyTo(K2_);
 		D[0].copyTo(D1_);
 		D[1].copyTo(D2_);
 	}
 
 	fs.release();
 
-	CHECK(M1_.size() == Size(3, 3));
-	CHECK(M2_.size() == Size(3, 3));
+	CHECK(K1_.size() == Size(3, 3));
+	CHECK(K2_.size() == Size(3, 3));
 	CHECK(D1_.size() == Size(5, 1));
 	CHECK(D2_.size() == Size(5, 1));
 
@@ -113,45 +116,54 @@ bool Calibrate::_loadCalibration(cv::Size img_size, std::pair<Mat, Mat> &map1, s
 
 	fs["R"] >> R_;
 	fs["T"] >> T_;
+	
+	/* re-calculate rectification from camera parameters
 	fs["R1"] >> R1_;
 	fs["R2"] >> R2_;
 	fs["P1"] >> P1_;
 	fs["P2"] >> P2_;
 	fs["Q"] >> Q_;
-
+	*/
 	fs.release();
 
 	img_size_ = img_size;
 
-	// TODO: normalize calibration
-	double scale_x = ((double) img_size.width) / 1280.0;
-	double scale_y = ((double) img_size.height) / 720.0;
+	if (calib_size.empty())
+	{
+		LOG(WARNING) << "Calibration resolution missing!";
+	}
+	else
+	{
+		double scale_x = ((double) img_size.width) / ((double) calib_size.width);
+		double scale_y = ((double) img_size.height) / ((double) calib_size.height);
 	
-	Mat scale(cv::Size(3, 3), CV_64F, 0.0);
-	scale.at<double>(0, 0) = scale_x;
-	scale.at<double>(1, 1) = scale_y;
-	scale.at<double>(2, 2) = 1.0;
+		Mat scale(cv::Size(3, 3), CV_64F, 0.0);
+		scale.at<double>(0, 0) = scale_x;
+		scale.at<double>(1, 1) = scale_y;
+		scale.at<double>(2, 2) = 1.0;
+
+		K1_ = scale * K1_;
+		K2_ = scale * K2_;
+	}
 
-	M1_ = scale * M1_;
-	M2_ = scale * M2_;
-	P1_ = scale * P1_;
-	P2_ = scale * P2_;
+	double alpha = value("alpha", 0.0);
+	cv::stereoRectify(K1_, D1_, K2_, D2_, img_size_, R_, T_, R1_, R2_, P1_, P2_, Q_, 0, alpha);
 
+	/* scaling not required as rectification is performed from scaled values
 	Q_.at<double>(0, 3) = Q_.at<double>(0, 3) * scale_x;
 	Q_.at<double>(1, 3) = Q_.at<double>(1, 3) * scale_y;
 	Q_.at<double>(2, 3) = Q_.at<double>(2, 3) * scale_x; // TODO: scaling?
 	Q_.at<double>(3, 3) = Q_.at<double>(3, 3) * scale_x;
+	*/
 
 	// cv::cuda::remap() works only with CV_32FC1
-	initUndistortRectifyMap(M1_, D1_, R1_, P1_, img_size_, CV_32FC1, map1.first, map2.first);
-	initUndistortRectifyMap(M2_, D2_, R2_, P2_, img_size_, CV_32FC1, map1.second, map2.second);
+	initUndistortRectifyMap(K1_, D1_, R1_, P1_, img_size_, CV_32FC1, map1.first, map2.first);
+	initUndistortRectifyMap(K2_, D2_, R2_, P2_, img_size_, CV_32FC1, map1.second, map2.second);
 
 	return true;
 }
 
 void Calibrate::updateCalibration(const ftl::rgbd::Camera &p) {
-	std::pair<Mat, Mat> map1, map2;
-
 	Q_.at<double>(3, 2) = 1.0 / p.baseline;
 	Q_.at<double>(2, 3) = p.fx;
 	Q_.at<double>(0, 3) = p.cx;
@@ -178,17 +190,19 @@ void Calibrate::_updateIntrinsics() {
 		// no rectification
 		R1 = Mat::eye(Size(3, 3), CV_64FC1);
 		R2 = R1;
-		P1 = M1_;
-		P2 = M2_;
+		P1 = Mat::zeros(Size(4, 3), CV_64FC1);
+		P2 = Mat::zeros(Size(4, 3), CV_64FC1);
+		K1_.copyTo(Mat(P1, cv::Rect(0, 0, 3, 3)));
+		K2_.copyTo(Mat(P2, cv::Rect(0, 0, 3, 3)));
 	}
 
 	// Set correct camera matrices for
 	// getCameraMatrix(), getCameraMatrixLeft(), getCameraMatrixRight()
-	C_l_ = P1;
-	C_r_ = P2;
+	Kl_ = Mat(P1, cv::Rect(0, 0, 3, 3));
+	Kr_ = Mat(P1, cv::Rect(0, 0, 3, 3));
 
-	initUndistortRectifyMap(M1_, D1_, R1, P1, img_size_, CV_32FC1, map1_.first, map2_.first);
-	initUndistortRectifyMap(M2_, D2_, R2, P2, img_size_, CV_32FC1, map1_.second, map2_.second);
+	initUndistortRectifyMap(K1_, D1_, R1, P1, img_size_, CV_32FC1, map1_.first, map2_.first);
+	initUndistortRectifyMap(K2_, D2_, R2, P2, img_size_, CV_32FC1, map1_.second, map2_.second);
 
 	// CHECK Is this thread safe!!!!
 	map1_gpu_.first.upload(map1_.first);
diff --git a/components/rgbd-sources/src/calibrate.hpp b/components/rgbd-sources/src/calibrate.hpp
index 7f6b262c1e7afcd2852f65a6d62333b5389f7615..4561b90a79129dd5a0d46d9d54bd005147d766a7 100644
--- a/components/rgbd-sources/src/calibrate.hpp
+++ b/components/rgbd-sources/src/calibrate.hpp
@@ -54,8 +54,9 @@ class Calibrate : public ftl::Configurable {
 	 * a 3D point cloud.
 	 */
 	const cv::Mat &getQ() const { return Q_; }
-	const cv::Mat &getCameraMatrixLeft() { return C_l_; }
-	const cv::Mat &getCameraMatrixRight() { return C_r_; }
+
+	const cv::Mat &getCameraMatrixLeft() { return Kl_; }
+	const cv::Mat &getCameraMatrixRight() { return Kr_; }
 	const cv::Mat &getCameraMatrix() { return getCameraMatrixLeft(); }
 
 private:
@@ -70,12 +71,17 @@ private:
 	std::pair<cv::cuda::GpuMat, cv::cuda::GpuMat> map1_gpu_;
 	std::pair<cv::cuda::GpuMat, cv::cuda::GpuMat> map2_gpu_;
 
-	cv::Mat Q_;
+	// parameters for rectification, see cv::stereoRectify() documentation
 	cv::Mat R_, T_, R1_, P1_, R2_, P2_;
-	cv::Mat M1_, D1_, M2_, D2_;
 
-	cv::Mat C_l_;
-	cv::Mat C_r_;
+	// disparity to depth matrix
+	cv::Mat Q_;
+	
+	// intrinsic paramters and distortion coefficients
+	cv::Mat K1_, D1_, K2_, D2_;
+
+	cv::Mat Kl_;
+	cv::Mat Kr_;
 
 	cv::Size img_size_;
 };
diff --git a/components/rgbd-sources/src/cuda_algorithms.hpp b/components/rgbd-sources/src/cuda_algorithms.hpp
index 0aa7399c0a24035bdbbb0442b3c429ddd03d145c..439c16cfc21fef08086bb69b7e84b1c8b49fec74 100644
--- a/components/rgbd-sources/src/cuda_algorithms.hpp
+++ b/components/rgbd-sources/src/cuda_algorithms.hpp
@@ -41,6 +41,9 @@ namespace cuda {
 	void disparity_to_depth(const cv::cuda::GpuMat &disparity, cv::cuda::GpuMat &depth,
 				const ftl::rgbd::Camera &c, cv::cuda::Stream &stream);
 
+	void optflow_filter(cv::cuda::GpuMat &disp, const cv::cuda::GpuMat &optflow,
+						cv::cuda::GpuMat &history, int n_max, float threshold,
+						cv::cuda::Stream &stream);
 
 }
 }
diff --git a/components/rgbd-sources/src/disparity.hpp b/components/rgbd-sources/src/disparity.hpp
index 2ab8223ca97f14752265be86ccf0ace0554eb20e..44215871d37b2944c08d072d63afd5bf871082e4 100644
--- a/components/rgbd-sources/src/disparity.hpp
+++ b/components/rgbd-sources/src/disparity.hpp
@@ -48,10 +48,11 @@ class Disparity : public ftl::Configurable {
 	virtual void compute(Frame &frame, cv::cuda::Stream &stream)=0;
 	virtual void compute(cv::cuda::GpuMat &l, cv::cuda::GpuMat &r, cv::cuda::GpuMat &disp, cv::cuda::Stream &stream)
 	{
-		ftl::rgbd::Frame frame;
-		frame.setChannel<cv::cuda::GpuMat>(kChanLeft) = l;
-		frame.setChannel<cv::cuda::GpuMat>(kChanRight) = r;
-		frame.setChannel<cv::cuda::GpuMat>(kChanDisparity) = disp;
+		// FIXME: What were these for?
+		//ftl::rgbd::Frame frame;
+		//frame.create<cv::cuda::GpuMat>(ftl::rgbd::Channel::Left) = l;
+		//frame.create<cv::cuda::GpuMat>(ftl::rgbd::Channel::Right) = r;
+		//frame.create<cv::cuda::GpuMat>(ftl::rgbd::Channel::Disparity) = disp;
 	}
 
 	/**
diff --git a/components/rgbd-sources/src/frame.cpp b/components/rgbd-sources/src/frame.cpp
index 8145280cc83d24347d9bb2fc07a5a9546bbe5dd7..a56a19355526d9cdcdf25faaecdc67c05d09469d 100644
--- a/components/rgbd-sources/src/frame.cpp
+++ b/components/rgbd-sources/src/frame.cpp
@@ -1,68 +1,201 @@
 
 #include <ftl/rgbd/frame.hpp>
 
-namespace ftl {
-namespace rgbd {
-
-template<> const cv::Mat& Frame::getChannel(const ftl::rgbd::channel_t& channel, cv::cuda::Stream &stream)
-{
-	size_t idx = _channelIdx(channel);
-	if (!(available_[idx] & mask_host))
-	{
-		if (available_[idx] & mask_gpu)
-		{
-			channels_gpu_[idx].download(channels_host_[idx], stream);
-			available_[idx] |= mask_host;
+using ftl::rgbd::Frame;
+using ftl::rgbd::Channels;
+using ftl::rgbd::Channel;
+
+static cv::Mat none;
+static cv::cuda::GpuMat noneGPU;
+
+void Frame::reset() {
+	channels_.clear();
+	gpu_.clear();
+}
+
+void Frame::download(Channel c, cv::cuda::Stream stream) {
+	download(Channels(c), stream);
+}
+
+void Frame::upload(Channel c, cv::cuda::Stream stream) {
+	upload(Channels(c), stream);
+}
+
+void Frame::download(Channels c, cv::cuda::Stream stream) {
+	for (size_t i=0u; i<Channels::kMax; ++i) {
+		if (c.has(i) && channels_.has(i) && gpu_.has(i)) {
+			data_[i].gpu.download(data_[i].host, stream);
+			gpu_ -= i;
 		}
 	}
-
-	return channels_host_[idx];
 }
 
-template<> const cv::Mat& Frame::getChannel(const ftl::rgbd::channel_t& channel)
-{
-	auto &stream = cv::cuda::Stream::Null();
-	auto &retval = getChannel<cv::Mat>(channel, stream);
-	stream.waitForCompletion();
-	return retval;
+void Frame::upload(Channels c, cv::cuda::Stream stream) {
+	for (size_t i=0u; i<Channels::kMax; ++i) {
+		if (c.has(i) && channels_.has(i) && !gpu_.has(i)) {
+			data_[i].gpu.upload(data_[i].host, stream);
+			gpu_ += i;
+		}
+	}
 }
 
-template<> cv::Mat& Frame::setChannel(const ftl::rgbd::channel_t& channel)
-{
-	size_t idx = _channelIdx(channel);
-	available_[idx] = mask_host;
-	return channels_host_[idx];
+bool Frame::empty(ftl::rgbd::Channels channels) {
+	for (auto c : channels) {
+		if (empty(c)) return true;
+	}
+	return false;
 }
 
-template<> const cv::cuda::GpuMat& Frame::getChannel(const ftl::rgbd::channel_t& channel, cv::cuda::Stream &stream)
-{
-	size_t idx = _channelIdx(channel);
-	if (!(available_[idx] & mask_gpu))
-	{
-		if (available_[idx] & mask_host)
-		{
-			channels_gpu_[idx].upload(channels_host_[idx], stream);
-			available_[idx] |= mask_gpu;
+void Frame::swapTo(ftl::rgbd::Channels channels, Frame &f) {
+	f.reset();
+
+	// For all channels in this frame object
+	for (auto c : channels_) {
+		// Should we swap this channel?
+		if (channels.has(c)) {
+			// Does 'f' have this channel?
+			//if (!f.hasChannel(c)) {
+				// No, so create it first
+				// FIXME: Allocate the memory as well?
+				if (isCPU(c)) f.create<cv::Mat>(c);
+				else f.create<cv::cuda::GpuMat>(c);
+			//}
+
+			auto &m1 = _get(c);
+			auto &m2 = f._get(c);
+
+			cv::swap(m1.host, m2.host);
+			cv::cuda::swap(m1.gpu, m2.gpu);
+
+			auto temptex = std::move(m2.tex);
+			m2.tex = std::move(m1.tex);
+			m1.tex = std::move(temptex);
 		}
 	}
-	
-	return channels_gpu_[idx];
 }
 
-template<> const cv::cuda::GpuMat& Frame::getChannel(const ftl::rgbd::channel_t& channel)
-{
-	auto &stream = cv::cuda::Stream::Null();
-	auto &retval = getChannel<cv::cuda::GpuMat>(channel, stream);
-	stream.waitForCompletion();
-	return retval;
+template<> cv::Mat& Frame::get(ftl::rgbd::Channel channel) {
+	if (channel == Channel::None) {
+		DLOG(WARNING) << "Cannot get the None channel from a Frame";
+		none.release();
+		return none;
+	}
+
+	if (isGPU(channel)) {
+		download(Channels(channel));
+		LOG(WARNING) << "Getting GPU channel on CPU without explicit 'download'";
+	}
+
+	// Add channel if not already there
+	if (!channels_.has(channel)) {
+		throw ftl::exception("Frame channel does not exist");
+	}
+
+	return _get(channel).host;
+}
+
+template<> cv::cuda::GpuMat& Frame::get(ftl::rgbd::Channel channel) {
+	if (channel == Channel::None) {
+		DLOG(WARNING) << "Cannot get the None channel from a Frame";
+		noneGPU.release();
+		return noneGPU;
+	}
+
+	if (isCPU(channel)) {
+		upload(Channels(channel));
+		LOG(WARNING) << "Getting CPU channel on GPU without explicit 'upload'";
+	}
+
+	// Add channel if not already there
+	if (!channels_.has(channel)) {
+		throw ftl::exception("Frame channel does not exist");
+	}
+
+	return _get(channel).gpu;
+}
+
+template<> const cv::Mat& Frame::get(ftl::rgbd::Channel channel) const {
+	if (channel == Channel::None) {
+		LOG(FATAL) << "Cannot get the None channel from a Frame";
+	}
+
+	if (isGPU(channel)) {
+		LOG(FATAL) << "Getting GPU channel on CPU without explicit 'download'";
+	}
+
+	if (!channels_.has(channel)) throw ftl::exception("Frame channel does not exist");
+
+	return _get(channel).host;
+}
+
+template<> const cv::cuda::GpuMat& Frame::get(ftl::rgbd::Channel channel) const {
+	if (channel == Channel::None) {
+		LOG(FATAL) << "Cannot get the None channel from a Frame";
+	}
+
+	if (isCPU(channel)) {
+		LOG(FATAL) << "Getting CPU channel on GPU without explicit 'upload'";
+	}
+
+	// Add channel if not already there
+	if (!channels_.has(channel)) {
+		throw ftl::exception("Frame channel does not exist");
+	}
+
+	return _get(channel).gpu;
+}
+
+template <> cv::Mat &Frame::create(ftl::rgbd::Channel c, const ftl::rgbd::FormatBase &f) {
+	if (c == Channel::None) {
+		throw ftl::exception("Cannot create a None channel");
+	}
+	channels_ += c;
+	gpu_ -= c;
+
+	auto &m = _get(c).host;
+
+	if (!f.empty()) {
+		m.create(f.size(), f.cvType);
+	}
+
+	return m;
+}
+
+template <> cv::cuda::GpuMat &Frame::create(ftl::rgbd::Channel c, const ftl::rgbd::FormatBase &f) {
+	if (c == Channel::None) {
+		throw ftl::exception("Cannot create a None channel");
+	}
+	channels_ += c;
+	gpu_ += c;
+
+	auto &m = _get(c).gpu;
+
+	if (!f.empty()) {
+		m.create(f.size(), f.cvType);
+	}
+
+	return m;
 }
 
-template<> cv::cuda::GpuMat& Frame::setChannel(const ftl::rgbd::channel_t& channel)
-{
-	size_t idx = _channelIdx(channel);
-	available_[idx] = mask_gpu;
-	return channels_gpu_[idx];
+template <> cv::Mat &Frame::create(ftl::rgbd::Channel c) {
+	if (c == Channel::None) {
+		throw ftl::exception("Cannot create a None channel");
+	}
+	channels_ += c;
+	gpu_ -= c;
+
+	auto &m = _get(c).host;
+	return m;
 }
 
+template <> cv::cuda::GpuMat &Frame::create(ftl::rgbd::Channel c) {
+	if (c == Channel::None) {
+		throw ftl::exception("Cannot create a None channel");
+	}
+	channels_ += c;
+	gpu_ += c;
+
+	auto &m = _get(c).gpu;
+	return m;
 }
-}
\ No newline at end of file
+
diff --git a/components/rgbd-sources/src/frameset.cpp b/components/rgbd-sources/src/frameset.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9b9a807d8599c23141b6c3806546bf8038ef30f9
--- /dev/null
+++ b/components/rgbd-sources/src/frameset.cpp
@@ -0,0 +1,39 @@
+#include <ftl/rgbd/frameset.hpp>
+
+using ftl::rgbd::FrameSet;
+using ftl::rgbd::Channels;
+using ftl::rgbd::Channel;
+
+void FrameSet::upload(ftl::rgbd::Channels c, cudaStream_t stream) {
+	for (auto &f : frames) {
+		f.upload(c, stream);
+	}
+}
+
+void FrameSet::download(ftl::rgbd::Channels c, cudaStream_t stream) {
+	for (auto &f : frames) {
+		f.download(c, stream);
+	}
+}
+
+void FrameSet::swapTo(ftl::rgbd::FrameSet &fs) {
+	UNIQUE_LOCK(fs.mtx, lk);
+
+	//if (fs.frames.size() != frames.size()) {
+		// Assume "this" is correct and "fs" is not.
+		fs.sources.clear();
+		for (auto s : sources) fs.sources.push_back(s);
+		fs.frames.resize(frames.size());
+	//}
+
+	fs.timestamp = timestamp;
+	fs.count = static_cast<int>(count);
+	fs.stale = stale;
+	fs.mask = static_cast<unsigned int>(mask);
+
+	for (size_t i=0; i<frames.size(); ++i) {
+		frames[i].swapTo(Channels::All(), fs.frames[i]);
+	}
+
+	stale = true;
+}
diff --git a/components/rgbd-sources/src/group.cpp b/components/rgbd-sources/src/group.cpp
index e61289220ad92fff8e278905a075f01d93b6ad9a..96ca3a82fd3e306656f047d4971ce4a29b00fe48 100644
--- a/components/rgbd-sources/src/group.cpp
+++ b/components/rgbd-sources/src/group.cpp
@@ -10,6 +10,7 @@ using ftl::rgbd::kFrameBufferSize;
 using std::vector;
 using std::chrono::milliseconds;
 using std::this_thread::sleep_for;
+using ftl::rgbd::Channel;
 
 Group::Group() : framesets_(kFrameBufferSize), head_(0) {
 	framesets_[0].timestamp = -1;
@@ -52,6 +53,8 @@ void Group::addSource(ftl::rgbd::Source *src) {
 	src->setCallback([this,ix,src](int64_t timestamp, cv::Mat &rgb, cv::Mat &depth) {
 		if (timestamp == 0) return;
 
+		auto chan = src->getChannel();
+
 		//LOG(INFO) << "SRC CB: " << timestamp << " (" << framesets_[head_].timestamp << ")";
 
 		UNIQUE_LOCK(mutex_, lk);
@@ -73,11 +76,15 @@ void Group::addSource(ftl::rgbd::Source *src) {
 
 				//LOG(INFO) << "Adding frame: " << ix << " for " << timestamp;
 				// Ensure channels match source mat format
-				fs.channel1[ix].create(rgb.size(), rgb.type());
-				fs.channel2[ix].create(depth.size(), depth.type());
+				//fs.channel1[ix].create(rgb.size(), rgb.type());
+				//fs.channel2[ix].create(depth.size(), depth.type());
+				fs.frames[ix].create<cv::Mat>(Channel::Colour, Format<uchar3>(rgb.size())); //.create(rgb.size(), rgb.type());
+				if (chan != Channel::None) fs.frames[ix].create<cv::Mat>(chan, ftl::rgbd::FormatBase(depth.cols, depth.rows, depth.type())); //.create(depth.size(), depth.type());
 
-				cv::swap(rgb, fs.channel1[ix]);
-				cv::swap(depth, fs.channel2[ix]);
+				//cv::swap(rgb, fs.channel1[ix]);
+				//cv::swap(depth, fs.channel2[ix]);
+				cv::swap(rgb, fs.frames[ix].get<cv::Mat>(Channel::Colour));
+				if (chan != Channel::None) cv::swap(depth, fs.frames[ix].get<cv::Mat>(chan));
 
 				++fs.count;
 				fs.mask |= (1 << ix);
@@ -271,8 +278,9 @@ void Group::_addFrameset(int64_t timestamp) {
 		framesets_[head_].count = 0;
 		framesets_[head_].mask = 0;
 		framesets_[head_].stale = false;
-		framesets_[head_].channel1.resize(sources_.size());
-		framesets_[head_].channel2.resize(sources_.size());
+		//framesets_[head_].channel1.resize(sources_.size());
+		//framesets_[head_].channel2.resize(sources_.size());
+		framesets_[head_].frames.resize(sources_.size());
 
 		if (framesets_[head_].sources.size() != sources_.size()) {
 			framesets_[head_].sources.clear();
@@ -301,8 +309,9 @@ void Group::_addFrameset(int64_t timestamp) {
 		framesets_[head_].count = 0;
 		framesets_[head_].mask = 0;
 		framesets_[head_].stale = false;
-		framesets_[head_].channel1.resize(sources_.size());
-		framesets_[head_].channel2.resize(sources_.size());
+		//framesets_[head_].channel1.resize(sources_.size());
+		//framesets_[head_].channel2.resize(sources_.size());
+		framesets_[head_].frames.resize(sources_.size());
 
 		if (framesets_[head_].sources.size() != sources_.size()) {
 			framesets_[head_].sources.clear();
diff --git a/components/rgbd-sources/src/middlebury_source.cpp b/components/rgbd-sources/src/middlebury_source.cpp
index a707bf9fab06a212b4146065b414999d21bc9adb..e82167fdcf6b4e2bfd1a38a00b50e3cdceeb2aef 100644
--- a/components/rgbd-sources/src/middlebury_source.cpp
+++ b/components/rgbd-sources/src/middlebury_source.cpp
@@ -16,14 +16,13 @@ MiddleburySource::MiddleburySource(ftl::rgbd::Source *host)
 
 static bool loadMiddleburyCalib(const std::string &filename, ftl::rgbd::Camera &params, double scaling) {
 	FILE* fp = fopen(filename.c_str(), "r");
-	char buff[512];
 	
-	float cam0[3][3];
+	float cam0[3][3] = {};
 	float cam1[3][3];
-	float doffs;
-	float baseline;
-	int width;
-	int height;
+	float doffs = 0.0f;
+	float baseline = 0.0f;
+	int width = 0;
+	int height = 0;
 	int ndisp;
 	int isint;
 	int vmin;
@@ -31,8 +30,8 @@ static bool loadMiddleburyCalib(const std::string &filename, ftl::rgbd::Camera &
 	float dyavg;
 	float dymax;
 
-	if (fp != nullptr)
-	{
+	if (fp != nullptr) {
+		char buff[512];
 		if (fgets(buff, sizeof(buff), fp) != nullptr) sscanf(buff, "cam0 = [%f %f %f; %f %f %f; %f %f %f]\n", &cam0[0][0], &cam0[0][1], &cam0[0][2], &cam0[1][0], &cam0[1][1], &cam0[1][2], &cam0[2][0], &cam0[2][1], &cam0[2][2]);
 		if (fgets(buff, sizeof(buff), fp) != nullptr) sscanf(buff, "cam1 = [%f %f %f; %f %f %f; %f %f %f]\n", &cam1[0][0], &cam1[0][1], &cam1[0][2], &cam1[1][0], &cam1[1][1], &cam1[1][2], &cam1[2][0], &cam1[2][1], &cam1[2][2]);
 		if (fgets(buff, sizeof(buff), fp) != nullptr) sscanf(buff, "doffs = %f\n", &doffs);
@@ -122,7 +121,7 @@ MiddleburySource::MiddleburySource(ftl::rgbd::Source *host, const string &dir)
 	mask_l_ = (mask_l == 0);
 
 	if (!host_->getConfig()["disparity"].is_object()) {
-		host_->getConfig()["disparity"] = {{"algorithm","libsgm"}};
+		host_->getConfig()["disparity"] = ftl::config::json_t{{"algorithm","libsgm"}};
 	}
 	
 	disp_ = Disparity::create(host_, "disparity");
diff --git a/components/rgbd-sources/src/middlebury_source.hpp b/components/rgbd-sources/src/middlebury_source.hpp
index 21c7d1f1c07d96130be44156fd17666868ab739b..d273d23a66d67c6618c0ac4a2062a780d9a3bddb 100644
--- a/components/rgbd-sources/src/middlebury_source.hpp
+++ b/components/rgbd-sources/src/middlebury_source.hpp
@@ -15,7 +15,7 @@ class Disparity;
 
 class MiddleburySource : public detail::Source {
 	public:
-	MiddleburySource(ftl::rgbd::Source *);
+	explicit MiddleburySource(ftl::rgbd::Source *);
 	MiddleburySource(ftl::rgbd::Source *, const std::string &dir);
 	~MiddleburySource() {};
 
diff --git a/components/rgbd-sources/src/net.cpp b/components/rgbd-sources/src/net.cpp
index 5c3c7ecb5dc3a11d0b4f96471ebe2eb8beef53d8..ba485c9402d888be0636902f3962cce2dd1e85ed 100644
--- a/components/rgbd-sources/src/net.cpp
+++ b/components/rgbd-sources/src/net.cpp
@@ -20,6 +20,7 @@ using std::vector;
 using std::this_thread::sleep_for;
 using std::chrono::milliseconds;
 using std::tuple;
+using ftl::rgbd::Channel;
 
 // ===== NetFrameQueue =========================================================
 
@@ -69,7 +70,7 @@ void NetFrameQueue::freeFrame(NetFrame &f) {
 
 // ===== NetSource =============================================================
 
-bool NetSource::_getCalibration(Universe &net, const UUID &peer, const string &src, ftl::rgbd::Camera &p, ftl::rgbd::channel_t chan) {
+bool NetSource::_getCalibration(Universe &net, const UUID &peer, const string &src, ftl::rgbd::Camera &p, ftl::rgbd::Channel chan) {
 	try {
 		while(true) {
 			auto [cap,buf] = net.call<tuple<unsigned int,vector<unsigned char>>>(peer_, "source_details", src, chan);
@@ -227,11 +228,11 @@ void NetSource::_recvPacket(short ttimeoff, const ftl::codecs::StreamPacket &spk
 	int64_t now = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now()).time_since_epoch().count();
 	if (!active_) return;
 
-	const ftl::rgbd::channel_t chan = host_->getChannel();
+	const ftl::rgbd::Channel chan = host_->getChannel();
 	int rchan = spkt.channel & 0x1;
 
 	// Ignore any unwanted second channel
-	if (chan == ftl::rgbd::kChanNone && rchan > 0) {
+	if (chan == ftl::rgbd::Channel::None && rchan > 0) {
 		LOG(INFO) << "Unwanted channel";
 		//return;
 		// TODO: Allow decode to be skipped
@@ -324,8 +325,8 @@ void NetSource::setPose(const Eigen::Matrix4d &pose) {
 	//Source::setPose(pose);
 }
 
-ftl::rgbd::Camera NetSource::parameters(ftl::rgbd::channel_t chan) {
-	if (chan == ftl::rgbd::kChanRight) {
+ftl::rgbd::Camera NetSource::parameters(ftl::rgbd::Channel chan) {
+	if (chan == ftl::rgbd::Channel::Right) {
 		auto uri = host_->get<string>("uri");
 		if (!uri) return params_;
 
@@ -340,7 +341,7 @@ ftl::rgbd::Camera NetSource::parameters(ftl::rgbd::channel_t chan) {
 void NetSource::_updateURI() {
 	UNIQUE_LOCK(mutex_,lk);
 	active_ = false;
-	prev_chan_ = ftl::rgbd::kChanNone;
+	prev_chan_ = ftl::rgbd::Channel::None;
 	auto uri = host_->get<string>("uri");
 
 	if (uri_.size() > 0) {
@@ -355,7 +356,7 @@ void NetSource::_updateURI() {
 		}
 		peer_ = *p;
 
-		has_calibration_ = _getCalibration(*host_->getNet(), peer_, *uri, params_, ftl::rgbd::kChanLeft);
+		has_calibration_ = _getCalibration(*host_->getNet(), peer_, *uri, params_, ftl::rgbd::Channel::Left);
 
 		host_->getNet()->bind(*uri, [this](short ttimeoff, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
 			//if (chunk == -1) {
@@ -395,7 +396,7 @@ bool NetSource::compute(int n, int b) {
 	// Send k frames before end to prevent unwanted pause
 	// Unless only a single frame is requested
 	if ((N_ <= maxN_/2 && maxN_ > 1) || N_ == 0) {
-		const ftl::rgbd::channel_t chan = host_->getChannel();
+		const ftl::rgbd::Channel chan = host_->getChannel();
 
 		N_ = maxN_;
 
diff --git a/components/rgbd-sources/src/net.hpp b/components/rgbd-sources/src/net.hpp
index 236ac6969759c7531c65681504b27aacae985d9d..51f31861fa3c9c39ea0cb53217e0fa3f764aeef3 100644
--- a/components/rgbd-sources/src/net.hpp
+++ b/components/rgbd-sources/src/net.hpp
@@ -39,7 +39,7 @@ class NetSource : public detail::Source {
 	bool isReady();
 
 	void setPose(const Eigen::Matrix4d &pose);
-	Camera parameters(channel_t chan);
+	Camera parameters(ftl::rgbd::Channel chan);
 
 	void reset();
 
@@ -57,7 +57,7 @@ class NetSource : public detail::Source {
 	int minB_;
 	int maxN_;
 	int default_quality_;
-	ftl::rgbd::channel_t prev_chan_;
+	ftl::rgbd::Channel prev_chan_;
 
 	ftl::rgbd::detail::ABRController abr_;
 	int last_bitrate_;
@@ -77,7 +77,7 @@ class NetSource : public detail::Source {
 
 	NetFrameQueue queue_;
 
-	bool _getCalibration(ftl::net::Universe &net, const ftl::UUID &peer, const std::string &src, ftl::rgbd::Camera &p, ftl::rgbd::channel_t chan);
+	bool _getCalibration(ftl::net::Universe &net, const ftl::UUID &peer, const std::string &src, ftl::rgbd::Camera &p, ftl::rgbd::Channel chan);
 	void _recv(const std::vector<unsigned char> &jpg, const std::vector<unsigned char> &d);
 	void _recvPacket(short ttimeoff, const ftl::codecs::StreamPacket &, const ftl::codecs::Packet &);
 	//void _recvChunk(int64_t frame, short ttimeoff, uint8_t bitrate, int chunk, const std::vector<unsigned char> &jpg, const std::vector<unsigned char> &d);
diff --git a/components/rgbd-sources/src/offilter.cpp b/components/rgbd-sources/src/offilter.cpp
index 03db4807a7f9fa045708d3cf17bf32556b7bf5b5..466aa9249517b91ac54726e821401e85272aa082 100644
--- a/components/rgbd-sources/src/offilter.cpp
+++ b/components/rgbd-sources/src/offilter.cpp
@@ -1,4 +1,5 @@
 #include "ftl/offilter.hpp"
+#include "cuda_algorithms.hpp"
 
 #ifdef HAVE_OPTFLOW
 
@@ -14,101 +15,27 @@ using std::vector;
 template<typename T> static bool inline isValidDisparity(T d) { return (0.0 < d) && (d < 256.0); } // TODO
 
 OFDisparityFilter::OFDisparityFilter(Size size, int n_frames, float threshold) :
-	n_(0), n_max_(n_frames), threshold_(threshold), size_(size)
+	n_max_(n_frames + 1), threshold_(threshold)
 {
+	CHECK((n_max_ > 1) && (n_max_ <= 32)) << "History length must be between 0 and 31!";
+	disp_old_ = cv::cuda::GpuMat(cv::Size(size.width * n_max_, size.height), CV_32FC1);
 	
-	disp_ = Mat::zeros(cv::Size(size.width * n_frames, size.height), CV_64FC1);
-	gray_ = Mat::zeros(size, CV_8UC1);
-
-	nvof_ = cv::cuda::NvidiaOpticalFlow_1_0::create(size.width, size.height,
+	/*nvof_ = cv::cuda::NvidiaOpticalFlow_1_0::create(size.width, size.height,
 													cv::cuda::NvidiaOpticalFlow_1_0::NV_OF_PERF_LEVEL_SLOW,
-													true, false, false, 0);
+													true, false, false, 0);*/
 	
 }
 
-void OFDisparityFilter::filter(Mat &disp, const Mat &gray)
+void OFDisparityFilter::filter(ftl::rgbd::Frame &frame, cv::cuda::Stream &stream)
 {
-
-	const int n = n_;
-	n_ = (n_ + 1) % n_max_;
-	
-	nvof_->calc(gray, gray_, flowxy_);
-	nvof_->upSampler(	flowxy_, size_.width, size_.height,
-						nvof_->getGridSize(), flowxy_up_);
-
-	CHECK(disp.type() == CV_32FC1);
-	CHECK(gray.type() == CV_8UC1);
-	CHECK(flowxy_up_.type() == CV_32FC2);
-
-	gray.copyTo(gray_);
-
-	vector<float> values(n_max_);
-
-	for (int y = 0; y < size_.height; y++)
-	{
-		float *D = disp_.ptr<float>(y);
-		float *d = disp.ptr<float>(y);
-		float *flow = flowxy_up_.ptr<float>(y);
-
-		for (int x = 0; x < size_.width; x++)
-		{
-			const float flow_l1 = abs(flow[2*x]) + abs(flow[2*x + 1]);
-
-			if (flow_l1 < threshold_)
-			{
-				values.clear();
-
-				if (isValidDisparity(d[x]))
-				{
-					bool updated = false;
-					for (int i = 0; i < n_max_; i++)
-					{
-						float &val = D[n_max_ * x + (n_max_ - i + n) % n_max_];
-						if (!isValidDisparity(val))
-						{
-							val = d[x];
-							updated = true;
-						}
-					}
-					if (!updated) { D[n_max_ * x + n] = d[x]; }
-				}
-
-				for (int i = 0; i < n_max_; i++)
-				{
-					float &val = D[n_max_ * x + i];
-					if (isValidDisparity(val)) { values.push_back(val); }
-				}
-
-				if (values.size() > 0) {
-					const auto median_it = values.begin() + values.size() / 2;
-					std::nth_element(values.begin(), median_it , values.end());
-					d[x] = *median_it;
-				}
-
-				/*
-				if (isValidDepth(d[x]) && isValidDepth(D[x]))
-				{
-					D[x] = D[x] * 0.66 + d[x] * (1.0 - 0.66);
-				}
-				if (isValidDepth(D[x]))
-				{
-					d[x] = D[x];
-				}
-				else
-				{
-					D[x] = d[x];
-				}
-				*/
-			}
-			else
-			{
-				for (int i = 0; i < n_max_; i++)
-				{
-					D[n_max_ * x + i] = 0.0;
-				}
-			}
-		}
-	}
+	frame.upload(Channel::Flow, stream);
+	const cv::cuda::GpuMat &optflow = frame.get<cv::cuda::GpuMat>(Channel::Flow);
+	//frame.get<cv::cuda::GpuMat>(Channel::Disparity);
+	stream.waitForCompletion();
+	if (optflow.empty()) { return; }
+
+	cv::cuda::GpuMat &disp = frame.create<cv::cuda::GpuMat>(Channel::Disparity);
+	ftl::cuda::optflow_filter(disp, optflow, disp_old_, n_max_, threshold_, stream);
 }
 
 #endif  // HAVE_OPTFLOW
diff --git a/components/rgbd-sources/src/realsense_source.cpp b/components/rgbd-sources/src/realsense_source.cpp
index df4c0fe2535426ac52808ea985911968efb74e15..b458aa3e77c8557ee828a8f71431bb5e4e665066 100644
--- a/components/rgbd-sources/src/realsense_source.cpp
+++ b/components/rgbd-sources/src/realsense_source.cpp
@@ -57,6 +57,9 @@ bool RealsenseSource::compute(int n, int b) {
     cv::Mat tmp(cv::Size((int)w, (int)h), CV_16UC1, (void*)depth.get_data(), depth.get_stride_in_bytes());
     tmp.convertTo(depth_, CV_32FC1, scale_);
     rgb_ = cv::Mat(cv::Size(w, h), CV_8UC4, (void*)rscolour_.get_data(), cv::Mat::AUTO_STEP);
+
+	auto cb = host_->callback();
+	if (cb) cb(timestamp_, rgb_, depth_);
     return true;
 }
 
diff --git a/components/rgbd-sources/src/realsense_source.hpp b/components/rgbd-sources/src/realsense_source.hpp
index 4eb182032ffa67cbe121993ed981398a57e0470a..371d305b7d27fc73ad85bba83965f58dcd28c45b 100644
--- a/components/rgbd-sources/src/realsense_source.hpp
+++ b/components/rgbd-sources/src/realsense_source.hpp
@@ -14,7 +14,7 @@ namespace detail {
 
 class RealsenseSource : public ftl::rgbd::detail::Source {
 	public:
-	RealsenseSource(ftl::rgbd::Source *host);
+	explicit RealsenseSource(ftl::rgbd::Source *host);
 	~RealsenseSource();
 
 	bool capture(int64_t ts) { timestamp_ = ts; return true; }
diff --git a/components/rgbd-sources/src/snapshot.cpp b/components/rgbd-sources/src/snapshot.cpp
index 8ce89fa080392eb755278f42d92c60a7b0fa0f1e..7a80ee677c6275f2446d0f2b404e3bc778745d08 100644
--- a/components/rgbd-sources/src/snapshot.cpp
+++ b/components/rgbd-sources/src/snapshot.cpp
@@ -169,7 +169,7 @@ bool SnapshotWriter::addRGBD(size_t source, const cv::Mat &rgb, const cv::Mat &d
 void SnapshotWriter::writeIndex() {
 	FileStorage fs(".yml", FileStorage::WRITE + FileStorage::MEMORY);
 
-	vector<string> channels = {"time", "rgb_left", "depth_left"};
+	vector<string> channels{"time", "rgb_left", "depth_left"};
 
 	fs << "sources" << sources_;
 	fs << "params" <<params_;
diff --git a/components/rgbd-sources/src/snapshot_source.hpp b/components/rgbd-sources/src/snapshot_source.hpp
index 1200f460cfb0ed68cc7cc1573b66c9135c605404..de1b0df48be79df732f51144226f5c7e6d2f0478 100644
--- a/components/rgbd-sources/src/snapshot_source.hpp
+++ b/components/rgbd-sources/src/snapshot_source.hpp
@@ -13,7 +13,7 @@ namespace detail {
 
 class SnapshotSource : public detail::Source {
 	public:
-	SnapshotSource(ftl::rgbd::Source *);
+	explicit SnapshotSource(ftl::rgbd::Source *);
 	SnapshotSource(ftl::rgbd::Source *, ftl::rgbd::Snapshot &snapshot, const std::string &id);
 	~SnapshotSource() {};
 
diff --git a/components/rgbd-sources/src/source.cpp b/components/rgbd-sources/src/source.cpp
index 10c9210f183c8f51d0b1e5424f0e4e441321cd86..35d23f27ad7edac18d3e3e02247296f1382be5e2 100644
--- a/components/rgbd-sources/src/source.cpp
+++ b/components/rgbd-sources/src/source.cpp
@@ -25,10 +25,11 @@ using ftl::rgbd::detail::NetSource;
 using ftl::rgbd::detail::ImageSource;
 using ftl::rgbd::detail::MiddleburySource;
 using ftl::rgbd::capability_t;
+using ftl::rgbd::Channel;
 
 Source::Source(ftl::config::json_t &cfg) : Configurable(cfg), pose_(Eigen::Matrix4d::Identity()), net_(nullptr) {
 	impl_ = nullptr;
-	params_ = {0};
+	params_ = {};
 	stream_ = 0;
 	timestamp_ = 0;
 	reset();
@@ -41,7 +42,7 @@ Source::Source(ftl::config::json_t &cfg) : Configurable(cfg), pose_(Eigen::Matri
 
 Source::Source(ftl::config::json_t &cfg, ftl::net::Universe *net) : Configurable(cfg), pose_(Eigen::Matrix4d::Identity()), net_(net) {
 	impl_ = nullptr;
-	params_ = {0};
+	params_ = {};
 	stream_ = 0;
 	timestamp_ = 0;
 	reset();
@@ -227,7 +228,7 @@ capability_t Source::getCapabilities() const {
 
 void Source::reset() {
 	UNIQUE_LOCK(mutex_,lk);
-	channel_ = kChanNone;
+	channel_ = Channel::None;
 	if (impl_) delete impl_;
 	impl_ = _createImplementation();
 }
@@ -272,71 +273,6 @@ bool Source::compute(int N, int B) {
 	return false;
 }
 
-void Source::writeFrames(int64_t ts, const cv::Mat &rgb, const cv::Mat &depth) {
-	if (!impl_) {
-		UNIQUE_LOCK(mutex_,lk);
-		rgb.copyTo(rgb_);
-		depth.copyTo(depth_);
-		timestamp_ = ts;
-	}
-}
-
-void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uint> &depth, cudaStream_t stream) {
-	if (!impl_) {
-		UNIQUE_LOCK(mutex_,lk);
-		timestamp_ = ts;
-		rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
-		cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
-		depth_.create(depth.height(), depth.width(), CV_32SC1);
-		cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(uint), depth_.rows, cudaMemcpyDeviceToHost, stream));
-		//cudaSafeCall(cudaStreamSynchronize(stream));  // TODO:(Nick) Don't wait here.
-		stream_ = stream;
-		//depth_.convertTo(depth_, CV_32F, 1.0f / 1000.0f);
-	} else {
-		LOG(ERROR) << "writeFrames cannot be done on this source: " << getURI();
-	}
-}
-
-void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<float> &depth, cudaStream_t stream) {
-	if (!impl_) {
-		UNIQUE_LOCK(mutex_,lk);
-		timestamp_ = ts;
-		rgb.download(rgb_, stream);
-		//rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
-		//cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
-		depth.download(depth_, stream);
-		//depth_.create(depth.height(), depth.width(), CV_32FC1);
-		//cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(float), depth_.rows, cudaMemcpyDeviceToHost, stream));
-		
-		stream_ = stream;
-		cudaSafeCall(cudaStreamSynchronize(stream_));
-		cv::cvtColor(rgb_,rgb_, cv::COLOR_BGRA2BGR);
-		cv::cvtColor(rgb_,rgb_, cv::COLOR_Lab2BGR);
-
-		if (callback_) callback_(timestamp_, rgb_, depth_);
-	}
-}
-
-void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uchar4> &rgb2, cudaStream_t stream) {
-	if (!impl_) {
-		UNIQUE_LOCK(mutex_,lk);
-		timestamp_ = ts;
-		rgb.download(rgb_, stream);
-		//rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
-		//cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
-		rgb2.download(depth_, stream);
-		//depth_.create(depth.height(), depth.width(), CV_32FC1);
-		//cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(float), depth_.rows, cudaMemcpyDeviceToHost, stream));
-		
-		stream_ = stream;
-		cudaSafeCall(cudaStreamSynchronize(stream_));
-		cv::cvtColor(rgb_,rgb_, cv::COLOR_BGRA2BGR);
-		cv::cvtColor(rgb_,rgb_, cv::COLOR_Lab2BGR);
-		cv::cvtColor(depth_,depth_, cv::COLOR_BGRA2BGR);
-		cv::cvtColor(depth_,depth_, cv::COLOR_Lab2BGR);
-	}
-}
-
 bool Source::thumbnail(cv::Mat &t) {
 	if (!impl_ && stream_ != 0) {
 		cudaSafeCall(cudaStreamSynchronize(stream_));
@@ -360,13 +296,13 @@ bool Source::thumbnail(cv::Mat &t) {
 	return !thumb_.empty();
 }
 
-bool Source::setChannel(ftl::rgbd::channel_t c) {
+bool Source::setChannel(ftl::rgbd::Channel c) {
 	channel_ = c;
 	// FIXME:(Nick) Verify channel is supported by this source...
 	return true;
 }
 
-const ftl::rgbd::Camera Source::parameters(ftl::rgbd::channel_t chan) const {
+const ftl::rgbd::Camera Source::parameters(ftl::rgbd::Channel chan) const {
 	return (impl_) ? impl_->parameters(chan) : parameters();
 }
 
diff --git a/components/rgbd-sources/src/stereovideo.cpp b/components/rgbd-sources/src/stereovideo.cpp
index ebc72d854e8d35a480f3d4ef5873e8b58d5bb086..6573f74f4d7cf1f3761f98a66c68c6963e10af31 100644
--- a/components/rgbd-sources/src/stereovideo.cpp
+++ b/components/rgbd-sources/src/stereovideo.cpp
@@ -12,6 +12,7 @@
 using ftl::rgbd::detail::Calibrate;
 using ftl::rgbd::detail::LocalSource;
 using ftl::rgbd::detail::StereoVideoSource;
+using ftl::rgbd::Channel;
 using std::string;
 
 StereoVideoSource::StereoVideoSource(ftl::rgbd::Source *host)
@@ -135,8 +136,8 @@ void StereoVideoSource::init(const string &file)
 	ready_ = true;
 }
 
-ftl::rgbd::Camera StereoVideoSource::parameters(ftl::rgbd::channel_t chan) {
-	if (chan == ftl::rgbd::kChanRight) {
+ftl::rgbd::Camera StereoVideoSource::parameters(Channel chan) {
+	if (chan == Channel::Right) {
 		cv::Mat q = calib_->getCameraMatrixRight();
 		ftl::rgbd::Camera params = {
 			q.at<double>(0,0),	// Fx
@@ -175,8 +176,8 @@ bool StereoVideoSource::capture(int64_t ts) {
 bool StereoVideoSource::retrieve() {
 	auto &frame = frames_[0];
 	frame.reset();
-	auto &left = frame.setChannel<cv::cuda::GpuMat>(ftl::rgbd::kChanLeft);
-	auto &right = frame.setChannel<cv::cuda::GpuMat>(ftl::rgbd::kChanRight);
+	auto &left = frame.create<cv::cuda::GpuMat>(Channel::Left);
+	auto &right = frame.create<cv::cuda::GpuMat>(Channel::Right);
 	lsrc_->get(left, right, calib_, stream2_);
 
 #ifdef HAVE_OPTFLOW
@@ -184,17 +185,18 @@ bool StereoVideoSource::retrieve() {
 	
 	if (use_optflow_)
 	{
-		auto &left_gray = frame.setChannel<cv::cuda::GpuMat>(kChanLeftGray);
-		auto &right_gray = frame.setChannel<cv::cuda::GpuMat>(kChanRightGray);
+		auto &left_gray = frame.create<cv::cuda::GpuMat>(Channel::LeftGray);
+		auto &right_gray = frame.create<cv::cuda::GpuMat>(Channel::RightGray);
 
 		cv::cuda::cvtColor(left, left_gray, cv::COLOR_BGR2GRAY, 0, stream2_);
 		cv::cuda::cvtColor(right, right_gray, cv::COLOR_BGR2GRAY, 0, stream2_);
 
-		if (frames_[1].hasChannel(kChanLeftGray))
+		if (frames_[1].hasChannel(Channel::LeftGray))
 		{
-			auto &left_gray_prev = frame.getChannel<cv::cuda::GpuMat>(kChanLeftGray, stream2_);
-			auto &optflow = frame.setChannel<cv::cuda::GpuMat>(kChanFlow);
-			nvof_->calc(left_gray, left_gray_prev, optflow_, stream2_);
+			//frames_[1].download(Channel::LeftGray);
+			auto &left_gray_prev = frames_[1].get<cv::cuda::GpuMat>(Channel::LeftGray);
+			auto &optflow = frame.create<cv::cuda::GpuMat>(Channel::Flow);
+			nvof_->calc(left_gray, left_gray_prev, optflow, stream2_);
 			// nvof_->upSampler() isn't implemented with CUDA
 			// cv::cuda::resize() does not work wiht 2-channel input
 			// cv::cuda::resize(optflow_, optflow, left.size(), 0.0, 0.0, cv::INTER_NEAREST, stream2_);
@@ -207,32 +209,33 @@ bool StereoVideoSource::retrieve() {
 }
 
 void StereoVideoSource::swap() {
-	auto tmp = frames_[0];
-	frames_[0] = frames_[1];
-	frames_[1] = tmp;
+	auto tmp = std::move(frames_[0]);
+	frames_[0] = std::move(frames_[1]);
+	frames_[1] = std::move(tmp);
 }
 
 bool StereoVideoSource::compute(int n, int b) {
 	auto &frame = frames_[1];
-	auto &left = frame.getChannel<cv::cuda::GpuMat>(ftl::rgbd::kChanLeft);
-	auto &right = frame.getChannel<cv::cuda::GpuMat>(ftl::rgbd::kChanRight);
+	auto &left = frame.get<cv::cuda::GpuMat>(Channel::Left);
+	auto &right = frame.get<cv::cuda::GpuMat>(Channel::Right);
 
-	const ftl::rgbd::channel_t chan = host_->getChannel();
+	const ftl::rgbd::Channel chan = host_->getChannel();
 	if (left.empty() || right.empty()) return false;
 
-	if (chan == ftl::rgbd::kChanDepth) {
+	if (chan == Channel::Depth) {
 		disp_->compute(frame, stream_);
 		
-		auto &disp = frame.getChannel<cv::cuda::GpuMat>(ftl::rgbd::kChanDisparity);
-		auto &depth = frame.setChannel<cv::cuda::GpuMat>(ftl::rgbd::kChanDepth);
+		auto &disp = frame.get<cv::cuda::GpuMat>(Channel::Disparity);
+		auto &depth = frame.create<cv::cuda::GpuMat>(Channel::Depth);
 		if (depth.empty()) depth = cv::cuda::GpuMat(left.size(), CV_32FC1);
 
 		ftl::cuda::disparity_to_depth(disp, depth, params_, stream_);
 		
 		left.download(rgb_, stream_);
 		depth.download(depth_, stream_);
+		//frame.download(Channel::Left + Channel::Depth);
 		stream_.waitForCompletion();  // TODO:(Nick) Move to getFrames
-	} else if (chan == ftl::rgbd::kChanRight) {
+	} else if (chan == Channel::Right) {
 		left.download(rgb_, stream_);
 		right.download(depth_, stream_);
 		stream_.waitForCompletion();  // TODO:(Nick) Move to getFrames
diff --git a/components/rgbd-sources/src/stereovideo.hpp b/components/rgbd-sources/src/stereovideo.hpp
index 9fe3ca529f2f32300cb85aa47bd958b78f78984c..9d3325e1ac27ec544abeb409149cb89817dc29d2 100644
--- a/components/rgbd-sources/src/stereovideo.hpp
+++ b/components/rgbd-sources/src/stereovideo.hpp
@@ -31,7 +31,7 @@ class StereoVideoSource : public detail::Source {
 	bool retrieve();
 	bool compute(int n, int b);
 	bool isReady();
-	Camera parameters(channel_t chan);
+	Camera parameters(ftl::rgbd::Channel chan);
 
 	//const cv::Mat &getRight() const { return right_; }
 
diff --git a/components/rgbd-sources/src/streamer.cpp b/components/rgbd-sources/src/streamer.cpp
index 4a9ccf529fbfcfac285312e1651d5a4b292fb849..676cf58cf3045111cf966d78d587c0e918a351e7 100644
--- a/components/rgbd-sources/src/streamer.cpp
+++ b/components/rgbd-sources/src/streamer.cpp
@@ -17,6 +17,7 @@ using ftl::rgbd::detail::StreamClient;
 using ftl::rgbd::detail::ABRController;
 using ftl::codecs::definition_t;
 using ftl::codecs::device_t;
+using ftl::rgbd::Channel;
 using ftl::net::Universe;
 using std::string;
 using std::list;
@@ -46,11 +47,11 @@ Streamer::Streamer(nlohmann::json &config, Universe *net)
 	hq_devices_ = (value("disable_hardware_encode", false)) ? device_t::Software : device_t::Any;
 
 	//group_.setFPS(value("fps", 20));
-	group_.setLatency(10);
+	group_.setLatency(4);
 
 	compress_level_ = value("compression", 1);
 	
-	net->bind("find_stream", [this](const std::string &uri) -> optional<UUID> {
+	net->bind("find_stream", [this](const std::string &uri) -> optional<ftl::UUID> {
 		SHARED_LOCK(mutex_,slk);
 
 		if (sources_.find(uri) != sources_.end()) {
@@ -91,7 +92,7 @@ Streamer::Streamer(nlohmann::json &config, Universe *net)
 	});
 
 	// Allow remote users to access camera calibration matrix
-	net->bind("source_details", [this](const std::string &uri, ftl::rgbd::channel_t chan) -> tuple<unsigned int,vector<unsigned char>> {
+	net->bind("source_details", [this](const std::string &uri, ftl::rgbd::Channel chan) -> tuple<unsigned int,vector<unsigned char>> {
 		vector<unsigned char> buf;
 		SHARED_LOCK(mutex_,slk);
 
@@ -110,11 +111,11 @@ Streamer::Streamer(nlohmann::json &config, Universe *net)
 		_addClient(source, N, rate, peer, dest);
 	});
 
-	net->bind("set_channel", [this](const string &uri, unsigned int chan) {
+	net->bind("set_channel", [this](const string &uri, Channel chan) {
 		SHARED_LOCK(mutex_,slk);
 
 		if (sources_.find(uri) != sources_.end()) {
-			sources_[uri]->src->setChannel((ftl::rgbd::channel_t)chan);
+			sources_[uri]->src->setChannel(chan);
 		}
 	});
 
@@ -365,9 +366,10 @@ void Streamer::_process(ftl::rgbd::FrameSet &fs) {
 		if (!src) continue;
 		if (!fs.sources[j]->isReady()) continue;
 		if (src->clientCount == 0) continue;
-		if (fs.channel1[j].empty() || (fs.sources[j]->getChannel() != ftl::rgbd::kChanNone && fs.channel2[j].empty())) continue;
+		//if (fs.channel1[j].empty() || (fs.sources[j]->getChannel() != ftl::rgbd::kChanNone && fs.channel2[j].empty())) continue;
+		if (!fs.frames[j].hasChannel(Channel::Colour) || !fs.frames[j].hasChannel(fs.sources[j]->getChannel())) continue;
 
-		bool hasChan2 = fs.sources[j]->getChannel() != ftl::rgbd::kChanNone;
+		bool hasChan2 = fs.sources[j]->getChannel() != Channel::None;
 
 		totalclients += src->clientCount;
 
@@ -387,14 +389,14 @@ void Streamer::_process(ftl::rgbd::FrameSet &fs) {
 				// Receiver only waits for channel 1 by default
 				// TODO: Each encode could be done in own thread
 				if (hasChan2) {
-					enc2->encode(fs.channel2[j], src->hq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
+					enc2->encode(fs.frames[j].get<cv::Mat>(fs.sources[j]->getChannel()), src->hq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
 						_transmitPacket(src, blk, 1, hasChan2, true);
 					});
 				} else {
 					if (enc2) enc2->reset();
 				}
 
-				enc1->encode(fs.channel1[j], src->hq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
+				enc1->encode(fs.frames[j].get<cv::Mat>(Channel::Colour), src->hq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
 					_transmitPacket(src, blk, 0, hasChan2, true);
 				});
 			}
@@ -415,14 +417,14 @@ void Streamer::_process(ftl::rgbd::FrameSet &fs) {
 				// Important to send channel 2 first if needed...
 				// Receiver only waits for channel 1 by default
 				if (hasChan2) {
-					enc2->encode(fs.channel2[j], src->lq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
+					enc2->encode(fs.frames[j].get<cv::Mat>(fs.sources[j]->getChannel()), src->lq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
 						_transmitPacket(src, blk, 1, hasChan2, false);
 					});
 				} else {
 					if (enc2) enc2->reset();
 				}
 
-				enc1->encode(fs.channel1[j], src->lq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
+				enc1->encode(fs.frames[j].get<cv::Mat>(Channel::Colour), src->lq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
 					_transmitPacket(src, blk, 0, hasChan2, false);
 				});
 			}
@@ -494,7 +496,7 @@ void Streamer::_transmitPacket(StreamSource *src, const ftl::codecs::Packet &pkt
 		frame_no_,
 		static_cast<uint8_t>((chan & 0x1) | ((hasChan2) ? 0x2 : 0x0))
 	};
-
+	LOG(INFO) << "codec:" << (int) pkt.codec;
 	// Lock to prevent clients being added / removed
 	//SHARED_LOCK(src->mutex,lk);
 	auto c = src->clients.begin();
diff --git a/components/rgbd-sources/src/virtual.cpp b/components/rgbd-sources/src/virtual.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0e6db973884a3c8361fcaae764cc1688d2434d9d
--- /dev/null
+++ b/components/rgbd-sources/src/virtual.cpp
@@ -0,0 +1,144 @@
+#include <ftl/rgbd/virtual.hpp>
+
+using ftl::rgbd::VirtualSource;
+using ftl::rgbd::Source;
+using ftl::rgbd::Channel;
+
+class VirtualImpl : public ftl::rgbd::detail::Source {
+	public:
+	explicit VirtualImpl(ftl::rgbd::Source *host, const ftl::rgbd::Camera &params) : ftl::rgbd::detail::Source(host) {
+		params_ = params;
+		capabilities_ = ftl::rgbd::kCapMovable | ftl::rgbd::kCapVideo | ftl::rgbd::kCapStereo;
+	}
+
+	~VirtualImpl() {
+
+	}
+
+	bool capture(int64_t ts) override {
+		timestamp_ = ts;
+		return true;
+	}
+
+	bool retrieve() override {
+		return true;
+	}
+
+	bool compute(int n, int b) override {
+		if (callback) {
+			frame.reset();
+
+			try {
+				callback(frame);
+			} catch (std::exception &e) {
+				LOG(ERROR) << "Exception in render callback: " << e.what();
+			} catch (...) {
+				LOG(ERROR) << "Unknown exception in render callback";
+			}
+
+			if (frame.hasChannel(Channel::Colour)) {
+				frame.download(Channel::Colour);
+				cv::swap(frame.get<cv::Mat>(Channel::Colour), rgb_);	
+			} else {
+				LOG(ERROR) << "Channel 1 frame in rendering";
+			}
+			
+			if ((host_->getChannel() != Channel::None) &&
+					frame.hasChannel(host_->getChannel())) {
+				frame.download(host_->getChannel());
+				cv::swap(frame.get<cv::Mat>(host_->getChannel()), depth_);
+			} else {
+				LOG(ERROR) << "Channel 2 frame in rendering";
+			}
+
+			auto cb = host_->callback();
+			if (cb) cb(timestamp_, rgb_, depth_);
+		}
+		return true;
+	}
+
+	bool isReady() override { return true; }
+
+	std::function<void(ftl::rgbd::Frame &)> callback;
+	ftl::rgbd::Frame frame;
+};
+
+VirtualSource::VirtualSource(ftl::config::json_t &cfg) : Source(cfg) {
+	auto params = params_;
+	impl_ = new VirtualImpl(this, params);
+}
+
+VirtualSource::~VirtualSource() {
+
+}
+
+void VirtualSource::onRender(const std::function<void(ftl::rgbd::Frame &)> &f) {
+	dynamic_cast<VirtualImpl*>(impl_)->callback = f;
+}
+
+/*
+void Source::writeFrames(int64_t ts, const cv::Mat &rgb, const cv::Mat &depth) {
+	if (!impl_) {
+		UNIQUE_LOCK(mutex_,lk);
+		rgb.copyTo(rgb_);
+		depth.copyTo(depth_);
+		timestamp_ = ts;
+	}
+}
+
+void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uint> &depth, cudaStream_t stream) {
+	if (!impl_) {
+		UNIQUE_LOCK(mutex_,lk);
+		timestamp_ = ts;
+		rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
+		cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
+		depth_.create(depth.height(), depth.width(), CV_32SC1);
+		cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(uint), depth_.rows, cudaMemcpyDeviceToHost, stream));
+		//cudaSafeCall(cudaStreamSynchronize(stream));  // TODO:(Nick) Don't wait here.
+		stream_ = stream;
+		//depth_.convertTo(depth_, CV_32F, 1.0f / 1000.0f);
+	} else {
+		LOG(ERROR) << "writeFrames cannot be done on this source: " << getURI();
+	}
+}
+
+void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<float> &depth, cudaStream_t stream) {
+	if (!impl_) {
+		UNIQUE_LOCK(mutex_,lk);
+		timestamp_ = ts;
+		rgb.download(rgb_, stream);
+		//rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
+		//cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
+		depth.download(depth_, stream);
+		//depth_.create(depth.height(), depth.width(), CV_32FC1);
+		//cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(float), depth_.rows, cudaMemcpyDeviceToHost, stream));
+		
+		stream_ = stream;
+		cudaSafeCall(cudaStreamSynchronize(stream_));
+		cv::cvtColor(rgb_,rgb_, cv::COLOR_BGRA2BGR);
+		cv::cvtColor(rgb_,rgb_, cv::COLOR_Lab2BGR);
+
+		if (callback_) callback_(timestamp_, rgb_, depth_);
+	}
+}
+
+void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uchar4> &rgb2, cudaStream_t stream) {
+	if (!impl_) {
+		UNIQUE_LOCK(mutex_,lk);
+		timestamp_ = ts;
+		rgb.download(rgb_, stream);
+		//rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
+		//cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
+		rgb2.download(depth_, stream);
+		//depth_.create(depth.height(), depth.width(), CV_32FC1);
+		//cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(float), depth_.rows, cudaMemcpyDeviceToHost, stream));
+		
+		stream_ = stream;
+		cudaSafeCall(cudaStreamSynchronize(stream_));
+		cv::cvtColor(rgb_,rgb_, cv::COLOR_BGRA2BGR);
+		cv::cvtColor(rgb_,rgb_, cv::COLOR_Lab2BGR);
+		cv::cvtColor(depth_,depth_, cv::COLOR_BGRA2BGR);
+		cv::cvtColor(depth_,depth_, cv::COLOR_Lab2BGR);
+	}
+}
+*/
\ No newline at end of file
diff --git a/components/rgbd-sources/test/CMakeLists.txt b/components/rgbd-sources/test/CMakeLists.txt
index 96e1441c5da6134eb17070d77e5ce9dff3a355f1..78bb6cec7e8c411ffbfe982a1b65320f56439bd5 100644
--- a/components/rgbd-sources/test/CMakeLists.txt
+++ b/components/rgbd-sources/test/CMakeLists.txt
@@ -5,6 +5,29 @@ add_executable(source_unit
 )
 target_include_directories(source_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
 target_link_libraries(source_unit
-	ftlcommon Eigen3::Eigen ${CUDA_LIBRARIES})
+	ftlcommon ftlcodecs ftlnet Eigen3::Eigen ${CUDA_LIBRARIES})
 
 add_test(SourceUnitTest source_unit)
+
+### Channel Unit ###############################################################
+add_executable(channel_unit
+	./tests.cpp
+	./channel_unit.cpp
+)
+target_include_directories(channel_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
+target_link_libraries(channel_unit
+	ftlcommon)
+
+add_test(ChannelUnitTest channel_unit)
+
+### Frame Unit #################################################################
+add_executable(frame_unit
+	./tests.cpp
+	./frame_unit.cpp
+	../src/frame.cpp
+)
+target_include_directories(frame_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
+target_link_libraries(frame_unit
+	ftlcommon ftlcodecs)
+
+add_test(FrameUnitTest frame_unit)
diff --git a/components/rgbd-sources/test/channel_unit.cpp b/components/rgbd-sources/test/channel_unit.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..25171678540f1970088d84e1e842865a4249455e
--- /dev/null
+++ b/components/rgbd-sources/test/channel_unit.cpp
@@ -0,0 +1,88 @@
+#include "catch.hpp"
+#include <ftl/rgbd/channels.hpp>
+
+using ftl::rgbd::Channel;
+using ftl::rgbd::Channels;
+
+TEST_CASE("channel casting", "") {
+	SECTION("cast channel to channels") {
+		Channels cs(Channel::Depth);
+
+        REQUIRE( (unsigned int)cs > 0 );
+        REQUIRE( cs.count() == 1 );
+	}
+
+    SECTION("cast channels to channel") {
+		Channels cs(Channel::Depth);
+        Channel c = (Channel)cs;
+
+        REQUIRE( c == Channel::Depth );
+	}
+}
+
+TEST_CASE("Channel or-ing", "") {
+	SECTION("Add channel to channel mask") {
+		Channels cs(Channel::Depth);
+
+        cs |= Channel::Right;
+
+        REQUIRE( (cs.count() == 2) );
+        REQUIRE( cs.has(Channel::Right) );
+        REQUIRE( cs.has(Channel::Depth) );
+	}
+
+    SECTION("Combine multiple channels in assignment") {
+		Channels cs;
+
+        cs = Channel::Right | Channel::Flow | Channel::Left;
+
+        REQUIRE( (cs.count() == 3) );
+        REQUIRE( cs.has(Channel::Right) );
+        REQUIRE( cs.has(Channel::Flow) );
+        REQUIRE( cs.has(Channel::Left) );
+	}
+
+    SECTION("Combine multiple channels at init") {
+		Channels cs = Channel::Right | Channel::Flow | Channel::Left;
+
+        REQUIRE( (cs.count() == 3) );
+        REQUIRE( cs.has(Channel::Right) );
+        REQUIRE( cs.has(Channel::Flow) );
+        REQUIRE( cs.has(Channel::Left) );
+	}
+}
+
+TEST_CASE("Channel adding", "") {
+	SECTION("Add channel to channel mask") {
+		Channels cs(Channel::Depth);
+
+        cs += Channel::Right;
+
+        REQUIRE( (cs.count() == 2) );
+        REQUIRE( cs.has(Channel::Right) );
+        REQUIRE( cs.has(Channel::Depth) );
+	}
+
+    SECTION("Combine multiple channels in assignment") {
+		Channels cs;
+
+        cs = Channel::Right + Channel::Flow + Channel::Left;
+
+        REQUIRE( (cs.count() == 3) );
+        REQUIRE( cs.has(Channel::Right) );
+        REQUIRE( cs.has(Channel::Flow) );
+        REQUIRE( cs.has(Channel::Left) );
+	}
+}
+
+TEST_CASE("Channel subtracting", "") {
+	SECTION("Remove channel from channel mask") {
+		Channels cs = Channel::Right | Channel::Flow | Channel::Left;
+
+        cs -= Channel::Flow;
+
+        REQUIRE( (cs.count() == 2) );
+        REQUIRE( cs.has(Channel::Right) );
+        REQUIRE( cs.has(Channel::Left) );
+	}
+}
diff --git a/components/rgbd-sources/test/frame_unit.cpp b/components/rgbd-sources/test/frame_unit.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6ad528a28fd677e207b05362c0021f7d302784dc
--- /dev/null
+++ b/components/rgbd-sources/test/frame_unit.cpp
@@ -0,0 +1,283 @@
+#include "catch.hpp"
+#include <ftl/rgbd/frame.hpp>
+
+using ftl::rgbd::Frame;
+using ftl::rgbd::Channel;
+using ftl::rgbd::Channels;
+using ftl::rgbd::Format;
+
+TEST_CASE("Frame::create() cpu mat", "") {
+	SECTION("in empty channel with format") {
+		Frame f;
+		auto &m = f.create<cv::Mat>(Channel::Colour, Format<float4>(200,200));
+
+		REQUIRE( m.type() == CV_32FC4 );
+		REQUIRE( m.cols == 200 );
+		REQUIRE( m.rows == 200 );
+	}
+
+	SECTION("in non-empty channel with format") {
+		Frame f;
+		f.create<cv::Mat>(Channel::Colour, Format<float>(200,100));
+		auto &m = f.create<cv::Mat>(Channel::Colour, Format<float4>(200,200));
+
+		REQUIRE( m.type() == CV_32FC4 );
+		REQUIRE( m.cols == 200 );
+		REQUIRE( m.rows == 200 );
+	}
+}
+
+TEST_CASE("Frame::get()", "") {
+	SECTION("get a non-existant host channel") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.get<cv::Mat>(Channel::Colour);
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( hadexception );
+	}
+
+	SECTION("get a non-existant gpu channel") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.get<cv::cuda::GpuMat>(Channel::Colour);
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( hadexception );
+	}
+
+	SECTION("get a valid host channel") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.create<cv::Mat>(Channel::Colour, Format<uchar3>(1024,1024));
+			auto &m = f.get<cv::Mat>(Channel::Colour);
+
+			REQUIRE( m.type() == CV_8UC3 );
+			REQUIRE( m.cols == 1024 );
+			REQUIRE( m.rows == 1024 );
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( !hadexception );
+	}
+
+	SECTION("get a valid gpu channel") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.create<cv::cuda::GpuMat>(Channel::Colour, Format<uchar3>(1024,1024));
+			auto &m = f.get<cv::cuda::GpuMat>(Channel::Colour);
+
+			REQUIRE( m.type() == CV_8UC3 );
+			REQUIRE( m.cols == 1024 );
+			REQUIRE( m.rows == 1024 );
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( !hadexception );
+	}
+
+	SECTION("get a cpu mat from gpu channel") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.create<cv::cuda::GpuMat>(Channel::Colour, Format<uchar3>(1024,1024));
+			REQUIRE( f.isGPU(Channel::Colour) );
+
+			auto &m = f.get<cv::Mat>(Channel::Colour);
+
+			REQUIRE( f.isCPU(Channel::Colour) );
+			REQUIRE( m.type() == CV_8UC3 );
+			REQUIRE( m.cols == 1024 );
+			REQUIRE( m.rows == 1024 );
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( !hadexception );
+	}
+
+	SECTION("get a gpu mat from cpu channel") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.create<cv::Mat>(Channel::Colour, Format<uchar3>(1024,1024));
+			REQUIRE( f.isCPU(Channel::Colour) );
+			
+			auto &m = f.get<cv::cuda::GpuMat>(Channel::Colour);
+
+			REQUIRE( f.isGPU(Channel::Colour) );
+			REQUIRE( m.type() == CV_8UC3 );
+			REQUIRE( m.cols == 1024 );
+			REQUIRE( m.rows == 1024 );
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( !hadexception );
+	}
+}
+
+TEST_CASE("Frame::createTexture()", "") {
+	SECTION("Missing format and no existing mat") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.createTexture<float>(Channel::Depth);
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( hadexception );
+	}
+
+	SECTION("Missing format but with existing host mat") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.create<cv::Mat>(Channel::Depth, Format<float>(100,100));
+			auto &t = f.createTexture<float>(Channel::Depth);
+
+			REQUIRE( t.width() == 100 );
+			REQUIRE( t.cvType() == CV_32FC1 );
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( !hadexception );
+	}
+
+	SECTION("Missing format but with incorrect existing host mat") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.create<cv::Mat>(Channel::Depth, Format<uchar4>(100,100));
+			f.createTexture<float>(Channel::Depth);
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( hadexception );
+	}
+
+	SECTION("With format and no existing mat") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			auto &t = f.createTexture<float>(Channel::Depth, Format<float>(1024,1024));
+			REQUIRE( t.cvType() == CV_32FC1 );
+			REQUIRE( t.cudaTexture() > 0 );
+			REQUIRE( t.devicePtr() != nullptr );
+
+			auto &m = f.get<cv::cuda::GpuMat>(Channel::Depth);
+			REQUIRE( m.data == reinterpret_cast<uchar*>(t.devicePtr()) );
+			REQUIRE( m.type() == CV_32FC1 );
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( !hadexception );
+	}
+
+	SECTION("Unchanged type is same texture object") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			auto &t = f.createTexture<float>(Channel::Depth, Format<float>(1024,1024));
+			REQUIRE( t.cvType() == CV_32FC1 );
+			
+			auto tex = t.cudaTexture();
+			float *ptr = t.devicePtr();
+
+			REQUIRE( ptr != nullptr );
+
+			auto &t2 = f.createTexture<float>(Channel::Depth, Format<float>(1024,1024));
+
+			REQUIRE( tex == t2.cudaTexture() );
+			REQUIRE( ptr == t2.devicePtr() );
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( !hadexception );
+	}
+}
+
+TEST_CASE("Frame::getTexture()", "") {
+	SECTION("Missing texture") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.getTexture<float>(Channel::Depth);
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( hadexception );
+	}
+
+	SECTION("Texture of incorrect type") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.createTexture<uchar4>(Channel::Depth, Format<uchar4>(100,100));
+			f.getTexture<float>(Channel::Depth);
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( hadexception );
+	}
+
+	SECTION("Valid texture get") {
+		Frame f;
+		bool hadexception = false;
+
+		try {
+			f.createTexture<uchar4>(Channel::Colour, Format<uchar4>(100,100));
+			auto &t = f.getTexture<uchar4>(Channel::Colour);
+
+			REQUIRE( t.cvType() == CV_8UC4 );
+			REQUIRE( t.width() == 100 );
+		} catch (ftl::exception &e) {
+			hadexception = true;
+		}
+
+		REQUIRE( !hadexception );
+	}
+}
+
+TEST_CASE("Frame::swapTo()", "") {
+	SECTION("Single host channel to empty frame") {
+		Frame f1;
+		Frame f2;
+
+		f1.create<cv::Mat>(Channel::Colour, Format<uchar3>(100,100));
+		f1.swapTo(Channels::All(), f2);
+
+		REQUIRE( f2.hasChannel(Channel::Colour) );
+		REQUIRE( (f2.get<cv::Mat>(Channel::Colour).cols == 100) );
+	}
+}
diff --git a/components/rgbd-sources/test/source_unit.cpp b/components/rgbd-sources/test/source_unit.cpp
index 4b14bee1887c956a2b646009b82d779e663b97d7..dca38be2ffb6e06b8be416c8de71a7a799c77867 100644
--- a/components/rgbd-sources/test/source_unit.cpp
+++ b/components/rgbd-sources/test/source_unit.cpp
@@ -14,7 +14,7 @@ class Snapshot {};
 
 class SnapshotReader {
 	public:
-	SnapshotReader(const std::string &) {}
+	explicit SnapshotReader(const std::string &) {}
 	Snapshot readArchive() { return Snapshot(); };
 };
 
@@ -22,7 +22,7 @@ namespace detail {
 
 class ImageSource : public ftl::rgbd::detail::Source {
 	public:
-	ImageSource(ftl::rgbd::Source *host) : ftl::rgbd::detail::Source(host) {
+	explicit ImageSource(ftl::rgbd::Source *host) : ftl::rgbd::detail::Source(host) {
 		last_type = "image";
 	}
 	ImageSource(ftl::rgbd::Source *host, const std::string &f) : ftl::rgbd::detail::Source(host) {
@@ -37,7 +37,7 @@ class ImageSource : public ftl::rgbd::detail::Source {
 
 class StereoVideoSource : public ftl::rgbd::detail::Source {
 	public:
-	StereoVideoSource(ftl::rgbd::Source *host) : ftl::rgbd::detail::Source(host) {
+	explicit StereoVideoSource(ftl::rgbd::Source *host) : ftl::rgbd::detail::Source(host) {
 		last_type = "video";
 	}
 	StereoVideoSource(ftl::rgbd::Source *host, const std::string &f) : ftl::rgbd::detail::Source(host) {
@@ -52,7 +52,7 @@ class StereoVideoSource : public ftl::rgbd::detail::Source {
 
 class NetSource : public ftl::rgbd::detail::Source {
 	public:
-	NetSource(ftl::rgbd::Source *host) : ftl::rgbd::detail::Source(host) {
+	explicit NetSource(ftl::rgbd::Source *host) : ftl::rgbd::detail::Source(host) {
 		last_type = "net";
 	}
 
@@ -76,7 +76,7 @@ class SnapshotSource : public ftl::rgbd::detail::Source {
 
 class RealsenseSource : public ftl::rgbd::detail::Source {
 	public:
-	RealsenseSource(ftl::rgbd::Source *host) : ftl::rgbd::detail::Source(host) {
+	explicit RealsenseSource(ftl::rgbd::Source *host) : ftl::rgbd::detail::Source(host) {
 		last_type = "realsense";
 	}
 
@@ -122,11 +122,11 @@ using ftl::rgbd::Source;
 using ftl::config::json_t;
 
 TEST_CASE("ftl::create<Source>(cfg)", "[rgbd]") {
-	json_t global = {{"$id","ftl://test"}};
+	json_t global = json_t{{"$id","ftl://test"}};
 	ftl::config::configure(global);
 
 	SECTION("with valid image file uri") {
-		json_t cfg = {
+		json_t cfg = json_t{
 			{"$id","ftl://test/1"},
 			{"uri","file://" FTL_SOURCE_DIRECTORY "/components/rgbd-sources/test/data/image.png"}
 		};
@@ -139,7 +139,7 @@ TEST_CASE("ftl::create<Source>(cfg)", "[rgbd]") {
 	}
 
 	SECTION("with valid video file uri") {
-		json_t cfg = {
+		json_t cfg = json_t{
 			{"$id","ftl://test/2"},
 			{"uri","file://" FTL_SOURCE_DIRECTORY "/components/rgbd-sources/test/data/video.mp4"}
 		};
@@ -152,7 +152,7 @@ TEST_CASE("ftl::create<Source>(cfg)", "[rgbd]") {
 	}
 
 	SECTION("with valid net uri") {
-		json_t cfg = {
+		json_t cfg = json_t{
 			{"$id","ftl://test/2"},
 			{"uri","ftl://utu.fi/dummy"}
 		};
@@ -165,7 +165,7 @@ TEST_CASE("ftl::create<Source>(cfg)", "[rgbd]") {
 	}
 
 	SECTION("with an invalid uri") {
-		json_t cfg = {
+		json_t cfg = json_t{
 			{"$id","ftl://test/2"},
 			{"uri","not a uri"}
 		};
@@ -177,7 +177,7 @@ TEST_CASE("ftl::create<Source>(cfg)", "[rgbd]") {
 	}
 
 	SECTION("with an invalid file uri") {
-		json_t cfg = {
+		json_t cfg = json_t{
 			{"$id","ftl://test/2"},
 			{"uri","file:///not/a/file"}
 		};
@@ -189,7 +189,7 @@ TEST_CASE("ftl::create<Source>(cfg)", "[rgbd]") {
 	}
 
 	SECTION("with a missing file") {
-		json_t cfg = {
+		json_t cfg = json_t{
 			{"$id","ftl://test/2"},
 			{"uri","file:///data/image2.png"}
 		};
@@ -202,11 +202,11 @@ TEST_CASE("ftl::create<Source>(cfg)", "[rgbd]") {
 }
 
 TEST_CASE("Source::set(uri)", "[rgbd]") {
-	json_t global = {{"$id","ftl://test"}};
+	json_t global = json_t{{"$id","ftl://test"}};
 	ftl::config::configure(global);
 
 	SECTION("change to different valid URI type") {
-		json_t cfg = {
+		json_t cfg = json_t{
 			{"$id","ftl://test/1"},
 			{"uri","file://" FTL_SOURCE_DIRECTORY "/components/rgbd-sources/test/data/image.png"}
 		};
diff --git a/components/scene-sources/include/ftl/scene/framescene.hpp b/components/scene-sources/include/ftl/scene/framescene.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0235a60331899ca125514e0905c845bafd1e466c
--- /dev/null
+++ b/components/scene-sources/include/ftl/scene/framescene.hpp
@@ -0,0 +1,28 @@
+#ifndef _FTL_SCENE_FRAMESCENE_HPP_
+#define _FTL_SCENE_FRAMESCENE_HPP_
+
+#include <ftl/scene/scene.hpp>
+
+namespace ftl {
+namespace scene {
+
+/**
+ * A scene represented internally as a set of image frames that together
+ * define a point cloud.
+ */
+class FrameScene : public ftl::scene::Scene {
+	public:
+	FrameScene();
+	~FrameScene();
+
+	bool update(ftl::rgbd::FrameSet &);
+
+	bool render(ftl::rgbd::Source *, ftl::rgbd::Frame &);
+	bool encode(std::vector<uint8_t> &);
+	bool decode(const std::vector<uint8_t> &);
+};
+
+}
+}
+
+#endif  // _FTL_SCENE_FRAMESCENE_HPP_
diff --git a/components/scene-sources/include/ftl/scene/scene.hpp b/components/scene-sources/include/ftl/scene/scene.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..856819cf403982d54ea3fd12a3cd195b9d437919
--- /dev/null
+++ b/components/scene-sources/include/ftl/scene/scene.hpp
@@ -0,0 +1,21 @@
+#ifndef _FTL_RECONSTRUCT_SCENE_HPP_
+#define _FTL_RECONSTRUCT_SCENE_HPP_
+
+namespace ftl {
+namespace scene {
+
+class Scene {
+    public:
+    Scene();
+    virtual ~Scene();
+
+    virtual bool render(ftl::rgbd::Source *, ftl::rgbd::Frame &)=0;
+
+	virtual bool encode(std::vector<uint8_t> &)=0;
+	virtual bool decode(const std::vector<uint8_t> &)=0;
+};
+
+}  // scene
+}  // ftl
+
+#endif  // _FTL_RECONSTRUCT_SCENE_HPP_
diff --git a/config/config_vision.jsonc b/config/config_vision.jsonc
new file mode 100644
index 0000000000000000000000000000000000000000..b73446cefe06aaaf3663b8fe2823822878b5a1a8
--- /dev/null
+++ b/config/config_vision.jsonc
@@ -0,0 +1,129 @@
+{
+	//"$id": "ftl://utu.fi",
+	"$schema": "",
+	"calibrations": {
+		"default": {
+			"use_intrinsics": true,
+			"use_extrinsics": true,
+			"alpha": 0.0
+		}
+	},
+	
+	"disparity": {
+		"libsgm": {
+			"algorithm": "libsgm",
+			"width": 1280,
+			"height": 720,
+			"use_cuda": true,
+			"minimum": 0,
+			"maximum": 256,
+			"tau": 0.0,
+			"gamma": 0.0,
+			"window_size": 5,
+			"sigma": 1.5,
+			"lambda": 8000.0,
+			"uniqueness":  0.65,
+			"use_filter": true,
+			"P1": 8,
+			"P2": 130,
+			"filter_radius": 11,
+			"filter_iter": 2,
+			"use_off": true,
+			"off_size": 24,
+			"off_threshold": 0.75,
+			"T": 60,
+			"T_add": 0,
+			"T_del": 25,
+			"T_x" : 3.0,
+			"alpha" : 0.6,
+			"beta" : 1.7,
+			"epsilon" : 15.0
+		},
+		
+		"rtcensus": {
+			"algorithm": "rtcensus",
+			"use_cuda": true,
+			"minimum": 0,
+			"maximum": 256,
+			"tau": 0.0,
+			"gamma": 0.0,
+			"window_size": 5,
+			"sigma": 1.5,
+			"lambda": 8000.0,
+			"use_filter": true,
+			"filter_radius": 3,
+			"filter_iter": 4	
+		}
+	},
+	
+	"sources": {
+		"stereocam": {
+			"uri": "device:video",
+			"feed": {
+				"flip": false,
+				"nostereo": false,
+				"scale": 1.0,
+				"flip_vert": false,
+				"max_fps": 500,
+				"width": 1280,
+				"height": 720,
+				"crosshair": false
+			},
+			"use_optflow" : true,
+			"calibration": { "$ref": "#calibrations/default" },
+			"disparity": { "$ref": "#disparity/libsgm" }
+		},
+		"stereovid": {},
+		"localhost": {}
+		
+	},
+	
+	"vision_default": {
+		"fps": 20,
+		"source": { "$ref": "#sources/stereocam" },
+		"middlebury": { "$ref": "#middlebury/none" },
+		"display": { "$ref": "#displays/none" },
+		"net": { "$ref": "#net/default_vision" },
+		"stream": { }
+	},
+	
+	// Listen to localhost
+	"net": {
+		"default_vision": {
+			"listen": "tcp://*:9001",
+			"peers": [],
+			"tcp_send_buffer": 100000 //204800
+		},
+		"default_reconstruct": {
+			"listen": "tcp://*:9002",
+			"peers": []
+		}
+	},
+	
+	"displays": {
+		"none": {
+			"flip_vert": false,
+			"disparity": false,
+			"points": false,
+			"depth": false,
+			"left": false,
+			"right": false
+		},
+		"left": {
+			"flip_vert": false,
+			"disparity": false,
+			"points": false,
+			"depth": false,
+			"left": true,
+			"right": false
+		}
+	},
+	
+	"middlebury": {
+		"none": {
+			"dataset": "",
+			"threshold": 10.0,
+			"scale": 0.25
+		}
+	}
+}
diff --git a/web-service/server/src/index.js b/web-service/server/src/index.js
index 0e4062f95f6ee1e9c79498d8e5f55b3b165b21c4..6dd821d19282cbe97dba5eaf131d39bf4226ccaa 100644
--- a/web-service/server/src/index.js
+++ b/web-service/server/src/index.js
@@ -46,8 +46,8 @@ function RGBDClient(peer, N, rate, dest) {
 /**
  * Actually send a frame over network to the client.
  */
-RGBDClient.prototype.push = function(uri, frame, ttime, chunk,  rgb, depth) {
-	this.peer.send(uri, frame, ttime, chunk, rgb, depth);
+RGBDClient.prototype.push = function(uri, latency, spacket, packet) {
+	this.peer.send(uri, latency, spacket, packet);
 	this.txcount++;
 }
 
@@ -72,14 +72,23 @@ function RGBDStream(uri, peer) {
 	this.rxmax = 10;
 
 	// Add RPC handler to receive frames from the source
-	peer.bind(uri, (frame, ttime, chunk, rgb, depth) => {
+	peer.bind(uri, (latency, spacket, packet) => {
 		// Forward frames to all clients
-		this.pushFrames(frame, ttime, chunk, rgb, depth);
+		this.pushFrames(latency, spacket, packet);
 		this.rxcount++;
 		if (this.rxcount >= this.rxmax && this.clients.length > 0) {
 			this.subscribe();
 		}
 	});
+
+	/*peer.bind(uri, (frame, ttime, chunk, rgb, depth) => {
+		// Forward frames to all clients
+		this.pushFrames(frame, ttime, chunk, rgb, depth);
+		this.rxcount++;
+		if (this.rxcount >= this.rxmax && this.clients.length > 0) {
+			this.subscribe();
+		}
+	});*/
 }
 
 RGBDStream.prototype.addClient = function(peer, N, rate, dest) {
@@ -99,15 +108,19 @@ RGBDStream.prototype.subscribe = function() {
 	this.rxcount = 0;
 	this.rxmax = 10;
 	//console.log("Subscribe to ", this.uri);
-	this.peer.send("get_stream", this.uri, 10, 0, [Peer.uuid], this.uri);
+	// TODO: Don't hard code 9 here, instead use 9 for thumbnails and 0 for
+	// the video...
+	this.peer.send("get_stream", this.uri, 10, 9, [Peer.uuid], this.uri);
 }
 
-RGBDStream.prototype.pushFrames = function(frame, ttime, chunk, rgb, depth) {
-	this.rgb = rgb;
-	this.depth = depth;
+RGBDStream.prototype.pushFrames = function(latency, spacket, packet) {
+	if (spacket[1] & 0x1) this.depth = packet[4];
+	else this.rgb = packet[4];
+
+	console.log("Frame = ", packet[0], packet[1]);
 
 	for (let i=0; i < this.clients.length; i++) {
-		this.clients[i].push(this.uri, frame, ttime, chunk, rgb, depth);
+		this.clients[i].push(this.uri, latency, spacket, packet);
 	}
 
 	let i=0;