diff --git a/applications/gui2/src/views/camera.cpp b/applications/gui2/src/views/camera.cpp
index f63455ab930911d8634be7b649e7fffe3bd42ddc..482a9505cb563e720a93ca7a542ef07eed552a40 100644
--- a/applications/gui2/src/views/camera.cpp
+++ b/applications/gui2/src/views/camera.cpp
@@ -627,7 +627,10 @@ CameraView::CameraView(ftl::gui2::Screen* parent, ftl::gui2::Camera* ctrl) :
 	panel_ = new ftl::gui2::MediaPanel(screen(), ctrl, this);
 	tools_ = new ftl::gui2::ToolPanel(screen(), ctrl, this);
 
-	imview_->setFlipped(ctrl->isVR());
+	imview_->setFlipped(ctrl->isVR() || ctrl_->value("flip_image", false));
+	ctrl_->on("flip_image", [this](){
+		imview_->setFlipped(ctrl_->isVR() || ctrl_->value("flip_image", false));
+	});
 
 	auto *mod = ctrl_->screen->getModule<ftl::gui2::Statistics>();
 	if (ctrl_->isMovable()) {
diff --git a/components/renderers/cpp/CMakeLists.txt b/components/renderers/cpp/CMakeLists.txt
index bf3c36d83994ba0f5055816a0f3ccb070147271a..5346849e22b23a31471ca6b3a1b52dc8eb37209d 100644
--- a/components/renderers/cpp/CMakeLists.txt
+++ b/components/renderers/cpp/CMakeLists.txt
@@ -12,6 +12,7 @@ add_library(ftlrender
 	src/overlay.cpp
 	src/gltexture.cpp
 	src/touch.cu
+	src/GLRender.cpp
 	#src/assimp_render.cpp
 	#src/assimp_scene.cpp
 )
diff --git a/components/renderers/cpp/include/ftl/render/GLRender.hpp b/components/renderers/cpp/include/ftl/render/GLRender.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7959a6ebb0ecaec5b899563a82e3f5b9f73a92c2
--- /dev/null
+++ b/components/renderers/cpp/include/ftl/render/GLRender.hpp
@@ -0,0 +1,75 @@
+#ifndef _FTL_RENDER_GL_HPP_
+#define _FTL_RENDER_GL_HPP_
+
+#include <ftl/render/renderer.hpp>
+#include <ftl/rgbd/frameset.hpp>
+#include <ftl/render/render_params.hpp>
+#include <ftl/codecs/channels.hpp>
+#include <ftl/utility/gltexture.hpp>
+
+// Forward declare
+namespace nanogui {
+class GLShader;
+}
+
+namespace ftl {
+namespace render {
+
+class Colouriser;
+
+class GLRender : public ftl::render::FSRenderer {
+	public:
+	explicit GLRender(nlohmann::json &config);
+	~GLRender();
+
+	void begin(ftl::rgbd::Frame &, ftl::codecs::Channel) override;
+	void end() override;
+
+	bool submit(ftl::data::FrameSet *in, ftl::codecs::Channels<0>, const Eigen::Matrix4d &t) override;
+
+	void render() override;
+
+	void blend(ftl::codecs::Channel) override;
+
+	void cancel() override;
+
+	private:
+	ftl::rgbd::Frame *out_=nullptr;
+
+	ftl::render::Colouriser *colouriser_;
+
+	cv::Scalar background_;
+	float3 light_dir_;
+	uchar4 light_diffuse_;
+	uchar4 light_ambient_;
+	ftl::render::Parameters params_;
+	float3 light_pos_;
+
+	ftl::codecs::Channel out_chan_;
+
+	struct SubmitState {
+		ftl::rgbd::FrameSet *fs;
+		ftl::codecs::Channels<0> channels;
+		Eigen::Matrix4d transform;
+	};
+
+	std::vector<SubmitState> sets_;
+
+	nanogui::GLShader *shader_;
+	std::vector<float> vertices_;
+	std::vector<int> indices_;
+	unsigned int outTex_=0;
+	unsigned int outPBO_=0;
+	unsigned int fbuf_=0;
+	unsigned int depthbuf_=0;
+	cudaGraphicsResource *cuda_res_=nullptr;
+
+	//std::unordered_map<unsigned int, ftl::utility::GLTexture> textures_;
+	ftl::utility::GLTexture depth_input_;
+	ftl::utility::GLTexture colour_input_;
+};
+
+}
+}
+
+#endif  // _FTL_RENDER_GL_HPP_
diff --git a/components/renderers/cpp/src/GLRender.cpp b/components/renderers/cpp/src/GLRender.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d7d66aa82c72586d0ab8b5d8b5b25c2a98882b88
--- /dev/null
+++ b/components/renderers/cpp/src/GLRender.cpp
@@ -0,0 +1,322 @@
+#include <nanogui/glutil.h>
+#include <ftl/render/GLRender.hpp>
+#include <ftl/exception.hpp>
+#include <cuda_runtime.h>
+#include <cuda_gl_interop.h>
+
+#include <loguru.hpp>
+
+using ftl::render::GLRender;
+using ftl::codecs::Channel;
+
+namespace {
+	constexpr char const *const rendVertexShader =
+		R"(#version 330
+		in vec3 vertex;
+		uniform mat4 pose;
+		uniform sampler2D depthMap;
+		uniform float cx;
+		uniform float cy;
+		uniform float f;
+		uniform float width;
+		uniform float height;
+		out float depths;
+		out vec2 uvs;
+
+		void main() {
+			vec2 tc = vec2(vertex.x / width, vertex.y / height);
+			uvs = tc;
+			float d = float(texture(depthMap, tc));
+			depths = d;
+			float x = ((vertex.x+cx) / f) * d;
+			float y = ((vertex.y+cy) / f) * d;
+			vec4 v = vec4(x, y, d, 1.0);
+			vec4 vert = pose*v;
+			gl_Position = vert;
+		})";
+
+	constexpr char const *const rendFragmentShader =
+		R"(#version 330
+		layout(location = 0) out vec4 color;
+		uniform sampler2D colourMap;
+		in vec2 uv;
+
+		void main() {
+			color = vec4(texture(colourMap, uv).rgb,1.0);
+		})";
+
+	constexpr char const *const rendGeomShader =
+		R"(#version 330
+		layout (triangles) in;
+		layout (triangle_strip, max_vertices=3) out;
+		in float depths[];
+		in vec2 uvs[];
+		out vec2 uv;
+
+		void main() {
+			if (depths[0] > 0 && depths[1] > 0 && depths[2] > 0) {  
+				gl_Position = gl_in[0].gl_Position; 
+				uv = uvs[0];
+				EmitVertex();
+				gl_Position = gl_in[1].gl_Position; 
+				uv = uvs[1];
+				EmitVertex();
+				gl_Position = gl_in[2].gl_Position;
+				uv = uvs[2]; 
+				EmitVertex();
+				EndPrimitive();
+			}
+		})";
+}
+
+GLRender::GLRender(nlohmann::json &config) : ftl::render::FSRenderer(config) {
+	shader_ = new nanogui::GLShader();
+}
+
+GLRender::~GLRender() {
+	delete shader_;
+}
+
+void GLRender::begin(ftl::rgbd::Frame &out, ftl::codecs::Channel c) {
+	out_ = &out;
+	out.createTexture<uchar4>(c);
+}
+
+void GLRender::end() {
+	sets_.clear();
+	out_ = nullptr;
+}
+
+bool GLRender::submit(ftl::data::FrameSet *in, ftl::codecs::Channels<0> chans, const Eigen::Matrix4d &t) {
+	auto &s = sets_.emplace_back();
+	s.fs = in;
+	s.channels = chans;
+	s.transform = t;
+
+	// Generate depth map opengl textures
+	/*for (const auto &f : in->frames) {
+		auto &tex = textures_[f.id().id];
+		tex.copyFrom(f.get<cv::cuda::GpuMat>(Channel::Depth));
+	}*/
+
+	return true;
+}
+
+void GLRender::render() {
+	// Create render target.
+
+	if (sets_.empty() || !out_) return;
+
+	glDisable(GL_STENCIL_TEST);
+	glDisable(GL_SCISSOR_TEST);
+	glEnable(GL_DEPTH_TEST);
+
+	const auto &outintrin = out_->getLeft();
+
+	if (outTex_ == 0) {
+		fbuf_ = 0;
+		glGenFramebuffers(1, &fbuf_);
+		glBindFramebuffer(GL_FRAMEBUFFER, fbuf_);
+
+		depthbuf_ = 0;
+		glGenRenderbuffers(1, &depthbuf_);
+		glBindRenderbuffer(GL_RENDERBUFFER, depthbuf_);
+		glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, outintrin.width, outintrin.height);
+
+		glGenTextures(1, &outTex_);
+		glBindTexture(GL_TEXTURE_2D, outTex_);
+		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, outintrin.width, outintrin.height, 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+		glBindTexture(GL_TEXTURE_2D, 0);
+
+		glGenBuffers(1, &outPBO_);
+		// Make this the current UNPACK buffer (OpenGL is state-based)
+		glBindBuffer(GL_PIXEL_PACK_BUFFER, outPBO_);
+		// Allocate data for the buffer. 4-channel 8-bit image or 1-channel float
+		glBufferData(GL_PIXEL_PACK_BUFFER, outintrin.width * outintrin.height * 4, NULL, GL_DYNAMIC_COPY);
+
+		cudaSafeCall(cudaGraphicsGLRegisterBuffer(&cuda_res_, outPBO_, cudaGraphicsRegisterFlagsNone)); // cudaGraphicsRegisterFlagsWriteDiscard
+		glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+
+		//glBindTexture(GL_TEXTURE_2D, outTex_.texture());
+		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, outTex_, 0);
+		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthbuf_);
+
+		glBindRenderbuffer(GL_RENDERBUFFER, 0);
+		glBindTexture(GL_TEXTURE_2D, 0);
+
+		auto err = glGetError();
+		if (err != 0) LOG(ERROR) << "OpenGL FB error: " << err;
+
+		//GLenum DrawBuffers[1] = {GL_COLOR_ATTACHMENT0};
+		//glDrawBuffers(1, DrawBuffers);
+
+		if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+			throw FTL_Error("Could not create opengl frame buffer");
+		}
+
+		glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+		err = glGetError();
+		if (err != 0) LOG(ERROR) << "OpenGL FB error: " << err;
+		LOG(INFO) << "GL Frame Buffer Created";
+	}
+
+	if (fbuf_ == 0) {
+		LOG(ERROR) << "No framebuffer";
+	}
+
+	auto cvstream = cv::cuda::StreamAccessor::wrapStream(out_->stream());
+	/*auto colmati = outTex_.map(out_->stream());
+	colmati.setTo(cv::Scalar(255,0,0,255), cvstream);
+	outTex_.unmap(out_->stream());
+	cudaSafeCall(cudaStreamSynchronize(out_->stream()));*/
+
+	glBindFramebuffer(GL_FRAMEBUFFER, fbuf_);
+	glViewport(0,0,outintrin.width,outintrin.height);
+	//LOG(INFO) << "Render " << outintrin.width << "x" << outintrin.height << " " << fbuf_;
+	glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+	//GLfloat clearColor[4] = {255.0f,0.0f,0.0f,255.0f};
+	//glClearBufferfv( GL_COLOR, 0, clearColor );
+
+	auto err = glGetError();
+	if (err != 0) LOG(ERROR) << "OpenGL error: " << err;
+
+	// Build+upload vertices and indices if not already there.
+	if (vertices_.empty()) {
+		shader_->init("RenderShader", rendVertexShader, rendFragmentShader, rendGeomShader);
+		shader_->bind();
+		const auto &f = sets_[0].fs->firstFrame().cast<ftl::rgbd::Frame>();
+		const ftl::rgbd::Camera &intrin = f.getLeft();
+
+		int ix = 0;
+		int iix = 0;
+		vertices_.resize(intrin.width*intrin.height*3);
+		indices_.resize(intrin.width*intrin.height*3*4);
+
+		for (int y=0; y<intrin.height; ++y) {
+		for (int x=0; x<intrin.width; ++x) {
+			//float3 p = intrin.screenToCam(x,y,-1.0f);
+			//LOG(INFO) << "POINT " << p.x << "," << p.y << "," << p.z;
+			vertices_[ix++] = float(x);
+			vertices_[ix++] = float(y);
+			vertices_[ix++] = 1.0f;
+			//vertices_[ix++] = float(x) / float(intrin.width);
+			//vertices_[ix++] = float(y) / float(intrin.height);
+
+			indices_[iix++] = x + y * intrin.width;
+			indices_[iix++] = x - 1 + y * intrin.width;
+			indices_[iix++] = x + (y+1) * intrin.width;
+
+			indices_[iix++] = x + y * intrin.width;
+			indices_[iix++] = x + 1 + y * intrin.width;
+			indices_[iix++] = x + (y+1) * intrin.width;
+
+			indices_[iix++] = x + y * intrin.width;
+			indices_[iix++] = x - 1 + y * intrin.width;
+			indices_[iix++] = x + (y-1) * intrin.width;
+
+			indices_[iix++] = x + y * intrin.width;
+			indices_[iix++] = x + 1 + y * intrin.width;
+			indices_[iix++] = x + (y-1) * intrin.width;
+		}
+		}
+
+		shader_->uploadAttrib("vertex", vertices_.size()/3, 3, sizeof(float)*3, GL_FLOAT, false, vertices_.data());
+		shader_->uploadAttrib("indices", indices_.size(), 1, sizeof(int), GL_UNSIGNED_INT, true, indices_.data());
+	} else {
+		shader_->bind();
+	}
+
+	float l = outintrin.screenToCam(0u,outintrin.height/2u,0.1f).x;
+	float r = outintrin.screenToCam(outintrin.width,outintrin.height/2u,0.1f).x;
+	float t = outintrin.screenToCam(outintrin.width/2u,0u,0.1f).y;
+	float b = outintrin.screenToCam(outintrin.width/2u,outintrin.height,0.1f).y;
+	Eigen::Matrix4f p = nanogui::frustum(l, r, b, t, 0.1f, 12.0f);//.transpose();
+	Eigen::Matrix4f v = out_->getPose().cast<float>().inverse();
+
+	for (const auto &s : sets_) {
+		for (const auto &f : s.fs->frames) {
+			const auto &rgbdf = f.cast<ftl::rgbd::Frame>();
+			const auto &intrin = rgbdf.getLeft();
+
+			Eigen::Matrix4f m = rgbdf.getPose().cast<float>();//.inverse();
+			m = p * v * m;
+			m = m; //.transpose();
+
+			depth_input_.copyFrom(f.get<cv::cuda::GpuMat>(Channel::Depth));
+			colour_input_.copyFrom(f.get<cv::cuda::GpuMat>(Channel::Colour));
+
+			// Set pose
+			shader_->setUniform("pose", m);
+			shader_->setUniform("width", float(intrin.width));
+			shader_->setUniform("height", float(intrin.height));
+			shader_->setUniform("f", float(intrin.fx));
+			shader_->setUniform("cx", float(intrin.cx));
+			shader_->setUniform("cy", float(intrin.cy));
+
+			GLuint tex = depth_input_.texture();
+			if (tex == 0) LOG(ERROR) << "No depth texture";
+			glActiveTexture(GL_TEXTURE0);
+    		glBindTexture(GL_TEXTURE_2D, tex);
+			shader_->setUniform("depthMap", 0);
+
+			tex = colour_input_.texture();
+			if (tex == 0) LOG(ERROR) << "No colour texture";
+			glActiveTexture(GL_TEXTURE1);
+    		glBindTexture(GL_TEXTURE_2D, tex);
+			shader_->setUniform("colourMap", 1);
+
+			// Set the depth map texture
+			//glDrawElements(GL_TRIANGLES, (GLsizei) indices_.size(), GL_UNSIGNED_INT, (const void *)(0));
+			//shader_.drawArray(GL_POINTS, 0, vertices_.size()/3);
+			shader_->drawIndexed(GL_TRIANGLES, 0, indices_.size()/3);
+
+			//LOG(INFO) << "Points drawn: " << vertices_.size()/3;
+		}
+	}
+
+	//LOG(INFO) << "GL Render done, copying output";
+
+	glBindBuffer(GL_PIXEL_PACK_BUFFER, outPBO_);
+	// Select the appropriate texture
+	glBindTexture(GL_TEXTURE_2D, outTex_);
+
+	//glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width_, height_, GL_BGRA, GL_UNSIGNED_BYTE, NULL);
+	glReadPixels(0, 0, outintrin.width, outintrin.height, GL_BGRA, GL_UNSIGNED_BYTE, 0);
+
+	glBindTexture(GL_TEXTURE_2D, 0);
+	glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+	glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+	//glFlush();
+	//glFinish();
+	err = glGetError();
+	if (err != 0) LOG(ERROR) << "OpenGL error: " << err;
+
+	void *devptr;
+	size_t size;
+	cudaSafeCall(cudaGraphicsMapResources(1, &cuda_res_, out_->stream()));
+	cudaSafeCall(cudaGraphicsResourceGetMappedPointer(&devptr, &size, cuda_res_));
+	cv::cuda::GpuMat colmat(outintrin.height, outintrin.width, CV_8UC4, devptr, outintrin.width*4);
+
+	//auto colmat = outTex_.map(out_->stream());
+	//colmat.setTo(cv::Scalar(255,0,0,255), cvstream);
+	//LOG(INFO) << "COLMAT: " << colmat.cols << ", " << colmat.rows << ", " << uint64_t(colmat.data);
+	colmat.copyTo(out_->get<cv::cuda::GpuMat>(Channel::Colour), cvstream);
+	//outTex_.unmap(out_->stream());
+
+	cudaSafeCall(cudaGraphicsUnmapResources(1, &cuda_res_, out_->stream()));
+}
+
+void GLRender::blend(ftl::codecs::Channel c) {
+
+}
+
+void GLRender::cancel() {
+
+}
diff --git a/components/renderers/cpp/src/gltexture.cpp b/components/renderers/cpp/src/gltexture.cpp
index 2bb106ecd0a225fe27f0dcf099c57720135518c4..0d2bc77f53387a6c7e6bf088aaa4609e9867ed21 100644
--- a/components/renderers/cpp/src/gltexture.cpp
+++ b/components/renderers/cpp/src/gltexture.cpp
@@ -57,14 +57,14 @@ void GLTexture::make(int width, int height, Type type) {
 		if (type_ == Type::BGRA) {
 			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);
 		} else if (type_ == Type::Float) {
-			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);
+			glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, width, height, 0, GL_RED, GL_FLOAT, nullptr);
 		}
 		log_error();
 
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 		log_error();
 
 		glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
