diff --git a/CMakeLists.txt b/CMakeLists.txt
index fb73e5cf9d35ad7de7e1a2cbf66826d802c0b749..6640f5d7ee46c3511254604ae0840e49cb9e91d6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -230,6 +230,7 @@ add_subdirectory(applications/calibration)
 add_subdirectory(applications/groupview)
 add_subdirectory(applications/player)
 add_subdirectory(applications/recorder)
+add_subdirectory(applications/merger)
 
 if (HAVE_AVFORMAT)
 	add_subdirectory(applications/ftl2mkv)
diff --git a/applications/ftl2mkv/src/main.cpp b/applications/ftl2mkv/src/main.cpp
index b555dcbf7f3494dbd66f87415f51cbb0cec3c49e..598e372ca524d33e358c1f312fdeba5429fa0ea8 100644
--- a/applications/ftl2mkv/src/main.cpp
+++ b/applications/ftl2mkv/src/main.cpp
@@ -81,7 +81,7 @@ int main(int argc, char **argv) {
 		LOG(ERROR) << "Missing input ftl file.";
 		return -1;
 	} else {
-		filename = paths[0];
+		filename = paths[paths.size()-1];
 	}
 
 	std::ifstream f;
@@ -99,7 +99,7 @@ int main(int argc, char **argv) {
 
 	AVOutputFormat *fmt;
 	AVFormatContext *oc;
-	AVStream *video_st[10] = {nullptr};
+	AVStream *video_st[10][2] = {nullptr};
 
 	av_register_all();
 
@@ -130,28 +130,24 @@ int main(int argc, char **argv) {
     LOG(INFO) << "Converting...";
 
     int current_stream = root->value("stream", 0);
-    int current_channel = 0;
+    int current_channel = root->value("channel", -1);
 
 	//bool stream_added[10] = {false};
 
 	// TODO: In future, find a better way to discover number of streams...
 	// Read entire file to find all streams before reading again to write data
 	bool res = r.read(90000000000000, [&current_stream,&current_channel,&r,&video_st,oc](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
-        if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel)) return;
+        if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel) && current_channel != -1) return;
         if (spkt.streamID == current_stream || current_stream == 255) {
 
-            if (pkt.codec == codec_t::POSE) {
-                return;
-            }
-
-            if (pkt.codec == codec_t::CALIBRATION) {
+            if (pkt.codec != codec_t::HEVC) {
                 return;
             }
 
 			if (spkt.streamID >= 10) return;  // TODO: Allow for more than 10
 
-			if (video_st[spkt.streamID] == nullptr) {
-				video_st[spkt.streamID] = add_video_stream(oc, pkt);
+			if (video_st[spkt.streamID][(spkt.channel == Channel::Left) ? 0 : 1] == nullptr) {
+				video_st[spkt.streamID][(spkt.channel == Channel::Left) ? 0 : 1] = add_video_stream(oc, pkt);
 			}
 		}
 	});
