diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index 0407bd45c56417199195ab104bcbd45e8d407b29..53a0d0593a457921cc82dfe2759484a1967f18dc 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -129,7 +129,29 @@ void ftl::gui::Camera::drawUpdated(std::vector<ftl::rgbd::FrameSet*> &fss) {
 }
 
 void ftl::gui::Camera::draw(std::vector<ftl::rgbd::FrameSet*> &fss) {
-	if (fid_ != 255) return;
+	if (fid_ != 255) {
+		for (auto *fs : fss) {
+			if (!usesFrameset(fs->id)) continue;
+
+			ftl::rgbd::Frame *frame = nullptr;
+
+			if ((size_t)fid_ >= fs->frames.size()) return;
+			frame = &fs->frames[fid_];
+
+			auto &buf = colouriser_->colourise(*frame, channel_, 0);
+
+			// For non-virtual cameras, copy the CUDA texture into the opengl
+			// texture device-to-device.
+			texture1_.make(buf.width(), buf.height());
+			auto dst1 = texture1_.map(0);
+			cudaMemcpy2D(dst1.data, dst1.step1(), buf.devicePtr(), buf.pitch(), buf.width()*4, buf.height(), cudaMemcpyDeviceToDevice);
+			texture1_.unmap(0);
+
+			width_ = texture1_.width();
+			height_ = texture1_.height();
+			return;
+		}
+	}
 	//if (fsid_ >= fss.size()) return;
 
 	//auto &fs = *fss[fsid_];
@@ -270,6 +292,14 @@ void ftl::gui::Camera::_draw(std::vector<ftl::rgbd::FrameSet*> &fss) {
 	frame_.reset();
 	frame_.setOrigin(&state_);
 
+	// Make sure an OpenGL pixel buffer exists
+	texture1_.make(state_.getLeft().width, state_.getLeft().height);
+	if (isStereo()) texture2_.make(state_.getRight().width, state_.getRight().height);
+
+	// Map the GL pixel buffer to a GpuMat
+	frame_.create<cv::cuda::GpuMat>(Channel::Colour) = texture1_.map(renderer_->getCUDAStream());
+	if (isStereo()) frame_.create<cv::cuda::GpuMat>(Channel::Colour2) = texture2_.map((renderer2_) ? renderer2_->getCUDAStream() : 0);
+
 	{
 		FTL_Profile("Render",0.034);
 		renderer_->begin(frame_, Channel::Colour);
@@ -317,10 +347,17 @@ void ftl::gui::Camera::_draw(std::vector<ftl::rgbd::FrameSet*> &fss) {
 
 	channels_ = frame_.getChannels();
 
+	// Unmap GL buffer from CUDA and finish updating GL texture
+	texture1_.unmap(renderer_->getCUDAStream());
+	if (isStereo()) texture2_.unmap(renderer2_->getCUDAStream());
+
+	width_ = texture1_.width();
+	height_ = texture1_.height();
+
 	if (isStereo()) {
-		_downloadFrames(frame_.getTexture<uchar4>(Channel::Colour), frame_.getTexture<uchar4>(Channel::Colour2));
+		//_downloadFrames(frame_.getTexture<uchar4>(Channel::Colour), frame_.getTexture<uchar4>(Channel::Colour2));
 	} else {
-		_downloadFrame(frame_.getTexture<uchar4>(Channel::Colour));
+		//_downloadFrame(frame_.getTexture<uchar4>(Channel::Colour));
 	}
 
 	if (screen_->root()->value("show_poses", false)) {
@@ -358,38 +395,6 @@ void ftl::gui::Camera::_draw(std::vector<ftl::rgbd::FrameSet*> &fss) {
 	}
 }
 
-void ftl::gui::Camera::_downloadFrames(ftl::cuda::TextureObject<uchar4> &a, ftl::cuda::TextureObject<uchar4> &b) {
-	im1_.create(cv::Size(a.width(), a.height()), CV_8UC4);
-	a.to_gpumat().download(im1_);
-
-	// OpenGL (0,0) bottom left
-	cv::flip(im1_, im1_, 0);
-
-	width_ = im1_.cols;
-	height_ = im1_.rows;
-
-	im2_.create(cv::Size(b.width(), b.height()), CV_8UC4);
-	b.to_gpumat().download(im2_);
-	cv::flip(im2_, im2_, 0);
-
-	if (im2_.cols != im1_.cols || im2_.rows != im1_.rows) {
-		throw FTL_Error("Left and right images are different sizes");
-	}
-}
-
-void ftl::gui::Camera::_downloadFrame(ftl::cuda::TextureObject<uchar4> &a) {
-	im1_.create(cv::Size(a.width(), a.height()), CV_8UC4);
-	a.to_gpumat().download(im1_);
-
-	// OpenGL (0,0) bottom left
-	cv::flip(im1_, im1_, 0);
-
-	width_ = im1_.cols;
-	height_ = im1_.rows;
-
-	im2_ = cv::Mat();
-}
-
 void ftl::gui::Camera::update(int fsid, const ftl::codecs::Channels<0> &c) {
 	if (!isVirtual() && ((1 << fsid) & fsmask_)) {
 		channels_ = c;
@@ -408,11 +413,6 @@ void ftl::gui::Camera::update(std::vector<ftl::rgbd::FrameSet*> &fss) {
 	//if (fss.size() <= fsid_) return;
 	if (fid_ == 255) {
 		name_ = "Virtual Camera";
-		// Do a draw if not active. If active the draw function will be called
-		// directly.
-		if (screen_->activeCamera() != this) {
-			//_draw(fss);
-		}
 	} else {
 		for (auto *fs : fss) {
 			if (!usesFrameset(fs->id)) continue;
@@ -422,13 +422,6 @@ void ftl::gui::Camera::update(std::vector<ftl::rgbd::FrameSet*> &fss) {
 			if ((size_t)fid_ >= fs->frames.size()) return;
 			frame = &fs->frames[fid_];
 
-			auto &buf = colouriser_->colourise(*frame, channel_, 0);
-			if (isStereo() && frame->hasChannel(Channel::Right)) {
-				_downloadFrames(buf, frame->createTexture<uchar4>(Channel::Right));
-			} else {
-				_downloadFrame(buf);
-			}
-
 			auto n = frame->get<std::string>("name");
 			if (n) {
 				name_ = *n;
@@ -715,10 +708,10 @@ const void ftl::gui::Camera::captureFrame() {
 		{
 			UNIQUE_LOCK(mutex_, lk);
 			if (im1_.rows != 0) {
-				texture1_.update(im1_);
+				//texture1_.update(im1_);
 			}
 			if (isStereo() && im2_.rows != 0) {
-				texture2_.update(im2_);
+				//texture2_.update(im2_);
 			}
 		}
 	}
diff --git a/applications/gui/src/camera.hpp b/applications/gui/src/camera.hpp
index 9e034c83a9509aa7663648d3f12ecc8390b39bd0..8a62198d966686631a9fe299efc531bd50572b90 100644
--- a/applications/gui/src/camera.hpp
+++ b/applications/gui/src/camera.hpp
@@ -166,8 +166,6 @@ class Camera {
 	float baseline_;
 	#endif
 
-	void _downloadFrames(ftl::cuda::TextureObject<uchar4> &, ftl::cuda::TextureObject<uchar4> &);
-	void _downloadFrame(ftl::cuda::TextureObject<uchar4> &);
 	void _draw(std::vector<ftl::rgbd::FrameSet*> &fss);
 };
 
diff --git a/applications/gui/src/gltexture.cpp b/applications/gui/src/gltexture.cpp
index d12c193796ededa636c096de35f9809cad7857e3..ec83997a3ba72ad744e4fd429ccc89151f24247a 100644
--- a/applications/gui/src/gltexture.cpp
+++ b/applications/gui/src/gltexture.cpp
@@ -3,10 +3,20 @@
 #include <nanogui/opengl.h>
 #include <loguru.hpp>
 
+#include <ftl/cuda_common.hpp>
+#include <cuda_gl_interop.h>
+
+#include <ftl/exception.hpp>
+
 using ftl::gui::GLTexture;
 
 GLTexture::GLTexture() {
 	glid_ = std::numeric_limits<unsigned int>::max();
+	glbuf_ = std::numeric_limits<unsigned int>::max();
+	cuda_res_ = nullptr;
+	width_ = 0;
+	height_ = 0;
+	changed_ = true;
 }
 
 GLTexture::~GLTexture() {
@@ -14,6 +24,7 @@ GLTexture::~GLTexture() {
 }
 
 void GLTexture::update(cv::Mat &m) {
+	LOG(INFO) << "DEPRECATED";
 	if (m.rows == 0) return;
 	if (glid_ == std::numeric_limits<unsigned int>::max()) {
 		glGenTextures(1, &glid_);
@@ -25,10 +36,98 @@ void GLTexture::update(cv::Mat &m) {
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 	} else {
-		glBindTexture(GL_TEXTURE_2D, glid_);
+		//glBindTexture(GL_TEXTURE_2D, glid_);
 		// TODO Allow for other formats
-		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m.cols, m.rows, 0, GL_BGRA, GL_UNSIGNED_BYTE, m.data);
+		//glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m.cols, m.rows, 0, GL_BGRA, GL_UNSIGNED_BYTE, m.data);
 	}
 	auto err = glGetError();
 	if (err != 0) LOG(ERROR) << "OpenGL Texture error: " << err;
 }
+
+void GLTexture::make(int width, int height) {
+	if (width != width_ || height != height_) {
+		free();
+	}
+
+	width_ = width;
+	height_ = height;
+
+	if (width == 0 || height == 0) {
+		throw FTL_Error("Invalid texture size");
+	}
+
+	if (glid_ == std::numeric_limits<unsigned int>::max()) {
+		glGenTextures(1, &glid_);
+		glBindTexture(GL_TEXTURE_2D, glid_);
+		//cv::Mat m(cv::Size(100,100), CV_8UC3);
+		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, 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_NEAREST);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+		//auto err = glGetError();
+		//if (err != 0) LOG(ERROR) << "OpenGL Texture error: " << err;
+
+		glBindTexture(GL_TEXTURE_2D, 0);
+
+		glGenBuffers(1, &glbuf_);
+		// Make this the current UNPACK buffer (OpenGL is state-based)
+		glBindBuffer(GL_PIXEL_UNPACK_BUFFER, glbuf_);
+		// Allocate data for the buffer. 4-channel 8-bit image
+		glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4, NULL, GL_DYNAMIC_COPY);
+
+		cudaSafeCall(cudaGraphicsGLRegisterBuffer(&cuda_res_, glbuf_, cudaGraphicsRegisterFlagsWriteDiscard));
+		glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+	}
+}
+
+void GLTexture::free() {
+	if (glid_ != std::numeric_limits<unsigned int>::max()) {
+		glDeleteTextures(1, &glid_);
+		glid_ = std::numeric_limits<unsigned int>::max();
+	}
+
+	if (glbuf_ != std::numeric_limits<unsigned int>::max()) {
+		cudaSafeCall(cudaGraphicsUnregisterResource( cuda_res_ ));
+		cuda_res_ = nullptr;
+		glDeleteBuffers(1, &glbuf_);
+		glbuf_ = std::numeric_limits<unsigned int>::max();
+	}
+}
+
+cv::cuda::GpuMat GLTexture::map(cudaStream_t stream) {
+	void *devptr;
+	size_t size;
+	cudaSafeCall(cudaGraphicsMapResources(1, &cuda_res_, stream));
+	cudaSafeCall(cudaGraphicsResourceGetMappedPointer(&devptr, &size, cuda_res_));
+	return cv::cuda::GpuMat(height_, width_, CV_8UC4, devptr);
+}
+
+void GLTexture::unmap(cudaStream_t stream) {
+	cudaSafeCall(cudaGraphicsUnmapResources(1, &cuda_res_, stream));
+	changed_ = true;
+
+	//glActiveTexture(GL_TEXTURE0);
+	glBindBuffer( GL_PIXEL_UNPACK_BUFFER, glbuf_);
+	// Select the appropriate texture
+	glBindTexture( GL_TEXTURE_2D, glid_);
+	// Make a texture from the buffer
+	glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, width_, height_, GL_BGRA, GL_UNSIGNED_BYTE, NULL);
+	glBindBuffer( GL_PIXEL_UNPACK_BUFFER, 0);
+}
+
+unsigned int GLTexture::texture() const {
+	if (glbuf_ < std::numeric_limits<unsigned int>::max()) {
+		/*//glActiveTexture(GL_TEXTURE0);
+		glBindBuffer( GL_PIXEL_UNPACK_BUFFER, glbuf_);
+		// Select the appropriate texture
+		glBindTexture( GL_TEXTURE_2D, glid_);
+		// Make a texture from the buffer
+		glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, width_, height_, GL_BGRA, GL_UNSIGNED_BYTE, NULL);
+		glBindBuffer( GL_PIXEL_UNPACK_BUFFER, 0);*/
+
+		return glid_;
+	} else {
+		return glid_;
+	}
+}
diff --git a/applications/gui/src/gltexture.hpp b/applications/gui/src/gltexture.hpp
index 88bf1a18bff29634f09c902c9a56707d74a9d61d..c9e5146bbc26ebfd3175bd37ad1113d0f3360415 100644
--- a/applications/gui/src/gltexture.hpp
+++ b/applications/gui/src/gltexture.hpp
@@ -3,6 +3,10 @@
 
 #include <opencv2/core/mat.hpp>
 
+#include <cuda_runtime.h>
+
+struct cudaGraphicsResource;
+
 namespace ftl {
 namespace gui {
 
@@ -12,11 +16,26 @@ class GLTexture {
 	~GLTexture();
 
 	void update(cv::Mat &m);
-	unsigned int texture() const { return glid_; }
+	void make(int width, int height);
+	unsigned int texture() const;
 	bool isValid() const { return glid_ != std::numeric_limits<unsigned int>::max(); }
 
+	cv::cuda::GpuMat map(cudaStream_t stream);
+	void unmap(cudaStream_t stream);
+
+	void free();
+
+	int width() const { return width_; }
+	int height() const { return height_; }
+
 	private:
 	unsigned int glid_;
+	unsigned int glbuf_;
+	int width_;
+	int height_;
+	bool changed_;
+
+	cudaGraphicsResource *cuda_res_;
 };
 
 }
diff --git a/applications/gui/src/main.cpp b/applications/gui/src/main.cpp
index 71afa31edd64936fb7ef534a2aa5e53208bbfd1a..2677d67358af7a7bb9906e2d1ee93b99a372df5b 100644
--- a/applications/gui/src/main.cpp
+++ b/applications/gui/src/main.cpp
@@ -8,11 +8,17 @@
 
 #include "screen.hpp"
 
+#include <cuda_gl_interop.h>
+
 
 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");
 
+	int cuda_device;
+	cudaSafeCall(cudaGetDevice(&cuda_device));
+	//cudaSafeCall(cudaGLSetGLDevice(cuda_device));
+
 	ftl::ctrl::Master *controller = new ftl::ctrl::Master(root, net);
 	controller->onLog([](const ftl::ctrl::LogEvent &e){
 		const int v = e.verbosity;
diff --git a/applications/gui/src/screen.cpp b/applications/gui/src/screen.cpp
index efd853eaf9fdc7596871284bc84a9c0b8624171e..7d144d5cbe727d110133281e5adec09b726ee3ab 100644
--- a/applications/gui/src/screen.cpp
+++ b/applications/gui/src/screen.cpp
@@ -38,7 +38,7 @@ namespace {
 		in vec2 vertex;
 		out vec2 uv;
 		void main() {
-			uv = vertex;
+			uv = vec2(vertex.x, 1.0 - vertex.y);
 			vec2 scaledVertex = (vertex * scaleFactor) + position;
 			gl_Position  = vec4(2.0*scaledVertex.x - 1.0,
 								2.0*scaledVertex.y - 1.0,
@@ -520,13 +520,14 @@ void ftl::gui::Screen::draw(NVGcontext *ctx) {
 	if (camera_) {
 		imageSize = {camera_->width(), camera_->height()};
 
+		glActiveTexture(GL_TEXTURE0);
 		mImageID = camera_->getLeft().texture();
 		leftEye_ = mImageID;
 		rightEye_ = camera_->getRight().texture();
 
 		//if (camera_->getChannel() != ftl::codecs::Channel::Left) { mImageID = rightEye_; }
 
-		if (mImageID < std::numeric_limits<unsigned int>::max() && imageSize[0] > 0) {
+		if (camera_->getLeft().isValid() && imageSize[0] > 0) {
 			auto mScale = (screenSize.cwiseQuotient(imageSize).minCoeff()) * zoom_;
 			Vector2f scaleFactor = mScale * imageSize.cwiseQuotient(screenSize);
 			Vector2f positionInScreen(pos_x_, pos_y_);
@@ -541,8 +542,10 @@ void ftl::gui::Screen::draw(NVGcontext *ctx) {
 			mShader.bind();
 			glActiveTexture(GL_TEXTURE0);
 			glBindTexture(GL_TEXTURE_2D, leftEye_);
+			//camera_->getLeft().texture();
 			glActiveTexture(GL_TEXTURE1);
 			glBindTexture(GL_TEXTURE_2D, (camera_->isStereo() && camera_->getRight().isValid()) ? rightEye_ : leftEye_);
+			//(camera_->isStereo() && camera_->getRight().isValid()) ? camera_->getRight().texture() : camera_->getLeft().texture();
 			mShader.setUniform("image1", 0);
 			mShader.setUniform("image2", 1);
 			mShader.setUniform("blendAmount", (camera_->isStereo()) ? root_->value("blending", 0.5f) : 1.0f);
@@ -550,6 +553,8 @@ void ftl::gui::Screen::draw(NVGcontext *ctx) {
 			mShader.setUniform("position", imagePosition);
 			mShader.drawIndexed(GL_TRIANGLES, 0, 2);
 			//glDisable(GL_SCISSOR_TEST);
+
+			glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
 		}
 	} else {
 		// Must periodically render the cameras here to update any thumbnails.
diff --git a/applications/gui/src/src_window.cpp b/applications/gui/src/src_window.cpp
index 5620e1928632dae4a9f34390fee702f5827bb526..07fcb3ce3b2191151eda2518ac10b1a02c7f62b4 100644
--- a/applications/gui/src/src_window.cpp
+++ b/applications/gui/src/src_window.cpp
@@ -368,26 +368,28 @@ void SourceWindow::draw(NVGcontext *ctx) {
 		UNIQUE_LOCK(mutex_, lk);
 		//refresh_thumbs_ = false;
 
-		if (thumbs_.size() < cameras_.size()) thumbs_.resize(cameras_.size());
+		//if (thumbs_.size() < cameras_.size()) thumbs_.resize(cameras_.size());
 
 		//for (size_t i=0; i<thumbs_.size(); ++i) {
 		int i = 0;
 		for (auto &camera : cameras_) {
 			cv::Mat t;
 			auto *cam = camera.second.camera;
-			if (cam) {
+			//if (cam) {
 				//cam->draw(framesets_);
-				if (cam->thumbnail(t)) {
-					thumbs_[i].update(t);
-				}
-			}
+			//	if (cam->thumbnail(t)) {
+			//		thumbs_[i].update(t);
+			//	}
+			//}
 
 			if (!camera.second.thumbview) camera.second.thumbview = new ftl::gui::ThumbView(ipanel_, screen_, cam);
+			camera.second.thumbview->setFixedSize(nanogui::Vector2i(320,180));
 
 			/*if ((size_t)ipanel_->childCount() < i+1) {
 				new ftl::gui::ThumbView(ipanel_, screen_, cam);
 			}*/
-			if (thumbs_[i].isValid()) dynamic_cast<nanogui::ImageView*>(camera.second.thumbview)->bindImage(thumbs_[i].texture());
+			//if (thumbs_[i].isValid()) dynamic_cast<nanogui::ImageView*>(camera.second.thumbview)->bindImage(thumbs_[i].texture());
+			if (cam->getLeft().isValid()) dynamic_cast<nanogui::ImageView*>(camera.second.thumbview)->bindImage(cam->getLeft().texture());
 			++i;
 		}
 
diff --git a/components/renderers/cpp/include/ftl/render/CUDARender.hpp b/components/renderers/cpp/include/ftl/render/CUDARender.hpp
index 4b74bcc490b9367eed7568a9a7d2de6b37db8e62..7a9d46e5b32befa9a1f3338ecf4212ed80562cac 100644
--- a/components/renderers/cpp/include/ftl/render/CUDARender.hpp
+++ b/components/renderers/cpp/include/ftl/render/CUDARender.hpp
@@ -35,6 +35,8 @@ class CUDARender : public ftl::render::Renderer {
 		params_.viewPortMode = mode;
 	}
 
+	cudaStream_t getCUDAStream() const { return stream_; }
+
 	protected:
 	void _renderChannel(ftl::rgbd::Frame &out, ftl::codecs::Channel channel_in, const Eigen::Matrix4d &t, cudaStream_t stream);