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 26d2e6e42dfbf890e5dc2056f09d08e9df034296..c4905e1e5c9713d8c89f7d926548c439797bc627 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>
 
@@ -246,53 +248,43 @@ static void run(ftl::Configurable *root) {
 		}
 	}
 
-	std::vector<ftl::rgbd::FrameSet> scene_A(sourcecounts.size());  // Output of align process
-	std::vector<ftl::rgbd::FrameSet> scene_B(sourcecounts.size());  // 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::rgbd::VirtualSource *vs = ftl::create<ftl::rgbd::VirtualSource>(root, "virtual");
 	//root->set("tags", nlohmann::json::array({ root->getID()+"/virtual" }));
-	ftl::render::Triangular *splat = ftl::create<ftl::render::Triangular>(root, "renderer", &(scene_B[0]));
-	std::vector<ftl::rgbd::Group *> groups;
-	ftl::ILW *align = ftl::create<ftl::ILW>(root, "merge");
 
 	int o = root->value("origin_pose", 0) % sources.size();
-	virt->setPose(sources[o]->getPose());
+	vs->setPose(sources[o]->getPose());
 
-	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[0].frames) {
-				auto &col = f.get<cv::cuda::GpuMat>(Channel::Colour);
-				cv::cuda::cvtColor(col,col, cv::COLOR_Lab2BGR); // TODO: Add stream
-			}
-		}
-		splat->render(virt, out);
-		renderpipe->apply(out, out, virt, 0);
-	});
-	stream->add(virt);
+	vector<ftl::Reconstruction*> groups;
 
 	size_t cumulative = 0;
 	for (auto c : sourcecounts) {
-		auto group = new ftl::rgbd::Group;
+		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++) {
-			Source *in = sources[i];
-			in->setChannel(Channel::Depth);
-			group->addSource(in);
+			reconstr->addSource(sources[i]);
 		}
-		groups.push_back(group);
+		groups.push_back(reconstr);
 		cumulative += c;
 	}
 
-	// ---- Recording code -----------------------------------------------------
+	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"); 
 
+	vs->onRender([vs, &groups, &renderpipe](ftl::rgbd::Frame &out) {
+		for (auto &reconstr : groups) {
+			reconstr->render(vs, out);
+		}
+		renderpipe->apply(out, out, vs, 0);
+	});
+	stream->add(vs);
+
+	// ---- Recording code -----------------------------------------------------
+	/*
 	std::ofstream fileout;
 	ftl::codecs::Writer writer(fileout);
 	auto recorder = [&writer,&groups](ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
@@ -331,73 +323,13 @@ static void run(ftl::Configurable *root) {
 			fileout.close();
 		}
 	});
-
+	*/
 	// -------------------------------------------------------------------------
 
 	stream->setLatency(6);  // FIXME: This depends on source!?
 	//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::CrossSupport>("cross2")->set("discon_support", true);
-	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
-
-	size_t size = groups.size();
-	for (size_t i = 0; i < size; ++i) {
-		groups[i]->setLatency(4);
-		groups[i]->setName("ReconGroup-" + std::to_string(i));
-		groups[i]->sync([splat,virt,&busy,&slave,&scene_A,&scene_B,&align,controls,pipeline1,i](ftl::rgbd::FrameSet &fs) -> bool {
-			//cudaSetDevice(scene->getCUDADevice());
-
-			//if (slave.isPaused()) return true;
-			if (controls->value("paused", false)) return true;
-			
-			// TODO: busy needs to be an array now.
-			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[i]);
-
-			ftl::pool.push([&scene_B,&scene_A,&busy,&slave,&align,pipeline1,i](int id) {
-				//cudaSetDevice(scene->getCUDADevice());
-				// TODO: Release frameset here...
-				//cudaSafeCall(cudaStreamSynchronize(scene->getIntegrationStream()));
-
-				UNIQUE_LOCK(scene_A[i].mtx, lk);
-
-				pipeline1->apply(scene_A[i], scene_A[i], 0);
-				//align->process(scene_A[i]);
-
-
-				// 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[i].swapTo(scene_B[i]);
-				LOG(INFO) << "Align complete... " << scene_A[i].timestamp;
-				busy = false;
-			});
-			return true;
-		});
-	}
-
 	LOG(INFO) << "Start timer";
 	ftl::timer::start(true);
 
@@ -411,10 +343,8 @@ static void run(ftl::Configurable *root) {
 
 	LOG(INFO) << "Deleting...";
 
-	delete align;
-	delete splat;
 	delete stream;
-	delete virt;
+	delete vs;
 	delete net;
 	for (auto g : groups) {
 		delete g;
diff --git a/applications/reconstruct/src/reconstruction.cpp b/applications/reconstruct/src/reconstruction.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bbaf7f8b5e6414140233aa9480ccc1239c7de262
--- /dev/null
+++ b/applications/reconstruct/src/reconstruction.cpp
@@ -0,0 +1,77 @@
+#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;
+
+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::render(ftl::rgbd::VirtualSource *vs, ftl::rgbd::Frame &out) {
+	renderer_->render(vs, out);
+}
\ No newline at end of file
diff --git a/applications/reconstruct/src/reconstruction.hpp b/applications/reconstruct/src/reconstruction.hpp
index bde04000706607bb752ab9447015fd3259c344a0..340e65bf2f47650400d4c6850c1ae1310a0458aa 100644
--- a/applications/reconstruct/src/reconstruction.hpp
+++ b/applications/reconstruct/src/reconstruction.hpp
@@ -1,11 +1,19 @@
 #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();
+	Reconstruction(nlohmann::json &config, const std::string name);
 	~Reconstruction();
 
 	void addSource(ftl::rgbd::Source *);
@@ -16,8 +24,10 @@ class Reconstruction : public ftl::Configurable {
 	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::rgbd::FrameSet fs_;
 	ftl::operators::Graph *pipeline_;
 	ftl::render::Triangular *renderer_;
 };