diff --git a/CMakeLists.txt b/CMakeLists.txt
index f2e08767ef9cb02acad177159f7c2f1129672ed2..2cab36015d7c3c7e42c026362e0b3958e3960882 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -394,8 +394,8 @@ if (WIN32) # TODO(nick) Should do based upon compiler (VS)
 	set(OS_LIBS "")
 else()
 	add_definitions(-DUNIX)
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -fPIC -msse3 -Wall")
-	set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_DEBUG -pg")
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -fPIC -msse3 -Wall -Werror=return-type")
+	set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -D_DEBUG -pg")
 	set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -mfpmath=sse")
 	set(OS_LIBS "dl")
 endif()
diff --git a/applications/gui/src/config_window.hpp b/applications/gui/src/config_window.hpp
index a7acd117116553f3e902bdc6640d4f67e71b1f30..06f989e0b6ee3ac328264dc2b2bf0245956e5c7c 100644
--- a/applications/gui/src/config_window.hpp
+++ b/applications/gui/src/config_window.hpp
@@ -21,7 +21,7 @@ class ConfigWindow : public nanogui::Window {
 
 	private:
 	ftl::ctrl::Master *ctrl_;
-	
+
 	void _buildForm(const std::string &uri);
 	void _addElements(nanogui::FormHelper *form, const std::string &suri);
 	bool exists(const std::string &uri);
diff --git a/applications/gui2/CMakeLists.txt b/applications/gui2/CMakeLists.txt
index 5dcee07d03f95a005f1eaefeeb0f72352151dc1e..d3d1a8c5887fcaf483a5874fb952fdbdab0e4af7 100644
--- a/applications/gui2/CMakeLists.txt
+++ b/applications/gui2/CMakeLists.txt
@@ -2,17 +2,34 @@
 #include_directories(${PROJECT_SOURCE_DIR}/reconstruct/include)
 #include_directories(${PROJECT_BINARY_DIR})
 
+function(add_gui_module NAME)
+	list(APPEND GUI2SRC "src/modules/${NAME}/control.cpp")
+
+	get_filename_component(FULLPATH "src/modules/${NAME}/view.cpp" ABSOLUTE)
+	if (EXISTS ${FULLPATH})
+		list(APPEND GUI2SRC "src/modules/${NAME}/view.cpp")
+	endif()
+
+	set(GUI2SRC ${GUI2SRC} PARENT_SCOPE)
+endfunction()
+
 set(GUI2SRC
 	src/main.cpp
 	src/inputoutput.cpp
 	src/screen.cpp
 	src/view.cpp
 	src/gltexture.cpp
-	src/modules/home.cpp
+	src/frameview.cpp
 )
 
+add_gui_module("config")
+add_gui_module("camera")
+add_gui_module("thumbnails")
+add_gui_module("renderer")
+add_gui_module("record")
+
 if (HAVE_OPENVR)
-	list(APPEND GUI2SRC "src/vr/vr.cpp")
+	#list(APPEND GUI2SRC "src/vr/vr.cpp")
 endif()
 
 # Various preprocessor definitions have been generated by NanoGUI
diff --git a/applications/gui2/README.md b/applications/gui2/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..0707c342bb38c3bc258b852ad2dbb5c9fc9b93d0
--- /dev/null
+++ b/applications/gui2/README.md
@@ -0,0 +1,51 @@
+GUI
+
+Nanogui based graphical user interface.
+
+General:
+ * Do not modify gui outside gui thread (main). Modifications must be done in
+   GUI callbacks or draw().
+ * Expensive processing should be moved out of gui thread (draw() and callbacks)
+ * Module is only required to implement Module. Each module is expected to be
+   loaded only once (this design decision could be modified).
+
+Classes
+
+Screen
+ * Implements main screen: toolbar and view
+ * Interface for registering new modules.
+ * Interface for adding/removing buttons
+ * Interface for setting active View. Inactive view is removed and destroyed if
+   no other references are remaining.
+ * Note: toolbar could be a module, but other modules likely assume it is
+   always available anyways.
+
+Module (controller)
+ * GUI module class wraps pointers for io, config and net. Initialization should
+   add necessary buttons to Screen
+ * Build necessary callbacks to process data from InputOutput to view.
+   Note: If callback passes data to view, callback handle should be owned by
+   the view or Module has to keep a nanogui reference to the View.
+
+View
+ * active view will be the main window; only one view can be active at time
+ * button callbacks (eg. those registered by module init) may change active view
+ * Destroyed when view is changed. Object lifetime can be used to remove
+   callbacks from InputOutput (TODO: only one active callback supported at the
+   moment)
+ * Implementations do not have to inherit from View. Popup/Window/Widget... can
+   be used to implement UI components available from any mode (config, record).
+
+InputOutput
+ * Contains all relevant datastructures and objects for FTL system.
+ * Network
+ * Pipelines (incl. rendering) TODO
+ * Audio player TODO
+ * Recording TODO
+
+
+NanoGUI notes:
+ * Window instances can only be deleted with dispose().
+ * If sub-windows are created, they have to hold to a reference to parent
+   object if they share resources (eg. in lambda callbacks
+   [ref = std::move(ref)] could be used).
diff --git a/applications/gui2/src/frameview.cpp b/applications/gui2/src/frameview.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ce135aa655684069222d10e98775af2a39c56e0d
--- /dev/null
+++ b/applications/gui2/src/frameview.cpp
@@ -0,0 +1,128 @@
+#include <loguru.hpp>
+
+#include <nanogui/screen.h>
+
+#include <ftl/cuda_common.hpp>
+#include <ftl/cuda_texture.hpp>
+#include <ftl/exception.hpp>
+
+#include "gltexture.hpp"
+#include "frameview.hpp"
+
+using ftl::gui2::FrameView;
+
+namespace {
+	// modify gl_Position to flip y axis (flip sign)
+	constexpr char const *const GLFrameViewVertexShader =
+		R"(#version 330
+		uniform vec2 scaleFactor;
+		uniform vec2 position;
+		in vec2 vertex;
+		out vec2 uv;
+		void main() {
+			uv = vertex;
+			vec2 scaledVertex = (vertex * scaleFactor) + position;
+			gl_Position  = vec4(2.0*scaledVertex.x - 1.0,
+								1.0 - 2.0*scaledVertex.y,
+								0.0, 1.0);
+
+		})";
+
+	// set color.w = 1.0f ; possibly bug: incorrect value from getTexture()?
+	constexpr char const *const GLFrameViewFragmentShader =
+		R"(#version 330
+		uniform sampler2D image;
+		out vec4 color;
+		in vec2 uv;
+		void main() {
+			color = texture(image, uv);
+			color.w = 1.0f;
+		})";
+
+}
+
+void buildFrameViewShader(nanogui::GLShader &shader) {
+
+	shader.init("GLFrameViewShader", GLFrameViewVertexShader,
+					GLFrameViewFragmentShader);
+
+	nanogui::MatrixXu indices(3, 2);
+	indices.col(0) << 0, 1, 2;
+	indices.col(1) << 2, 3, 1;
+
+	nanogui::MatrixXf vertices(2, 4);
+	vertices.col(0) << 0, 0;
+	vertices.col(1) << 1, 0;
+	vertices.col(2) << 0, 1;
+	vertices.col(3) << 1, 1;
+
+	shader.bind();
+	shader.uploadIndices(indices);
+	shader.uploadAttrib("vertex", vertices);
+}
+
+FrameView::FrameView(nanogui::Widget *parent) :
+		Widget(parent), texture(ftl::gui2::GLTexture::Type::BGRA) {
+
+	if (glfwGetCurrentContext() == nullptr) {
+		throw FTL_Error("No current OpenGL context");
+	}
+	buildFrameViewShader(mShader);
+}
+
+void FrameView::draw(NVGcontext *ctx) {
+	Widget::draw(ctx);
+	if (flush_) {
+		nvgEndFrame(ctx); // Flush the NanoVG draw stack, not necessary to call nvgBeginFrame afterwards.
+	}
+	if (copy) {
+		auto &buffer = frame.createTexture<uchar4>(channel, true);
+		texture.make(buffer.width(), buffer.height());
+		auto dst = texture.map(stream);
+		cudaMemcpy2D(dst.data, dst.step1(), buffer.devicePtr(), buffer.pitch(), buffer.width()*4, buffer.height(), cudaMemcpyDeviceToDevice);
+		texture.unmap(stream);
+		copy = false;
+	}
+
+	if (!texture.isValid()) {
+		return;
+	}
+
+	const nanogui::Vector2f imageSize = nanogui::Vector2f(texture.width(), texture.height());
+	const nanogui::Vector2f widgetSize = size().cast<float>();
+	const nanogui::Vector2f screenSize = screen()->size().cast<float>();
+
+	// scale to fill screen
+	float scale = widgetSize.cwiseQuotient(imageSize).minCoeff();
+	const nanogui::Vector2f scaleFactor = imageSize.cwiseQuotient(screenSize)* scale;
+	const nanogui::Vector2f scaledImageSize = scale * imageSize;
+
+	// center
+	const nanogui::Vector2f offset = (widgetSize - scaledImageSize) / 2;
+	const nanogui::Vector2f positionInScreen = absolutePosition().cast<float>() + offset;
+	const nanogui::Vector2f imagePosition = positionInScreen.cwiseQuotient(screenSize);
+
+	mShader.bind();
+	glEnable(GL_SCISSOR_TEST);
+	float r = screen()->pixelRatio();
+	glScissor(positionInScreen.x() * r,
+				(screenSize.y() - positionInScreen.y() - size().y()) * r,
+				size().x() * r, size().y() * r);
+
+	glActiveTexture(GL_TEXTURE0);
+	glBindTexture(GL_TEXTURE_2D, texture.texture());
+	mShader.setUniform("image", 0);
+	mShader.setUniform("scaleFactor", scaleFactor);
+	mShader.setUniform("position", imagePosition);
+	mShader.drawIndexed(GL_TRIANGLES, 0, 2);
+	glDisable(GL_SCISSOR_TEST);
+}
+
+void FrameView::set(ftl::rgbd::Frame &f, ftl::codecs::Channel c, cudaStream_t s, bool cp) {
+	f.swapTo(frame);
+	channel = c;
+	stream = s;
+	if (cp) {
+		copy = true;
+	}
+}
diff --git a/applications/gui2/src/frameview.hpp b/applications/gui2/src/frameview.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b315ff9d071d43ff6dc2eff1dae1c3251feec3e3
--- /dev/null
+++ b/applications/gui2/src/frameview.hpp
@@ -0,0 +1,35 @@
+
+#include <ftl/rgbd/frame.hpp>
+
+#include <nanogui/widget.h>
+#include <nanogui/glutil.h>
+
+namespace ftl {
+namespace gui2 {
+
+/// Widget for ftl::cuda::TextureObject<uchar4>
+class FrameView : public nanogui::Widget {
+public:
+	FrameView(nanogui::Widget* parent);
+	virtual void draw(NVGcontext *ctx) override;
+
+	/** Set frame. If copy == true, buffer will be copied to OpenGL framebuffer
+	 *  at next draw() call. */
+	void set(ftl::rgbd::Frame &frame, ftl::codecs::Channel channel, cudaStream_t stream=0, bool copy=false);
+	/** should NanoVG draw stack be flushed before drawing the texture? */
+	void setFlush(bool v) { flush_ = v; }
+
+private:
+	ftl::rgbd::Frame frame;
+	ftl::codecs::Channel channel;
+	cudaStream_t stream;
+
+	GLTexture texture;
+	nanogui::GLShader mShader;
+
+	std::atomic<bool> copy = false;
+	bool flush_ = false;
+};
+
+}
+}
diff --git a/applications/gui2/src/gltexture.cpp b/applications/gui2/src/gltexture.cpp
index 5e6d68469616430fbbd7b2a364416fbc112551f6..fe6f7a121ffb4af28a2e1d741b57287f29f9c2f2 100644
--- a/applications/gui2/src/gltexture.cpp
+++ b/applications/gui2/src/gltexture.cpp
@@ -16,7 +16,6 @@ GLTexture::GLTexture(GLTexture::Type type) {
 	cuda_res_ = nullptr;
 	width_ = 0;
 	height_ = 0;
-	changed_ = true;
 	type_ = type;
 }
 
