diff --git a/applications/reconstruct/CMakeLists.txt b/applications/reconstruct/CMakeLists.txt
index be1fe443ef1c8cf3fb14d207cc75ee81bf7d8437..9d9065d3972a9a915059d2e7c88f17f33c676c42 100644
--- a/applications/reconstruct/CMakeLists.txt
+++ b/applications/reconstruct/CMakeLists.txt
@@ -4,14 +4,14 @@
 
 set(REPSRC
 	src/main.cpp
-	src/voxel_scene.cpp
+	#src/voxel_scene.cpp
 	#src/ray_cast_sdf.cu
 	src/camera_util.cu
 	#src/ray_cast_sdf.cpp
 	src/registration.cpp
 	#src/virtual_source.cpp
-	src/splat_render.cpp
-	src/dibr.cu
+	#src/splat_render.cpp
+	#src/dibr.cu
 	src/mls.cu
 	src/depth_camera.cu
 	src/depth_camera.cpp
diff --git a/applications/reconstruct/include/ftl/cuda_matrix_util.hpp b/components/common/cpp/include/ftl/cuda_matrix_util.hpp
similarity index 100%
rename from applications/reconstruct/include/ftl/cuda_matrix_util.hpp
rename to components/common/cpp/include/ftl/cuda_matrix_util.hpp
diff --git a/components/renderers/cpp/CMakeLists.txt b/components/renderers/cpp/CMakeLists.txt
index 33f910ca0342096bb551b430374776a86de89b2f..89cfe508deac5307fd92f1446d87eaeac202071b 100644
--- a/components/renderers/cpp/CMakeLists.txt
+++ b/components/renderers/cpp/CMakeLists.txt
@@ -1,6 +1,6 @@
 add_library(ftlrender
-	src/display.cpp
-	src/rgbd_display.cpp
+	src/splat_render.cpp
+	src/splatter.cu
 )
 
 # These cause errors in CI build and are being removed from PCL in newer versions
@@ -11,6 +11,6 @@ target_include_directories(ftlrender PUBLIC
 	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
 	$<INSTALL_INTERFACE:include>
 	PRIVATE src)
-target_link_libraries(ftlrender ftlrgbd ftlcommon ftlnet Eigen3::Eigen Threads::Threads glog::glog ${OpenCV_LIBS} ${PCL_LIBRARIES})
+target_link_libraries(ftlrender ftlrgbd ftlcommon Eigen3::Eigen Threads::Threads ${OpenCV_LIBS})
 
 #ADD_SUBDIRECTORY(test)
diff --git a/components/renderers/cpp/include/ftl/render/renderer.hpp b/components/renderers/cpp/include/ftl/render/renderer.hpp
index d8817ad9192de3e31a83108e531dfb009490a066..f564ac7a564c5ec7134f40d22444c983b245601e 100644
--- a/components/renderers/cpp/include/ftl/render/renderer.hpp
+++ b/components/renderers/cpp/include/ftl/render/renderer.hpp
@@ -1,13 +1,32 @@
 #ifndef _FTL_RENDER_RENDERER_HPP_
 #define _FTL_RENDER_RENDERER_HPP_
 
+#include <ftl/configurable.hpp>
+#include <ftl/rgbd/virtual.hpp>
+#include <ftl/cuda_common.hpp>
+
 namespace ftl {
 namespace render {
 
+/**
+ * Abstract class for all renderers. A renderer takes some 3D scene and
+ * generates a virtual camera perspective of that scene. The scene might be
+ * based upon a point cloud, or an entirely virtual mesh or procedural scene.
+ * It is intended that multiple scenes can be rendered into a single virtual
+ * view using a compositing renderer, such a renderer accepting any kind of
+ * renderer for compositing and hence relying on this base class.
+ */
 class Renderer : public ftl::Configurable {
     public:
-    Renderer();
-    virtual ~Renderer();
+    explicit Renderer(nlohmann::json &config) : Configurable(config) {};
+    virtual ~Renderer() {};
+
+    /**
+     * Generate a single virtual camera frame. The frame takes its pose from
+     * the virtual camera object passed, and writes the result into the
+     * virtual camera.
+     */
+    virtual bool render(ftl::rgbd::VirtualSource *, cudaStream_t)=0;
 };
 
 }
diff --git a/components/renderers/cpp/include/ftl/render/splat_render.hpp b/components/renderers/cpp/include/ftl/render/splat_render.hpp
index 828db11fad6bbb5e3aeeae0899bc15549fab1708..5357bf5c30c600b7a544f5aee87abc6eb9947c69 100644
--- a/components/renderers/cpp/include/ftl/render/splat_render.hpp
+++ b/components/renderers/cpp/include/ftl/render/splat_render.hpp
@@ -1,13 +1,8 @@
 #ifndef _FTL_RECONSTRUCTION_SPLAT_HPP_
 #define _FTL_RECONSTRUCTION_SPLAT_HPP_
 
-#include <ftl/configurable.hpp>
-#include <ftl/rgbd/source.hpp>
-#include <ftl/depth_camera.hpp>
-#include <ftl/voxel_scene.hpp>
-//#include <ftl/ray_cast_util.hpp>
-#include <ftl/cuda_common.hpp>
-
+#include <ftl/render/renderer.hpp>
+#include <ftl/rgbd/frameset.hpp>
 #include "splat_params.hpp"
 
 namespace ftl {
@@ -21,26 +16,29 @@ namespace render {
  * on a separate machine or at a later time, the advantage being to save local
  * processing resources and that the first pass result may compress better.
  */
-class Splatter {
+class Splatter : public ftl::render::Renderer {
 	public:
-	explicit Splatter(ftl::voxhash::SceneRep *scene);
+	explicit Splatter(nlohmann::json &config, const ftl::rgbd::FrameSet &fs);
 	~Splatter();
 
-	void render(int64_t ts, ftl::rgbd::Source *src, cudaStream_t stream=0);
+	void render(ftl::rgbd::VirtualSource *src, cudaStream_t stream=0);
 
-	void setOutputDevice(int);
+	//void setOutputDevice(int);
 
 	private:
 	int device_;
-	ftl::cuda::TextureObject<int> depth1_;
+	/*ftl::cuda::TextureObject<int> depth1_;
 	ftl::cuda::TextureObject<int> depth3_;
 	ftl::cuda::TextureObject<uchar4> colour1_;
 	ftl::cuda::TextureObject<float4> colour_tmp_;
 	ftl::cuda::TextureObject<float> depth2_;
 	ftl::cuda::TextureObject<uchar4> colour2_;
-	ftl::cuda::TextureObject<float4> normal1_;
-	SplatParams params_;
-	ftl::voxhash::SceneRep *scene_;
+	ftl::cuda::TextureObject<float4> normal1_;*/
+	//SplatParams params_;
+
+	ftl::rgbd::Frame output_;
+	ftl::rgbd::Frame temp_;
+	const ftl::rgbd::FrameSet &scene_;
 };
 
 }
diff --git a/components/renderers/cpp/src/splat_params.hpp b/components/renderers/cpp/src/splat_params.hpp
index 8ae5bf345e4e0d348c414cc1ce7bb52190d5ffff..4f9c8882b161d7774388e8d9fff7337cb1d6e685 100644
--- a/components/renderers/cpp/src/splat_params.hpp
+++ b/components/renderers/cpp/src/splat_params.hpp
@@ -3,7 +3,7 @@
 
 #include <ftl/cuda_util.hpp>
 #include <ftl/cuda_matrix_util.hpp>
-#include <ftl/depth_camera_params.hpp>
+#include <ftl/rgbd/camera.hpp>
 
 namespace ftl {
 namespace render {
@@ -18,10 +18,10 @@ struct __align__(16) SplatParams {
 	float4x4 m_viewMatrixInverse;
 
 	uint m_flags;
-	float voxelSize;
+	//float voxelSize;
 	float depthThreshold;
 
-	DepthCameraParams camera;
+	ftl::rgbd::Camera camera;
 };
 
 }
diff --git a/components/renderers/cpp/src/splat_render.cpp b/components/renderers/cpp/src/splat_render.cpp
index 21e1b1db3952422e82be757fec56d3c75de0b4c8..fc92d541092b79f7eb744fedd3f73445f187b8ee 100644
--- a/components/renderers/cpp/src/splat_render.cpp
+++ b/components/renderers/cpp/src/splat_render.cpp
@@ -1,12 +1,11 @@
-#include "splat_render.hpp"
-#include "splat_render_cuda.hpp"
-#include "compactors.hpp"
-#include "depth_camera_cuda.hpp"
+#include <ftl/render/splat_render.hpp>
+#include "splatter_cuda.hpp"
 
 using ftl::render::Splatter;
 using ftl::rgbd::Channel;
+using ftl::rgbd::Format;
 
-Splatter::Splatter(ftl::voxhash::SceneRep *scene) : scene_(scene) {
+Splatter::Splatter(nlohmann::json &config, const ftl::rgbd::FrameSet &fs) : ftl::render::Renderer(config), scene_(fs) {
 
 }
 
@@ -14,15 +13,25 @@ Splatter::~Splatter() {
 
 }
 
-void Splatter::render(int64_t ts, ftl::rgbd::Source *src, cudaStream_t stream) {
+void Splatter::render(ftl::rgbd::VirtualSource *src, cudaStream_t stream) {
 	if (!src->isReady()) return;
 
 	const auto &camera = src->parameters();
 
-	cudaSafeCall(cudaSetDevice(scene_->getCUDADevice()));
+	//cudaSafeCall(cudaSetDevice(scene_->getCUDADevice()));
 
-	// Create buffers if they don't exists
-	if ((unsigned int)depth1_.width() != camera.width || (unsigned int)depth1_.height() != camera.height) {
+	output_.create<cv::cuda::GpuMat>(Channel::Depth, Format<float>(camera.width, camera.height));
+	output_.create<cv::cuda::GpuMat>(Channel::Colour, Format<uchar4>(camera.width, camera.height));
+
+	temp_.create<cv::cuda::GpuMat>(Channel::Colour, Format<float4>(camera.width, camera.height));
+	temp_.create<cv::cuda::GpuMat>(Channel::Colour2, Format<uchar4>(camera.width, camera.height));
+	temp_.create<cv::cuda::GpuMat>(Channel::Confidence, Format<float>(camera.width, camera.height));
+	temp_.create<cv::cuda::GpuMat>(Channel::Depth, Format<int>(camera.width, camera.height));
+	temp_.create<cv::cuda::GpuMat>(Channel::Depth2, Format<int>(camera.width, camera.height));
+	temp_.create<cv::cuda::GpuMat>(Channel::Normals, Format<float4>(camera.width, camera.height));
+
+	// Create buffers if they don't exist
+	/*if ((unsigned int)depth1_.width() != camera.width || (unsigned int)depth1_.height() != camera.height) {
 		depth1_ = ftl::cuda::TextureObject<int>(camera.width, camera.height);
 	}
 	if ((unsigned int)depth3_.width() != camera.width || (unsigned int)depth3_.height() != camera.height) {
@@ -42,7 +51,7 @@ void Splatter::render(int64_t ts, ftl::rgbd::Source *src, cudaStream_t stream) {
 	}
 	if ((unsigned int)colour2_.width() != camera.width || (unsigned int)colour2_.height() != camera.height) {
 		colour2_ = ftl::cuda::TextureObject<uchar4>(camera.width, camera.height);
-	}
+	}*/
 
 	// Parameters object to pass to CUDA describing the camera
 	SplatParams params;
@@ -53,27 +62,27 @@ void Splatter::render(int64_t ts, ftl::rgbd::Source *src, cudaStream_t stream) {
 
 	params.m_viewMatrix = MatrixConversion::toCUDA(src->getPose().cast<float>().inverse());
 	params.m_viewMatrixInverse = MatrixConversion::toCUDA(src->getPose().cast<float>());
-	params.voxelSize = scene_->getHashParams().m_virtualVoxelSize;
-	params.camera.flags = 0;
-	params.camera.fx = camera.fx;
+	//params.voxelSize = scene_->getHashParams().m_virtualVoxelSize;
+	params.camera = camera;
+	/*params.camera.fx = camera.fx;
 	params.camera.fy = camera.fy;
 	params.camera.mx = -camera.cx;
 	params.camera.my = -camera.cy;
 	params.camera.m_imageWidth = camera.width;
 	params.camera.m_imageHeight = camera.height;
 	params.camera.m_sensorDepthWorldMax = camera.maxDepth;
-	params.camera.m_sensorDepthWorldMin = camera.minDepth;
+	params.camera.m_sensorDepthWorldMin = camera.minDepth;*/
 
 	//ftl::cuda::compactifyAllocated(scene_->getHashData(), scene_->getHashParams(), stream);
 	//LOG(INFO) << "Occupied: " << scene_->getOccupiedCount();
 
-	if (scene_->value("voxels", false)) {
+	//if (scene_->value("voxels", false)) {
 		// TODO:(Nick) Stereo for voxel version
-		ftl::cuda::isosurface_point_image(scene_->getHashData(), depth1_, params, stream);
+		//ftl::cuda::isosurface_point_image(scene_->getHashData(), depth1_, params, stream);
 		//ftl::cuda::splat_points(depth1_, depth2_, params, stream);
 		//ftl::cuda::dibr(depth2_, colour1_, scene_->cameraCount(), params, stream);
-		src->writeFrames(ts, colour1_, depth2_, stream);
-	} else {
+		//src->writeFrames(ts, colour1_, depth2_, stream);
+	//} else {
 		ftl::cuda::clear_depth(depth1_, stream);
 		ftl::cuda::clear_depth(depth3_, stream);
 		ftl::cuda::clear_depth(depth2_, stream);
@@ -88,7 +97,7 @@ void Splatter::render(int64_t ts, ftl::rgbd::Source *src, cudaStream_t stream) {
 
 		if (src->getChannel() == Channel::Depth) {
 			//ftl::cuda::int_to_float(depth1_, depth2_, 1.0f / 1000.0f, stream);
-			if (src->value("splatting",  false)) {
+			if (value("splatting",  false)) {
 				//ftl::cuda::splat_points(depth1_, colour1_, normal1_, depth2_, colour2_, params, stream);
 				ftl::cuda::int_to_float(depth1_, depth2_, 1.0f / 1000.0f, stream);
 				src->writeFrames(ts, colour1_, depth2_, stream);
@@ -117,7 +126,7 @@ void Splatter::render(int64_t ts, ftl::rgbd::Source *src, cudaStream_t stream) {
 			ftl::cuda::dibr(depth1_, colour1_, normal1_, depth2_, colour_tmp_, depth3_, scene_->cameraCount(), params, stream);
 			src->writeFrames(ts, colour1_, colour2_, stream);
 		} else {
-			if (src->value("splatting",  false)) {
+			if (value("splatting",  false)) {
 				//ftl::cuda::splat_points(depth1_, colour1_, normal1_, depth2_, colour2_, params, stream);
 				src->writeFrames(ts, colour1_, depth2_, stream);
 			} else {
@@ -125,7 +134,7 @@ void Splatter::render(int64_t ts, ftl::rgbd::Source *src, cudaStream_t stream) {
 				src->writeFrames(ts, colour1_, depth2_, stream);
 			}
 		}
-	}
+	//}
 
 	//ftl::cuda::median_filter(depth1_, depth2_, stream);
 	//ftl::cuda::splat_points(depth1_, depth2_, params, stream);
diff --git a/components/renderers/cpp/src/splatter.cu b/components/renderers/cpp/src/splatter.cu
new file mode 100644
index 0000000000000000000000000000000000000000..586eb40fd97bf56d27c9d87c5ab4d7d68c11da5b
--- /dev/null
+++ b/components/renderers/cpp/src/splatter.cu
@@ -0,0 +1,134 @@
+#include "splat_params.hpp"
+#include <ftl/rgbd/camera.hpp>
+#include <ftl/cuda_common.hpp>
+
+using ftl::cuda::TextureObject;
+using ftl::render::SplatParams;
+
+/*
+ * Pass 1: Directly render each camera into virtual view but with no upsampling
+ * for sparse points.
+ */
+ __global__ void dibr_merge_kernel(TextureObject<float4> points, TextureObject<int> depth, int cam, SplatParams params) {
+	const int x = blockIdx.x*blockDim.x + threadIdx.x;
+	const int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+	const float3 worldPos = make_float3(tex2D<float4>(points, x, y));
+	if (worldPos.x == MINF) return;
+
+    // Find the virtual screen position of current point
+	const float3 camPos = params.m_viewMatrix * worldPos;
+	if (camPos.z < params.camera.minDepth) return;
+	if (camPos.z > params.camera.maxDepth) return;
+
+	const float d = camPos.z;
+
+	const uint2 screenPos = params.camera.camToScreen<uint2>(camPos);
+	const unsigned int cx = screenPos.x;
+	const unsigned int cy = screenPos.y;
+	if (d > params.camera.minDepth && d < params.camera.maxDepth && cx < depth.width() && cy < depth.height()) {
+		// Transform estimated point to virtual cam space and output z
+		atomicMin(&depth(cx,cy), d * 1000.0f);
+	}
+}
+
+__device__ inline float4 make_float4(const uchar4 &c) {
+    return make_float4(c.x,c.y,c.z,c.w);
+}
+
+/*
+ * Pass 2: Accumulate attribute contributions if the points pass a visibility test.
+ */
+__global__ void dibr_attribute_contrib_kernel(
+        TextureObject<uchar4> colour_in,    // Original colour image
+        TextureObject<float4> points,       // Original 3D points
+        TextureObject<int> depth_in,        // Virtual depth map
+        TextureObject<float4> colour_out,   // Accumulated output
+        //TextureObject<float4> normal_out,
+        TextureObject<float> contrib_out,
+        SplatParams params) {
+        
+	//const ftl::voxhash::DepthCameraCUDA &camera = c_cameras[cam];
+
+	const int tid = (threadIdx.x + threadIdx.y * blockDim.x);
+	//const int warp = tid / WARP_SIZE;
+	const int x = (blockIdx.x*blockDim.x + threadIdx.x) / WARP_SIZE;
+	const int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+	const float3 worldPos = make_float3(tex2D<float4>(camera.points, x, y));
+	//const float3 normal = make_float3(tex2D<float4>(camera.normal, x, y));
+	if (worldPos.x == MINF) return;
+    //const float r = (camera.poseInverse * worldPos).z / camera.params.fx;
+
+	const float3 camPos = params.m_viewMatrix * worldPos;
+	if (camPos.z < params.camera.minDepth) return;
+	if (camPos.z > params.camera.maxDepth) return;
+	const uint2 screenPos = params.camera.camToScreen<uint2>(camPos);
+
+    const int upsample = 8; //min(UPSAMPLE_MAX, int((5.0f*r) * params.camera.fx / camPos.z));
+
+	// Not on screen so stop now...
+	if (screenPos.x >= depth_in.width() || screenPos.y >= depth_in.height()) return;
+            
+    // Is this point near the actual surface and therefore a contributor?
+    const float d = ((float)depth_in.tex2D((int)screenPos.x, (int)screenPos.y)/1000.0f);
+    //if (abs(d - camPos.z) > DEPTH_THRESHOLD) return;
+
+    // TODO:(Nick) Should just one thread load these to shared mem?
+    const float4 colour = make_float4(colour_in.tex2D<uchar4>(x, y));
+    //const float4 normal = tex2D<float4>(camera.normal, x, y);
+
+	// Each thread in warp takes an upsample point and updates corresponding depth buffer.
+	const int lane = tid % WARP_SIZE;
+	for (int i=lane; i<upsample*upsample; i+=WARP_SIZE) {
+		const float u = (i % upsample) - (upsample / 2);
+		const float v = (i / upsample) - (upsample / 2);
+
+        // Use the depth buffer to determine this pixels 3D position in camera space
+        const float d = ((float)depth_in.tex2D(screenPos.x+u, screenPos.y+v)/1000.0f);
+		const float3 nearest = params.camera.screenToCam((int)(screenPos.x+u),(int)(screenPos.y+v),d);
+
+        // What is contribution of our current point at this pixel?
+        const float weight = ftl::cuda::spatialWeighting(length(nearest - camPos), SMOOTHING_MULTIPLIER_C*(nearest.z/params.camera.fx));
+        if (screenPos.x+u < colour_out.width() && screenPos.y+v < colour_out.height() && weight > 0.0f) {  // TODO: Use confidence threshold here
+            const float4 wcolour = colour * weight;
+			const float4 wnormal = normal * weight;
+			
+			//printf("Z %f\n", d);
+
+            // Add this points contribution to the pixel buffer
+            atomicAdd((float*)&colour_out(screenPos.x+u, screenPos.y+v), wcolour.x);
+            atomicAdd((float*)&colour_out(screenPos.x+u, screenPos.y+v)+1, wcolour.y);
+            atomicAdd((float*)&colour_out(screenPos.x+u, screenPos.y+v)+2, wcolour.z);
+            atomicAdd((float*)&colour_out(screenPos.x+u, screenPos.y+v)+3, wcolour.w);
+            //atomicAdd((float*)&normal_out(screenPos.x+u, screenPos.y+v), wnormal.x);
+            //atomicAdd((float*)&normal_out(screenPos.x+u, screenPos.y+v)+1, wnormal.y);
+            //atomicAdd((float*)&normal_out(screenPos.x+u, screenPos.y+v)+2, wnormal.z);
+            //atomicAdd((float*)&normal_out(screenPos.x+u, screenPos.y+v)+3, wnormal.w);
+            atomicAdd(&contrib_out(screenPos.x+u, screenPos.y+v), weight);
+        }
+	}
+}
+
+
+
+
+__global__ void dibr_normalise_kernel(
+    TextureObject<float4> colour_in,
+    TextureObject<uchar4> colour_out,
+    //TextureObject<float4> normals,
+    TextureObject<float> contribs) {
+const unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;
+const unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;
+
+if (x < colour_in.width() && y < colour_in.height()) {
+    const float4 colour = colour_in.tex2D((int)x,(int)y);
+    //const float4 normal = normals.tex2D((int)x,(int)y);
+    const float contrib = contribs.tex2D((int)x,(int)y);
+
+    if (contrib > 0.0f) {
+        colour_out(x,y) = make_uchar4(colour.x / contrib, colour.y / contrib, colour.z / contrib, 0);
+        //normals(x,y) = normal / contrib;
+    }
+}
+}
diff --git a/components/renderers/cpp/src/splat_render_cuda.hpp b/components/renderers/cpp/src/splatter_cuda.hpp
similarity index 85%
rename from components/renderers/cpp/src/splat_render_cuda.hpp
rename to components/renderers/cpp/src/splatter_cuda.hpp
index e60fc8c27c0d39ef8798803a526891c5da2fca62..125d43390be2514a0e8f301b37d426d3fccb4350 100644
--- a/components/renderers/cpp/src/splat_render_cuda.hpp
+++ b/components/renderers/cpp/src/splatter_cuda.hpp
@@ -1,10 +1,7 @@
 #ifndef _FTL_RECONSTRUCTION_SPLAT_CUDA_HPP_
 #define _FTL_RECONSTRUCTION_SPLAT_CUDA_HPP_
 
-#include <ftl/depth_camera.hpp>
-#include <ftl/voxel_hash.hpp>
-//#include <ftl/ray_cast_util.hpp>
-
+#include <ftl/cuda_common.hpp>
 #include "splat_params.hpp"
 
 namespace ftl {
@@ -84,21 +81,6 @@ __device__ inline float intersectDistance(const float3 &n, const float3 &p0, con
      return PINF; 
 }
 
-/**
- * NOTE: Not strictly isosurface currently since it includes the internals
- * of objects up to at most truncation depth.
- */
-void isosurface_point_image(const ftl::voxhash::HashData& hashData,
-			const ftl::cuda::TextureObject<int> &depth,
-			const ftl::render::SplatParams &params, cudaStream_t stream);
-
-//void isosurface_point_image_stereo(const ftl::voxhash::HashData& hashData,
-//		const ftl::voxhash::HashParams& hashParams,
-//		const RayCastData &rayCastData, const RayCastParams &params,
-//		cudaStream_t stream);
-
-// TODO: isosurface_point_cloud
-
 void splat_points(const ftl::cuda::TextureObject<int> &depth_in,
 		const ftl::cuda::TextureObject<uchar4> &colour_in,
 		const ftl::cuda::TextureObject<float4> &normal_in,
diff --git a/components/rgbd-sources/include/ftl/rgbd/channels.hpp b/components/rgbd-sources/include/ftl/rgbd/channels.hpp
index 591e0e312b0c5a161dc40ed0d4966852d8781971..9c7f24b036c95271f990780d61395c1b595fbb1e 100644
--- a/components/rgbd-sources/include/ftl/rgbd/channels.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/channels.hpp
@@ -12,9 +12,11 @@ enum struct Channel : int {
     Colour = 0,
     Left = 0,
     Depth = 1,
-    Right,
-    Disparity,
-    Deviation,
+    Right = 2,
+    Colour2 = 2,
+    Disparity = 3,
+    Depth2 = 3,
+    Deviation = 4,
     Normals,
     Confidence,
     Flow,
diff --git a/components/rgbd-sources/include/ftl/rgbd/source.hpp b/components/rgbd-sources/include/ftl/rgbd/source.hpp
index 07391d76f10f4eec69f4a7df97123dfc59270a0c..e14950e63e45df89ea144f599e3750b1daa02444 100644
--- a/components/rgbd-sources/include/ftl/rgbd/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/source.hpp
@@ -121,17 +121,6 @@ class Source : public ftl::Configurable {
 	 */
 	void getDepth(cv::Mat &d);
 
-	/**
-	 * Write frames into source buffers from an external renderer. Virtual
-	 * sources do not have an internal generator of frames but instead have
-	 * their data provided from an external rendering class. This function only
-	 * works when there is no internal generator.
-	 */
-	void writeFrames(int64_t ts, const cv::Mat &rgb, const cv::Mat &depth);
-	void writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uint> &depth, cudaStream_t stream);
-	void writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<float> &depth, cudaStream_t stream);
-	void writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uchar4> &rgb2, cudaStream_t stream);
-
 	int64_t timestamp() const { return timestamp_; }
 
 	/**
@@ -214,7 +203,7 @@ class Source : public ftl::Configurable {
 	void removeCallback() { callback_ = nullptr; }
 
 
-	private:
+	protected:
 	detail::Source *impl_;
 	cv::Mat rgb_;
 	cv::Mat depth_;
diff --git a/components/rgbd-sources/include/ftl/rgbd/virtual.hpp b/components/rgbd-sources/include/ftl/rgbd/virtual.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e8e8edd406779191234b76bd36d6893dd4cd8d9b
--- /dev/null
+++ b/components/rgbd-sources/include/ftl/rgbd/virtual.hpp
@@ -0,0 +1,25 @@
+#ifndef _FTL_RGBD_VIRTUAL_HPP_
+#define _FTL_RGBD_VIRTUAL_HPP_
+
+#include <ftl/rgbd/source.hpp>
+
+namespace ftl {
+namespace rgbd {
+
+class VirtualSource : public ftl::rgbd::Source {
+    public:
+    explicit VirtualSource(ftl::config::json_t &cfg);
+
+    /**
+	 * Write frames into source buffers from an external renderer. Virtual
+	 * sources do not have an internal generator of frames but instead have
+	 * their data provided from an external rendering class. This function only
+	 * works when there is no internal generator.
+	 */
+    void write(int64_t ts, ftl::rgbd::Frame &frame, cudaStream_t stream=0);
+};
+
+}
+}
+
+#endif  // _FTL_RGBD_VIRTUAL_HPP_
diff --git a/components/rgbd-sources/src/source.cpp b/components/rgbd-sources/src/source.cpp
index d33473fc38fbf55670e8b8844ffa5a03487e606d..35d23f27ad7edac18d3e3e02247296f1382be5e2 100644
--- a/components/rgbd-sources/src/source.cpp
+++ b/components/rgbd-sources/src/source.cpp
@@ -273,71 +273,6 @@ bool Source::compute(int N, int B) {
 	return false;
 }
 
-void Source::writeFrames(int64_t ts, const cv::Mat &rgb, const cv::Mat &depth) {
-	if (!impl_) {
-		UNIQUE_LOCK(mutex_,lk);
-		rgb.copyTo(rgb_);
-		depth.copyTo(depth_);
-		timestamp_ = ts;
-	}
-}
-
-void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uint> &depth, cudaStream_t stream) {
-	if (!impl_) {
-		UNIQUE_LOCK(mutex_,lk);
-		timestamp_ = ts;
-		rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
-		cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
-		depth_.create(depth.height(), depth.width(), CV_32SC1);
-		cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(uint), depth_.rows, cudaMemcpyDeviceToHost, stream));
-		//cudaSafeCall(cudaStreamSynchronize(stream));  // TODO:(Nick) Don't wait here.
-		stream_ = stream;
-		//depth_.convertTo(depth_, CV_32F, 1.0f / 1000.0f);
-	} else {
-		LOG(ERROR) << "writeFrames cannot be done on this source: " << getURI();
-	}
-}
-
-void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<float> &depth, cudaStream_t stream) {
-	if (!impl_) {
-		UNIQUE_LOCK(mutex_,lk);
-		timestamp_ = ts;
-		rgb.download(rgb_, stream);
-		//rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
-		//cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
-		depth.download(depth_, stream);
-		//depth_.create(depth.height(), depth.width(), CV_32FC1);
-		//cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(float), depth_.rows, cudaMemcpyDeviceToHost, stream));
-		
-		stream_ = stream;
-		cudaSafeCall(cudaStreamSynchronize(stream_));
-		cv::cvtColor(rgb_,rgb_, cv::COLOR_BGRA2BGR);
-		cv::cvtColor(rgb_,rgb_, cv::COLOR_Lab2BGR);
-
-		if (callback_) callback_(timestamp_, rgb_, depth_);
-	}
-}
-
-void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uchar4> &rgb2, cudaStream_t stream) {
-	if (!impl_) {
-		UNIQUE_LOCK(mutex_,lk);
-		timestamp_ = ts;
-		rgb.download(rgb_, stream);
-		//rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
-		//cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
-		rgb2.download(depth_, stream);
-		//depth_.create(depth.height(), depth.width(), CV_32FC1);
-		//cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(float), depth_.rows, cudaMemcpyDeviceToHost, stream));
-		
-		stream_ = stream;
-		cudaSafeCall(cudaStreamSynchronize(stream_));
-		cv::cvtColor(rgb_,rgb_, cv::COLOR_BGRA2BGR);
-		cv::cvtColor(rgb_,rgb_, cv::COLOR_Lab2BGR);
-		cv::cvtColor(depth_,depth_, cv::COLOR_BGRA2BGR);
-		cv::cvtColor(depth_,depth_, cv::COLOR_Lab2BGR);
-	}
-}
-
 bool Source::thumbnail(cv::Mat &t) {
 	if (!impl_ && stream_ != 0) {
 		cudaSafeCall(cudaStreamSynchronize(stream_));
diff --git a/components/rgbd-sources/src/virtual.cpp b/components/rgbd-sources/src/virtual.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9d64f53c73b2902d8fe074d84a8192657c36d1fc
--- /dev/null
+++ b/components/rgbd-sources/src/virtual.cpp
@@ -0,0 +1,66 @@
+/*
+void Source::writeFrames(int64_t ts, const cv::Mat &rgb, const cv::Mat &depth) {
+	if (!impl_) {
+		UNIQUE_LOCK(mutex_,lk);
+		rgb.copyTo(rgb_);
+		depth.copyTo(depth_);
+		timestamp_ = ts;
+	}
+}
+
+void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uint> &depth, cudaStream_t stream) {
+	if (!impl_) {
+		UNIQUE_LOCK(mutex_,lk);
+		timestamp_ = ts;
+		rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
+		cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
+		depth_.create(depth.height(), depth.width(), CV_32SC1);
+		cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(uint), depth_.rows, cudaMemcpyDeviceToHost, stream));
+		//cudaSafeCall(cudaStreamSynchronize(stream));  // TODO:(Nick) Don't wait here.
+		stream_ = stream;
+		//depth_.convertTo(depth_, CV_32F, 1.0f / 1000.0f);
+	} else {
+		LOG(ERROR) << "writeFrames cannot be done on this source: " << getURI();
+	}
+}
+
+void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<float> &depth, cudaStream_t stream) {
+	if (!impl_) {
+		UNIQUE_LOCK(mutex_,lk);
+		timestamp_ = ts;
+		rgb.download(rgb_, stream);
+		//rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
+		//cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
+		depth.download(depth_, stream);
+		//depth_.create(depth.height(), depth.width(), CV_32FC1);
+		//cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(float), depth_.rows, cudaMemcpyDeviceToHost, stream));
+		
+		stream_ = stream;
+		cudaSafeCall(cudaStreamSynchronize(stream_));
+		cv::cvtColor(rgb_,rgb_, cv::COLOR_BGRA2BGR);
+		cv::cvtColor(rgb_,rgb_, cv::COLOR_Lab2BGR);
+
+		if (callback_) callback_(timestamp_, rgb_, depth_);
+	}
+}
+
+void Source::writeFrames(int64_t ts, const ftl::cuda::TextureObject<uchar4> &rgb, const ftl::cuda::TextureObject<uchar4> &rgb2, cudaStream_t stream) {
+	if (!impl_) {
+		UNIQUE_LOCK(mutex_,lk);
+		timestamp_ = ts;
+		rgb.download(rgb_, stream);
+		//rgb_.create(rgb.height(), rgb.width(), CV_8UC4);
+		//cudaSafeCall(cudaMemcpy2DAsync(rgb_.data, rgb_.step, rgb.devicePtr(), rgb.pitch(), rgb_.cols * sizeof(uchar4), rgb_.rows, cudaMemcpyDeviceToHost, stream));
+		rgb2.download(depth_, stream);
+		//depth_.create(depth.height(), depth.width(), CV_32FC1);
+		//cudaSafeCall(cudaMemcpy2DAsync(depth_.data, depth_.step, depth.devicePtr(), depth.pitch(), depth_.cols * sizeof(float), depth_.rows, cudaMemcpyDeviceToHost, stream));
+		
+		stream_ = stream;
+		cudaSafeCall(cudaStreamSynchronize(stream_));
+		cv::cvtColor(rgb_,rgb_, cv::COLOR_BGRA2BGR);
+		cv::cvtColor(rgb_,rgb_, cv::COLOR_Lab2BGR);
+		cv::cvtColor(depth_,depth_, cv::COLOR_BGRA2BGR);
+		cv::cvtColor(depth_,depth_, cv::COLOR_Lab2BGR);
+	}
+}
+*/
\ No newline at end of file