@@ -77,7 +77,7 @@ void GLTexture::make(int width, int height, Type type) {
 		// Allocate data for the buffer. 4-channel 8-bit image or 1-channel float
 		glBufferData(GL_PIXEL_UNPACK_BUFFER, stride_ * height * 4, NULL, GL_DYNAMIC_COPY);
 
-		cudaSafeCall(cudaGraphicsGLRegisterBuffer(&cuda_res_, glbuf_, cudaGraphicsRegisterFlagsWriteDiscard));
+		cudaSafeCall(cudaGraphicsGLRegisterBuffer(&cuda_res_, glbuf_, cudaGraphicsRegisterFlagsNone)); // cudaGraphicsRegisterFlagsWriteDiscard
 		glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
 		log_error();
 	}
@@ -175,13 +175,13 @@ void GLTexture::copyFrom(const cv::Mat &im, cudaStream_t stream) {
 
 void GLTexture::copyFrom(const cv::cuda::GpuMat &im, cudaStream_t stream) {
 
-	if (im.rows == 0 || im.cols == 0 || im.channels() != 4 || im.type() != CV_8UC4) {
+	if (im.rows == 0 || im.cols == 0 || !(im.type() == CV_8UC4 || im.type() == CV_32F)) {
 		LOG(ERROR) << __FILE__ << ":" << __LINE__ << ": " << "bad OpenCV format";
 		return;
 	}
 
 	auto cvstream = cv::cuda::StreamAccessor::wrapStream(stream);
-	make(im.cols, im.rows, ftl::utility::GLTexture::Type::BGRA);
+	make(im.cols, im.rows, (im.type() == CV_32F) ? ftl::utility::GLTexture::Type::Float : ftl::utility::GLTexture::Type::BGRA);
 	auto dst = map(stream);
 	im.copyTo(dst, cvstream);
 	unmap(stream);
diff --git a/components/streams/src/renderers/screen_render.cpp b/components/streams/src/renderers/screen_render.cpp
index a530750cce22f889814929507de54790bc435d28..4a0c5f2b75681e46cbd12df46cdbcb9f80ba27aa 100644
--- a/components/streams/src/renderers/screen_render.cpp
+++ b/components/streams/src/renderers/screen_render.cpp
@@ -29,9 +29,11 @@ ScreenRender::ScreenRender(ftl::render::Source *host, ftl::stream::Feed *feed)
 		"name"
 	});*/
 
-	renderer_ = std::unique_ptr<ftl::render::CUDARender>(
-		ftl::create<ftl::render::CUDARender>(host_, "renderer")
-	);
+	//renderer_ = nullptr;
+	
+	/*std::unique_ptr<ftl::render::GLRender>(
+		ftl::create<ftl::render::GLRender>(host_, "renderer")
+	);*/
 
 	intrinsics_ = ftl::create<ftl::Configurable>(host_, "intrinsics");
 
@@ -39,10 +41,10 @@ ScreenRender::ScreenRender(ftl::render::Source *host, ftl::stream::Feed *feed)
 		calibration_uptodate_.clear();
 	});
 