@@ -24,31 +23,6 @@ GLTexture::~GLTexture() {
 	//glDeleteTextures(1, &glid_);
 }
 
-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_);
-		glBindTexture(GL_TEXTURE_2D, glid_);
-		//cv::Mat m(cv::Size(100,100), CV_8UC3);
-		if (type_ == Type::BGRA) {
-			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m.cols, m.rows, 0, GL_BGRA, GL_UNSIGNED_BYTE, m.data);
-		} else if (type_ == Type::Float) {
-			glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, m.cols, m.rows, 0, GL_RED, GL_FLOAT, m.data);
-		}
-		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);
-	} else {
-		//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);
-	}
-	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();
@@ -114,6 +88,7 @@ void GLTexture::free() {
 }
 
 cv::cuda::GpuMat GLTexture::map(cudaStream_t stream) {
+	mtx_.lock();
 	void *devptr;
 	size_t size;
 	cudaSafeCall(cudaGraphicsMapResources(1, &cuda_res_, stream));
@@ -122,8 +97,9 @@ cv::cuda::GpuMat GLTexture::map(cudaStream_t stream) {
 }
 
 void GLTexture::unmap(cudaStream_t stream) {
+	// note: code must not throw, otherwise mtx_.unlock() does not happen
+
 	cudaSafeCall(cudaGraphicsUnmapResources(1, &cuda_res_, stream));
-	changed_ = true;
 
 	//glActiveTexture(GL_TEXTURE0);
 	glBindBuffer( GL_PIXEL_UNPACK_BUFFER, glbuf_);
@@ -139,6 +115,8 @@ void GLTexture::unmap(cudaStream_t stream) {
 	glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
 	glBindTexture(GL_TEXTURE_2D, 0);
 	glBindBuffer( GL_PIXEL_UNPACK_BUFFER, 0);
+
+	mtx_.unlock();
 }
 
 unsigned int GLTexture::texture() const {
@@ -153,6 +131,6 @@ unsigned int GLTexture::texture() const {
 
 		return glid_;
 	} else {
-		return glid_;
+		throw FTL_Error("No OpenGL texture; use make() first");
 	}
 }
diff --git a/applications/gui2/src/gltexture.hpp b/applications/gui2/src/gltexture.hpp
index ce4e19f46220e56a4be82ff8122a6a509c1946c0..b748c4490f966398a7506a9b9fe353fe95da1ec4 100644
--- a/applications/gui2/src/gltexture.hpp
+++ b/applications/gui2/src/gltexture.hpp
@@ -1,9 +1,6 @@
-#ifndef _FTL_GUI_GLTEXTURE_HPP_
-#define _FTL_GUI_GLTEXTURE_HPP_
+#pragma once
 
-#include <opencv2/core/mat.hpp>
-
-#include <cuda_runtime.h>
+#include <ftl/cuda_common.hpp>
 
 struct cudaGraphicsResource;
 
@@ -11,7 +8,7 @@ namespace ftl {
 namespace gui2 {
 
 class GLTexture {
-	public:
+public:
 	enum class Type {
 		RGBA,
 		BGRA,
@@ -21,32 +18,34 @@ class GLTexture {
 	explicit GLTexture(Type);
 	~GLTexture();
 
-	void update(cv::Mat &m);
+	bool isValid() const { return glid_ != std::numeric_limits<unsigned int>::max(); }
+	int width() const { return width_; }
+	int height() const { return height_; }
+
+	std::mutex& mutex() { return mtx_; }
+
+	// acquire mutex before make() or free()
 	void make(int width, int height);
+	void free();
 	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:
 
-	private:
 	unsigned int glid_;
 	unsigned int glbuf_;
 	int width_;
 	int height_;
 	int stride_;
-	bool changed_;
+
 	Type type_;
 
+	std::mutex mtx_; // for locking while in use (opengl thread calls lock() or cuda mapped)
+
 	cudaGraphicsResource *cuda_res_;
 };
 
 }
 }
-
-#endif  // _FTL_GUI_GLTEXTURE_HPP_
diff --git a/applications/gui2/src/inputoutput.cpp b/applications/gui2/src/inputoutput.cpp
index bb7375484df0ea49018536a863a816cd46bf44ae..76a4f5fce8f940a5757de8cde9684e41c8e7fe4a 100644
--- a/applications/gui2/src/inputoutput.cpp
+++ b/applications/gui2/src/inputoutput.cpp
@@ -8,11 +8,13 @@ using ftl::gui2::InputOutput;
 
 using ftl::codecs::Channel;
 
-InputOutput::InputOutput(ftl::Configurable *root, ftl::net::Universe *net) : net_(net) {
+InputOutput::InputOutput(ftl::Configurable *root, ftl::net::Universe *net) :
+		audio_callbacks_(), video_callbacks_(), net_(net) {
+
 	UNIQUE_LOCK(mtx_, lk);
 
-	controller_ = std::unique_ptr<ftl::ctrl::Master>(new ftl::ctrl::Master(root, net));
-	controller_->onLog([](const ftl::ctrl::LogEvent &e){
+	master_ = std::unique_ptr<ftl::ctrl::Master>(new ftl::ctrl::Master(root, net));
+	master_->onLog([](const ftl::ctrl::LogEvent &e){
 		const int v = e.verbosity;
 		switch (v) {
 		case -2:	LOG(ERROR) << "Remote log: " << e.message; break;
@@ -78,11 +80,16 @@ InputOutput::InputOutput(ftl::Configurable *root, ftl::net::Universe *net) : net
 		}
 	});
 
-	receiver_->onFrameSet([this](ftl::rgbd::FrameSet &fs){ return process_callbacks_video(fs); });
+	receiver_->onFrameSet([this](ftl::rgbd::FrameSet &fs){
+		//processFrameSet_(fs);
+		video_callbacks_.trigger(fs);
+		return true;
+	});
 
 	speaker_ = std::unique_ptr<ftl::audio::Speaker>(ftl::create<ftl::audio::Speaker>(root, "speaker_test"));
 
 	receiver_->onAudio([this](ftl::audio::FrameSet &fs) {
+		audio_callbacks_.trigger(fs);
 		/*
 		if (framesets_.size() == 0) return true;
 		auto *c = screen_->activeCamera();
@@ -91,7 +98,8 @@ InputOutput::InputOutput(ftl::Configurable *root, ftl::net::Universe *net) : net
 		speaker_->queue(fs.timestamp, fs.frames[0]);
 
 		//LOG(INFO) << "Audio delay = " << (fs.timestamp - framesets_[0]->timestamp + renddelay);
-		*/return true;
+		*/
+		return true;
 	});
 
 	/*ftl::timer::add(ftl::timer::kTimerMain, [this](int64_t ts) {
@@ -155,28 +163,71 @@ InputOutput::InputOutput(ftl::Configurable *root, ftl::net::Universe *net) : net
 	stream_->begin();
 }
 
-bool InputOutput::process_callbacks_video(ftl::rgbd::FrameSet &fs) {
-	UNIQUE_LOCK(mtx_, lk);
-	for (auto &f : cb_video_) {
-		f(fs);
-	}
-	return true;
-}
+void InputOutput::processFrameSet_(ftl::rgbd::FrameSet &fs) {
+	// Request the channels required by current camera configuration
+	/*if (fromstream) {
+		auto cs = _aggregateChannels(fs.id);
 
-bool InputOutput::process_callbacks_audio(ftl::audio::FrameSet &fs) {
-	UNIQUE_LOCK(mtx_, lk);
-	for (auto &f : cb_audio_) {
-		f(fs);
+		auto avail = static_cast<const ftl::stream::Stream*>(interceptor_)->available(fs.id);
+		if (cs.has(Channel::Depth) && !avail.has(Channel::Depth) && avail.has(Channel::Right)) {
+			cs -= Channel::Depth;
+			cs += Channel::Right;
+		}
+		interceptor_->select(fs.id, cs);
+	}*/
+
+	// Make sure there are enough framesets allocated
+	/*{
+		UNIQUE_LOCK(mutex_, lk);
+		_checkFrameSets(fs.id);
+	}*/
+
+	// !config()->value("drop_partial_framesets", false)
+	if (!fs.test(ftl::data::FSFlag::PARTIAL)) {
+		// Enforce interpolated colour and GPU upload
+		for (size_t i=0; i<fs.frames.size(); ++i) {
+			if (!fs.hasFrame(i)) {
+				continue;
+			}
+			fs.frames[i].createTexture<uchar4>(Channel::Colour, true);
+
+			// TODO: Do all channels. This is a fix for screen capture sources.
+			// pre_pipelines_[fs.id]->getStream()
+			if (!fs.frames[i].isGPU(Channel::Colour)) {
+				fs.frames[i].upload(ftl::codecs::Channels<0>(Channel::Colour), 0);
+			}
+		}
+
+		//fs.mask &= pre_pipelines_[fs.id]->value("frame_mask", 0xFFFF);
+
+		/*{
+			FTL_Profile("Prepipe",0.020);
+			pre_pipelines_[fs.id]->apply(fs, fs, 0);
+		}*/
+
+		//fs.swapTo(*framesets_[fs.id]);
+	} else {
+		LOG(WARNING) << "Dropping frameset: " << fs.timestamp;
 	}
-	return true;
+
+	/*
+	size_t i=0;
+	for (auto cam : cameras_) {
+		// Only update the camera periodically unless the active camera
+		if (screen_->activeCamera() == cam.second.camera ||
+			(screen_->activeCamera() == nullptr && cycle_ % cameras_.size() == i++))  cam.second.camera->update(framesets_);
+
+		ftl::codecs::Channels<0> channels;
+		if (fromstream) channels = cstream->available(fs.id);
+		//if ((*framesets_[fs.id]).frames.size() > 0) channels += (*framesets_[fs.id]).frames[0].getChannels();
+		cam.second.camera->update(fs.id, channels);
+	}*/
 }
 
-void InputOutput::subscribe(const std::function<bool(ftl::rgbd::FrameSet&)> &f) {
-	UNIQUE_LOCK(mtx_, lk);
-	cb_video_.push_back(f);
+ ftl::Handle InputOutput::addCallback(const std::function<bool(ftl::rgbd::FrameSet&)> f) {
+	return video_callbacks_.on(f);
 }
 
-void InputOutput::subscribe(const std::function<bool(ftl::audio::FrameSet&)> &f) {
-	UNIQUE_LOCK(mtx_, lk);
-	cb_audio_.push_back(f);
+ ftl::Handle InputOutput::addCallback(const std::function<bool(ftl::audio::FrameSet&)> f) {
+	return audio_callbacks_.on(f);
 }
diff --git a/applications/gui2/src/inputoutput.hpp b/applications/gui2/src/inputoutput.hpp
index ad444c3f408b7bf79845aceacea9b202db0fc6f1..e1fb2abd9e44634dcab6b35abfede5efcccbe5ed 100644
--- a/applications/gui2/src/inputoutput.hpp
+++ b/applications/gui2/src/inputoutput.hpp
@@ -3,6 +3,7 @@
 #include <memory>
 #include <mutex>
 
+#include <ftl/handle.hpp>
 #include <ftl/configuration.hpp>
 #include <ftl/net/universe.hpp>
 #include <ftl/master.hpp>
@@ -21,33 +22,27 @@ public:
 	InputOutput(const InputOutput&) = delete;
 	void operator=(const InputOutput&) = delete;
 
-	void subscribe(const std::function<bool(ftl::rgbd::FrameSet&)>&);
-	void subscribe(const std::function<bool(ftl::audio::FrameSet&)>&);
-	void unsubscribe();
-	void unsubscribe_audio();
-	void unsubscribe_video();
+	ftl::Handle addCallback(std::function<bool(ftl::rgbd::FrameSet&)>);
+	ftl::Handle addCallback(std::function<bool(ftl::audio::FrameSet&)>);
 
 	ftl::net::Universe* net() const;
+	ftl::ctrl::Master* master() const { return master_.get(); };
 
 private:
-	bool process_callbacks_video(ftl::rgbd::FrameSet &fs);
-	bool process_callbacks_audio(ftl::audio::FrameSet &fs);
+	ftl::Handler<ftl::audio::FrameSet&> audio_callbacks_;
+	ftl::Handler<ftl::rgbd::FrameSet&> video_callbacks_;
+
+	void processFrameSet_(ftl::rgbd::FrameSet &fs);
 
 	std::mutex mtx_;
 	ftl::net::Universe* net_;
 
-	std::unique_ptr<ftl::ctrl::Master> controller_;
+	std::unique_ptr<ftl::ctrl::Master> master_;
 	std::unique_ptr<ftl::stream::Muxer> stream_;
 	std::unique_ptr<ftl::stream::Intercept> interceptor_;
 	std::unique_ptr<ftl::stream::File> recorder_;
 	std::unique_ptr<ftl::stream::Receiver> receiver_;
 	std::unique_ptr<ftl::audio::Speaker> speaker_;
-	//std::unordered_map<std::string, ftl::stream::Stream*> available_;
-
-	std::vector<uintptr_t> cb_video_id_;
-	std::vector<uintptr_t> cb_audio_id_;
-	std::vector<std::function<bool(ftl::rgbd::FrameSet&)>> cb_video_;
-	std::vector<std::function<bool(ftl::audio::FrameSet&)>> cb_audio_;
 
 	int frameset_counter_ = 0;
 };
diff --git a/applications/gui2/src/main.cpp b/applications/gui2/src/main.cpp
index 7a85838c7670be91282a991c3187fca8f0c93229..d18bb2a7e4045671cfe808f9d04dfa44fba90402 100644
--- a/applications/gui2/src/main.cpp
+++ b/applications/gui2/src/main.cpp
@@ -12,77 +12,120 @@
 #include <cuda_gl_interop.h>
 
 #include "inputoutput.hpp"
-#include "modules.hpp"
+#include "module.hpp"
 #include "screen.hpp"
 
+#include "modules.hpp"
+
 using std::unique_ptr;
+using std::make_unique;
+
+/**
+ * FTL Graphical User Interface
+ * Single screen, loads configuration and sets up networking and input/output.
+ * Loads required modules to gui.
+ */
+class FTLGui {
+public:
+	FTLGui(int argc, char **argv);
+	~FTLGui();
+
+	template<typename T>
+	T* loadModule(const std::string &name);
+	void mainloop();
+
+private:
+	std::unique_ptr<ftl::Configurable> root_;
+	std::unique_ptr<ftl::net::Universe> net_;
+	std::unique_ptr<ftl::gui2::InputOutput> io_;
+
+	nanogui::ref<ftl::gui2::Screen> screen_;
+};
+
+template<typename T>
+T* FTLGui::loadModule(const std::string &name) {
+	return screen_->addModule<T>(name, root_.get(), screen_.get(), io_.get());
+}
 
-int main(int argc, char **argv) {
+FTLGui::FTLGui(int argc, char **argv) {
+	using namespace ftl::gui2;
+
+	screen_ = new Screen();
 
 	int cuda_device;
 	cudaSafeCall(cudaGetDevice(&cuda_device));
 	//cudaSafeCall(cudaGLSetGLDevice(cuda_device));
 
-	auto root = unique_ptr<ftl::Configurable>(ftl::configure(argc, argv, "gui_default"));
-	auto net = unique_ptr<ftl::net::Universe>(ftl::create<ftl::net::Universe>(root.get(), "net"));
+	root_ = unique_ptr<ftl::Configurable>(ftl::configure(argc, argv, "gui_default"));
+	net_ = unique_ptr<ftl::net::Universe>(ftl::create<ftl::net::Universe>(root_.get(), "net"));
+	io_ = make_unique<ftl::gui2::InputOutput>(root_.get(), net_.get());
+
+	net_->start();
+	net_->waitConnections();
 
-	auto io = ftl::gui2::InputOutput(root.get(), net.get());
+	loadModule<ThumbnailsController>("home")->activate();
+	loadModule<Camera>("camera");
+	loadModule<ConfigWindowController>("configwindow");
+	loadModule<Renderer>("renderer");
+	//loadModule<RecordController>("record");
+}
+
+FTLGui::~FTLGui() {
+	net_->shutdown();
+}
 
-	net->start();
-	net->waitConnections();
+void FTLGui::mainloop() {
+	// implements similar main loop as nanogui::mainloop()
 
 	ftl::timer::start();
 
-	try {
-		nanogui::init();
-		{
-			nanogui::ref<ftl::gui2::Screen> gui =
-				new ftl::gui2::Screen();
-			nanogui::ref<ftl::gui2::HomeView> home =
-				new ftl::gui2::HomeView(root.get(), &io);
-
-			gui->setView(home);
-			gui->drawAll();
-			gui->setVisible(true);
-			float last_draw_time = 0.0f;
-
-			while (ftl::running) {
-				if (!gui->visible()) {
-					ftl::running = false;
-				}
-				else if (glfwWindowShouldClose(gui->glfwWindow())) {
-					gui->setVisible(false);
-					ftl::running = false;
-				}
-				else {
-					float now = float(glfwGetTime());
-					float delta = now - last_draw_time;
-
-					// Generate poses and render and virtual frame here
-					// at full FPS (25 without VR and 90 with VR currently)
-					//gui->drawFast();
-
-					// Only draw the GUI at 25fps
-					if (delta >= 0.04f) {
-						last_draw_time = now;
-						gui->drawAll();
-					}
-				}
-
-				// Wait for mouse/keyboard or empty refresh events
-				glfwPollEvents();
-			}
-
-			// Process events once more
-			glfwPollEvents();
-
-			LOG(INFO) << "Stopping...";
-			ftl::timer::stop(false);
-			ftl::pool.stop(true);
-			LOG(INFO) << "All threads stopped.";
+	screen_->setVisible(true);
+	screen_->drawAll();
+
+	float last_draw_time = 0.0f;
+
+	while (ftl::running) {
+		if (!screen_->visible()) {
+			ftl::running = false;
+		}
+		else if (glfwWindowShouldClose(screen_->glfwWindow())) {
+			screen_->setVisible(false);
+			ftl::running = false;
+		}
+		else {
+			float now = float(glfwGetTime());
+			//float delta = now - last_draw_time;
+
+			// Generate poses and render and virtual frame here
+			// at full FPS (25 without VR and 90 with VR currently)
+			//screen_->render();
+
+			// Only draw the GUI at 25fps
+			//if (delta >= 0.04f) {
+				last_draw_time = now;
+				screen_->drawAll();
+			//}
 		}
 
-		nanogui::shutdown();
+		// Wait for mouse/keyboard or empty refresh events
+		glfwWaitEvents(); // VR headest issues
+		//glfwPollEvents();
+	}
+
+	// Process events once more
+	glfwPollEvents();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+int main(int argc, char **argv) {
+
+	nanogui::init();
+
+	FTLGui gui(argc, argv);
+
+	try {
+		gui.mainloop();
 	}
 	catch (const ftl::exception &e) {
 
@@ -99,6 +142,12 @@ int main(int argc, char **argv) {
 		return -1;
 	}
 
-	net->shutdown();
+	nanogui::shutdown();
+
+	LOG(INFO) << "Stopping...";
+	ftl::timer::stop(false);
+	ftl::pool.stop(true);
+	LOG(INFO) << "All threads stopped.";
+
 	return 0;
 }
diff --git a/applications/gui2/src/module.hpp b/applications/gui2/src/module.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b00f4b55b80fca23b93557febae04e763345e550
--- /dev/null
+++ b/applications/gui2/src/module.hpp
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "view.hpp"
+#include "inputoutput.hpp"
+
+#include <ftl/configurable.hpp>
+
+#include <nanogui/entypo.h>
+#include <nanogui/button.h>
+
+namespace ftl {
+namespace gui2 {
+
+class Screen;
+
+class Module : public ftl::Configurable {
+public:
+	Module(nlohmann::json &config, Screen *screen, InputOutput *io) :
+		Configurable(config), screen(screen), io(io) {}
+
+	/// Cerform any initialization
+	virtual void init() {};
+	virtual ~Module() {};
+
+protected:
+	ftl::gui2::Screen* const screen;
+	ftl::gui2::InputOutput* const io;
+};
+
+}
+}
diff --git a/applications/gui2/src/modules.hpp b/applications/gui2/src/modules.hpp
index 8fefa25c7e92ed225eea670e07b5d83976d6388d..ca7f8b708a10f0c7549a1fed232ebb2c06b076f0 100644
--- a/applications/gui2/src/modules.hpp
+++ b/applications/gui2/src/modules.hpp
@@ -1,2 +1,7 @@
 #pragma once
-#include "modules/home.hpp"
+
+#include "modules/thumbnails/control.hpp"
+#include "modules/record/control.hpp"
+#include "modules/camera/control.hpp"
+#include "modules/config/control.hpp"
+#include "modules/renderer/control.hpp"
diff --git a/applications/gui2/src/modules/camera/control.cpp b/applications/gui2/src/modules/camera/control.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a872963a9d80469afe6168b2f0708ad462682c6c
--- /dev/null
+++ b/applications/gui2/src/modules/camera/control.cpp
@@ -0,0 +1,26 @@
+#include "control.hpp"
+#include "view.hpp"
+
+using ftl::gui2::Camera;
+
+void Camera::activate() {
+	auto view = new ftl::gui2::CameraView(screen);
+
+	view->setHandle(
+		io->addCallback([this, view](ftl::rgbd::FrameSet& fs){
+		view->update(fs, source_idx);
+		screen->redraw();
+		return true;
+	}));
+
+	screen->setView(view);
+}
+
+/*void Camera::deactivate() {
+	io->removeCallbackVideo();
+	io->removeCallbackAudio();
+}*/
+
+void Camera::setSource(int idx) {
+	source_idx = idx;
+}
diff --git a/applications/gui2/src/modules/camera/control.hpp b/applications/gui2/src/modules/camera/control.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c513ed5e4a4e960d3bae78515d921d51b9dd62f6
--- /dev/null
+++ b/applications/gui2/src/modules/camera/control.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "../../module.hpp"
+#include "../../screen.hpp"
+
+namespace ftl {
+namespace gui2 {
+
+class Camera : public Module {
+public:
+	using Module::Module;
+
+	virtual void activate();
+
+	void setSource(int);
+
+private:
+	int source_idx = -1;
+};
+
+}
+}
diff --git a/applications/gui2/src/modules/camera/view.cpp b/applications/gui2/src/modules/camera/view.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..77212bc96f12f4503647ad25050ddceaf783bd2b
--- /dev/null
+++ b/applications/gui2/src/modules/camera/view.cpp
@@ -0,0 +1,34 @@
+#include <nanogui/screen.h>
+
+#include "view.hpp"
+
+using ftl::gui2::CameraView;
+
+CameraView::CameraView(nanogui::Widget* parent) : View(parent) {
+
+	fview = new ftl::gui2::FrameView(this);
+	fview->setFlush(false); // buffer will be rendered as "background"
+}
+
+void CameraView::draw(NVGcontext *ctx) {
+
+	fview->setFixedSize(size());
+	performLayout(ctx);
+	View::draw(ctx);
+}
+
+void CameraView::update(ftl::rgbd::FrameSet &fs, int idx) {
+	auto channel = ftl::codecs::Channel::Colour;
+
+	if (!fs.hasFrame(idx)) {
+		return;
+	}
+	auto &frame = fs.frames[idx];
+
+	if (!frame.hasChannel(channel)) {
+		return;
+	}
+
+	fview->set(frame, channel, 0, true);
+
+}
diff --git a/applications/gui2/src/modules/camera/view.hpp b/applications/gui2/src/modules/camera/view.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f1a2f57267b0a54c588e9470d467b028bd07bfba
--- /dev/null
+++ b/applications/gui2/src/modules/camera/view.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "../../view.hpp"
+#include "../../gltexture.hpp"
+#include "../../frameview.hpp"
+
+#include <nanogui/imageview.h>
+
+namespace ftl {
+namespace gui2 {
+
+class CameraView : public View {
+public:
+	CameraView(nanogui::Widget* parent);
+	virtual void draw(NVGcontext *ctx) override;
+	void update(ftl::rgbd::FrameSet&fs, int id);
+	void setHandle(ftl::Handle&& handle) { handle_ = std::move(handle); }
+private:
+	ftl::gui2::FrameView *fview = nullptr;
+	ftl::Handle handle_;
+};
+
+}
+}
diff --git a/applications/gui2/src/modules/config/control.cpp b/applications/gui2/src/modules/config/control.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9abc46918fa23ee3fffa4b5f6557a735d19f9fe8
--- /dev/null
+++ b/applications/gui2/src/modules/config/control.cpp
@@ -0,0 +1,27 @@
+#include "control.hpp"
+
+using ftl::gui2::ConfigWindowController;
+
+void ConfigWindowController::init() {
+	button = screen->addButton();
+	button->setIcon(ENTYPO_ICON_COG);
+	button->setTooltip("Config");
+	button->setCallback([this](){
+		button->setPushed(false);
+		show();
+	});
+	button->setVisible(true);
+}
+
+void ConfigWindowController::show() {
+	if (screen->childIndex(window) == -1) {
+		window = new ftl::gui2::ConfigWindow(screen, io->master());
+	}
+	window->requestFocus();
+	window->setVisible(true);
+	screen->performLayout();
+}
+
+ConfigWindowController::~ConfigWindowController() {
+	// remove window?
+}
diff --git a/applications/gui2/src/modules/config/control.hpp b/applications/gui2/src/modules/config/control.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8a4baa52f5c17ed0f9fdb6c21deeb07e8a3b2f5f
--- /dev/null
+++ b/applications/gui2/src/modules/config/control.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "../../module.hpp"
+#include "../../screen.hpp"
+
+#include "view.hpp"
+
+namespace ftl {
+namespace gui2 {
+
+/**
+ * Controller for thumbnail view.
+ */
+class ConfigWindowController : public Module {
+public:
+	using Module::Module;
+	virtual ~ConfigWindowController();
+
+	virtual void init() override;
+	virtual void show();
+
+private:
+	nanogui::ToolButton *button;
+	ftl::gui2::ConfigWindow *window = nullptr;
+};
+
+}
+}
diff --git a/applications/gui2/src/modules/config/view.cpp b/applications/gui2/src/modules/config/view.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d19af3101465405452f1de855bbd6064b23373bc
--- /dev/null
+++ b/applications/gui2/src/modules/config/view.cpp
@@ -0,0 +1,237 @@
+
+#include <loguru.hpp>
+
+#include <nanogui/layout.h>
+#include <nanogui/label.h>
+#include <nanogui/button.h>
+#include <nanogui/entypo.h>
+#include <nanogui/formhelper.h>
+#include <nanogui/vscrollpanel.h>
+#include <nanogui/opengl.h>
+
+#include <nlohmann/json.hpp>
+
+#include <vector>
+#include <string>
+
+#include "view.hpp"
+
+using ftl::gui2::ConfigWindow;
+using std::string;
+using std::vector;
+using ftl::config::json_t;
+
+class SearchBox : public nanogui::TextBox {
+private:
+	std::vector<std::string> configurables_;
+	Widget *buttons_;
+	std::string previous;
+
+	void _setVisible(const std::string &str) {
+		// Check whether the search string has changed to prevent
+		// unnecessary searching.
+		if (str != previous) {
+			for (int i = configurables_.size()-1; i >= 0; --i) {
+				if (configurables_[i].find(mValueTemp) != std::string::npos) {
+					buttons_->childAt(i)->setVisible(true);
+				} else {
+					buttons_->childAt(i)->setVisible(false);
+				}
+			}
+			previous = str;
+		}
+	}
+
+public:
+	SearchBox(Widget *parent, std::vector<std::string> &configurables) : nanogui::TextBox(parent, ""), configurables_(configurables) {
+		setAlignment(TextBox::Alignment::Left);
+		setEditable(true);
+		setPlaceholder("Search");
+	}
+
+	~SearchBox() {
+	}
+
+	bool keyboardEvent(int key, int scancode, int action, int modifier) {
+		TextBox::keyboardEvent(key, scancode, action, modifier);
+		_setVisible(mValueTemp);
+		return true;
+	}
+
+	void setButtons(Widget *buttons) {
+		buttons_ = buttons;
+	}
+};
+
+static std::string titleForURI(const ftl::URI &uri) {
+	auto *cfg = ftl::config::find(uri.getBaseURI());
+	if (cfg && cfg->get<std::string>("title")) {
+		return *cfg->get<std::string>("title");
+	} else if (uri.getPath().size() > 0) {
+		return uri.getPathSegment(-1);
+	} else {
+		return uri.getHost();
+	}
+}
+
+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));
+
+	auto close = new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS);
+	close->setCallback([this](){ dispose();});
+
+	auto configurables = ftl::config::list();
+	const auto size = configurables.size();
+
+	new Label(this, "Select Configurable","sans-bold");
+
+	auto searchBox = new SearchBox(this, configurables);
+
+	auto vscroll = new VScrollPanel(this);
+	vscroll->setFixedHeight(300);
+	auto buttons = new Widget(vscroll);
+	buttons->setLayout(new BoxLayout(Orientation::Vertical, Alignment::Fill));
+
+	searchBox->setButtons(buttons);
+
+	std::vector<std::string> configurable_titles(size);
+	for (size_t i = 0; i < size; ++i) {
+		ftl::URI uri(configurables[i]);
+		std::string label = uri.getFragment();
+
+		size_t pos = label.find_last_of("/");
+		if (pos != std::string::npos) label = label.substr(pos+1);
+
+		std::string parentName = configurables[i];
+		size_t pos2 = parentName.find_last_of("/");
+		if (pos2 != std::string::npos) parentName = parentName.substr(0,pos2);
+
+		// FIXME: Does not indicated parent indentation ... needs sorting?
+
+		if (i > 0 && parentName == configurables[i-1]) {
+			ftl::URI uri(configurables[i-1]);
+			configurable_titles[i-1] = std::string("[") + titleForURI(uri) + std::string("] ") + uri.getFragment();
+
+			auto *prev = dynamic_cast<Button*>(buttons->childAt(buttons->childCount()-1));
+			prev->setCaption(configurable_titles[i-1]);
+			prev->setBackgroundColor(nanogui::Color(0.3f,0.3f,0.3f,1.0f));
+			prev->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
+			prev->setIconPosition(Button::IconPosition::Left);
+			prev->setIcon(ENTYPO_ICON_FOLDER);
+		}
+
+		configurable_titles[i] = label;
+
+		auto itembutton = new nanogui::Button(buttons, configurable_titles[i]);
+		std::string c = configurables[i];
+		itembutton->setTooltip(c);
+		itembutton->setBackgroundColor(nanogui::Color(0.9f,0.9f,0.9f,0.9f));
+		itembutton->setCallback([this,c]() {
+			_buildForm(c);
+			setVisible(false);
+			dispose();
+		});
+	}
+}
+
+ConfigWindow::~ConfigWindow() {
+	LOG(INFO) << "ConfigWindow::~ConfigWindow()";
+}
+
+void ConfigWindow::_addElements(nanogui::FormHelper *form, const std::string &suri) {
+	using namespace nanogui;
+
+	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()) {
+			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,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,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,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,suri](const string &f){
+				ftl::config::update(suri+"/"+key, f);
+			}, [data,key]() -> string {
+				return data[key].get<string>();
+			});
+		} else if (i.value().is_object()) {
+			string key = i.key();
+
+			// Checking the URI with exists() prevents unloaded local configurations from being shown.
+			if (suri.find('#') != string::npos && exists(suri+string("/")+key)) {
+				form->addButton(key, [this,suri,key]() {
+					_buildForm(suri+string("/")+key);
+				})->setIcon(ENTYPO_ICON_FOLDER);
+			} else if (exists(suri+string("#")+key)) {
+				form->addButton(key, [this,suri,key]() {
+					_buildForm(suri+string("#")+key);
+				})->setIcon(ENTYPO_ICON_FOLDER);
+			}
+		}
+	}
+}
+
+void ConfigWindow::_buildForm(const std::string &suri) {
+	using namespace nanogui;
+
+	ftl::URI uri(suri);
+
+	FormHelper *form = new FormHelper(this->screen());
+	form->addWindow(Vector2i(100,50), uri.getFragment());
+	form->window()->setTheme(theme());
+
+	_addElements(form, suri);
+
+	// prevent parent window from being destroyed too early
+	incRef();
+
+	auto close = new nanogui::Button(
+		form->window()->buttonPanel(),
+		"",
+		ENTYPO_ICON_CROSS);
+
+	close->setCallback([this, form](){
+		form->window()->dispose();
+		decRef();
+	});
+	form->window()->screen()->performLayout();
+}
+
+bool ConfigWindow::exists(const std::string &uri) {
+	return ftl::config::find(uri) != nullptr;
+}
diff --git a/applications/gui2/src/modules/config/view.hpp b/applications/gui2/src/modules/config/view.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3396a6dd09917e74dcea7fd565ff77c97bae823b
--- /dev/null
+++ b/applications/gui2/src/modules/config/view.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <nanogui/window.h>
+#include <nanogui/formhelper.h>
+
+#include <ftl/master.hpp>
+#include <ftl/uuid.hpp>
+#include <ftl/net_configurable.hpp>
+
+namespace ftl {
+namespace gui2 {
+
+/**
+ * Allow configurable editing.
+ */
+class ConfigWindow : public nanogui::Window {
+	public:
+	ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl);
+	~ConfigWindow();
+
+	private:
+	ftl::ctrl::Master *ctrl_;
+
+	void _buildForm(const std::string &uri);
+	void _addElements(nanogui::FormHelper *form, const std::string &suri);
+	bool exists(const std::string &uri);
+};
+
+}
+}
diff --git a/applications/gui2/src/modules/home.cpp b/applications/gui2/src/modules/home.cpp
deleted file mode 100644
index 7c77a78bc5b43df67f81096c2d41b3ea5eda89e8..0000000000000000000000000000000000000000
--- a/applications/gui2/src/modules/home.cpp
+++ /dev/null
@@ -1,65 +0,0 @@
-#include "home.hpp"
-
-using ftl::gui2::HomeView;
-
-namespace {
-	constexpr char const *const defaultImageViewVertexShader =
-		R"(#version 330
-		uniform vec2 scaleFactor;
-		uniform vec2 position;
-		in vec2 vertex;
-		out vec2 uv;
-		void main() {
-			uv = vec2(vertex.x, vertex.y);
-			vec2 scaledVertex = (vertex * scaleFactor) + position;
-			gl_Position  = vec4(2.0*scaledVertex.x - 1.0,
-								2.0*scaledVertex.y - 1.0,
-								0.0, 1.0);
-		})";
-
-	constexpr char const *const defaultImageViewFragmentShader =
-		R"(#version 330
-		uniform sampler2D image1;
-		uniform sampler2D image2;
-		uniform sampler2D depthImage;
-		uniform float blendAmount;
-		out vec4 color;
-		in vec2 uv;
-		void main() {
-			color = blendAmount * texture(image1, uv) + (1.0 - blendAmount) * texture(image2, uv);
-			color.w = 1.0f;
-			gl_FragDepth = texture(depthImage, uv).r;
-		})";
-}
-
-HomeView::HomeView(ftl::Configurable *cfg, ftl::gui2::InputOutput *io) :
-		ftl::gui2::View(cfg, io), texture_(ftl::gui2::GLTexture::Type::RGBA) {
-
-	io_->subscribe([this](ftl::rgbd::FrameSet &fs) { return process_FrameSet(fs); });
-
-	mShader.init("RGBDShader", defaultImageViewVertexShader, defaultImageViewFragmentShader);
-	nanogui::MatrixXu indices(3, 2);
-	indices.col(0) << 0, 1, 2;
-	indices.col(1) << 2, 3, 1;
-
-	nanogui::MatrixXf vertices(2, 4);
-	vertices.col(0) << 0, 0;
-	vertices.col(1) << 1, 0;
-	vertices.col(2) << 0, 1;
-	vertices.col(3) << 1, 1;
-
-	mShader.bind();
-	mShader.uploadIndices(indices);
-	mShader.uploadAttrib("vertex", vertices);
-
-}
-
-HomeView::~HomeView() {
-	mShader.free();
-}
-
-bool HomeView::process_FrameSet(ftl::rgbd::FrameSet &fs) {
-	auto im = fs.frames[0].fastDownload(ftl::codecs::Channel::Left, cv::cuda::Stream::Null());
-	texture_.make(im.cols, im.rows);
-	return true;
-}
diff --git a/applications/gui2/src/modules/home.hpp b/applications/gui2/src/modules/home.hpp
deleted file mode 100644
index 7484ce2e9fc53142eb515b0ff2decfe453454ee7..0000000000000000000000000000000000000000
--- a/applications/gui2/src/modules/home.hpp
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-
-#include <nanogui/glutil.h>
-
-#include "../view.hpp"
-#include "../gltexture.hpp"
-
-namespace ftl {
-namespace gui2 {
-
-class HomeView : public View {
-public:
-	HomeView(ftl::Configurable *config, InputOutput *io);
-	~HomeView();
-
-private:
-	bool process_FrameSet(ftl::rgbd::FrameSet&);
-
-	nanogui::GLShader mShader;
-	GLTexture texture_;
-};
-
-}
-}
diff --git a/applications/gui2/src/modules/record/control.cpp b/applications/gui2/src/modules/record/control.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..928be5832c0eeb0877254103561e3e98eb85e605
--- /dev/null
+++ b/applications/gui2/src/modules/record/control.cpp
@@ -0,0 +1,21 @@
+#include "control.hpp"
+
+using ftl::gui2::RecordController;
+
+void RecordController::init() {
+	button_ = screen->addButton();
+	LOG(INFO) << "RECORD INIT";
+}
+
+void RecordController::activate() {
+	button_->setPushed(false);
+	if (active) {
+		button_->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
+		button_->setPushed(false);
+	}
+	else {
+		button_->setTextColor(nanogui::Color(1.0f,.1f,.1f,1.0f));
+		button_->setPushed(false);
+	}
+	active = !active;
+}
diff --git a/applications/gui2/src/modules/record/control.hpp b/applications/gui2/src/modules/record/control.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1154621c330be09a11eb254d81ed15f666990c59
--- /dev/null
+++ b/applications/gui2/src/modules/record/control.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "../../module.hpp"
+#include "../../screen.hpp"
+
+namespace ftl {
+namespace gui2 {
+
+class RecordController : public Module {
+public:
+	using Module::Module;
+
+	virtual void init() override;
+	virtual void activate();
+
+private:
+	nanogui::ToolButton* button_;
+	bool active = false;
+};
+
+}
+}
diff --git a/applications/gui2/src/modules/renderer/control.cpp b/applications/gui2/src/modules/renderer/control.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..597d1ba0b80095f3cc50df021ce0e28b001cc688
--- /dev/null
+++ b/applications/gui2/src/modules/renderer/control.cpp
@@ -0,0 +1,136 @@
+#include "control.hpp"
+
+using ftl::gui2::Renderer;
+
+using ftl::codecs::Channel;
+using ftl::rgbd::FrameSet;
+using ftl::rgbd::Frame;
+
+void Renderer::init() {
+	renderer_ = std::unique_ptr<ftl::render::CUDARender>(ftl::create<ftl::render::CUDARender>(this, std::string("vcam0")));
+	fsmask_ = renderer_->value("fsmask", fsmask_);
+		renderer_->on("fsmask", [this](const ftl::config::Event &e) {
+			fsmask_ = renderer_->value("fsmask", fsmask_);
+		});
+
+		// Allow Pose origin to be changed
+		pose_source_ = renderer_->value("pose_source", pose_source_);
+		renderer_->on("pose_source", [this](const ftl::config::Event &e) {
+			pose_source_ = renderer_->value("pose_source", pose_source_);
+		});
+
+		intrinsics_ = ftl::create<ftl::Configurable>(renderer_.get(), "intrinsics");
+
+		state_.getLeft() = ftl::rgbd::Camera::from(intrinsics_);
+		state_.getRight() = state_.getLeft();
+
+		intrinsics_->on("width", [this](const ftl::config::Event &e) {
+			state_.getLeft() = ftl::rgbd::Camera::from(intrinsics_);
+			state_.getRight() = state_.getLeft();
+		});
+
+		intrinsics_->on("focal", [this](const ftl::config::Event &e) {
+			state_.getLeft() = ftl::rgbd::Camera::from(intrinsics_);
+			state_.getRight() = state_.getLeft();
+		});
+
+		/*{
+			Eigen::Matrix4d pose;
+			pose.setIdentity();
+			state_.setPose(pose);
+
+			for (auto &t : transforms_) {
+				t.setIdentity();
+			}
+		}
+		{
+			double camera_initial_x = intrinsics_->value("camera_x", 0.0);
+			double camera_initial_y = intrinsics_->value("camera_y", -1.75);
+			double camera_initial_z = intrinsics_->value("camera_z", 0.0);
+
+			double lookat_initial_x = intrinsics_->value("lookat_x", 1.0);
+			double lookat_initial_y = intrinsics_->value("lookat_y", 0.0);
+			double lookat_initial_z = intrinsics_->value("lookat_z", 0.0);
+
+			Eigen::Vector3f head(camera_initial_x, camera_initial_y, camera_initial_z);
+			Eigen::Vector3f lookat(lookat_initial_x, lookat_initial_y, lookat_initial_z);
+			// TODO up vector
+			Eigen::Matrix4f pose = nanogui::lookAt(head, head+lookat, Eigen::Vector3f(0.0f, 1.0f, 0.0f));
+
+			//eye_ = Eigen::Vector3d(camera_initial_x, camera_initial_y, camera_initial_z);
+			//neye_ = Eigen::Vector4d(eye_(0), eye_(1), eye_(2), 0.0);
+			//rotmat_ = pose.cast<double>();
+			//rotmat_.block(0, 3, 3, 1).setZero();
+		}*/
+}
+
+void Renderer::render(std::vector<ftl::rgbd::FrameSet*> &fss) {
+	frame_.reset();
+	frame_.setOrigin(&state_);
+	frame_.create<cv::cuda::GpuMat>(Channel::Colour);
+	frame_.create<cv::cuda::GpuMat>(Channel::Depth);
+
+	// TODO
+	Eigen::Matrix4d pose = Eigen::Matrix4d::Identity();
+
+	{
+		//FTL_Profile("Render",0.034);
+		renderer_->begin(frame_, Channel::Colour);
+		/*if (isStereo()) {
+			if (!renderer2_) {
+				renderer2_ = ftl::create<ftl::render::CUDARender>(screen_->root(), std::string("vcam")+std::to_string(vcamcount++));
+			}
+			renderer2_->begin(frame_, Channel::Colour2);
+		}*/
+
+		try {
+			for (auto *fs : fss) {
+				if (!usesFrameset(fs->id)) continue;
+
+				fs->mtx.lock();
+				renderer_->submit(fs, ftl::codecs::Channels<0>(Channel::Colour), pose);
+				//if (isStereo()) renderer2_->submit(fs, ftl::codecs::Channels<0>(Channel::Colour), transforms_[fs->id]);
+
+				//if (enable_overlay) {
+					// Generate and upload an overlay image.
+				//	overlayer_->apply(*fs, overlay_, state_);
+				//	frame_.upload(Channel::Overlay, renderer_->getCUDAStream());
+				//}
+			}
+
+			renderer_->render();
+			//if (isStereo()) renderer2_->render();
+
+			//if (enable_overlay) {
+			//	renderer_->blend(Channel::Overlay);
+			//}
+
+			renderer_->end();
+			//if (isStereo()) renderer2_->end();
+		} catch(std::exception &e) {
+			LOG(ERROR) << "Exception in render: " << e.what();
+		}
+
+		for (auto *fs : fss) {
+			if (!usesFrameset(fs->id)) continue;
+			fs->mtx.unlock();
+		}
+	}
+	/*
+	if (!post_pipe_) {
+		post_pipe_ = ftl::config::create<ftl::operators::Graph>(screen_->root(), "post_filters");
+		post_pipe_->append<ftl::operators::FXAA>("fxaa");
+		post_pipe_->append<ftl::operators::GTAnalysis>("gtanalyse");
+	}
+
+	post_pipe_->apply(frame_, frame_, 0);
+
+	channels_ = frame_.getChannels();*/
+
+
+	// Normalize depth map
+	frame_.get<cv::cuda::GpuMat>(Channel::Depth).convertTo(frame_.get<cv::cuda::GpuMat>(Channel::Depth), CV_32F, 1.0/8.0);
+}
+
+Renderer::~Renderer() {
+}
diff --git a/applications/gui2/src/modules/renderer/control.hpp b/applications/gui2/src/modules/renderer/control.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..033f59b08aa1f4c5b216c5357842a5d515576e27
--- /dev/null
+++ b/applications/gui2/src/modules/renderer/control.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "../../module.hpp"
+#include "../../screen.hpp"
+
+#include <ftl/render/CUDARender.hpp>
+
+namespace ftl {
+namespace gui2 {
+
+/**
+ * Controller for thumbnail view.
+ */
+class Renderer : public Module {
+public:
+	using Module::Module;
+	virtual ~Renderer();
+
+	virtual void init() override;
+	virtual void render(std::vector<ftl::rgbd::FrameSet*> &fss);
+	bool usesFrameset(int id) const { return true; }
+
+private:
+	std::unique_ptr<ftl::render::CUDARender> renderer_;
+	unsigned int fsmask_ = 0;  // Frameset Mask
+	std::string pose_source_;
+	ftl::Configurable *intrinsics_;
+	ftl::operators::Graph *post_pipe_;
+	ftl::rgbd::Frame frame_;
+	ftl::rgbd::FrameState state_;
+};
+
+}
+}
diff --git a/applications/gui2/src/modules/thumbnails/control.cpp b/applications/gui2/src/modules/thumbnails/control.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4fbc218ebe21aea54478dcce623f48a36d51f556
--- /dev/null
+++ b/applications/gui2/src/modules/thumbnails/control.cpp
@@ -0,0 +1,46 @@
+#include "control.hpp"
+#include "view.hpp"
+
+#include "../camera/control.hpp"
+
+#include <nanogui/entypo.h>
+
+using ftl::gui2::ThumbnailsController;
+
+void ThumbnailsController::init() {
+	button = screen->addButton();
+	button->setIcon(ENTYPO_ICON_HOME);
+	button->setTooltip("Home");
+	button->setCallback([this](){
+		button->setPushed(false);
+		activate();
+	});
+	button->setVisible(true);
+}
+
+void ThumbnailsController::activate() {
+	show_thumbnails();
+}
+
+ThumbnailsController::~ThumbnailsController() {
+
+}
+
+void ThumbnailsController::show_thumbnails() {
+	auto thumb_view = new ftl::gui2::Thumbnails(screen, this);
+
+	thumb_view->setHandle(
+		io->addCallback([this, thumb_view](ftl::rgbd::FrameSet& fs){
+			thumb_view->update(fs);
+			screen->redraw();
+			return true;
+	}));
+
+	screen->setView(thumb_view);
+}
+
+void ThumbnailsController::show_camera(int frame_idx) {
+	auto* camera = screen->getModule<ftl::gui2::Camera>();
+	camera->setSource(frame_idx);
+	camera->activate();
+}
diff --git a/applications/gui2/src/modules/thumbnails/control.hpp b/applications/gui2/src/modules/thumbnails/control.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f457d5dfb9e498559da4fbb11948d3c7583ad610
--- /dev/null
+++ b/applications/gui2/src/modules/thumbnails/control.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "../../module.hpp"
+#include "../../screen.hpp"
+
+namespace ftl {
+namespace gui2 {
+
+/**
+ * Controller for thumbnail view.
+ */
+class ThumbnailsController : public Module {
+public:
+	using Module::Module;
+	virtual ~ThumbnailsController();
+
+	virtual void init() override;
+	virtual void activate();
+
+	void show_thumbnails();
+	void show_camera(int frame_idx);
+
+private:
+	nanogui::ToolButton *button;
+};
+
+}
+}
diff --git a/applications/gui2/src/modules/thumbnails/view.cpp b/applications/gui2/src/modules/thumbnails/view.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..282593ed4afef72260778df4711453d43084653d
--- /dev/null
+++ b/applications/gui2/src/modules/thumbnails/view.cpp
@@ -0,0 +1,105 @@
+#include "view.hpp"
+
+#include <opencv2/imgproc.hpp>
+#include <opencv2/imgcodecs.hpp>
+#include <opencv2/cudaarithm.hpp>
+
+#include <ftl/operators/antialiasing.hpp>
+#include <ftl/cuda/normals.hpp>
+#include <ftl/render/colouriser.hpp>
+#include <ftl/cuda/transform.hpp>
+#include <ftl/operators/gt_analysis.hpp>
+#include <ftl/operators/poser.hpp>
+#include <ftl/cuda/colour_cuda.hpp>
+#include <ftl/streams/parsers.hpp>
+
+#include <nanogui/vscrollpanel.h>
+#include <nanogui/layout.h>
+
+using ftl::gui2::Thumbnails;
+using ftl::gui2::ThumbView;
+
+ThumbView::ThumbView(nanogui::Widget *parent, ThumbnailsController *control, int idx) :
+		ftl::gui2::FrameView(parent), control(control), idx(idx) {
+	setCursor(nanogui::Cursor::Hand);
+}
+
+bool ThumbView::mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) {
+	if (!down) {
+		// this widget (and view it belongs to) MUST NOT receive any events
+		// after control->show_camera() (automatic destruction with reference
+		// counting, use nanogui::ref<> for View to keep keep it alive
+		// [not tested])
+		control->show_camera(idx);
+	}
+	return true;
+}
+
+void ThumbView::draw(NVGcontext *ctx) {
+	ftl::gui2::FrameView::draw(ctx);
+	nvgScissor(ctx, mPos.x(), mPos.y(), mSize.x(), mSize.y());
+	nvgFontSize(ctx, 14);
+	nvgFontFace(ctx, "sans-bold");
+	nvgResetScissor(ctx);
+}
+
+Thumbnails::Thumbnails(nanogui::Widget *parent, ftl::gui2::ThumbnailsController *control) :
+		View(parent), control(control) {
+
+	panel = new nanogui::Widget(this);
+	panel->setLayout(
+				new nanogui::GridLayout(nanogui::Orientation::Horizontal, 3,
+										nanogui::Alignment::Middle, 0, 10));
+
+}
+
+Thumbnails::~Thumbnails() {
+
+}
+
+void Thumbnails::update(ftl::rgbd::FrameSet &fs) {
+	// Just swap frameset: creating opengl buffers (FrameView) requires
+	// active context. Easiest way is to generate elements at draw()
+
+	std::unique_lock<std::mutex> lk(mtx_, std::defer_lock);
+	if (lk.try_lock()) {
+		fs.swapTo(fs_);
+	}
+}
+
+void Thumbnails::draw(NVGcontext *ctx) {
+	std::unique_lock<std::mutex> lk(mtx_);
+	const auto channel = ftl::codecs::Channel::Colour;
+
+	bool update = false;
+
+	for (unsigned int i = 0; i < fs_.frames.size(); i++) {
+
+		if (thumbnails_.size() == i) {
+			thumbnails_.push_back(new ftl::gui2::ThumbView(panel, control, i));
+			thumbnails_[i]->setFixedSize(thumbsize_);
+		}
+
+		if (!fs_.hasFrame(i)) {
+			continue;
+		}
+
+		auto &frame = fs_.frames[i];
+		if (!frame.hasChannel(channel)) {
+			continue;
+		}
+
+		thumbnails_[i]->set(frame, channel, 0, true);
+		update = true;
+	}
+
+	if (update) {
+		performLayout(ctx);
+		// center
+		nanogui::Vector2i margin = (size() - panel->size())/2;
+		panel->setPosition(margin);
+	}
+
+	View::draw(ctx);
+}
+
diff --git a/applications/gui2/src/modules/thumbnails/view.hpp b/applications/gui2/src/modules/thumbnails/view.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0459bffad6434f068f7dc4ae5cddd8d3a756bfdc
--- /dev/null
+++ b/applications/gui2/src/modules/thumbnails/view.hpp
@@ -0,0 +1,52 @@
+#pragma once
+#include "../../view.hpp"
+#include "../../gltexture.hpp"
+#include "../../frameview.hpp"
+
+#include "control.hpp"
+
+#include <nanogui/glcanvas.h>
+#include <nanogui/glutil.h>
+#include <nanogui/imageview.h>
+
+namespace ftl {
+namespace gui2 {
+
+class ThumbView : public ftl::gui2::FrameView {
+public:
+	ThumbView(nanogui::Widget *parent, ThumbnailsController *control, int idx);
+	~ThumbView() {}
+
+	virtual bool mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) override;
+	virtual void draw(NVGcontext *ctx) override;
+
+private:
+	ThumbnailsController *control;
+	const int idx;
+};
+
+class Thumbnails : public View {
+public:
+	Thumbnails(nanogui::Widget *parent, ThumbnailsController *controller);
+	virtual ~Thumbnails();
+
+	void update(ftl::rgbd::FrameSet &fs);
+	virtual void draw(NVGcontext *ctx) override;
+
+	void setHandle(ftl::Handle&& handle) { handle_ = std::move(handle); }
+
+private:
+	ftl::gui2::ThumbnailsController *control;
+	nanogui::Widget* panel;
+
+	std::vector<ThumbView*> thumbnails_;
+
+	std::mutex mtx_;
+	ftl::rgbd::FrameSet fs_;
+
+	nanogui::Vector2i thumbsize_ = nanogui::Vector2i(320,180);
+	ftl::Handle handle_;
+};
+
+}
+}
diff --git a/applications/gui2/src/screen.cpp b/applications/gui2/src/screen.cpp
index 64be6251a79761e4a82689d49229ca4522b7bc30..57e05178f40017a981d0d63592c5617a4a51367e 100644
--- a/applications/gui2/src/screen.cpp
+++ b/applications/gui2/src/screen.cpp
@@ -12,7 +12,6 @@
 
 #include "window.hpp"
 #include "screen.hpp"