@@ -172,14 +168,10 @@ int main(int argc, char **argv) {
 	bool seen_key[10] = {false};
 
     res = r.read(90000000000000, [&current_stream,&current_channel,&r,&video_st,oc,&seen_key](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
-        if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel)) return;
+        if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel) && current_channel != -1) return;
         if (spkt.streamID == current_stream || current_stream == 255) {
 
-            if (pkt.codec == codec_t::POSE) {
-                return;
-            }
-
-            if (pkt.codec == codec_t::CALIBRATION) {
+            if (pkt.codec != codec_t::HEVC) {
                 return;
             }
 
@@ -201,7 +193,7 @@ int main(int argc, char **argv) {
 			if (keyframe) avpkt.flags |= AV_PKT_FLAG_KEY;
 			avpkt.pts = spkt.timestamp - r.getStartTime();
 			avpkt.dts = avpkt.pts;
-			avpkt.stream_index= video_st[spkt.streamID]->index;
+			avpkt.stream_index= video_st[spkt.streamID][(spkt.channel == Channel::Left) ? 0 : 1]->index;
 			avpkt.data= const_cast<uint8_t*>(pkt.data.data());
 			avpkt.size= pkt.data.size();
 			avpkt.duration = 1;
@@ -220,7 +212,8 @@ int main(int argc, char **argv) {
 	//avcodec_close(video_st->codec);
 
 	for (int i=0; i<10; ++i) {
-		if (video_st[i]) av_free(video_st[i]);
+		if (video_st[i][0]) av_free(video_st[i][0]);
+		if (video_st[i][1]) av_free(video_st[i][1]);
 	}
 
 	if (!(fmt->flags & AVFMT_NOFILE)) {
diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index ae0d5627a94a84afb819a9b1daf62b6bddacccae..0a9d04c044e70b615f24733380818ff1a46a1739 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -3,6 +3,8 @@
 #include "screen.hpp"
 #include <nanogui/glutil.h>
 
+#include <fstream>
+
 #ifdef HAVE_OPENVR
 #include "vr.hpp"
 #endif
@@ -137,6 +139,13 @@ ftl::gui::Camera::Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src) : scr
 	sdepth_ = false;
 	ftime_ = (float)glfwGetTime();
 	pause_ = false;
+	recording_ = false;
+	fileout_ = new std::ofstream();
+	writer_ = new ftl::codecs::Writer(*fileout_);
+	recorder_ = std::function([this](ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+		ftl::codecs::StreamPacket s = spkt;
+		writer_->write(s, pkt);
+	});
 
 	channel_ = Channel::Left;
 
@@ -166,7 +175,8 @@ ftl::gui::Camera::Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src) : scr
 }
 
 ftl::gui::Camera::~Camera() {
-
+	delete writer_;
+	delete fileout_;
 }
 
 ftl::rgbd::Source *ftl::gui::Camera::source() {
@@ -258,7 +268,6 @@ bool ftl::gui::Camera::setVR(bool on) {
 		src_->set("focal", intrinsic(0, 0));
 		src_->set("centre_x", intrinsic(0, 2));
 		src_->set("centre_y", intrinsic(1, 2));
-		LOG(INFO) << intrinsic;
 		
 		intrinsic = getCameraMatrix(screen_->getVR(), vr::Eye_Right);
 		CHECK(intrinsic(0, 2) < 0 && intrinsic(1, 2) < 0);
@@ -366,8 +375,9 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 	if (src_ && src_->isReady()) {
 		UNIQUE_LOCK(mutex_, lk);
 
-		if (isVR()) {
+		if (screen_->isVR()) {
 			#ifdef HAVE_OPENVR
+			
 			vr::VRCompositor()->WaitGetPoses(rTrackedDevicePose_, vr::k_unMaxTrackedDeviceCount, NULL, 0 );
 
 			if ((channel_ == Channel::Right) && rTrackedDevicePose_[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid )
@@ -494,6 +504,37 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 	return texture1_;
 }
 
+void ftl::gui::Camera::snapshot() {
+	UNIQUE_LOCK(mutex_, lk);
+	char timestamp[18];
+	std::time_t t = std::time(NULL);
+	std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+	cv::Mat image;
+	cv::flip(im1_, image, 0);
+	cv::imwrite(std::string(timestamp) + ".png", image);
+}
+
+void ftl::gui::Camera::toggleVideoRecording() {
+	if (recording_) {
+		src_->removeRawCallback(recorder_);
+		writer_->end();
+		fileout_->close();
+		recording_ = false;
+	} else {
+		char timestamp[18];
+		std::time_t t=std::time(NULL);
+		std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+		fileout_->open(std::string(timestamp) + ".ftl");
+
+		writer_->begin();
+		src_->addRawCallback(recorder_);
+
+		src_->inject(Channel::Calibration, src_->parameters(), Channel::Left, src_->getCapabilities());
+		src_->inject(src_->getPose());
+		recording_ = true;
+	}
+}
+
 nlohmann::json ftl::gui::Camera::getMetaData() {
 	return nlohmann::json();
 }
diff --git a/applications/gui/src/camera.hpp b/applications/gui/src/camera.hpp
index 24dcbacfdc51d20166ffc49175f268140e4dcdf3..43cf9c783b1e4b5e3ae003c428b8d6126273ef40 100644
--- a/applications/gui/src/camera.hpp
+++ b/applications/gui/src/camera.hpp
@@ -2,6 +2,7 @@
 #define _FTL_GUI_CAMERA_HPP_
 
 #include <ftl/rgbd/source.hpp>
+#include <ftl/codecs/writer.hpp>
 #include "gltexture.hpp"
 
 #include <string>
@@ -43,7 +44,7 @@ class Camera {
 	
 	void togglePause();
 	void isPaused();
-	const ftl::codecs::Channels &availableChannels();
+	const ftl::codecs::Channels &availableChannels() { return channels_; }
 
 	const GLTexture &captureFrame();
 	const GLTexture &getLeft() const { return texture1_; }
@@ -51,6 +52,10 @@ class Camera {
 
 	bool thumbnail(cv::Mat &thumb);
 
+	void snapshot();
+
+	void toggleVideoRecording();
+
 	nlohmann::json getMetaData();
 
 	StatisticsImage *stats_ = nullptr;
@@ -85,6 +90,10 @@ class Camera {
 	ftl::codecs::Channels channels_;
 	cv::Mat im1_; // first channel (left)
 	cv::Mat im2_; // second channel ("right")
+	bool recording_;
+	std::ofstream *fileout_;
+	ftl::codecs::Writer *writer_;
+	ftl::rgbd::RawCallback recorder_;
 
 	MUTEX mutex_;
 
diff --git a/applications/gui/src/config_window.cpp b/applications/gui/src/config_window.cpp
index fcba8ff7ad2e033ab790790b03aed4a3af25af15..322f817ae264386ab3a54bfddc72c7d5f588fc36 100644
--- a/applications/gui/src/config_window.cpp
+++ b/applications/gui/src/config_window.cpp
@@ -2,10 +2,10 @@
 
 #include <nanogui/layout.h>
 #include <nanogui/label.h>
-#include <nanogui/combobox.h>
 #include <nanogui/button.h>
 #include <nanogui/entypo.h>
 #include <nanogui/formhelper.h>
+#include <nanogui/vscrollpanel.h>
 
 #include <vector>
 #include <string>
@@ -15,28 +15,25 @@ using std::string;
 using std::vector;
 using ftl::config::json_t;
 
-ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const ftl::UUID &peer) : ConfigWindow(parent, ctrl, std::optional<ftl::UUID>(peer)) {
-
-}
-
-ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const std::optional<ftl::UUID> &peer)
-		: nanogui::Window(parent, "Settings"), ctrl_(ctrl), peer_(peer) {
+ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
+		: nanogui::Window(parent, "Settings"), ctrl_(ctrl) {
 	using namespace nanogui;
 
 	setLayout(new GroupLayout());
 	setPosition(Vector2i(parent->width()/2.0f - 100.0f, parent->height()/2.0f - 100.0f));
 	//setModal(true);
 
-	if (peer) {
-		configurables_ = ctrl->getConfigurables(peer.value());
-	} else {
-		configurables_ = ftl::config::list();
-	}
+	configurables_ = ftl::config::list();
 
 	new Label(this, "Select Configurable","sans-bold");
 
+	auto vscroll = new VScrollPanel(this);
+	vscroll->setFixedHeight(300);
+	Widget *buttons = new Widget(vscroll);
+	buttons->setLayout(new BoxLayout(Orientation::Vertical, Alignment::Fill));
+
 	for (auto c : configurables_) {
-		auto itembutton = new Button(this, c);
+		auto itembutton = new Button(buttons, c);
 		itembutton->setCallback([this,c]() {
 			LOG(INFO) << "Change configurable: " << c;
 			_buildForm(c);
@@ -52,67 +49,51 @@ ConfigWindow::~ConfigWindow() {
 
 }
 
-class ConfigWindow::References {
-	public:
-	References(ftl::NetConfigurable* nc, ftl::config::json_t* config, const std::string* suri) : nc(nc), config(config), suri(suri) {
-	}
-
-	~References() {
-		delete nc;
-		delete config;
-		delete suri;
-	}
-
-	private:
-	ftl::NetConfigurable* nc;
-	ftl::config::json_t* config;
-	const std::string* suri;
-};
-
-std::vector<ftl::gui::ConfigWindow::References *> ConfigWindow::_addElements(nanogui::FormHelper *form, ftl::Configurable &nc, const std::string &suri, std::function<ftl::Configurable*(const std::string*, std::vector<References *>&)> construct) {
+void ConfigWindow::_addElements(nanogui::FormHelper *form, const std::string &suri) {
 	using namespace nanogui;
 
-	std::vector<References *> references;
-
-	auto data = nc.getConfig();
+	Configurable *configurable = ftl::config::find(suri);
+	ftl::config::json_t data;
+	if (configurable) {
+		configurable->refresh();
+		data = configurable->getConfig();
+	}
 
 	for (auto i=data.begin(); i!=data.end(); ++i) {
 		if (i.key() == "$id") continue;
 
 		if (i.key() == "$ref" && i.value().is_string()) {
 			LOG(INFO) << "Follow $ref: " << i.value();
-			const std::string* suri = new std::string(i.value().get<string>());
-			ftl::Configurable* rc = construct(suri, references);
-			auto new_references = _addElements(form, *rc, *suri, construct);
-			references.insert(references.end(), new_references.begin(), new_references.end());
+			const std::string suri = std::string(i.value().get<string>());
+			_addElements(form, suri);
 			continue;
 		}
 
 		if (i.value().is_boolean()) {
 			string key = i.key();
-			form->addVariable<bool>(i.key(), [this,data,key,&nc](const bool &b){
-				nc.set(key, b);
+			form->addVariable<bool>(i.key(), [this,data,key,suri](const bool &b){
+				ftl::config::update(suri+"/"+key, b);
 			}, [data,key]() -> bool {
 				return data[key].get<bool>();
 			});
 		} else if (i.value().is_number_integer()) {
 			string key = i.key();
-			form->addVariable<int>(i.key(), [this,data,key,&nc](const int &f){
-				nc.set(key, f);
+			form->addVariable<int>(i.key(), [this,data,key,suri](const int &f){
+				ftl::config::update(suri+"/"+key, f);
 			}, [data,key]() -> int {
 				return data[key].get<int>();
 			});
 		} else if (i.value().is_number_float()) {
 			string key = i.key();
-			form->addVariable<float>(i.key(), [this,data,key,&nc](const float &f){
-				nc.set(key, f);
+			form->addVariable<float>(i.key(), [this,data,key,suri](const float &f){
+				ftl::config::update(suri+"/"+key, f);
 			}, [data,key]() -> float {
 				return data[key].get<float>();
 			});
 		} else if (i.value().is_string()) {
 			string key = i.key();
-			form->addVariable<string>(i.key(), [this,data,key,&nc](const string &f){
-				nc.set(key, f);
+			form->addVariable<string>(i.key(), [this,data,key,suri](const string &f){
+				ftl::config::update(suri+"/"+key, f);
 			}, [data,key]() -> string {
 				return data[key].get<string>();
 			});
@@ -131,8 +112,6 @@ std::vector<ftl::gui::ConfigWindow::References *> ConfigWindow::_addElements(nan
 			}
 		}
 	}
-
-	return references;
 }
 
 void ConfigWindow::_buildForm(const std::string &suri) {
@@ -145,50 +124,15 @@ void ConfigWindow::_buildForm(const std::string &suri) {
 	form->addWindow(Vector2i(100,50), uri.getFragment());
 	form->window()->setTheme(theme());
 
-	ftl::config::json_t* config;
-	config = new ftl::config::json_t;
-	const std::string* allocated_suri = new std::string(suri);
-	std::vector<ftl::gui::ConfigWindow::References *> references;
-
-	ftl::Configurable* nc;
-
-	if (peer_) {
-		*config = ctrl_->get(peer_.value(), suri);
-		nc = new ftl::NetConfigurable(peer_.value(), *allocated_suri, *ctrl_, *config);
+	_addElements(form, suri);
 
-		references = _addElements(form, *nc, *allocated_suri, [this](auto suri, auto &references) {
-			ftl::config::json_t* config = new ftl::config::json_t;
-			*config = ctrl_->get(peer_.value(), *suri);
-			auto nc = new ftl::NetConfigurable(peer_.value(), *suri, *ctrl_, *config);
-			auto r = new References(nc, config, suri);
-			references.push_back(r);
-			return nc;
-		});
-	} else {
-		nc = ftl::config::find(suri);
-		if (nc) {
-			references = _addElements(form, *nc, *allocated_suri, [this](auto suri, auto &references) {
-				return ftl::config::find(*suri);
-			});
-		}
-	}
-
-	auto closebutton = form->addButton("Close", [this,form,config,allocated_suri,nc,references]() {
+	auto closebutton = form->addButton("Close", [this,form]() {
 		form->window()->setVisible(false);
-		for(auto r : references) {
-			delete r;
-		}
-		if (peer_) {
-			delete nc;
-		}
-		delete config;
-		delete allocated_suri;
 		delete form;
 	});
 	closebutton->setIcon(ENTYPO_ICON_CROSS);
 }
 
 bool ConfigWindow::exists(const std::string &uri) {
-	// If the Configurable is a NetConfigurable, the URI is not checked.
-	return peer_ || ftl::config::find(uri);
-}
\ No newline at end of file
+	return ftl::config::find(uri) != nullptr;
+}
diff --git a/applications/gui/src/config_window.hpp b/applications/gui/src/config_window.hpp
index 23279d93cee1d601e53e0147b4edee8d5309a483..a0fe74155fe5320f983871d8c9237c7571625670 100644
--- a/applications/gui/src/config_window.hpp
+++ b/applications/gui/src/config_window.hpp
@@ -16,23 +16,15 @@ namespace gui {
  */
 class ConfigWindow : public nanogui::Window {
 	public:
-	ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const ftl::UUID &peer);
-	ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const std::optional<ftl::UUID> &peer = std::nullopt);
+	ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl);
 	~ConfigWindow();
 
 	private:
-	/*
-	References holds the pointers to a NetConfigurable and all its members so that
-	they can all be returned from _addElements() and then simultaneously deleted
-	as the form is closed.
-	*/
-	class References;
 	ftl::ctrl::Master *ctrl_;
-	std::optional<ftl::UUID> peer_;
 	std::vector<std::string> configurables_;
 	
 	void _buildForm(const std::string &uri);
-	std::vector<References *> _addElements(nanogui::FormHelper *form, ftl::Configurable &nc, const std::string &suri, std::function<ftl::Configurable*(const std::string*, std::vector<References *>&)> construct);
+	void _addElements(nanogui::FormHelper *form, const std::string &suri);
 	bool exists(const std::string &uri);
 };
 
diff --git a/applications/gui/src/ctrl_window.cpp b/applications/gui/src/ctrl_window.cpp
index 67a3682bd2fcfcb55a68fb9c71b6dd55aad36491..fc82c45838a568efc8b4e8d456cdb3fbbef2c8b4 100644
--- a/applications/gui/src/ctrl_window.cpp
+++ b/applications/gui/src/ctrl_window.cpp
@@ -69,7 +69,7 @@ ControlWindow::ControlWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
 
 	button = new Button(tools, "", ENTYPO_ICON_COG);
 	button->setCallback([this,parent] {
-		auto cfgwin = new ConfigWindow(parent, ctrl_, _getActiveID());
+		auto cfgwin = new ConfigWindow(parent, ctrl_);
 		cfgwin->setTheme(theme());
 	});
 	button->setTooltip("Edit node configuration");
diff --git a/applications/gui/src/main.cpp b/applications/gui/src/main.cpp
index 50bc3638c2e26197fbcf5b74dadacd4b6a49d0ad..5ebc552fa0ee0664c7a0b7898a723b90bf87f29c 100644
--- a/applications/gui/src/main.cpp
+++ b/applications/gui/src/main.cpp
@@ -2,6 +2,7 @@
 #include <ftl/net/universe.hpp>
 #include <ftl/rgbd.hpp>
 #include <ftl/master.hpp>
+#include <ftl/net_configurable.hpp>
 
 #include <loguru.hpp>
 
@@ -12,9 +13,6 @@ int main(int argc, char **argv) {
 	auto root = ftl::configure(argc, argv, "gui_default");
 	ftl::net::Universe *net = ftl::create<ftl::net::Universe>(root, "net");
 
-	net->start();
-	net->waitConnections();
-
 	ftl::ctrl::Master *controller = new ftl::ctrl::Master(root, net);
 	controller->onLog([](const ftl::ctrl::LogEvent &e){
 		const int v = e.verbosity;
@@ -25,6 +23,35 @@ int main(int argc, char **argv) {
 		}
 	});
 
+	std::map<ftl::UUID, std::vector<ftl::NetConfigurable*>> peerConfigurables;
+
+	// FIXME: Move this elsewhere, it is not just for GUI
+	net->onConnect([&controller, &peerConfigurables](ftl::net::Peer *p) {
+		ftl::UUID peer = p->id();
+		auto cs = controller->getConfigurables(peer);
+		for (auto c : cs) {
+			//LOG(INFO) << "NET CONFIG: " << c;
+			ftl::config::json_t *configuration = new ftl::config::json_t;
+			*configuration = controller->get(peer, c);
+			if (!configuration->empty()) {
+				ftl::NetConfigurable *nc = new ftl::NetConfigurable(peer, c, *controller, *configuration);
+				peerConfigurables[peer].push_back(nc);
+			}
+		}
+	});
+
+	net->onDisconnect([&peerConfigurables](ftl::net::Peer *p) {
+		ftl::UUID peer = p->id();
+		for (ftl::NetConfigurable *nc : peerConfigurables[peer]) {
+			ftl::config::json_t *configuration = &(nc->getConfig());
+			delete nc;
+			delete configuration;
+		}
+	});
+
+	net->start();
+	net->waitConnections();
+
 	/*auto available = net.findAll<string>("list_streams");
 	for (auto &a : available) {
 		std::cout << " -- " << a << std::endl;
@@ -57,4 +84,4 @@ int main(int argc, char **argv) {
 	delete root;
 
 	return 0;
-}
\ No newline at end of file
+}
diff --git a/applications/gui/src/media_panel.cpp b/applications/gui/src/media_panel.cpp
index 800940199e2488ca1b11bd723498059efd63d374..693024bd3c6a89ccb0ac6e893b01641dbd77c574 100644
--- a/applications/gui/src/media_panel.cpp
+++ b/applications/gui/src/media_panel.cpp
@@ -1,6 +1,5 @@
 #include "media_panel.hpp"
 #include "screen.hpp"
-#include "camera.hpp"
 
 #include <nanogui/layout.h>
 #include <nanogui/button.h>
@@ -18,7 +17,6 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 	using namespace nanogui;
 
 	paused_ = false;
-	writer_ = nullptr;
 	disable_switch_channels_ = false;
 
 	setLayout(new BoxLayout(Orientation::Horizontal,
@@ -35,29 +33,62 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 		if (cam) cam->showPoseWindow();
 	});
 
-	button = new Button(this, "", ENTYPO_ICON_CONTROLLER_RECORD);
-	button->setFlags(Button::ToggleButton);
-	button->setChangeCallback([this,button](bool state) {
-		if (state){
-			auto *cam = screen_->activeCamera();
-
-			button->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
-			char timestamp[18];
-			std::time_t t=std::time(NULL);
-			std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
-			writer_ = new ftl::rgbd::SnapshotStreamWriter(std::string(timestamp) + ".tar.gz", 1000 / 25);
-			writer_->addSource(cam->source());
-			writer_->start();
-		} else {
-			button->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
-			if (writer_) {
-				writer_->stop();
-				delete writer_;
-				writer_ = nullptr;
+	virtualCameraRecording_ = std::optional<ftl::gui::Camera*>();
+	sceneRecording_ = std::optional<ftl::Configurable*>();
+
+	auto recordbutton = new PopupButton(this, "", ENTYPO_ICON_CONTROLLER_RECORD);
+	recordbutton->setTooltip("Record");
+	recordbutton->setSide(Popup::Side::Right);
+	recordbutton->setChevronIcon(0);
+	auto recordpopup = recordbutton->popup();
+	recordpopup->setLayout(new GroupLayout());
+	recordpopup->setTheme(screen->toolbuttheme);
+	recordpopup->setAnchorHeight(150);
+	auto itembutton = new Button(recordpopup, "2D snapshot (.png)");
+	itembutton->setCallback([this,recordbutton]() {
+		screen_->activeCamera()->snapshot();
+		recordbutton->setPushed(false);
+	});
+	itembutton = new Button(recordpopup, "Virtual camera recording (.ftl)");
+	itembutton->setCallback([this,recordbutton]() {
+		auto activeCamera = screen_->activeCamera();
+		activeCamera->toggleVideoRecording();
+		recordbutton->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
+		recordbutton->setPushed(false);
+		virtualCameraRecording_ = std::optional<ftl::gui::Camera*>(activeCamera);
+	});
+	itembutton = new Button(recordpopup, "3D scene recording (.ftl)");
+	itembutton->setCallback([this,recordbutton]() {
+		auto tag = screen_->activeCamera()->source()->get<std::string>("uri");
+		if (tag) {
+			auto tagvalue = tag.value();
+			auto configurables = ftl::config::findByTag(tagvalue);
+			if (configurables.size() > 0) {
+				ftl::Configurable *configurable = configurables[0];
+				configurable->set("record", true);
+				recordbutton->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
+				sceneRecording_ = std::optional<ftl::Configurable*>(configurable);
 			}
 		}
-		//if (state) ... start
-		//else ... stop
+		recordbutton->setPushed(false);
+	});
+	itembutton = new Button(recordpopup, "Detailed recording options");
+
+	recordbutton->setCallback([this,recordbutton](){
+		if (virtualCameraRecording_) {
+			virtualCameraRecording_.value()->toggleVideoRecording();
+			recordbutton->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
+
+			// Prevents the popup from being opened, though it is shown while the button
+			// is being pressed.
+			recordbutton->setPushed(false);
+			virtualCameraRecording_ = std::nullopt;
+		} else if (sceneRecording_) {
+			sceneRecording_.value()->set("record", false);
+			recordbutton->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
+			recordbutton->setPushed(false);
+			sceneRecording_ = std::nullopt;
+		}
 	});
 
 	button = new Button(this, "", ENTYPO_ICON_CONTROLLER_STOP);
@@ -67,8 +98,9 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 
 	button = new Button(this, "", ENTYPO_ICON_CONTROLLER_PAUS);
 	button->setCallback([this,button]() {
-		paused_ = !paused_;
-		screen_->control()->pause();
+		//paused_ = !paused_;
+		paused_ = !(bool)ftl::config::get("[reconstruction]/controls/paused");
+		ftl::config::update("[reconstruction]/controls/paused", paused_);
 		if (paused_) {
 			button->setIcon(ENTYPO_ICON_CONTROLLER_PLAY);
 		} else {
@@ -114,11 +146,11 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 	*/
 
 #ifdef HAVE_OPENVR
-	if (this->screen_->hasVR()) {
+	if (this->screen_->isHmdPresent()) {
 		auto button_vr = new Button(this, "VR");
 		button_vr->setFlags(Button::ToggleButton);
 		button_vr->setChangeCallback([this, button_vr](bool state) {
-			if (!screen_->useVR()) {
+			if (!screen_->isVR()) {
 				if (screen_->switchVR(true) == true) {
 					button_vr->setTextColor(nanogui::Color(0.5f,0.5f,1.0f,1.0f));
 					this->button_channels_->setEnabled(false);
diff --git a/applications/gui/src/media_panel.hpp b/applications/gui/src/media_panel.hpp
index 0279cb3fadab41003ceefef538e8f42a20f00139..df0b0802294cbe64800c850faa60fadf4f3de55b 100644
--- a/applications/gui/src/media_panel.hpp
+++ b/applications/gui/src/media_panel.hpp
@@ -1,6 +1,8 @@
 #ifndef _FTL_GUI_MEDIAPANEL_HPP_
 #define _FTL_GUI_MEDIAPANEL_HPP_
 
+#include "camera.hpp"
+
 #include <nanogui/window.h>
 
 namespace ftl {
@@ -30,6 +32,15 @@ class MediaPanel : public nanogui::Window {
 	nanogui::PopupButton *button_channels_;
 	nanogui::Button *right_button_;
 	nanogui::Button *depth_button_;
+
+	/**
+	 * These members indicate which type of recording is active, if any.
+	 * They also include a pointer to an object which is used
+	 * to end the recording. Only one of these members should have a value
+	 * at any given time.
+	 */
+	std::optional<ftl::gui::Camera*> virtualCameraRecording_;
+	std::optional<ftl::Configurable*> sceneRecording_;
 };
 
 }
diff --git a/applications/gui/src/screen.cpp b/applications/gui/src/screen.cpp
index c359e227d9be39ab98df2cd8e816323b6a0ecbef..76665281fc1b9fcbb1b0b45609c509d02f88a69d 100644
--- a/applications/gui/src/screen.cpp
+++ b/applications/gui/src/screen.cpp
@@ -6,7 +6,6 @@
 #include <nanogui/window.h>
 #include <nanogui/layout.h>
 #include <nanogui/imageview.h>
-#include <nanogui/combobox.h>
 #include <nanogui/label.h>
 #include <nanogui/toolbutton.h>
 #include <nanogui/popupbutton.h>
@@ -69,7 +68,6 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 
 	#ifdef HAVE_OPENVR
 	HMD_ = nullptr;
-	has_vr_ = vr::VR_IsHmdPresent();
 	#endif
 
 	zoom_ = root_->value("zoom", 1.0f);
@@ -86,6 +84,8 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 		pos_y_ = root_->value("position_y", 0.0f);
 	});
 
+	shortcuts_ = ftl::create<ftl::Configurable>(root_, "shortcuts");
+
 	setSize(Vector2i(1280,720));
 
 	toolbuttheme = new Theme(*theme());
@@ -229,30 +229,29 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 		popup->setVisible(false);
 	});
 
-	popbutton = new PopupButton(innertool, "", ENTYPO_ICON_COG);
-	popbutton->setIconExtraScale(1.5f);
-	popbutton->setTheme(toolbuttheme);
-	popbutton->setTooltip("Settings");
-	popbutton->setFixedSize(Vector2i(40,40));
-	popbutton->setSide(Popup::Side::Right);
-	popbutton->setChevronIcon(0);
-	// popbutton->setPosition(Vector2i(5,height()-50));
-	popup = popbutton->popup();
-	popup->setLayout(new GroupLayout());
-	popup->setTheme(toolbuttheme);
+	itembutton = new Button(innertool, "", ENTYPO_ICON_COG);
+	itembutton->setIconExtraScale(1.5f);
+	itembutton->setTheme(toolbuttheme);
+	itembutton->setTooltip("Settings");
+	itembutton->setFixedSize(Vector2i(40,40));
+
+	itembutton->setCallback([this]() {
+		auto config_window = new ConfigWindow(this, ctrl_);
+		config_window->setTheme(windowtheme);
+	});
 
+	/*
 	//net_->onConnect([this,popup](ftl::net::Peer *p) {
 	{
 		LOG(INFO) << "NET CONNECT";
 		auto node_details = ctrl_->getSlaves();
-		std::vector<std::string> node_titles;
 
 		for (auto &d : node_details) {
 			LOG(INFO) << "ADDING TITLE: " << d.dump();
 			auto peer = ftl::UUID(d["id"].get<std::string>());
 			auto itembutton = new Button(popup, d["title"].get<std::string>());
 			itembutton->setCallback([this,popup,peer]() {
-				auto config_window = new ConfigWindow(this, ctrl_, peer);
+				auto config_window = new ConfigWindow(this, ctrl_);
 				config_window->setTheme(windowtheme);
 			});
 		}
@@ -264,6 +263,7 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 		auto config_window = new ConfigWindow(this, ctrl_);
 		config_window->setTheme(windowtheme);
 	});
+	*/
 
 	//configwindow_ = new ConfigWindow(parent, ctrl_);
 	cwindow_ = new ftl::gui::ControlWindow(this, controller);
@@ -320,14 +320,14 @@ bool ftl::gui::Screen::initVR() {
 	return true;
 }
 
-bool ftl::gui::Screen::useVR() {
+bool ftl::gui::Screen::isVR() {
 	auto *cam = activeCamera();
 	if (HMD_ == nullptr || cam == nullptr) { return false; }
 	return cam->isVR();
 }
 
 bool ftl::gui::Screen::switchVR(bool on) {
-	if (useVR() == on) { return on; }
+	if (isVR() == on) { return on; }
 
 	if (on && (HMD_ == nullptr) && !initVR()) {
 		return false;
@@ -339,8 +339,13 @@ bool ftl::gui::Screen::switchVR(bool on) {
 		activeCamera()->setVR(false);
 	}
 	
-	return useVR();
+	return isVR();
 }
+
+bool ftl::gui::Screen::isHmdPresent() {
+	return vr::VR_IsHmdPresent();
+}
+
 #endif
 
 ftl::gui::Screen::~Screen() {
@@ -431,12 +436,31 @@ bool ftl::gui::Screen::mouseButtonEvent(const nanogui::Vector2i &p, int button,
 	}
 }
 
+static std::string generateKeyComboStr(int key, int modifiers) {
+	std::string res = "";
+
+	switch(modifiers) {
+	case 1:		res += "Shift+"; break;
+	case 2:		res += "Ctrl+"; break;
+	case 3:		res += "Ctrl+Shift+"; break;
+	case 4:		res += "Alt+"; break;
+	default: break;
+	}
+
+	if (key < 127 && key >= 32) {
+		char buf[2] = { (char)key, 0 };
+		return res + std::string(buf);
+	} else {
+		return "";
+	}
+}
+
 bool ftl::gui::Screen::keyboardEvent(int key, int scancode, int action, int modifiers) {
 	using namespace Eigen;
 	if (nanogui::Screen::keyboardEvent(key, scancode, action, modifiers)) {
 		return true;
 	} else {
-		LOG(INFO) << "Key press " << key << " - " << action << " - " << modifiers;
+		//LOG(INFO) << "Key press " << key << " - " << action << " - " << modifiers;
 
 		if (key >= 262 && key <= 267) {
 			if (camera_) camera_->keyMovement(key, modifiers);
@@ -444,9 +468,42 @@ bool ftl::gui::Screen::keyboardEvent(int key, int scancode, int action, int modi
 		} else if (action == 1 && key == 'H') {
 			swindow_->setVisible(false);
 			cwindow_->setVisible(false);
-		} else if (action == 1 && key == 32) {
-			ctrl_->pause();
-			return true;
+		} else if (action == 1) {
+			std::string combo = generateKeyComboStr(key, modifiers);
+
+			if (combo.size() > 0) {
+				LOG(INFO) << "Key combo = " << combo;
+
+				auto s = shortcuts_->get<nlohmann::json>(combo);
+				if (s) {
+					//LOG(INFO) << "FOUND KEYBOARD SHORTCUT";
+					std::string op = (*s).value("op",std::string("="));
+					std::string uri = (*s).value("uri",std::string(""));
+
+					if (op == "toggle") {
+						auto v = ftl::config::get(uri);
+						if (v.is_boolean()) {
+							ftl::config::update(uri, !v.get<bool>());
+						}
+					} else if (op == "+=") {
+						auto v = ftl::config::get(uri);
+						if (v.is_number_float()) {
+							ftl::config::update(uri, v.get<float>() + (*s).value("value",0.0f));
+						} else if (v.is_number_integer()) {
+							ftl::config::update(uri, v.get<int>() + (*s).value("value",0));
+						}
+					} else if (op == "-=") {
+						auto v = ftl::config::get(uri);
+						if (v.is_number_float()) {
+							ftl::config::update(uri, v.get<float>() - (*s).value("value",0.0f));
+						} else if (v.is_number_integer()) {
+							ftl::config::update(uri, v.get<int>() - (*s).value("value",0));
+						}
+					} else if (op == "=") {
+						ftl::config::update(uri, (*s)["value"]);
+					}
+				}
+			}
 		}
 		return false;
 	}
@@ -467,7 +524,7 @@ void ftl::gui::Screen::draw(NVGcontext *ctx) {
 		if (camera_->getChannel() != ftl::codecs::Channel::Left) { mImageID = rightEye_; }
 
 		#ifdef HAVE_OPENVR
-		if (useVR() && imageSize[0] > 0 && camera_->getLeft().isValid() && camera_->getRight().isValid()) {
+		if (isVR() && 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 );
diff --git a/applications/gui/src/screen.hpp b/applications/gui/src/screen.hpp
index a195f4ccdd45500c534039709ba6762fdd640b22..90cd519bb3681126f4dc4f0d258e342953562433 100644
--- a/applications/gui/src/screen.hpp
+++ b/applications/gui/src/screen.hpp
@@ -48,19 +48,18 @@ class Screen : public nanogui::Screen {
 	// initialize OpenVR
 	bool initVR();
 
-	// is VR available (HMD was found at initialization)
-	bool hasVR() const { return has_vr_; }
-
 	// is VR mode on/off
-	bool useVR();
+	bool isVR();
 
 	// toggle VR on/off
 	bool switchVR(bool mode);
 
+	bool isHmdPresent();
+
 	vr::IVRSystem* getVR() { return HMD_; }
 
 #else
-	bool hasVR() const { return false; }
+	bool isVR() { return false; }
 #endif
 
 	nanogui::Theme *windowtheme;
@@ -102,8 +101,9 @@ class Screen : public nanogui::Screen {
 
 	bool show_two_images_ = false;
 
+	ftl::Configurable *shortcuts_;
+
 	#ifdef HAVE_OPENVR
-	bool has_vr_;
 	vr::IVRSystem *HMD_;
 	#endif
 };
diff --git a/applications/gui/src/src_window.cpp b/applications/gui/src/src_window.cpp
index 8e087d1dd42e25b00afd0efaf7b5dd22d5139a0d..0126810a511b85a1168fe06c638651e533f11a0d 100644
--- a/applications/gui/src/src_window.cpp
+++ b/applications/gui/src/src_window.cpp
@@ -63,7 +63,7 @@ SourceWindow::SourceWindow(ftl::gui::Screen *screen)
 		UNIQUE_LOCK(mutex_, lk);
 		_updateCameras(screen_->net()->findAll<string>("list_streams"));
 	});
-	
+
 	UNIQUE_LOCK(mutex_, lk);
 
 	std::vector<ftl::rgbd::Source*> srcs = ftl::createArray<ftl::rgbd::Source>(screen_->root(), "sources", screen_->net());
@@ -74,6 +74,14 @@ SourceWindow::SourceWindow(ftl::gui::Screen *screen)
 	_updateCameras(screen_->control()->getNet()->findAll<string>("list_streams"));
 }
 
+std::vector<ftl::gui::Camera*> SourceWindow::getCameras() {
+	auto cameras = std::vector<ftl::gui::Camera*>(cameras_.size());
+	for (const auto &kv : cameras_) {
+		cameras.push_back(kv.second);
+	}
+	return cameras;
+}
+
 void SourceWindow::_updateCameras(const vector<string> &netcams) {
 	for (auto s : netcams) {
 		if (cameras_.find(s) == cameras_.end()) {
diff --git a/applications/gui/src/src_window.hpp b/applications/gui/src/src_window.hpp
index b2fe8a9e0957f3345a719a0c37bac46bf473089c..dab3f5d4301de73d0dda2bbd32b321c3f796d160 100644
--- a/applications/gui/src/src_window.hpp
+++ b/applications/gui/src/src_window.hpp
@@ -25,7 +25,7 @@ class SourceWindow : public nanogui::Window {
 	explicit SourceWindow(ftl::gui::Screen *screen);
 	~SourceWindow();
 
-	const std::vector<ftl::gui::Camera*> &getCameras();
+	std::vector<ftl::gui::Camera*> getCameras();
 
 	virtual void draw(NVGcontext *ctx);
 
diff --git a/applications/merger/CMakeLists.txt b/applications/merger/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ccc625afb0241966628bf8bffa790cf8a041afa7
--- /dev/null
+++ b/applications/merger/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(FTLMERGER
+	src/main.cpp
+)
+
+add_executable(ftl-merge ${FTLMERGER})
+
+target_include_directories(ftl-merge PRIVATE src)
+
+target_link_libraries(ftl-merge ftlcommon ftlcodecs ftlrgbd Threads::Threads ${OpenCV_LIBS})
+
+
diff --git a/applications/merger/src/main.cpp b/applications/merger/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d7b3f2b92d5976082f1995420cc6486061b714b2
--- /dev/null
+++ b/applications/merger/src/main.cpp
@@ -0,0 +1,104 @@
+#include <loguru.hpp>
+#include <ftl/configuration.hpp>
+#include <ftl/codecs/reader.hpp>
+#include <ftl/codecs/writer.hpp>
+#include <ftl/codecs/packet.hpp>
+#include <ftl/rgbd/camera.hpp>
+#include <ftl/codecs/hevc.hpp>
+
+#include <fstream>
+
+int main(int argc, char **argv) {
+	auto root = ftl::configure(argc, argv, "merger_default");
+
+	std::string outputfile = root->value("out", std::string("output.ftl"));
+	std::vector<std::string> paths = *root->get<std::vector<std::string>>("paths");
+	int timeoff = int(root->value("offset", 0.0f) * 1000.0f);
+	int stream_mask1 = root->value("mask1",0xFF);
+	int stream_mask2 = root->value("mask2",0xFF);
+
+	if (paths.size() == 0) {
+		LOG(ERROR) << "Missing input ftl file(s).";
+		return -1;
+	}
+
+	// Generate the output writer...
+	std::ofstream of;
+	of.open(outputfile);
+	if (!of.is_open()) {
+		LOG(ERROR) << "Could not open output file: " << outputfile;
+		return -1;
+	}
+
+	ftl::codecs::Writer out(of);
+	out.begin();
+
+	std::vector<std::ifstream> fs;
+	std::vector<ftl::codecs::Reader*> rs;
+	fs.resize(paths.size());
+	rs.resize(paths.size());
+
+	for (size_t i=0; i<paths.size(); ++i) {
+		fs[i].open(paths[i]);
+		if (!fs[i].is_open()) {
+			LOG(ERROR) << "Could not open file: " << paths[i];
+			return -1;
+		}
+
+		LOG(INFO) << "Opening("<< i <<"): " << paths[i];
+
+		rs[i] = new ftl::codecs::Reader(fs[i]);
+		if (!rs[i]->begin()) {
+			LOG(ERROR) << "Bad ftl file format";
+			return -1;
+		}
+	}
+
+	std::map<int,int> idmap;
+	int lastid = 0;
+
+	bool res = rs[0]->read(90000000000000, [&rs,&out,&idmap,&lastid,stream_mask1,stream_mask2,timeoff](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+		if (((0x1 << spkt.streamID) & stream_mask1) == 0) return;
+
+		ftl::codecs::StreamPacket spkt2 = spkt;
+		if (idmap.find(spkt.streamID) == idmap.end()) {
+			idmap[spkt.streamID] = lastid++;
+		}
+		spkt2.streamID = idmap[spkt.streamID];
+
+		// Now read all other sources up to the same packet timestamp.
+		out.write(spkt2, pkt);
+
+		for (size_t j=1; j<rs.size(); ++j) {
+			ftl::codecs::Reader *r = rs[j];
+
+			// FIXME: Need to truncate other stream if the following returns
+			// no frames, meaning the timeshift causes this stream to run out
+			// before the main stream.
+			rs[j]->read(spkt.timestamp+timeoff+1, [&out,&idmap,&lastid,j,r,stream_mask2,timeoff](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+				if (((0x1 << spkt.streamID) & stream_mask2) == 0) return;
+				if (int(spkt.channel) < 32 && spkt.timestamp < r->getStartTime()+timeoff) return;
+
+				ftl::codecs::StreamPacket spkt2 = spkt;
+				if (idmap.find(spkt.streamID + (j << 16)) == idmap.end()) {
+					idmap[spkt.streamID+(j << 16)] = lastid++;
+				}
+				spkt2.streamID = idmap[spkt.streamID + (j << 16)];
+				spkt2.timestamp -= timeoff;
+
+				out.write(spkt2, pkt);
+			});
+		}
+	});
+
+	out.end();
+	of.close();
+
+	for (size_t i=0; i<rs.size(); ++i) {
+		rs[i]->end();
+		delete rs[i];
+		fs[i].close();
+	}
+
+	return 0;
+}
\ No newline at end of file
diff --git a/applications/reconstruct/CMakeLists.txt b/applications/reconstruct/CMakeLists.txt
index b089d4a1aeae9654b50865c7400ca0e381f874e6..5dc0ef5dbce517fb88cdf0fb0cc9fdc7b84a778a 100644
--- a/applications/reconstruct/CMakeLists.txt
+++ b/applications/reconstruct/CMakeLists.txt
@@ -20,6 +20,7 @@ set(REPSRC
 	src/ilw/fill.cu
 	src/ilw/discontinuity.cu
 	src/ilw/correspondence.cu
+	src/reconstruction.cpp
 )
 
 add_executable(ftl-reconstruct ${REPSRC})
diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp
index 0603dad4028d69223b0ce5ddc31d36eafc900c14..4b802d49f85ebe618fd27ecbfe48b46b2f40b5b1 100644
--- a/applications/reconstruct/src/main.cpp
+++ b/applications/reconstruct/src/main.cpp
@@ -18,6 +18,8 @@
 #include <ftl/codecs/writer.hpp>
 #include <ftl/codecs/reader.hpp>
 
+#include "reconstruction.hpp"
+
 #include "ilw/ilw.hpp"
 #include <ftl/render/tri_render.hpp>
 
@@ -136,7 +138,16 @@ static void run(ftl::Configurable *root) {
 	net->start();
 	net->waitConnections();
 	
-	// Check paths for an FTL file to load...
+	std::vector<int> sourcecounts;
+
+	// Add sources from the configuration file as a single group.
+	auto configuration_sources = root->getConfig()["sources"];
+	size_t configuration_size = configuration_sources.size();
+	if (configuration_size > 0) {
+		sourcecounts.push_back(configuration_size);
+	}
+
+	// Check paths for FTL files to load.
 	auto paths = (*root->get<nlohmann::json>("paths"));
 	for (auto &x : paths.items()) {
 		std::string path = x.value().get<std::string>();
@@ -162,12 +173,14 @@ static void run(ftl::Configurable *root) {
 			int N = root->value("N", 100);
 
 			// For each stream found, add a source object
-			for (int i=0; i<=min(max_stream,N-1); ++i) {
+			int count = min(max_stream+1, N);
+			for (int i=0; i<count; ++i) {
 				root->getConfig()["sources"].push_back(nlohmann::json{{"uri",std::string("file://") + path + std::string("#") + std::to_string(i)}});
 			}
+			sourcecounts.push_back(count);
 		}
 	}
-	
+
 	// Create a vector of all input RGB-Depth sources
 	auto sources = ftl::createArray<Source>(root, "sources", net);
 
@@ -175,7 +188,7 @@ static void run(ftl::Configurable *root) {
 		LOG(ERROR) << "No sources configured!";
 		return;
 	}
-	
+
 	ConfigProxy *configproxy = nullptr;
 	if (net->numberOfPeers() > 0) {
 		configproxy = new ConfigProxy(net); // TODO delete
@@ -187,26 +200,6 @@ static void run(ftl::Configurable *root) {
 		configproxy->add(disparity, "source/disparity/cross", "cross");
 	}
 
-	// Create scene transform, intended for axis aligning the walls and floor
-	Eigen::Matrix4d transform;
-	if (root->getConfig()["transform"].is_object()) {
-		auto &c = root->getConfig()["transform"];
-		float rx = c.value("pitch", 0.0f);
-		float ry = c.value("yaw", 0.0f);
-		float rz = c.value("roll", 0.0f);
-		float x = c.value("x", 0.0f);
-		float y = c.value("y", 0.0f);
-		float z = c.value("z", 0.0f);
-
-		Eigen::Affine3d r = create_rotation_matrix(rx, ry, rz);
-		Eigen::Translation3d trans(Eigen::Vector3d(x,y,z));
-		Eigen::Affine3d t(trans);
-		transform = t.matrix() * r.matrix();
-		LOG(INFO) << "Set transform: " << transform;
-	} else {
-		transform.setIdentity();
-	}
-
 	// Must find pose for each source...
 	if (sources.size() > 1) {
 		std::map<std::string, Eigen::Matrix4d> transformations;
@@ -222,73 +215,73 @@ static void run(ftl::Configurable *root) {
 			string uri = input->getURI();
 			auto T = transformations.find(uri);
 			if (T == transformations.end()) {
-				LOG(ERROR) << "Camera pose for " + uri + " not found in transformations";
+				LOG(WARNING) << "Camera pose for " + uri + " not found in transformations";
 				//LOG(WARNING) << "Using only first configured source";
 				// TODO: use target source if configured and found
 				//sources = { sources[0] };
 				//sources[0]->setPose(Eigen::Matrix4d::Identity());
 				//break;
-				input->setPose(transform * input->getPose());
+				input->setPose(input->getPose());
 				continue;
 			}
-			input->setPose(transform * T->second);
+			input->setPose(T->second);
 		}
 	}
 
-	ftl::rgbd::FrameSet scene_A;  // Output of align process
-	ftl::rgbd::FrameSet scene_B;  // Input of render process
+	ftl::rgbd::FrameSet fs_out;
 
 	//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::VirtualSource *virt = ftl::create<ftl::rgbd::VirtualSource>(root, "virtual");
-	ftl::render::Triangular *splat = ftl::create<ftl::render::Triangular>(root, "renderer", &scene_B);
-	ftl::rgbd::Group *group = new ftl::rgbd::Group;
-	ftl::ILW *align = ftl::create<ftl::ILW>(root, "merge");
+	ftl::rgbd::VirtualSource *vs = ftl::create<ftl::rgbd::VirtualSource>(root, "virtual");
+	//root->set("tags", nlohmann::json::array({ root->getID()+"/virtual" }));
 
 	int o = root->value("origin_pose", 0) % sources.size();
-	virt->setPose(sources[o]->getPose());
+	vs->setPose(sources[o]->getPose());
+
+	vector<ftl::Reconstruction*> groups;
+
+	size_t cumulative = 0;
+	for (auto c : sourcecounts) {
+		std::string id = std::to_string(cumulative);
+		auto reconstr = ftl::create<ftl::Reconstruction>(root, id, id);
+		for (size_t i=cumulative; i<cumulative+c; i++) {
+			reconstr->addSource(sources[i]);
+		}
+		groups.push_back(reconstr);
+		cumulative += c;
+	}
 
 	auto *renderpipe = ftl::config::create<ftl::operators::Graph>(root, "render_pipe");
 	renderpipe->append<ftl::operators::ColourChannels>("colour");  // Generate interpolation texture...
 	renderpipe->append<ftl::operators::FXAA>("antialiasing"); 
 
-	// Generate virtual camera render when requested by streamer
-	virt->onRender([splat,virt,&scene_B,align,renderpipe](ftl::rgbd::Frame &out) {
-		//virt->setTimestamp(scene_B.timestamp);
-		// Do we need to convert Lab to BGR?
-		if (align->isLabColour()) {
-			for (auto &f : scene_B.frames) {
-				auto &col = f.get<cv::cuda::GpuMat>(Channel::Colour);
-				cv::cuda::cvtColor(col,col, cv::COLOR_Lab2BGR); // TODO: Add stream
-			}
+	vs->onRender([vs, &groups, &renderpipe](ftl::rgbd::Frame &out) {
+		for (auto &reconstr : groups) {
+			reconstr->render(vs, out);
 		}
-		splat->render(virt, out);
-		renderpipe->apply(out, out, virt, 0);
+		renderpipe->apply(out, out, vs, 0);
 	});
-	stream->add(virt);
-
-	for (size_t i=0; i<sources.size(); i++) {
-		Source *in = sources[i];
-		in->setChannel(Channel::Depth);
-		group->addSource(in);
-	}
+	stream->add(vs);
 
 	// ---- Recording code -----------------------------------------------------
-
 	std::ofstream fileout;
 	ftl::codecs::Writer writer(fileout);
-	auto recorder = [&writer,&group](ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
-		ftl::codecs::StreamPacket s = spkt;
-
-		// Patch stream ID to match order in group
-		s.streamID = group->streamID(src);
-		writer.write(s, pkt);
-	};
 
 	root->set("record", false);
 
+	// Add a recording callback to all reconstruction scenes
+	for (size_t i=0; i<sources.size(); ++i) {
+		sources[i]->addRawCallback([&writer,&groups,i](ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+			ftl::codecs::StreamPacket s = spkt;
+
+			// Patch stream ID to match order in group
+			s.streamID = i;
+			writer.write(s, pkt);
+		});
+	}
+
 	// Allow stream recording
-	root->on("record", [&group,&fileout,&writer,&recorder](const ftl::config::Event &e) {
+	root->on("record", [&groups,&fileout,&writer,&sources](const ftl::config::Event &e) {
 		if (e.entity->value("record", false)) {
 			char timestamp[18];
 			std::time_t t=std::time(NULL);
@@ -296,17 +289,15 @@ static void run(ftl::Configurable *root) {
 			fileout.open(std::string(timestamp) + ".ftl");
 
 			writer.begin();
-			group->addRawCallback(std::function(recorder));
 
 			// TODO: Write pose+calibration+config packets
-			auto sources = group->sources();
+
 			for (size_t i=0; i<sources.size(); ++i) {
 				//writeSourceProperties(writer, i, sources[i]);
 				sources[i]->inject(Channel::Calibration, sources[i]->parameters(), Channel::Left, sources[i]->getCapabilities());
 				sources[i]->inject(sources[i]->getPose()); 
 			}
 		} else {
-			group->removeRawCallback(recorder);
 			writer.end();
 			fileout.close();
 		}
@@ -318,62 +309,6 @@ static void run(ftl::Configurable *root) {
 	//stream->add(group);
 	stream->run();
 
-	bool busy = false;
-
-	// Create the source depth map pipeline
-	auto *pipeline1 = ftl::config::create<ftl::operators::Graph>(root, "pre_filters");
-	pipeline1->append<ftl::operators::ClipScene>("clipping");
-	pipeline1->append<ftl::operators::ColourChannels>("colour");  // Convert BGR to BGRA
-	//pipeline1->append<ftl::operators::HFSmoother>("hfnoise");  // Remove high-frequency noise
-	pipeline1->append<ftl::operators::Normals>("normals");  // Estimate surface normals
-	//pipeline1->append<ftl::operators::SmoothChannel>("smoothing");  // Generate a smoothing channel
-	//pipeline1->append<ftl::operators::ScanFieldFill>("filling");  // Generate a smoothing channel
-	pipeline1->append<ftl::operators::CrossSupport>("cross");
-	pipeline1->append<ftl::operators::DiscontinuityMask>("discontinuity");
-	pipeline1->append<ftl::operators::CullDiscontinuity>("remove_discontinuity");
-	//pipeline1->append<ftl::operators::AggreMLS>("mls");  // Perform MLS (using smoothing channel)
-	pipeline1->append<ftl::operators::VisCrossSupport>("viscross")->set("enabled", false);
-	pipeline1->append<ftl::operators::MultiViewMLS>("mvmls");
-	// Alignment
-
-
-	group->setLatency(4);
-	group->setName("ReconGroup");
-	group->sync([splat,virt,&busy,&slave,&scene_A,&scene_B,&align,controls,pipeline1](ftl::rgbd::FrameSet &fs) -> bool {
-		//cudaSetDevice(scene->getCUDADevice());
-
-		//if (slave.isPaused()) return true;
-		if (controls->value("paused", false)) return true;
-		
-		if (busy) {
-			LOG(INFO) << "Group frameset dropped: " << fs.timestamp;
-			return true;
-		}
-		busy = true;
-
-		// Swap the entire frameset to allow rapid return
-		fs.swapTo(scene_A);
-
-		ftl::pool.push([&scene_B,&scene_A,&busy,&slave,&align, pipeline1](int id) {
-			//cudaSetDevice(scene->getCUDADevice());
-			// TODO: Release frameset here...
-			//cudaSafeCall(cudaStreamSynchronize(scene->getIntegrationStream()));
-
-			UNIQUE_LOCK(scene_A.mtx, lk);
-
-			pipeline1->apply(scene_A, scene_A, 0);
-			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;
-	});
-
 	LOG(INFO) << "Start timer";
 	ftl::timer::start(true);
 
@@ -387,12 +322,12 @@ static void run(ftl::Configurable *root) {
 
 	LOG(INFO) << "Deleting...";
 
-	delete align;
-	delete splat;
 	delete stream;
-	delete virt;
+	delete vs;
 	delete net;
-	delete group;
+	for (auto g : groups) {
+		delete g;
+	}
 
 	ftl::config::cleanup();  // Remove any last configurable objects.
 	LOG(INFO) << "Done.";
diff --git a/applications/reconstruct/src/reconstruction.cpp b/applications/reconstruct/src/reconstruction.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..be99d1581c3555b1d52aab156bb9ed316d559de6
--- /dev/null
+++ b/applications/reconstruct/src/reconstruction.cpp
@@ -0,0 +1,112 @@
+#include "reconstruction.hpp"
+
+#include "ftl/operators/smoothing.hpp"
+#include "ftl/operators/colours.hpp"
+#include "ftl/operators/normals.hpp"
+#include "ftl/operators/filling.hpp"
+#include "ftl/operators/segmentation.hpp"
+#include "ftl/operators/mask.hpp"
+#include "ftl/operators/antialiasing.hpp"
+#include "ftl/operators/mvmls.hpp"
+#include "ftl/operators/clipping.hpp"
+
+using ftl::Reconstruction;
+using ftl::codecs::Channel;
+
+static Eigen::Affine3d create_rotation_matrix(float ax, float ay, float az) {
+	Eigen::Affine3d rx =
+		Eigen::Affine3d(Eigen::AngleAxisd(ax, Eigen::Vector3d(1, 0, 0)));
+	Eigen::Affine3d ry =
+		Eigen::Affine3d(Eigen::AngleAxisd(ay, Eigen::Vector3d(0, 1, 0)));
+	Eigen::Affine3d rz =
+		Eigen::Affine3d(Eigen::AngleAxisd(az, Eigen::Vector3d(0, 0, 1)));
+	return rz * rx * ry;
+}
+
+Reconstruction::Reconstruction(nlohmann::json &config, const std::string name) :
+	ftl::Configurable(config), busy_(false), fs_render_(), fs_align_() {
+	group_ = new ftl::rgbd::Group;
+	group_->setName("ReconGroup-" + name);
+	group_->setLatency(4);
+
+	renderer_ = ftl::create<ftl::render::Triangular>(this, "renderer", &fs_render_);
+
+	pipeline_ = ftl::config::create<ftl::operators::Graph>(this, "pre_filters");
+	pipeline_->append<ftl::operators::ClipScene>("clipping")->set("enabled", false);
+	pipeline_->append<ftl::operators::ColourChannels>("colour");  // Convert BGR to BGRA
+	//pipeline_->append<ftl::operators::HFSmoother>("hfnoise");  // Remove high-frequency noise
+	pipeline_->append<ftl::operators::Normals>("normals");  // Estimate surface normals
+	//pipeline_->append<ftl::operators::SmoothChannel>("smoothing");  // Generate a smoothing channel
+	//pipeline_->append<ftl::operators::ScanFieldFill>("filling");  // Generate a smoothing channel
+	pipeline_->append<ftl::operators::CrossSupport>("cross");
+	pipeline_->append<ftl::operators::DiscontinuityMask>("discontinuity");
+	pipeline_->append<ftl::operators::CrossSupport>("cross2")->set("discon_support", true);
+	pipeline_->append<ftl::operators::CullDiscontinuity>("remove_discontinuity");
+	//pipeline_->append<ftl::operators::AggreMLS>("mls");  // Perform MLS (using smoothing channel)
+	pipeline_->append<ftl::operators::VisCrossSupport>("viscross")->set("enabled", false);
+	pipeline_->append<ftl::operators::MultiViewMLS>("mvmls");
+
+	group_->sync([this](ftl::rgbd::FrameSet &fs) -> bool {
+		// TODO: pause
+		
+		if (busy_) {
+			LOG(INFO) << "Group frameset dropped: " << fs.timestamp;
+			return true;
+		}
+		busy_ = true;
+
+		// Swap the entire frameset to allow rapid return
+		fs.swapTo(fs_align_);
+
+		ftl::pool.push([this](int id) {
+			UNIQUE_LOCK(fs_align_.mtx, lk);
+			pipeline_->apply(fs_align_, fs_align_, 0);
+			
+			// TODO: To use second GPU, could do a download, swap, device change,
+			// then upload to other device. Or some direct device-2-device copy.
+			fs_align_.swapTo(fs_render_);
+
+			LOG(INFO) << "Align complete... " << fs_align_.timestamp;
+			busy_ = false;
+		});
+		return true;
+	});
+}
+
+Reconstruction::~Reconstruction() {
+	// TODO delete
+}
+
+void Reconstruction::addSource(ftl::rgbd::Source *src) {
+	src->setChannel(Channel::Depth);
+	group_->addSource(src); // TODO: check if source is already in group?
+}
+
+void Reconstruction::addRawCallback(const std::function<void(ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &cb) {
+	group_->addRawCallback(cb);
+}
+
+void Reconstruction::render(ftl::rgbd::VirtualSource *vs, ftl::rgbd::Frame &out) {
+	// Create scene transform, intended for axis aligning the walls and floor
+	Eigen::Matrix4d transform;
+	//if (getConfig()["transform"].is_object()) {
+		//auto &c = getConfig()["transform"];
+		float rx = value("transform_pitch", 0.0f);
+		float ry = value("transform_yaw", 0.0f);
+		float rz = value("transform_roll", 0.0f);
+		float x = value("transform_x", 0.0f);
+		float y = value("transform_y", 0.0f);
+		float z = value("transform_z", 0.0f);
+
+		Eigen::Affine3d r = create_rotation_matrix(rx, ry, rz);
+		Eigen::Translation3d trans(Eigen::Vector3d(x,y,z));
+		Eigen::Affine3d t(trans);
+		transform = t.matrix() * r.matrix();
+		//LOG(INFO) << "Set transform: " << transform;
+	//} else {
+	//	transform.setIdentity();
+	//}
+
+	Eigen::Affine3d sm = Eigen::Affine3d(Eigen::Scaling(double(value("scale", 1.0f))));
+	renderer_->render(vs, out, sm.matrix() * transform);
+}
\ No newline at end of file
diff --git a/applications/reconstruct/src/reconstruction.hpp b/applications/reconstruct/src/reconstruction.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6546f85c163142d4b1cbdf9f84081ca21d703907
--- /dev/null
+++ b/applications/reconstruct/src/reconstruction.hpp
@@ -0,0 +1,39 @@
+#ifndef _FTL_RECONSTRUCTION_HPP_
+#define _FTL_RECONSTRUCTION_HPP_
+
+#include "ftl/configurable.hpp"
+#include "ftl/rgbd/source.hpp"
+#include "ftl/rgbd/frame.hpp"
+#include "ftl/rgbd/group.hpp"
+#include "ftl/rgbd/frameset.hpp"
+#include "ftl/operators/operator.hpp"
+#include "ftl/render/tri_render.hpp"
+
+namespace ftl {
+
+class Reconstruction : public ftl::Configurable {
+	public:
+	Reconstruction(nlohmann::json &config, const std::string name);
+	~Reconstruction();
+
+	void addSource(ftl::rgbd::Source *);
+
+	void addRawCallback(const std::function<void(ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &cb);
+
+	/**
+	 * Do the render for a specified virtual camera.
+	 */
+	void render(ftl::rgbd::VirtualSource *vs, ftl::rgbd::Frame &out);
+
+	private:
+	bool busy_;
+	ftl::rgbd::FrameSet fs_render_;
+	ftl::rgbd::FrameSet fs_align_;
+	ftl::rgbd::Group *group_;
+	ftl::operators::Graph *pipeline_;
+	ftl::render::Triangular *renderer_;
+};
+
+}
+
+#endif  // _FTL_RECONSTRUCTION_HPP_
diff --git a/components/common/cpp/include/ftl/configurable.hpp b/components/common/cpp/include/ftl/configurable.hpp
index 219bd171edf4b7997cc35f97fa7787a0c4bb4559..61dca8ea3b883e97f2e26c2f1a5126c92f8aa520 100644
--- a/components/common/cpp/include/ftl/configurable.hpp
+++ b/components/common/cpp/include/ftl/configurable.hpp
@@ -101,6 +101,12 @@ class Configurable {
 
 	void patchPtr(nlohmann::json &newcfg) { config_ = &newcfg; }
 
+	/**
+	 * Allow configurables to refresh their internal state, perhaps from a
+	 * remote source.
+	 */
+	virtual void refresh();
+
 	protected:
 	nlohmann::json *config_;
 
diff --git a/components/common/cpp/include/ftl/configuration.hpp b/components/common/cpp/include/ftl/configuration.hpp
index 94fcc7dfa6b57e9bfd59fbcf36c5be80e0b82638..18aaf89f9433911dd71ec3d3519ff2125317e78e 100644
--- a/components/common/cpp/include/ftl/configuration.hpp
+++ b/components/common/cpp/include/ftl/configuration.hpp
@@ -47,6 +47,8 @@ void removeConfigurable(Configurable *cfg);
  */
 bool update(const std::string &puri, const json_t &value);
 
+json_t &get(const std::string &puri);
+
 /**
  * Resolve a JSON schema reference, but do not wait for a remote reference
  * if it is not available. A null entity is returned if not resolved.
@@ -72,6 +74,13 @@ json_t &resolveWait(const std::string &);
  */
 Configurable *find(const std::string &uri);
 
+/**
+ * Get all configurables that contain a specified tag. Tags are given under the
+ * "tags" property as an array of strings, but only during configurable
+ * construction.
+ */
+const std::vector<Configurable *> &findByTag(const std::string &tag);
+
 std::vector<std::string> list();
 
 /**
diff --git a/components/common/cpp/src/configurable.cpp b/components/common/cpp/src/configurable.cpp
index 5116292adab92672da3adb239ecb2ed3f4630011..8186713b11025f1799afbf26ca8f56532f1deb3f 100644
--- a/components/common/cpp/src/configurable.cpp
+++ b/components/common/cpp/src/configurable.cpp
@@ -59,4 +59,8 @@ void Configurable::on(const string &prop, function<void(const ftl::config::Event
 	} else {
 		(*ix).second.push_back(f);
 	}
-}
\ No newline at end of file
+}
+
+void Configurable::refresh() {
+	// Do nothing by default
+}
diff --git a/components/common/cpp/src/configuration.cpp b/components/common/cpp/src/configuration.cpp
index 89d539cbec5270b7d7a5bc57a0a113eb57456553..f8c982a811652252f14205b00e7f778d906ab8ed 100644
--- a/components/common/cpp/src/configuration.cpp
+++ b/components/common/cpp/src/configuration.cpp
@@ -165,6 +165,7 @@ static bool mergeConfig(const string path) {
 
 static std::map<std::string, json_t*> config_index;
 static std::map<std::string, ftl::Configurable*> config_instance;
+static std::map<std::string, std::vector<ftl::Configurable*>> tag_index;
 
 /*
  * Recursively URI index the JSON structure.
@@ -193,11 +194,16 @@ ftl::Configurable *ftl::config::find(const std::string &uri) {
 			actual_uri = rootCFG->getID() + uri;
 		}
 	}
+	
 	auto ix = config_instance.find(actual_uri);
 	if (ix == config_instance.end()) return nullptr;
 	else return (*ix).second;
 }
 
+const std::vector<Configurable*> &ftl::config::findByTag(const std::string &tag) {
+	return tag_index[tag];
+}
+
 std::vector<std::string> ftl::config::list() {
 	vector<string> r;
 	for (auto i : config_instance) {
@@ -219,27 +225,60 @@ void ftl::config::registerConfigurable(ftl::Configurable *cfg) {
 	} else {
 		config_instance[*uri] = cfg;
 		LOG(INFO) << "Registering instance: " << *uri;
+
+		auto tags = cfg->get<vector<string>>("tags");
+		if (tags) {
+			for (auto &t : *tags) {
+				//LOG(INFO) << "REGISTER TAG: " << t;
+				tag_index[t].push_back(cfg);
+			}
+		}
 	}
 }
 
 json_t null_json;
 
+/* To allow for custom tag format */
+static std::string preprocessURI(const std::string &uri) {
+	if (uri[0] == '[') {
+		size_t closing = uri.find_last_of(']');
+		string tags = uri.substr(1, closing-1);
+
+		// TODO: Allow for multiple tags
+
+		const auto &cfgs = ftl::config::findByTag(tags);
+
+		// FIXME: Check for more than one tag result
+		if (cfgs.size() > 0) {
+			//LOG(INFO) << "PREPROC URI " << cfgs[0]->getID() + uri.substr(closing+1);
+			return cfgs[0]->getID() + uri.substr(closing+1);
+		} else {
+			return uri;
+		}
+	} else if (uri[0] == '/') {
+		return rootCFG->getID() + uri;
+	} else {
+		return uri;
+	}
+}
+
 bool ftl::config::update(const std::string &puri, const json_t &value) {
 	// Remove last component of URI
 	string tail = "";
 	string head = "";
-	size_t last_hash = puri.find_last_of('#');
+	string uri = preprocessURI(puri);
+	size_t last_hash = uri.find_last_of('#');
 	if (last_hash != string::npos) {
-		size_t last = puri.find_last_of('/');
+		size_t last = uri.find_last_of('/');
 		if (last != string::npos && last > last_hash) {
-			tail = puri.substr(last+1);
-			head = puri.substr(0, last);
+			tail = uri.substr(last+1);
+			head = uri.substr(0, last);
 		} else {
-			tail = puri.substr(last_hash+1);
-			head = puri.substr(0, last_hash);
+			tail = uri.substr(last_hash+1);
+			head = uri.substr(0, last_hash);
 		}
 	} else {
-		LOG(WARNING) << "Expected a # in an update URI: " << puri;
+		LOG(WARNING) << "Expected a # in an update URI: " << uri;
 		return false;
 	}
 
@@ -272,6 +311,35 @@ bool ftl::config::update(const std::string &puri, const json_t &value) {
 	}
 }
 
+json_t &ftl::config::get(const std::string &puri) {
+	// Remove last component of URI
+	string tail = "";
+	string head = "";
+	string uri = preprocessURI(puri);
+	size_t last_hash = uri.find_last_of('#');
+	if (last_hash != string::npos) {
+		size_t last = uri.find_last_of('/');
+		if (last != string::npos && last > last_hash) {
+			tail = uri.substr(last+1);
+			head = uri.substr(0, last);
+		} else {
+			tail = uri.substr(last_hash+1);
+			head = uri.substr(0, last_hash);
+		}
+	} else {
+		LOG(WARNING) << "Expected a # in an update URI: " << uri;
+		return null_json;
+	}
+
+	Configurable *cfg = find(head);
+
+	if (cfg) {
+		return cfg->getConfig()[tail];
+	} else {
+		return null_json;
+	}
+}
+
 json_t &ftl::config::resolve(const std::string &puri, bool eager) {
 	string uri_str = puri;
 
@@ -513,11 +581,12 @@ Configurable *ftl::config::configure(int argc, char **argv, const std::string &r
 	// Process Arguments
 	auto options = ftl::config::read_options(&argv, &argc);
 	
-	vector<string> paths;
+	vector<string> paths(argc);
 	while (argc-- > 0) {
 		paths.push_back(argv[0]);
+		argv++;
 	}
-	
+
 	if (!findConfiguration(options["config"], paths)) {
 		LOG(FATAL) << "Could not find any configuration!";
 	}
diff --git a/components/net/cpp/include/ftl/net_configurable.hpp b/components/net/cpp/include/ftl/net_configurable.hpp
index bdd21c4700e7664cff996672585054c30b6e8e6a..2c6495410321c03b62e18a83f214d00b807bccdb 100644
--- a/components/net/cpp/include/ftl/net_configurable.hpp
+++ b/components/net/cpp/include/ftl/net_configurable.hpp
@@ -12,12 +12,14 @@ namespace ftl {
 	NetConfigurable(ftl::UUID peer, const std::string &suri, ftl::ctrl::Master &ctrl, ftl::config::json_t &config);
 	~NetConfigurable();
 
+	void refresh() override;
+
 	protected:
 	void inject(const std::string &name, nlohmann::json &value);
 
     private:
 	ftl::UUID peer;
-	const std::string &suri;
+	const std::string suri;
 	ftl::ctrl::Master &ctrl;
     };
 
diff --git a/components/net/cpp/src/net_configurable.cpp b/components/net/cpp/src/net_configurable.cpp
index be98cf7edbb221c61e9732a9e59d8546f1185644..cf597c5c77eec05205b6bf0de3f360c7fac178b2 100644
--- a/components/net/cpp/src/net_configurable.cpp
+++ b/components/net/cpp/src/net_configurable.cpp
@@ -10,3 +10,7 @@ ftl::NetConfigurable::~NetConfigurable(){}
 void ftl::NetConfigurable::inject(const std::string &name, nlohmann::json &value) {
     ctrl.set(peer, suri + std::string("/") + name, value);
 }
+
+void ftl::NetConfigurable::refresh() {
+    (*config_) = ctrl.get(peer, suri);
+}
diff --git a/components/operators/src/mvmls.cpp b/components/operators/src/mvmls.cpp
index 4c02a7607f1d6a506c1f60cbd1f25eabd2cb5732..e85f8271149537c920b838e9885c3a406bbb5b5b 100644
--- a/components/operators/src/mvmls.cpp
+++ b/components/operators/src/mvmls.cpp
@@ -61,7 +61,7 @@ bool MultiViewMLS::apply(ftl::rgbd::FrameSet &in, ftl::rgbd::FrameSet &out, cuda
             LOG(ERROR) << "Required normals channel missing for MLS";
             return false;
         }
-        if (!f.hasChannel(Channel::Support1)) {
+        if (!f.hasChannel(Channel::Support2)) {
             LOG(ERROR) << "Required cross support channel missing for MLS";
             return false;
         }
@@ -214,7 +214,7 @@ bool MultiViewMLS::apply(ftl::rgbd::FrameSet &in, ftl::rgbd::FrameSet &out, cuda
 			}
 
             ftl::cuda::mls_aggr_horiz(
-                f.createTexture<uchar4>(Channel::Support1),
+                f.createTexture<uchar4>(Channel::Support2),
                 f.createTexture<float4>(Channel::Normals),
                 normals_horiz_[i],
                 f.createTexture<float>(Channel::Depth),
@@ -228,7 +228,7 @@ bool MultiViewMLS::apply(ftl::rgbd::FrameSet &in, ftl::rgbd::FrameSet &out, cuda
             );
 
             ftl::cuda::mls_aggr_vert(
-                f.getTexture<uchar4>(Channel::Support1),
+                f.getTexture<uchar4>(Channel::Support2),
                 normals_horiz_[i],
                 f.getTexture<float4>(Channel::Normals),
                 centroid_horiz_[i],
diff --git a/components/operators/src/segmentation.cpp b/components/operators/src/segmentation.cpp
index fb015b6b16e1aa7e1dcac9735103c8c6af41567e..9bf7605e1ea9ef6b5dd73a4c3078ba70bb61abb9 100644
--- a/components/operators/src/segmentation.cpp
+++ b/components/operators/src/segmentation.cpp
@@ -14,18 +14,17 @@ CrossSupport::~CrossSupport() {
 }
 
 bool CrossSupport::apply(ftl::rgbd::Frame &in, ftl::rgbd::Frame &out, ftl::rgbd::Source *s, cudaStream_t stream) {
-	bool use_depth = config()->value("depth_region", false);
+	bool use_mask = config()->value("discon_support", false);
 
-	if (use_depth) {
+	if (use_mask) {
 		ftl::cuda::support_region(
-			in.createTexture<float>(Channel::Depth),
+			in.createTexture<int>(Channel::Mask),
 			out.createTexture<uchar4>(Channel::Support2, ftl::rgbd::Format<uchar4>(in.get<cv::cuda::GpuMat>(Channel::Colour).size())),
-			config()->value("depth_tau", 0.04f),
 			config()->value("v_max", 5),
 			config()->value("h_max", 5),
-			config()->value("symmetric", true), stream
+			config()->value("symmetric", false), stream
 		);
-	} //else {
+	} else {
 		ftl::cuda::support_region(
 			in.createTexture<uchar4>(Channel::Colour),
 			out.createTexture<uchar4>(Channel::Support1, ftl::rgbd::Format<uchar4>(in.get<cv::cuda::GpuMat>(Channel::Colour).size())),
@@ -34,7 +33,7 @@ bool CrossSupport::apply(ftl::rgbd::Frame &in, ftl::rgbd::Frame &out, ftl::rgbd:
 			config()->value("h_max", 5),
 			config()->value("symmetric", true), stream
 		);
-	//}
+	}
 
 	return true;
 }
diff --git a/components/operators/src/segmentation.cu b/components/operators/src/segmentation.cu
index c16e647931f7d4c4d92551f44f6242fc415bd91d..aaa81e2ad98b5171572b6768754b93f32abcd1eb 100644
--- a/components/operators/src/segmentation.cu
+++ b/components/operators/src/segmentation.cu
@@ -1,8 +1,10 @@
 #include "segmentation_cuda.hpp"
+#include "mask_cuda.hpp"
 
 #define T_PER_BLOCK 8
 
 using ftl::cuda::TextureObject;
+using ftl::cuda::Mask;
 
 template <typename T>
 __device__ inline float cross(T p1, T p2);
@@ -89,6 +91,64 @@ __device__ uchar4 calculate_support_region(const TextureObject<T> &img, int x, i
     return result;
 }
 
+__device__ uchar4 calculate_support_region(const TextureObject<int> &img, int x, int y, int v_max, int h_max) {
+    int x_min = max(0, x - h_max);
+    int x_max = min(img.width()-1, x + h_max);
+    int y_min = max(0, y - v_max);
+    int y_max = min(img.height()-1, y + v_max);
+
+	uchar4 result = make_uchar4(0, 0, 0, 0);
+
+	Mask m1(img.tex2D(x,y));
+
+	int u;
+    for (u=x-1; u >= x_min; --u) {
+		Mask m2(img.tex2D(u,y));
+        if (m2.isDiscontinuity()) {
+            result.x = x - u - 1;
+            break;
+		}
+	}
+	if (u < x_min) result.x = x - x_min;
+	
+    for (u=x+1; u <= x_max; ++u) {
+		Mask m2(img.tex2D(u,y));
+        if (m2.isDiscontinuity()) {
+            result.y = u - x - 1;
+            break;
+		}
+	}
+	if (u > x_max) result.y = x_max - x;
+
+	int v;
+    for (v=y-1; v >= y_min; --v) {
+		Mask m2(img.tex2D(x,v));
+        if (m2.isDiscontinuity()) {
+            result.z = y - v - 1;
+            break;
+		}
+	}
+	if (v < y_min) result.z = y - y_min;
+
+    for (v=y+1; v <= y_max; ++v) {
+		Mask m2(img.tex2D(x,v));
+        if (m2.isDiscontinuity()) {
+            result.w = v - y - 1;
+            break;
+		}
+	}
+	if (v > y_max) result.w = y_max - y;
+
+	// Make symetric left/right and up/down
+	if (false) {
+		result.x = min(result.x, result.y);
+		result.y = result.x;
+		result.z = min(result.z, result.w);
+		result.w = result.z;
+	}
+    return result;
+}
+
 template <typename T, bool SYM>
 __global__ void support_region_kernel(TextureObject<T> img, TextureObject<uchar4> region, float tau, int v_max, int h_max) {
     const int x = blockIdx.x*blockDim.x + threadIdx.x;
@@ -99,6 +159,15 @@ __global__ void support_region_kernel(TextureObject<T> img, TextureObject<uchar4
     region(x,y) = calculate_support_region<T,SYM>(img, x, y, tau, v_max, h_max);
 }
 
+__global__ void support_region_kernel(TextureObject<int> img, TextureObject<uchar4> region, int v_max, int h_max) {
+    const int x = blockIdx.x*blockDim.x + threadIdx.x;
+    const int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+    if (x < 0 || y < 0 || x >= img.width() || y >= img.height()) return;
+
+    region(x,y) = calculate_support_region(img, x, y, v_max, h_max);
+}
+
 void ftl::cuda::support_region(
         ftl::cuda::TextureObject<uchar4> &colour,
         ftl::cuda::TextureObject<uchar4> &region,
@@ -142,6 +211,26 @@ void ftl::cuda::support_region(
 	#endif
 }
 
+void ftl::cuda::support_region(
+		ftl::cuda::TextureObject<int> &mask,
+		ftl::cuda::TextureObject<uchar4> &region,
+		int v_max,
+		int h_max,
+		bool sym,
+		cudaStream_t stream) {
+
+	const dim3 gridSize((region.width() + T_PER_BLOCK - 1)/T_PER_BLOCK, (region.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
+	const dim3 blockSize(T_PER_BLOCK, T_PER_BLOCK);
+
+	support_region_kernel<<<gridSize, blockSize, 0, stream>>>(mask, region, v_max, h_max);
+	cudaSafeCall( cudaGetLastError() );
+
+
+	#ifdef _DEBUG
+	cudaSafeCall(cudaDeviceSynchronize());
+	#endif
+}
+
 __global__ void vis_support_region_kernel(TextureObject<uchar4> colour, TextureObject<uchar4> region, uchar4 bcolour, uchar4 acolour,
 		int ox, int oy, int dx, int dy) {
     const int x = blockIdx.x*blockDim.x + threadIdx.x;
diff --git a/components/operators/src/segmentation_cuda.hpp b/components/operators/src/segmentation_cuda.hpp
index c2cb390d9c0ee62a33127eec1720cf0d6fee8cae..1383489337dc968a33ec630facb597920518e2bf 100644
--- a/components/operators/src/segmentation_cuda.hpp
+++ b/components/operators/src/segmentation_cuda.hpp
@@ -18,6 +18,12 @@ void support_region(
 		float tau, int v_max, int h_max, bool sym,
 		cudaStream_t stream);
 
+void support_region(
+		ftl::cuda::TextureObject<int> &mask,
+		ftl::cuda::TextureObject<uchar4> &region,
+		int v_max, int h_max, bool sym,
+		cudaStream_t stream);
+
 void vis_support_region(
         ftl::cuda::TextureObject<uchar4> &colour,
         ftl::cuda::TextureObject<uchar4> &region,
diff --git a/components/renderers/cpp/include/ftl/cuda/normals.hpp b/components/renderers/cpp/include/ftl/cuda/normals.hpp
index dc3d0265ce4c142861bb0ca0b2e841dc81887449..bbf690f4f66178297158dea35528ba01629445a1 100644
--- a/components/renderers/cpp/include/ftl/cuda/normals.hpp
+++ b/components/renderers/cpp/include/ftl/cuda/normals.hpp
@@ -42,6 +42,11 @@ void normal_visualise(ftl::cuda::TextureObject<float4> &norm,
         const float3 &light, const uchar4 &diffuse, const uchar4 &ambient,
         cudaStream_t stream);
 
+void cool_blue(ftl::cuda::TextureObject<float4> &norm,
+        ftl::cuda::TextureObject<uchar4> &output,
+        const uchar4 &colouring, const float3x3 &pose,
+        cudaStream_t stream);
+
 void normal_filter(ftl::cuda::TextureObject<float4> &norm,
         ftl::cuda::TextureObject<float4> &points,
         const ftl::rgbd::Camera &camera, const float4x4 &pose,
diff --git a/components/renderers/cpp/include/ftl/render/renderer.hpp b/components/renderers/cpp/include/ftl/render/renderer.hpp
index 432be6839de24e94448afbaf407260ea44c5a508..605fa27d182fec5c6463faff397af83b25b43d85 100644
--- a/components/renderers/cpp/include/ftl/render/renderer.hpp
+++ b/components/renderers/cpp/include/ftl/render/renderer.hpp
@@ -26,7 +26,7 @@ class Renderer : public ftl::Configurable {
      * the virtual camera object passed, and writes the result into the
      * virtual camera.
      */
-    virtual bool render(ftl::rgbd::VirtualSource *, ftl::rgbd::Frame &)=0;
+    virtual bool render(ftl::rgbd::VirtualSource *, ftl::rgbd::Frame &, const Eigen::Matrix4d &)=0;
 };
 
 }
diff --git a/components/renderers/cpp/include/ftl/render/splat_render.hpp b/components/renderers/cpp/include/ftl/render/splat_render.hpp
index 3b36e8ec98dd37aaf40b0e3a7e12cad6b3b5a14e..8e51aadf15b8e32d05b3253e72cb2adc7e69f98b 100644
--- a/components/renderers/cpp/include/ftl/render/splat_render.hpp
+++ b/components/renderers/cpp/include/ftl/render/splat_render.hpp
@@ -22,7 +22,7 @@ class Splatter : public ftl::render::Renderer {
 	explicit Splatter(nlohmann::json &config, ftl::rgbd::FrameSet *fs);
 	~Splatter();
 
-	bool render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) override;
+	bool render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out, const Eigen::Matrix4d &t) override;
 	//void setOutputDevice(int);
 
 	protected:
diff --git a/components/renderers/cpp/include/ftl/render/tri_render.hpp b/components/renderers/cpp/include/ftl/render/tri_render.hpp
index 3d9183643e9f2fe183499cf8bbcc97699328ef60..27b6db318c1a751cfcb99dec829828b69237b618 100644
--- a/components/renderers/cpp/include/ftl/render/tri_render.hpp
+++ b/components/renderers/cpp/include/ftl/render/tri_render.hpp
@@ -19,11 +19,11 @@ class Triangular : public ftl::render::Renderer {
 	explicit Triangular(nlohmann::json &config, ftl::rgbd::FrameSet *fs);
 	~Triangular();
 
-	bool render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) override;
+	bool render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out, const Eigen::Matrix4d &t) override;
 	//void setOutputDevice(int);
 
 	protected:
-	void _renderChannel(ftl::rgbd::Frame &out, ftl::codecs::Channel channel_in, ftl::codecs::Channel channel_out, cudaStream_t stream);
+	void _renderChannel(ftl::rgbd::Frame &out, ftl::codecs::Channel channel_in, ftl::codecs::Channel channel_out, const Eigen::Matrix4d &t, cudaStream_t stream);
 
 	private:
 	int device_;
@@ -42,6 +42,8 @@ class Triangular : public ftl::render::Renderer {
 	ftl::render::SplatParams params_;
 	cudaStream_t stream_;
 	float3 light_pos_;
+	Eigen::Matrix4d transform_;
+	float scale_;
 
 	cv::cuda::GpuMat env_image_;
 	ftl::cuda::TextureObject<uchar4> env_tex_;
@@ -49,10 +51,10 @@ class Triangular : public ftl::render::Renderer {
 	//ftl::Filters *filters_;
 
 	template <typename T>
-	void __reprojectChannel(ftl::rgbd::Frame &, ftl::codecs::Channel in, ftl::codecs::Channel out, cudaStream_t);
-	void _reprojectChannel(ftl::rgbd::Frame &, ftl::codecs::Channel in, ftl::codecs::Channel out, cudaStream_t);
-	void _dibr(ftl::rgbd::Frame &, cudaStream_t);
-	void _mesh(ftl::rgbd::Frame &, ftl::rgbd::Source *, cudaStream_t);
+	void __reprojectChannel(ftl::rgbd::Frame &, ftl::codecs::Channel in, ftl::codecs::Channel out, const Eigen::Matrix4d &t, cudaStream_t);
+	void _reprojectChannel(ftl::rgbd::Frame &, ftl::codecs::Channel in, ftl::codecs::Channel out, const Eigen::Matrix4d &t, cudaStream_t);
+	void _dibr(ftl::rgbd::Frame &, const Eigen::Matrix4d &t, cudaStream_t);
+	void _mesh(ftl::rgbd::Frame &, ftl::rgbd::Source *, const Eigen::Matrix4d &t, cudaStream_t);
 };
 
 }
diff --git a/components/renderers/cpp/src/normals.cu b/components/renderers/cpp/src/normals.cu
index 97452555dfb61fd5015aa6a1745b39e2ddcca409..31034c0af6059f2b8014d7af780bd082bb34868a 100644
--- a/components/renderers/cpp/src/normals.cu
+++ b/components/renderers/cpp/src/normals.cu
@@ -399,6 +399,50 @@ void ftl::cuda::normal_visualise(ftl::cuda::TextureObject<float4> &norm,
 
 //==============================================================================
 
+__global__ void cool_blue_kernel(ftl::cuda::TextureObject<float4> norm,
+        ftl::cuda::TextureObject<uchar4> output,
+        uchar4 colouring, float3x3 pose) {
+    const unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;
+    const unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+    if(x >= norm.width() || y >= norm.height()) return;
+
+    //output(x,y) = make_uchar4(0,0,0,0);
+    float3 ray = pose * make_float3(0.0f,0.0f,1.0f);
+    ray = ray / length(ray);
+    float3 n = make_float3(norm.tex2D((int)x,(int)y));
+    float l = length(n);
+    if (l == 0) return;
+    n /= l;
+
+    const float d = 1.0f - max(dot(ray, n), 0.0f);
+    uchar4 original = output(x,y); //.tex2D(x,y);
+
+    output(x,y) = make_uchar4(
+        min(255.0f, colouring.x*d + original.x),
+        min(255.0f, colouring.y*d + original.y),
+        min(255.0f, colouring.z*d + original.z), 255);
+}
+
+void ftl::cuda::cool_blue(ftl::cuda::TextureObject<float4> &norm,
+        ftl::cuda::TextureObject<uchar4> &output,
+        const uchar4 &colouring, const float3x3 &pose,
+        cudaStream_t stream) {
+
+    const dim3 gridSize((norm.width() + T_PER_BLOCK - 1)/T_PER_BLOCK, (norm.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
+    const dim3 blockSize(T_PER_BLOCK, T_PER_BLOCK);
+
+    cool_blue_kernel<<<gridSize, blockSize, 0, stream>>>(norm, output, colouring, pose);
+
+    cudaSafeCall( cudaGetLastError() );
+    #ifdef _DEBUG
+    cudaSafeCall(cudaDeviceSynchronize());
+    //cutilCheckMsg(__FUNCTION__);
+    #endif
+}
+
+//==============================================================================
+
 __global__ void filter_normals_kernel(ftl::cuda::TextureObject<float4> norm,
         ftl::cuda::TextureObject<float4> output,
         ftl::rgbd::Camera camera, float4x4 pose, float thresh) {
diff --git a/components/renderers/cpp/src/splatter.cpp b/components/renderers/cpp/src/splatter.cpp
index 30c47dc1179018a552d8b083245eac71ba56dae2..fad38ba82a1c3925069afd982170761f8d57cd78 100644
--- a/components/renderers/cpp/src/splatter.cpp
+++ b/components/renderers/cpp/src/splatter.cpp
@@ -304,7 +304,7 @@ void Splatter::_renderChannel(
 	}
 }
 
-bool Splatter::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
+bool Splatter::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out, const Eigen::Matrix4d &t) {
 	SHARED_LOCK(scene_->mtx, lk);
 	if (!src->isReady()) return false;
 
diff --git a/components/renderers/cpp/src/splatter_cuda.hpp b/components/renderers/cpp/src/splatter_cuda.hpp
index c81977cf38a4380df4143d2cd8d0609c69de8897..53b9f675e9f26144e9f2d31fff9c889b3ef58720 100644
--- a/components/renderers/cpp/src/splatter_cuda.hpp
+++ b/components/renderers/cpp/src/splatter_cuda.hpp
@@ -117,6 +117,11 @@ namespace cuda {
         ftl::cuda::TextureObject<uchar4> &colour,
 		ftl::cuda::TextureObject<int> &mask,
         int id, uchar4 style, cudaStream_t stream);
+
+	void merge_convert_depth(
+        ftl::cuda::TextureObject<int> &d1,
+		ftl::cuda::TextureObject<float> &d2,
+        float factor, cudaStream_t stream);
 }
 }
 
diff --git a/components/renderers/cpp/src/tri_render.cpp b/components/renderers/cpp/src/tri_render.cpp
index 5912b6480b109db2c3cce960ae5bca6abe6531f8..567a9d139e45678c4d64534e481a8d5c107e3ad5 100644
--- a/components/renderers/cpp/src/tri_render.cpp
+++ b/components/renderers/cpp/src/tri_render.cpp
@@ -206,7 +206,7 @@ void Triangular::__blendChannel(ftl::rgbd::Frame &output, ftl::codecs::Channel i
 }*/
 
 template <typename T>
-void Triangular::__reprojectChannel(ftl::rgbd::Frame &output, ftl::codecs::Channel in, ftl::codecs::Channel out, cudaStream_t stream) {
+void Triangular::__reprojectChannel(ftl::rgbd::Frame &output, ftl::codecs::Channel in, ftl::codecs::Channel out, const Eigen::Matrix4d &t, cudaStream_t stream) {
 	cv::cuda::Stream cvstream = cv::cuda::StreamAccessor::wrapStream(stream);
 	temp_.create<GpuMat>(
 		AccumSelector<T>::channel,
@@ -228,7 +228,7 @@ void Triangular::__reprojectChannel(ftl::rgbd::Frame &output, ftl::codecs::Chann
 			cv::cuda::cvtColor(tmp,col, cv::COLOR_BGR2BGRA);
 		}
 
-		auto transform = MatrixConversion::toCUDA(s->getPose().cast<float>().inverse()) * params_.m_viewMatrixInverse;
+		auto transform = MatrixConversion::toCUDA(s->getPose().cast<float>().inverse() * t.cast<float>().inverse()) * params_.m_viewMatrixInverse;
 		auto transformR = MatrixConversion::toCUDA(s->getPose().cast<float>().inverse()).getFloat3x3();
 
 		if (mesh_) {
@@ -277,18 +277,18 @@ void Triangular::__reprojectChannel(ftl::rgbd::Frame &output, ftl::codecs::Chann
 	}
 }*/
 
-void Triangular::_reprojectChannel(ftl::rgbd::Frame &output, ftl::codecs::Channel in, ftl::codecs::Channel out, cudaStream_t stream) {
+void Triangular::_reprojectChannel(ftl::rgbd::Frame &output, ftl::codecs::Channel in, ftl::codecs::Channel out, const Eigen::Matrix4d &t, cudaStream_t stream) {
 	int type = output.get<GpuMat>(out).type(); // == CV_32F; //ftl::rgbd::isFloatChannel(channel);
 	
 	switch (type) {
-	case CV_32F		: __reprojectChannel<float>(output, in, out, stream); break;
-	case CV_32FC4	: __reprojectChannel<float4>(output, in, out, stream); break;
-	case CV_8UC4	: __reprojectChannel<uchar4>(output, in, out, stream); break;
+	case CV_32F		: __reprojectChannel<float>(output, in, out, t, stream); break;
+	case CV_32FC4	: __reprojectChannel<float4>(output, in, out, t, stream); break;
+	case CV_8UC4	: __reprojectChannel<uchar4>(output, in, out, t, stream); break;
 	default			: LOG(ERROR) << "Invalid output channel format";
 	}
 }
 
-void Triangular::_dibr(ftl::rgbd::Frame &out, cudaStream_t stream) {
+void Triangular::_dibr(ftl::rgbd::Frame &out, const Eigen::Matrix4d &t, cudaStream_t stream) {
 	cv::cuda::Stream cvstream = cv::cuda::StreamAccessor::wrapStream(stream);
 	temp_.get<GpuMat>(Channel::Depth2).setTo(cv::Scalar(0x7FFFFFFF), cvstream);
 
@@ -301,7 +301,7 @@ void Triangular::_dibr(ftl::rgbd::Frame &out, cudaStream_t stream) {
 			continue;
 		}
 
-		auto transform = params_.m_viewMatrix * MatrixConversion::toCUDA(s->getPose().cast<float>());
+		auto transform = params_.m_viewMatrix * MatrixConversion::toCUDA(t.cast<float>() * s->getPose().cast<float>());
 
 		ftl::cuda::dibr_merge(
 			f.createTexture<float>(Channel::Depth),
@@ -316,7 +316,7 @@ void Triangular::_dibr(ftl::rgbd::Frame &out, cudaStream_t stream) {
 	temp_.get<GpuMat>(Channel::Depth2).convertTo(out.get<GpuMat>(Channel::Depth), CV_32F, 1.0f / 100000.0f, cvstream);
 }
 
-void Triangular::_mesh(ftl::rgbd::Frame &out, ftl::rgbd::Source *src, cudaStream_t stream) {
+void Triangular::_mesh(ftl::rgbd::Frame &out, ftl::rgbd::Source *src, const Eigen::Matrix4d &t, cudaStream_t stream) {
 	cv::cuda::Stream cvstream = cv::cuda::StreamAccessor::wrapStream(stream);
 
 	bool do_blend = value("mesh_blend", true);
@@ -338,7 +338,7 @@ void Triangular::_mesh(ftl::rgbd::Frame &out, ftl::rgbd::Source *src, cudaStream
 			continue;
 		}
 
-		auto pose = MatrixConversion::toCUDA(s->getPose().cast<float>());
+		auto pose = MatrixConversion::toCUDA(t.cast<float>() * s->getPose().cast<float>());
 
 		// Calculate and save virtual view screen position of each source pixel
 		ftl::cuda::screen_coord(
@@ -374,7 +374,8 @@ void Triangular::_mesh(ftl::rgbd::Frame &out, ftl::rgbd::Source *src, cudaStream
 	}
 
 	// Convert from int depth to float depth
-	temp_.get<GpuMat>(Channel::Depth2).convertTo(out.get<GpuMat>(Channel::Depth), CV_32F, 1.0f / 100000.0f, cvstream);
+	//temp_.get<GpuMat>(Channel::Depth2).convertTo(out.get<GpuMat>(Channel::Depth), CV_32F, 1.0f / 100000.0f, cvstream);
+	ftl::cuda::merge_convert_depth(temp_.getTexture<int>(Channel::Depth2), out.createTexture<float>(Channel::Depth), 1.0f / 100000.0f, stream_);
 
 	//filters_->filter(out, src, stream);
 
@@ -388,7 +389,7 @@ void Triangular::_mesh(ftl::rgbd::Frame &out, ftl::rgbd::Source *src, cudaStream
 
 void Triangular::_renderChannel(
 		ftl::rgbd::Frame &out,
-		Channel channel_in, Channel channel_out, cudaStream_t stream)
+		Channel channel_in, Channel channel_out, const Eigen::Matrix4d &t, cudaStream_t stream)
 {
 	if (channel_out == Channel::None || channel_in == Channel::None) return;
 	cv::cuda::Stream cvstream = cv::cuda::StreamAccessor::wrapStream(stream);
@@ -413,7 +414,7 @@ void Triangular::_renderChannel(
 		accum_.get<GpuMat>(channel_out).setTo(cv::Scalar(0,0,0,0), cvstream);
 	}
 
-	_reprojectChannel(out, channel_in, channel_out, stream);
+	_reprojectChannel(out, channel_in, channel_out, t, stream);
 }
 
 /*
@@ -463,32 +464,15 @@ static cv::Scalar HSVtoRGB(int H, double S, double V) {
 	return cv::Scalar((Bs + m) * 255, (Gs + m) * 255, (Rs + m) * 255, 0);
 }
 
-bool Triangular::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
+bool Triangular::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out, const Eigen::Matrix4d &t) {
 	SHARED_LOCK(scene_->mtx, lk);
 	if (!src->isReady()) return false;
 
 	//scene_->upload(Channel::Colour + Channel::Depth, stream_);
 
 	const auto &camera = src->parameters();
-	//cudaSafeCall(cudaSetDevice(scene_->getCUDADevice()));
-
-	// Create all the required channels
-	
-	out.create<GpuMat>(Channel::Depth, Format<float>(camera.width, camera.height));
-	out.create<GpuMat>(Channel::Colour, Format<uchar4>(camera.width, camera.height));
-	out.createTexture<uchar4>(Channel::Colour, true);  // Force interpolated colour
-
-
-	if (scene_->frames.size() == 0) return false;
-	auto &g = scene_->frames[0].get<GpuMat>(Channel::Colour);
-
-	temp_.create<GpuMat>(Channel::Colour, Format<float4>(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)); //g.cols, g.rows));
-
 	cv::cuda::Stream cvstream = cv::cuda::StreamAccessor::wrapStream(stream_);
+	//cudaSafeCall(cudaSetDevice(scene_->getCUDADevice()));
 
 	// Parameters object to pass to CUDA describing the camera
 	SplatParams &params = params_;
@@ -500,20 +484,36 @@ bool Triangular::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 	params.m_viewMatrix = MatrixConversion::toCUDA(src->getPose().cast<float>().inverse());
 	params.m_viewMatrixInverse = MatrixConversion::toCUDA(src->getPose().cast<float>());
 	params.camera = camera;
-	// Clear all channels to 0 or max depth
 
-	out.get<GpuMat>(Channel::Depth).setTo(cv::Scalar(1000.0f), cvstream);
+	// Create all the required channels
+	
+	if (!out.hasChannel(Channel::Depth)) {
+		out.create<GpuMat>(Channel::Depth, Format<float>(camera.width, camera.height));
+		out.create<GpuMat>(Channel::Colour, Format<uchar4>(camera.width, camera.height));
+		out.createTexture<uchar4>(Channel::Colour, true);  // Force interpolated colour
 
-	if (env_image_.empty() || !value("environment_enabled", false)) {
-		out.get<GpuMat>(Channel::Colour).setTo(background_, cvstream);
-	} else {
-		auto pose = params.m_viewMatrixInverse.getFloat3x3();
-		ftl::cuda::equirectangular_reproject(
-			env_tex_,
-			out.createTexture<uchar4>(Channel::Colour, true),
-			camera, pose, stream_);
+		out.get<GpuMat>(Channel::Depth).setTo(cv::Scalar(1000.0f), cvstream);
+
+		if (env_image_.empty() || !value("environment_enabled", false)) {
+			out.get<GpuMat>(Channel::Colour).setTo(background_, cvstream);
+		} else {
+			auto pose = params.m_viewMatrixInverse.getFloat3x3();
+			ftl::cuda::equirectangular_reproject(
+				env_tex_,
+				out.createTexture<uchar4>(Channel::Colour, true),
+				camera, pose, stream_);
+		}
 	}
 
+	if (scene_->frames.size() == 0) return false;
+	auto &g = scene_->frames[0].get<GpuMat>(Channel::Colour);
+
+	temp_.create<GpuMat>(Channel::Colour, Format<float4>(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)); //g.cols, g.rows));
+
 	//LOG(INFO) << "Render ready: " << camera.width << "," << camera.height;
 
 	bool show_discon = value("show_discontinuity_mask", false);
@@ -601,13 +601,25 @@ bool Triangular::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 
 	// Create and render triangles for depth
 	if (mesh_) {
-		_mesh(out, src, stream_);
+		_mesh(out, src, t, stream_);
 	} else {
-		_dibr(out, stream_);
+		_dibr(out, t, stream_);
 	}
 
 	// Reprojection of colours onto surface
-	_renderChannel(out, Channel::Colour, Channel::Colour, stream_);
+	_renderChannel(out, Channel::Colour, Channel::Colour, t, stream_);
+
+	if (value("cool_effect", false)) {
+		auto pose = params.m_viewMatrixInverse.getFloat3x3();
+		auto col = parseCUDAColour(value("cool_effect_colour", std::string("#2222ff")));
+
+		ftl::cuda::cool_blue(
+			out.getTexture<float4>(Channel::Normals),
+			out.getTexture<uchar4>(Channel::Colour),
+			col, pose,
+			stream_	
+		);
+	}
 
 	if (value("show_bad_colour", false)) {
 		ftl::cuda::show_missing_colour(
@@ -645,7 +657,7 @@ bool Triangular::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 	else if (chan == Channel::Density) {
 		out.create<GpuMat>(chan, Format<float>(camera.width, camera.height));
 		out.get<GpuMat>(chan).setTo(cv::Scalar(0.0f), cvstream);
-		_renderChannel(out, Channel::Depth, Channel::Density, stream_);
+		_renderChannel(out, Channel::Depth, Channel::Density, t, stream_);
 	}
 	else if (chan == Channel::Right)
 	{
@@ -670,11 +682,11 @@ bool Triangular::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 
 		// Need to re-dibr due to pose change
 		if (mesh_) {
-			_mesh(out, src, stream_);
+			_mesh(out, src, t, stream_);
 		} else {
-			_dibr(out, stream_);
+			_dibr(out, t, stream_);
 		}
-		_renderChannel(out, Channel::Left, Channel::Right, stream_);
+		_renderChannel(out, Channel::Left, Channel::Right, t, stream_);
 
 	} else if (chan != Channel::None) {
 		if (ftl::codecs::isFloatChannel(chan)) {
@@ -684,7 +696,7 @@ bool Triangular::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 			out.create<GpuMat>(chan, Format<uchar4>(camera.width, camera.height));
 			out.get<GpuMat>(chan).setTo(background_, cvstream);
 		}
-		_renderChannel(out, chan, chan, stream_);
+		_renderChannel(out, chan, chan, t, stream_);
 	}
 
 	cudaSafeCall(cudaStreamSynchronize(stream_));
diff --git a/components/renderers/cpp/src/triangle_render.cu b/components/renderers/cpp/src/triangle_render.cu
index 7311e50b9bbbd7b7ba78f106e360998b937646d8..2e1966c4252fcf751639cc70a94dbfbdccd43b3b 100644
--- a/components/renderers/cpp/src/triangle_render.cu
+++ b/components/renderers/cpp/src/triangle_render.cu
@@ -165,6 +165,30 @@ void ftl::cuda::triangle_render1(TextureObject<float> &depth_in, TextureObject<i
     cudaSafeCall( cudaGetLastError() );
 }
 
+// ==== Merge convert ===========
+
+__global__ void merge_convert_kernel(
+		TextureObject<int> depth_in,
+		TextureObject<float> depth_out,
+		float alpha) {
+	const int x = blockIdx.x*blockDim.x + threadIdx.x;
+	const int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+	if (x < 0 || x >= depth_in.width() || y < 0 || y >= depth_in.height()) return;
+
+	float a = float(depth_in.tex2D(x,y))*alpha;
+	float b = depth_out.tex2D(x,y);
+	depth_out(x,y) = min(a,b);
+}
+
+void ftl::cuda::merge_convert_depth(TextureObject<int> &depth_in, TextureObject<float> &depth_out, float alpha, cudaStream_t stream) {
+	const dim3 gridSize((depth_in.width() + T_PER_BLOCK - 1)/T_PER_BLOCK, (depth_in.height() + T_PER_BLOCK - 1)/T_PER_BLOCK);
+	const dim3 blockSize(T_PER_BLOCK, T_PER_BLOCK);
+
+	merge_convert_kernel<<<gridSize, blockSize, 0, stream>>>(depth_in, depth_out, alpha);
+	cudaSafeCall( cudaGetLastError() );
+}
+
 // ==== BLENDER ========
 
 /*
diff --git a/components/rgbd-sources/include/ftl/rgbd/source.hpp b/components/rgbd-sources/include/ftl/rgbd/source.hpp
index 7484788796dba398c525a208ad44708deadf3b70..8958173597a89c00748712a3e1fae69fbc630af0 100644
--- a/components/rgbd-sources/include/ftl/rgbd/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/source.hpp
@@ -31,6 +31,8 @@ class SnapshotReader;
 class VirtualSource;
 class Player;
 
+typedef std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> RawCallback;
+
 /**
  * RGBD Generic data source configurable entity. This class hides the
  * internal implementation of an RGBD source by providing accessor functions
@@ -189,12 +191,12 @@ class Source : public ftl::Configurable {
 	 * Currently this only works for a net source since other sources don't
 	 * produce raw encoded data.
 	 */
-	void addRawCallback(const std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &);
+	void addRawCallback(const RawCallback &);
 
 	/**
 	 * THIS DOES NOT WORK CURRENTLY.
 	 */
-	void removeRawCallback(const std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &);
+	void removeRawCallback(const RawCallback &);
 
 	/**
 	 * INTERNAL. Used to send raw data to callbacks.