-	renderer_->value("projection", 0);
+	/*renderer_->value("projection", 0);
 	renderer_->onAny({"projection"}, [this]() {
 		calibration_uptodate_.clear();
-	});
+	});*/
 
 	filter_ = nullptr;
 	std::string source = host_->value("source", std::string(""));
@@ -82,9 +84,45 @@ bool ScreenRender::capture(int64_t ts) {
 	return true;
 }
 
+void ScreenRender::_createGLRender() {
+	if (renderer_) renderer_.reset();
+
+	renderer_ = std::unique_ptr<ftl::render::GLRender>(
+		ftl::create<ftl::render::GLRender>(host_, "renderer")
+	);
+}
+
+void ScreenRender::_createCUDARender() {
+	if (renderer_) renderer_.reset();
+	
+	renderer_ = std::unique_ptr<ftl::render::CUDARender>(
+		ftl::create<ftl::render::CUDARender>(host_, "renderer")
+	);
+
+	renderer_->value("projection", 0);
+	renderer_->onAny({"projection"}, [this]() {
+		calibration_uptodate_.clear();
+	});
+}
+
+void ScreenRender::_checkRenderer() {
+	auto t = host_->value("renderer_type", std::string("CUDA"));
+
+	if (renderer_ && rendtype_ == t) return;
+
+	rendtype_ = t;
+	if (t == "GL") {
+		_createGLRender();
+	} else {
+		_createCUDARender();
+	}
+}
+
 bool ScreenRender::retrieve(ftl::data::Frame &frame_out) {
 	//auto input = std::atomic_load(&input_);
 
+	_checkRenderer();
+
 	my_id_ = frame_out.frameset();
 	auto sets = filter_->getLatestFrameSets();
 	bool data_only = host_->value("data_only", false);
@@ -222,7 +260,7 @@ bool ScreenRender::retrieve(ftl::data::Frame &frame_out) {
 			if (!post_pipe_) {
 				post_pipe_ = ftl::config::create<ftl::operators::Graph>(host(), "post_filters");
 				post_pipe_->append<ftl::operators::Poser>("poser");
-				post_pipe_->append<ftl::operators::FXAA>("fxaa");
+				//post_pipe_->append<ftl::operators::FXAA>("fxaa");
 				post_pipe_->append<ftl::operators::GTAnalysis>("gtanalyse");
 			}
 
@@ -230,7 +268,7 @@ bool ScreenRender::retrieve(ftl::data::Frame &frame_out) {
 			cudaSafeCall(cudaStreamSynchronize(rgbdframe.stream()));
 
 			if (host_->value("enable_touch", false)) {
-				ftl::render::collision2touch(rgbdframe, renderer_->getCollisions(), sets, my_id_, host_->value("touch_min", 0.01f), host_->value("touch_max", 0.05f));
+				//ftl::render::collision2touch(rgbdframe, renderer_->getCollisions(), sets, my_id_, host_->value("touch_min", 0.01f), host_->value("touch_max", 0.05f));
 			}
 		}
 
diff --git a/components/streams/src/renderers/screen_render.hpp b/components/streams/src/renderers/screen_render.hpp
index 89320aed4b6e45a5cb73e693b25901470060b41b..7b86400bfec6a3e4e1eae33ace428812847fbcf3 100644
--- a/components/streams/src/renderers/screen_render.hpp
+++ b/components/streams/src/renderers/screen_render.hpp
@@ -4,7 +4,7 @@
 #include <ftl/data/creators.hpp>
 #include <ftl/data/new_frameset.hpp>
 #include <ftl/render/renderer.hpp>
-#include <ftl/render/CUDARender.hpp>
+#include <ftl/render/GLRender.hpp>
 #include <ftl/streams/feed.hpp>
 
 #include "../baserender.hpp"
@@ -32,11 +32,12 @@ class ScreenRender : public ftl::render::BaseSourceImpl {
 	ftl::stream::Feed *feed_;
 	ftl::stream::Feed::Filter *filter_;
 	ftl::data::FrameSetPtr input_;
-	std::unique_ptr<ftl::render::CUDARender> renderer_;
+	std::unique_ptr<ftl::render::FSRenderer> renderer_;
 	ftl::Configurable *intrinsics_;
 	uint32_t my_id_;
 	ftl::operators::Graph *post_pipe_;
 	std::atomic_flag calibration_uptodate_;
+	std::string rendtype_;
 
 	/*struct AudioMixerMapping {
 		int64_t last_timestamp=0;
@@ -44,6 +45,10 @@ class ScreenRender : public ftl::render::BaseSourceImpl {
 	};
 
 	std::unordered_map<uint32_t, AudioMixerMapping> mixmap_;*/
+
+	void _createGLRender();
+	void _createCUDARender();
+	void _checkRenderer();
 };
 
 }