-#include "modules.hpp"
 
 #include <loguru.hpp>
 
@@ -23,115 +22,132 @@ using Eigen::Vector2i;
 
 using ftl::gui2::Screen;
 
+static const int toolbar_w = 50;
+static const Vector2i wsize(1280+toolbar_w,720);
+
 Screen::Screen() :
 		nanogui::Screen(Eigen::Vector2i(1024, 768), "FT-Lab Remote Presence"),
-		mediatheme(nullptr),
-		toolbuttheme(nullptr),
-		windowtheme(nullptr),
-		toolbar(nullptr),
-		view(nullptr) {
+		mediatheme_(nullptr),
+		toolbuttheme_(nullptr),
+		windowtheme_(nullptr),
+		toolbar_(nullptr),
+		active_view_(nullptr) {
 
 	using namespace nanogui;
 
-	setSize(Vector2i(1280,720));
+	setSize(wsize);
 
 	// themes
-	toolbuttheme = new Theme(*theme());
-	toolbuttheme->mBorderDark = nanogui::Color(0,0);
-	toolbuttheme->mBorderLight = nanogui::Color(0,0);
-	toolbuttheme->mButtonGradientBotFocused = nanogui::Color(60,255);
-	toolbuttheme->mButtonGradientBotUnfocused = nanogui::Color(0,0);
-	toolbuttheme->mButtonGradientTopFocused = nanogui::Color(60,255);
-	toolbuttheme->mButtonGradientTopUnfocused = nanogui::Color(0,0);
-	toolbuttheme->mButtonGradientTopPushed = nanogui::Color(60,180);
-	toolbuttheme->mButtonGradientBotPushed = nanogui::Color(60,180);
-	toolbuttheme->mTextColor = nanogui::Color(0.9f,0.9f,0.9f,0.9f);
-
-	mediatheme = new Theme(*theme());
-	mediatheme->mIconScale = 1.2f;
-	mediatheme->mWindowDropShadowSize = 0;
-	mediatheme->mWindowFillFocused = nanogui::Color(45, 150);
-	mediatheme->mWindowFillUnfocused = nanogui::Color(45, 80);
-	mediatheme->mButtonGradientTopUnfocused = nanogui::Color(0,0);
-	mediatheme->mButtonGradientBotUnfocused = nanogui::Color(0,0);
-	mediatheme->mButtonGradientTopFocused = nanogui::Color(80,230);
-	mediatheme->mButtonGradientBotFocused = nanogui::Color(80,230);
-	mediatheme->mIconColor = nanogui::Color(255,255);
-	mediatheme->mTextColor = nanogui::Color(1.0f,1.0f,1.0f,1.0f);
-	mediatheme->mBorderDark = nanogui::Color(0,0);
-	mediatheme->mBorderMedium = nanogui::Color(0,0);
-	mediatheme->mBorderLight = nanogui::Color(0,0);
-	mediatheme->mDropShadow = nanogui::Color(0,0);
-	mediatheme->mButtonFontSize = 30;
-	mediatheme->mStandardFontSize = 20;
-
-	windowtheme = new Theme(*theme());
-	windowtheme->mWindowFillFocused = nanogui::Color(220, 200);
-	windowtheme->mWindowFillUnfocused = nanogui::Color(220, 200);
-	windowtheme->mWindowHeaderGradientBot = nanogui::Color(60,230);
-	windowtheme->mWindowHeaderGradientTop = nanogui::Color(60,230);
-	windowtheme->mTextColor = nanogui::Color(20,255);
-	windowtheme->mWindowCornerRadius = 0;
-	windowtheme->mButtonGradientBotFocused = nanogui::Color(210,255);
-	windowtheme->mButtonGradientBotUnfocused = nanogui::Color(190,255);
-	windowtheme->mButtonGradientTopFocused = nanogui::Color(230,255);
-	windowtheme->mButtonGradientTopUnfocused = nanogui::Color(230,255);
-	windowtheme->mButtonGradientTopPushed = nanogui::Color(170,255);
-	windowtheme->mButtonGradientBotPushed = nanogui::Color(210,255);
-	windowtheme->mBorderDark = nanogui::Color(150,255);
-	windowtheme->mBorderMedium = nanogui::Color(165,255);
-	windowtheme->mBorderLight = nanogui::Color(230,255);
-	windowtheme->mButtonFontSize = 16;
-	windowtheme->mTextColorShadow = nanogui::Color(0,0);
-	windowtheme->mWindowTitleUnfocused = windowtheme->mWindowTitleFocused;
-	windowtheme->mWindowTitleFocused = nanogui::Color(240,255);
-	windowtheme->mWindowDropShadowSize = 0;
-	windowtheme->mDropShadow = nanogui::Color(0,0);
-	windowtheme->mIconScale = 0.85f;
-
-	toolbar = new StationaryWindow(this, "");
-	toolbar->setPosition(Vector2i(0,0));
-	toolbar->setFixedWidth(50);
-	toolbar->setFixedHeight(height());
+	toolbuttheme_ = new Theme(*theme());
+	toolbuttheme_->mBorderDark = nanogui::Color(0,0);
+	toolbuttheme_->mBorderLight = nanogui::Color(0,0);
+	toolbuttheme_->mButtonGradientBotFocused = nanogui::Color(60,255);
+	toolbuttheme_->mButtonGradientBotUnfocused = nanogui::Color(0,0);
+	toolbuttheme_->mButtonGradientTopFocused = nanogui::Color(60,255);
+	toolbuttheme_->mButtonGradientTopUnfocused = nanogui::Color(0,0);
+	toolbuttheme_->mButtonGradientTopPushed = nanogui::Color(60,180);
+	toolbuttheme_->mButtonGradientBotPushed = nanogui::Color(60,180);
+	toolbuttheme_->mTextColor = nanogui::Color(0.9f,0.9f,0.9f,0.9f);
+	toolbuttheme_->mWindowDropShadowSize = 0;
+	toolbuttheme_->mDropShadow = nanogui::Color(0,0);
+
+	windowtheme_ = new Theme(*theme());
+	windowtheme_->mWindowHeaderGradientBot = nanogui::Color(0,0);
+	windowtheme_->mWindowHeaderGradientTop = nanogui::Color(0,0);
+	windowtheme_->mTextColor = nanogui::Color(20,255);
+	windowtheme_->mWindowCornerRadius = 0;
+	windowtheme_->mBorderDark = nanogui::Color(0,0);
+	windowtheme_->mBorderMedium = nanogui::Color(0,0);
+	windowtheme_->mBorderLight = nanogui::Color(0,0);
+	windowtheme_->mWindowFillFocused = nanogui::Color(64, 0);
+	windowtheme_->mWindowFillUnfocused= nanogui::Color(64, 0);
+	windowtheme_->mWindowDropShadowSize = 0;
+	windowtheme_->mDropShadow = nanogui::Color(0, 0);
+
+	toolbar_ = new FixedWindow(this);
+	toolbar_->setPosition(Vector2i(0,0));
+	toolbar_->setFixedWidth(toolbar_w);
+	toolbar_->setFixedHeight(height());
 
 	setResizeCallback([this](Vector2i s) {
-		toolbar->setFixedSize({toolbar->width(), s[1]});
-		toolbar->setPosition({0, 0});
-		if (view) {
-			view->setFixedSize({max(s[0] - toolbar->width(), 0), s[1]});
-			view->setPosition({toolbar->width(), 0});
+		toolbar_->setFixedSize({toolbar_->width(), s[1]});
+		toolbar_->setPosition({0, 0});
+		if (active_view_) {
+			active_view_->setFixedSize({max(s[0] - toolbar_->width(), 0), s[1]});
+			active_view_->setPosition({toolbar_->width(), 0});
 		}
-		this->performLayout();
+		performLayout();
 	});
 
-	auto tools = new Widget(toolbar);
-	tools->setLayout(new BoxLayout(	Orientation::Vertical,
+	tools_ = new Widget(toolbar_);
+	tools_->setLayout(new BoxLayout( Orientation::Vertical,
 									Alignment::Middle, 0, 10));
-	tools->setPosition(Vector2i(5,10));
+	tools_->setPosition(Vector2i(5,10));
 
-	auto button = new ToolButton(tools, ENTYPO_ICON_HOME);
-	button->setIconExtraScale(1.5f);
-	button->setTheme(toolbuttheme);
-	button->setTooltip("Home");
-	button->setFixedSize(Vector2i(40,40));
-	button->setCallback([this]() {
+	setVisible(true);
+	performLayout();
+}
 
-	});
+Screen::~Screen() {
+	for (auto [name, ptr] : modules_) {
+		std::ignore = name;
+		delete ptr;
+	}
+}
 
-	setVisible(true);
+void Screen::redraw() {
+	// glfwPostEmptyEvent() is safe to call from any thread
+	// https://www.glfw.org/docs/3.3/intro_guide.html#thread_safety
+	glfwPostEmptyEvent();
+}
+
+void Screen::setView(ftl::gui2::View *view) {
+
+	view->setPosition(Vector2i(toolbar_->width(), 0));
+	view->setFixedSize(Vector2i(width() - toolbar_->width(), height()));
+	view->setTheme(windowtheme_);
+	view->setVisible(true);
+
+	if (active_view_) {
+		active_view_->setVisible(false);
+
+		// View requires same cleanup as Window (see screen.cpp) before removed.
+		if (std::find(mFocusPath.begin(), mFocusPath.end(), active_view_) != mFocusPath.end()) {
+			mFocusPath.clear();
+		}
+		if (mDragWidget == active_view_) {
+			mDragWidget = nullptr;
+		}
+
+		removeChild(active_view_);
+	}
+
+	active_view_ = view;
+	LOG(INFO) << "number of children (Screen): "<< mChildren.size();
 	performLayout();
 }
 
-Screen::~Screen() {}
+void Screen::render() {
+	if (active_view_) {
+		active_view_->render();
+	}
+}
+
+nanogui::ToolButton* Screen::addButton() {
+	auto button = new nanogui::ToolButton(tools_, ENTYPO_ICON_MENU, "");
+	button->setIconExtraScale(1.5f);
+	button->setTheme(toolbuttheme_);
+	button->setFixedSize(Vector2i(40,40));
+	return button;
+}
 
-void Screen::setView(ftl::gui2::View *window) {
-	if (view) {
-		view->setVisible(false);
-		this->removeChild(view);
+ftl::gui2::Module* Screen::addModule_(const std::string &name, ftl::gui2::Module* ptr) {
+	ptr->init();
+	if (modules_.find(name) != modules_.end()) {
+		LOG(WARNING) << "Module " << name  << " already loaded. Removing old module";
+		delete modules_[name];
 	}
-	view = window;
-	view->setTheme(windowtheme);
-	this->addChild(view);
-	view->setFixedSize(Vector2i(width() - toolbar->width(), height()));
-	view->setVisible(true);
+
+	modules_[name] = ptr;
+	return ptr;
 }
diff --git a/applications/gui2/src/screen.hpp b/applications/gui2/src/screen.hpp
index 0ddef9ff07bb53606a39fdf69a52aa0ebf0dd5e2..83f09c2c89196ed71dad7303ddc18f771052e448 100644
--- a/applications/gui2/src/screen.hpp
+++ b/applications/gui2/src/screen.hpp
@@ -3,13 +3,14 @@
 #include <nanogui/screen.h>
 #include <nanogui/glutil.h>
 
-#include <memory>
+#include <nanogui/toolbutton.h>
 
-#ifdef HAVE_OPENVR
-#include <openvr/openvr.h>
-#endif
+#include <map>
+#include <memory>
+#include <typeinfo>
 
 #include "view.hpp"
+#include "module.hpp"
 
 namespace ftl {
 namespace gui2 {
@@ -19,19 +20,99 @@ class Screen : public nanogui::Screen {
 	explicit Screen();
 	~Screen();
 
-	void render();
-	void setView(ftl::gui2::View *view);
+	void render(); // necessary?
+	/** Redraw the screen (triggers an empty event). Thread safe. */
+	void redraw();
+
+	void activate(Module *ptr);
+
+	/** set active view (existing object */
+	void setView(ftl::gui2::View* view);
+	/** set active view (create new object)*/
+	template<typename T, typename ... Args>
+	void setView(Args ... args);
+
+	/** Add a module.*/
+	template<typename T, typename ... Args>
+	T* addModule(const std::string &name, ftl::Configurable *config, Args ... args);
+
+	/** Get a pointer to module. Module identified by name, exception thrown if not found */
+	template<typename T>
+	T* getModule(const std::string &name);
+
+	/** Get a pointer to module. Module indentified by dynamic type from template parameter.
+	 * Throws an exception if not found. If more than one possible match (same module
+	 * loaded multiple times), return value can be any.
+	 */
+	template<typename T>
+	T* getModule();
+
+	// prever above template (explicit who manages delete)
+	// template<typename T>
+	// T* addModule(T* ptr) { return addModule_(ptr); }
+
+	// TODO removeModule() as well?
+
+	/** add a button to toolbar */
+	nanogui::ToolButton* addButton();
 
 	private:
+	Module* addModule_(const std::string &name, Module* ptr);
+
+	//std::mutex mtx_; // not used: do not modify gui outside gui (main) thread
+	std::map<std::string, ftl::gui2::Module*> modules_;
 
+	nanogui::Theme* mediatheme_;
+	nanogui::Theme* toolbuttheme_;
+	nanogui::Theme* windowtheme_;
 
-	nanogui::Theme* mediatheme;
-	nanogui::Theme* toolbuttheme;
-	nanogui::Theme* windowtheme;
+	nanogui::Widget *toolbar_;
+	nanogui::Widget *tools_;
 
-	nanogui::Widget *toolbar;
-	ftl::gui2::View *view;
+	ftl::gui2::View *active_view_;
 };
 
+template<typename T, typename ... Args>
+void Screen::setView(Args ... args) {
+	setView_(new T(this, args ...));
+}
+
+template<typename T, typename ... Args>
+T* Screen::addModule(const std::string &name, ftl::Configurable *config, Args ... args) {
+	return dynamic_cast<T*>(
+		addModule_(
+			name,
+			ftl::config::create<T>(config, name, args ...)
+		)
+	);
+}
+
+template<typename T>
+T* Screen::getModule(const std::string &name) {
+	if (modules_.find(name) == modules_.end()) {
+		throw ftl::exception("module: " + name + " not found");
+	}
+
+	auto* ptr = dynamic_cast<T*>(modules_[name]);
+
+	if (ptr == nullptr) {
+		throw ftl::exception("bad cast, module requested with wrong type");
+	}
+
+	return ptr;
+}
+
+template<typename T>
+T* Screen::getModule() {
+	for (auto& [name, ptr] : modules_) {
+		std::ignore = name;
+		if (typeid(*ptr) == typeid(T)) {
+			return dynamic_cast<T*>(ptr);
+		}
+	}
+
+	throw ftl::exception("module not found");
+}
+
 }
 }
diff --git a/applications/gui2/src/toolbar.cpp b/applications/gui2/src/toolbar.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/applications/gui2/src/toolbar.hpp b/applications/gui2/src/toolbar.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3f59c932d39b02caf58f2abac65bdd9246f0a7da
--- /dev/null
+++ b/applications/gui2/src/toolbar.hpp
@@ -0,0 +1,2 @@
+#pragma once
+
diff --git a/applications/gui2/src/view.cpp b/applications/gui2/src/view.cpp
index a59ca1da9905dd3c04bd1bb3ad9d615baf85014b..9e65837bc5a143ed7106d365776d56dbb4fcae56 100644
--- a/applications/gui2/src/view.cpp
+++ b/applications/gui2/src/view.cpp
@@ -2,9 +2,3 @@
 
 using ftl::gui2::View;
 
-View::View(ftl::Configurable *config, ftl::gui2::InputOutput *io,
-		const std::string &title) :
-		StationaryWindow(nullptr, title), config_(config), io_(io) {
-
-
-}
diff --git a/applications/gui2/src/view.hpp b/applications/gui2/src/view.hpp
index 0a31a0a3de3412f4abff6d5ed365347dc5f5c945..ef747dc1832a4859f5e20490562e12882286d601 100644
--- a/applications/gui2/src/view.hpp
+++ b/applications/gui2/src/view.hpp
@@ -3,20 +3,18 @@
 #include "window.hpp"
 #include "inputoutput.hpp"
 
+#include <nanogui/vscrollpanel.h>
+
 namespace ftl {
 namespace gui2 {
 
-class View : public nanogui::StationaryWindow {
+class View : public nanogui::Widget {
 public:
-	View(ftl::Configurable *config, ftl::gui2::InputOutput *io, const std::string &title = "");
-	virtual ~View() {}
-
-	virtual void render() {}
+	using nanogui::Widget::Widget;
 
+	virtual ~View() {}
 
-	protected:
-	ftl::Configurable *config_;
-	ftl::gui2::InputOutput *io_;
+	virtual void render() {} // TODO remove if VR works?
 };
 
 };
diff --git a/applications/gui2/src/window.hpp b/applications/gui2/src/window.hpp
index 8dcb2339b842cdfa9f9b00d635c13bef591e1d7e..1e3f125de3c51e33914873b7246b511136b2ba3f 100644
--- a/applications/gui2/src/window.hpp
+++ b/applications/gui2/src/window.hpp
@@ -7,9 +7,13 @@ namespace nanogui {
 /**
  * Non-movable Window widget
  */
-class StationaryWindow : public nanogui::Window {
-	using Window::Window;
+class FixedWindow : public nanogui::Window {
+public:
+	FixedWindow(nanogui::Widget *parent, const char* name="") :
+		nanogui::Window(parent, name) {};
+
 	virtual bool mouseDragEvent(const Vector2i&, const Vector2i &, int, int) override { return false; }
+	virtual ~FixedWindow() {}
 };
 
 }
diff --git a/components/common/cpp/include/ftl/handle.hpp b/components/common/cpp/include/ftl/handle.hpp
index d7e2f8089f87e4664cafaf8ebc886539a491eb1a..ac634443e89673ea54f6ad0217a415289dd42a12 100644
--- a/components/common/cpp/include/ftl/handle.hpp
+++ b/components/common/cpp/include/ftl/handle.hpp
@@ -52,7 +52,11 @@ struct Handle {
 		return *this;
 	}
 
-	inline ~Handle() { if (handler_) handler_->remove(*this); }
+	inline ~Handle() {
+		if (handler_) {
+			handler_->remove(*this);
+		}
+	}
 
 	private:
 	BaseHandler *handler_;
@@ -117,4 +121,4 @@ ftl::Handle ftl::BaseHandler::make_handle(BaseHandler *h, int id) {
 	return ftl::Handle(h, id);
 }
 
-#endif
\ No newline at end of file
+#endif