From cab7ba8880a58d955801373ed9209d8a13441764 Mon Sep 17 00:00:00 2001
From: Nicolas Pope <nicolas.pope@utu.fi>
Date: Fri, 12 Jun 2020 13:42:47 +0300
Subject: [PATCH] New Frame Class

---
 CMakeLists.txt                                |    4 +
 components/common/cpp/include/ftl/handle.hpp  |   16 +-
 components/common/cpp/test/handle_unit.cpp    |   16 +
 .../streams/include/ftl/streams/feed.hpp      |   33 +
 components/structures/CMakeLists.txt          |    4 +-
 .../structures/include/ftl/data/channels.hpp  |   97 ++
 .../structures/include/ftl/data/framepool.hpp |   43 +
 .../structures/include/ftl/data/new_frame.hpp |  435 +++--
 components/structures/src/new_frame.cpp       |  279 +++-
 components/structures/src/pool.cpp            |   75 +
 components/structures/test/CMakeLists.txt     |   18 +-
 .../structures/test/frame_example_1.cpp       |  135 ++
 components/structures/test/frame_unit.cpp     | 1405 +++++++++++++++--
 components/structures/test/pool_unit.cpp      |  120 ++
 lib/libsgm/src/CMakeLists.txt                 |    2 +
 lib/libstereo/CMakeLists.txt                  |    1 +
 16 files changed, 2354 insertions(+), 329 deletions(-)
 create mode 100644 components/streams/include/ftl/streams/feed.hpp
 create mode 100644 components/structures/include/ftl/data/channels.hpp
 create mode 100644 components/structures/include/ftl/data/framepool.hpp
 create mode 100644 components/structures/src/pool.cpp
 create mode 100644 components/structures/test/frame_example_1.cpp
 create mode 100644 components/structures/test/pool_unit.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 96fb65323..4df38ff5c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -43,6 +43,10 @@ MACRO( VERSION_STR_TO_INTS major minor patch version )
 
 ENDMACRO( VERSION_STR_TO_INTS )
 
+if (CMAKE_COMPILER_IS_GNUCXX)
+	set(CMAKE_CUDA_HOST_COMPILER gcc-7)
+endif()
+
 find_package( OpenCV REQUIRED COMPONENTS core imgproc highgui cudaimgproc calib3d imgcodecs videoio aruco cudaarithm cudastereo cudaoptflow face tracking quality xfeatures2d)
 find_package( Threads REQUIRED )
 find_package( URIParser REQUIRED )
diff --git a/components/common/cpp/include/ftl/handle.hpp b/components/common/cpp/include/ftl/handle.hpp
index 7a2348d5b..d7e2f8089 100644
--- a/components/common/cpp/include/ftl/handle.hpp
+++ b/components/common/cpp/include/ftl/handle.hpp
@@ -72,7 +72,7 @@ struct Handler : BaseHandler {
 	 * Add a new callback function. It returns a `Handle` object that must
 	 * remain in scope, the destructor of the `Handle` will remove the callback.
 	 */
-	Handle on(const std::function<bool(ARGS...)> &f) {
+	[[nodiscard]] Handle on(const std::function<bool(ARGS...)> &f) {
 		std::unique_lock<std::mutex> lk(mutex_);
 		int id = id_++;
 		callbacks_[id] = f;
@@ -87,13 +87,15 @@ struct Handler : BaseHandler {
 	 */
 	void trigger(ARGS ...args) {
 		std::unique_lock<std::mutex> lk(mutex_);
-		try {
-			for (auto &f : callbacks_) {
-				f.second(std::forward<ARGS...>(args...));
+		//try {
+			for (auto i=callbacks_.begin(); i!=callbacks_.end(); ) {
+				bool keep = i->second(std::forward<ARGS>(args)...);
+				if (!keep) i = callbacks_.erase(i);
+				else ++i;
 			}
-		} catch (const std::exception &e) {
-			LOG(ERROR) << "Exception in callback: " << e.what();
-		}
+		//} catch (const std::exception &e) {
+		//	LOG(ERROR) << "Exception in callback: " << e.what();
+		//}
 	}
 
 	/**
diff --git a/components/common/cpp/test/handle_unit.cpp b/components/common/cpp/test/handle_unit.cpp
index 1c1c27d61..313967597 100644
--- a/components/common/cpp/test/handle_unit.cpp
+++ b/components/common/cpp/test/handle_unit.cpp
@@ -23,6 +23,22 @@ TEST_CASE( "Handle release on cancel" ) {
 	REQUIRE(calls == 5);
 }
 
+TEST_CASE( "Handle release on false return" ) {
+	Handler<int> handler;
+
+	int calls = 0;
+
+	auto h = handler.on([&calls](int i) {
+		calls += i;
+		return false;
+	});
+
+	handler.trigger(5);
+	REQUIRE(calls == 5);
+	handler.trigger(5);
+	REQUIRE(calls == 5);
+}
+
 TEST_CASE( "Handle multiple triggers" ) {
 	Handler<int> handler;
 
diff --git a/components/streams/include/ftl/streams/feed.hpp b/components/streams/include/ftl/streams/feed.hpp
new file mode 100644
index 000000000..890eb64e5
--- /dev/null
+++ b/components/streams/include/ftl/streams/feed.hpp
@@ -0,0 +1,33 @@
+#ifndef _FTL_STREAMS_FEED_HPP_
+#define _FTL_STREAMS_FEED_HPP_
+
+#include <ftl/configurable.hpp>
+
+namespace ftl {
+namespace streams {
+
+using FrameSetHandler = std::function<bool(ftl::data::FrameSet&)>;
+
+class Feed : public ftl::Configurable {
+	public:
+
+	void addStream(ftl::streams::Stream *s);
+
+	ftl::Handler onFrameSet(const FrameSetHandler &fsh);
+
+	uint32_t allocateFrameId();
+
+	void createFrame(uint32_t id, ftl::data::Frame &f);
+
+	void createFrameSet(uint32_t id, int size, ftl::data::FrameSet &fs);
+
+	private:
+	ftl::streams::Receiver *receiver_;
+	ftl::streams::Sender *sender_;
+	std::unordered_map<uint32_t, ftl::data::Session> stores_;
+};
+
+}
+}
+
+#endif
\ No newline at end of file
diff --git a/components/structures/CMakeLists.txt b/components/structures/CMakeLists.txt
index 5df4a3870..b47288977 100644
--- a/components/structures/CMakeLists.txt
+++ b/components/structures/CMakeLists.txt
@@ -1,10 +1,12 @@
 
-add_library(ftldata STATIC ./src/new_frame.cpp)
+add_library(ftldata ./src/new_frame.cpp ./src/pool.cpp)
 
 target_include_directories(ftldata PUBLIC
 	${CMAKE_CURRENT_SOURCE_DIR}/include)
 
 target_link_libraries(ftldata ftlcommon Eigen3::Eigen ftlcodecs)
 
+if (BUILD_TESTS)
 add_subdirectory(test)
+endif()
 
diff --git a/components/structures/include/ftl/data/channels.hpp b/components/structures/include/ftl/data/channels.hpp
new file mode 100644
index 000000000..341759b13
--- /dev/null
+++ b/components/structures/include/ftl/data/channels.hpp
@@ -0,0 +1,97 @@
+#ifndef _FTL_DATA_CHANNELS_HPP_
+#define _FTL_DATA_CHANNELS_HPP_
+
+#include <string>
+#include <ftl/codecs/channels.hpp>
+#include <ftl/exception.hpp>
+
+namespace ftl {
+namespace data {
+
+
+/** Kind of channel in terms of data persistence */
+enum class StorageMode {
+	PERSISTENT,		// Most recent value, even from previous fram
+	TRANSIENT,		// Only most recent value since last frame
+	AGGREGATE		// All changes since last frame
+};
+
+/** If a channel has changed, what is the current status of that change. */
+enum class ChangeType {
+	UNCHANGED,
+	LOCAL,			// Explicit local modification occurred
+	FOREIGN,		// Received externally, to be forwarded
+	COMPLETED		// Received externally, not to be forwarded
+};
+
+/** Current status of the data contained within a channel. */
+enum class ChannelStatus {
+	INVALID,		// Any data is stale and should not be referenced
+	VALID,			// Contains currently valid data
+	FLUSHED,		// Has already been transmitted, now read-only
+	DISPATCHED		// Externally received, can't be flushed but can be modified locally
+};
+
+/* Internal structure for channel configurations. */
+struct ChannelConfig {
+	std::string name;
+	StorageMode mode;
+	size_t type_id;
+};
+
+/**
+ * Add a channel configuration to the registry. By default channels are not
+ * in the registry and this means they have no name or specified type. Non
+ * registered channels can still be used but no runtime checks are performed.
+ */
+void registerChannel(ftl::codecs::Channel, const ChannelConfig &config);
+
+/** Used by unit tests. */
+void clearRegistry();
+
+/**
+ * Check if channel is marked as persistent storage in the registry.
+ */
+bool isPersistent(ftl::codecs::Channel);
+
+bool isAggregate(ftl::codecs::Channel);
+
+/**
+ * Get channel type hash_code as from `std::type_info::hash_code()`. This
+ * returns 0 if not registered or registered as allowing any time, 0 means
+ * accept any type.
+ */
+size_t getChannelType(ftl::codecs::Channel);
+
+template <typename T>
+void verifyChannelType(ftl::codecs::Channel c) {
+	size_t t = getChannelType(c);
+	if (t > 0 && t != typeid(T).hash_code()) throw FTL_Error("Incorrect type for channel " << static_cast<unsigned int>(c));
+}
+
+/**
+ * Get the registered string name for channel, or an empty string if no name.
+ */
+std::string getChannelName(ftl::codecs::Channel);
+
+/** Unsupported */
+ftl::codecs::Channel getChannelByName(const std::string &name);
+
+/**
+ * Helper to register a channel using a template specified type.
+ */
+template <typename T>
+void make_channel(ftl::codecs::Channel c, const std::string &name, StorageMode mode) {
+	// TODO: Generate packer + unpacker?
+	registerChannel(c, {name, mode, typeid(T).hash_code()});
+}
+
+template <>
+inline void make_channel<void>(ftl::codecs::Channel c, const std::string &name, StorageMode mode) {
+	registerChannel(c, {name, mode, 0});
+}
+
+}
+}
+
+#endif
diff --git a/components/structures/include/ftl/data/framepool.hpp b/components/structures/include/ftl/data/framepool.hpp
new file mode 100644
index 000000000..204965ff7
--- /dev/null
+++ b/components/structures/include/ftl/data/framepool.hpp
@@ -0,0 +1,43 @@
+#ifndef _FTL_DATA_FRAMEPOOL_HPP_
+#define _FTL_DATA_FRAMEPOOL_HPP_
+
+#include <ftl/data/new_frame.hpp>
+#include <list>
+#include <unordered_map>
+
+namespace ftl {
+namespace data {
+
+class Pool {
+	public:
+	explicit Pool(size_t min_n, size_t max_n);
+	~Pool();
+
+	ftl::data::Frame allocate(uint32_t id, int64_t timestamp);
+	void release(Frame &f);
+
+	ftl::data::Session &session(uint32_t id);
+
+	size_t size(uint32_t id);
+
+	size_t size();
+
+	private:
+	struct PoolData {
+		std::list<ftl::data::Frame*> pool;
+		ftl::data::Session session;
+		int64_t last_timestamp;
+	};
+
+	std::unordered_map<uint32_t, PoolData> pool_;
+	size_t min_n_;
+	size_t max_n_;
+	size_t ideal_n_;
+
+	PoolData &_getPool(uint32_t);
+};
+
+}
+}
+
+#endif
\ No newline at end of file
diff --git a/components/structures/include/ftl/data/new_frame.hpp b/components/structures/include/ftl/data/new_frame.hpp
index 963e91b5f..5a45662de 100644
--- a/components/structures/include/ftl/data/new_frame.hpp
+++ b/components/structures/include/ftl/data/new_frame.hpp
@@ -8,55 +8,79 @@
 #include <list>
 #include <unordered_map>
 #include <ftl/codecs/channels.hpp>
+#include <ftl/data/channels.hpp>
 #include <ftl/exception.hpp>
+#include <ftl/handle.hpp>
+
+template<typename T> struct is_list : public std::false_type {};
+
+template<typename T>
+struct is_list<std::list<T>> : public std::true_type {};
 
 namespace ftl {
+namespace streams { class Feed; }
 namespace data {
 
 class Session;
+class Pool;
 
-/** Kind of channel in terms of data persistence */
-enum class ChannelMode {
-	PERSISTENT,		// Most recent value, even from previous fram
-	IMMEDIATE,		// Only most recent value since last frame
-	SEQUENCE		// All changes since last frame
-};
-
-struct ChannelConfig {
-	std::string name;
-	ChannelMode mode;
-	size_t type_id;
+enum FrameStatus {
+	CREATED,   // Initial state, before store
+	STORED,    // Changed to this after call to `store`
+	FLUSHED,   // Changed to this after call to `flush`
+	RELEASED   // Destroyed or moved
 };
 
+/**
+ * Helper class to enable aggregation of aggregate channels.
+ */
 template <typename T>
-ChannelConfig make_channel(const std::string &name, ChannelMode mode) {
-	// TODO: Generate packer + unpacker?
-	return {name, mode, typeid(T).hash_code()};
-}
+struct Aggregator {
+	T &list;
+	bool aggregate=true;
+
+	Aggregator &operator=(const T &l) {
+		if (aggregate) list.insert(list.end(), l.begin(), l.end());
+		else list = l;
+		return *this;
+	}
 
-//template <>
-//ChannelConfig make_channel<void>(const std::string &name, ChannelMode mode) {
-//	return {name, mode, 0};
-//}
+	Aggregator &operator=(T &&l) {
+		if (aggregate) list.splice(list.end(), l, l.begin(), l.end());
+		else list = std::move(l);
+		return *this;
+	}
+
+	operator T() { return list; }
+	operator T() const { return list; }
+};
 
 class Frame {
+	friend class Session;
+	friend class ftl::data::Pool;
+	friend class ftl::streams::Feed;
+
+	private:
+	// Only Feed class should construct
+	Frame(Pool *ppool, Session *parent, uint32_t pid, int64_t ts) : timestamp_(ts), id(pid), pool_(ppool), parent_(parent), status_(FrameStatus::CREATED) {};
+	int64_t timestamp_=0;
+
 	public:
-	uint32_t id=0;
-	int64_t timestamp=0;
+	const uint32_t id=0;
+
+	inline int64_t timestamp() const { return timestamp_; }
 
 	public:
-	Frame() : parent_(nullptr) {};
-	explicit Frame(Session *parent) : parent_(parent) {};
-	~Frame() { flush(); };
+	Frame()=delete;
+	
+	~Frame();
 
 	Frame(Frame &&f) {
-		f.swapTo(*this);
-		f.reset();
+		f.moveTo(*this);
 	}
 
 	Frame &operator=(Frame &&f) {
-		f.swapTo(*this);
-		f.reset();
+		f.moveTo(*this);
 		return *this;
 	}
 
@@ -64,18 +88,34 @@ class Frame {
 	Frame(const Frame &)=delete;
 	Frame &operator=(const Frame &)=delete;
 
-	inline bool has(ftl::codecs::Channel c);
+	inline FrameStatus status() const { return status_; }
+
+	inline size_t size() const { return data_.size(); }
+
+	bool has(ftl::codecs::Channel c) const;
+
+	inline bool hasOwn(ftl::codecs::Channel c) const;
+
+	inline bool changed(ftl::codecs::Channel c) const;
+
+	inline bool readonly(ftl::codecs::Channel c) const;
 
-	inline bool changed(ftl::codecs::Channel c);
+	inline bool flushed(ftl::codecs::Channel c) const;
 
-	inline const std::unordered_set<ftl::codecs::Channel> &changed() const { return changed_; }
+	inline ftl::data::ChangeType getChangeType(ftl::codecs::Channel c) const;
+
+	inline const std::unordered_map<ftl::codecs::Channel, ChangeType> &changed() const { return changed_; }
 
 	template <typename T>
-	bool isType(ftl::codecs::Channel c);
+	bool isType(ftl::codecs::Channel c) const;
 
 	template <typename T>
 	const T &get(ftl::codecs::Channel c) const;
 
+	const std::any &getAny(ftl::codecs::Channel c) const;
+
+	std::any &getAnyMutable(ftl::codecs::Channel c);
+
 	template <typename T>
 	const T *getPtr(ftl::codecs::Channel c) const noexcept;
 
@@ -83,47 +123,109 @@ class Frame {
 	T *getMutable(ftl::codecs::Channel c);
 
 	inline void touch(ftl::codecs::Channel c) {
-		changed_.emplace(c);
+		changed_[c] = ChangeType::LOCAL;
+	}
+
+	inline void touch(ftl::codecs::Channel c, ChangeType type) {
+		changed_[c] = type;
 	}
 
 	inline void untouch(ftl::codecs::Channel c) {
 		changed_.erase(c);
 	}
 
-	template <typename T>
-	T &create(ftl::codecs::Channel c, const T &value);
+	template <typename T, std::enable_if_t<!is_list<T>::value,int> = 0>
+	T &create(ftl::codecs::Channel c);
+
+	template <typename T, std::enable_if_t<is_list<T>::value,int> = 0>
+	ftl::data::Aggregator<T> create(ftl::codecs::Channel c);
+
+	/**
+	 * Creates a channel data entry with a forced change status. This also
+	 * changes the channel status to `DISPATCHED`. If the storage mode is
+	 * `persistent` this adds to session store instead of local frame store,
+	 * although the change status is added to the local frame.
+	 * 
+	 * To be used by receive, no one else.
+	 */
+	template <typename T, std::enable_if_t<!is_list<T>::value,int> = 0>
+	T &createChange(ftl::codecs::Channel c, ftl::data::ChangeType t);
+
+	template <typename T, std::enable_if_t<is_list<T>::value,int> = 0>
+	ftl::data::Aggregator<T> createChange(ftl::codecs::Channel c, ftl::data::ChangeType t);
 
 	template <typename T>
-	T &create(ftl::codecs::Channel c);
+	T &createChange(ftl::codecs::Channel c, ftl::data::ChangeType t, std::vector<uint8_t> &data);
+
+	const std::vector<uint8_t> &getEncoded(ftl::codecs::Channel c) const;
 
 	template <typename T, typename ...ARGS>
 	T &emplace(ftl::codecs::Channel, ARGS...);
 
-	template <typename T>
-	void push(ftl::codecs::Channel c, const T &v);
+	template <typename T, std::enable_if_t<!is_list<T>::value,int> = 0>
+	T &set(ftl::codecs::Channel c);
 
-	template <typename T>
-	void set(ftl::codecs::Channel c, const T &v);
+	template <typename T, std::enable_if_t<is_list<T>::value,int> = 0>
+	ftl::data::Aggregator<T> set(ftl::codecs::Channel c);
+
+	/**
+	 * Will remove a channel by changing its status and will not remove data.
+	 */
+	void remove(ftl::codecs::Channel);
+
+	/**
+	 * Will remove a channel and destroy all data associated with it.
+	 */
+	void hardRemove(ftl::codecs::Channel);
 
-	inline void on(ftl::codecs::Channel c, const std::function<bool(Frame&,ftl::codecs::Channel)> &cb);
+	inline ftl::Handle onChange(ftl::codecs::Channel c, const std::function<bool(Frame&,ftl::codecs::Channel)> &cb);
 
-	inline void on(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb);
+	inline ftl::Handle onChange(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb);
 
+	inline ftl::Handle onFlush(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb);
+
+	/**
+	 * Merge the given frame parameter into this frame. It is a move operation
+	 * on a per channel basis.
+	 */
 	void merge(Frame &);
 
-	void swapTo(Frame &);
+	void moveTo(Frame &);
 
 	void swapChanged(Frame &);
 
 	void swapChannel(ftl::codecs::Channel, Frame &);
 
-	inline void reset() { changed_.clear(); }
+	/**
+	 * Discard all change status without removing the data.
+	 */
+	inline void resetChanges() { changed_.clear(); }
+
+	/**
+	 * Clears all state to an empty condition without releasing memory.
+	 */
+	void reset();
+
+	/**
+	 * Deletes all memory and resets to starting condition. This should not
+	 * be used, instead use `release()` which will save the memory into a pool
+	 * rather than deleting it completely.
+	 */
+	void hardReset();
 
-	void clear();
+	/**
+	 * Free memory into the memory pool. This also implicitly resets.
+	 */
+	void release();
 
 	/** Send changes back through origin stream. */
 	bool flush();
 
+	bool flush(ftl::codecs::Channel c);
+
+	/** Copy persistent changes to session. To be called before dispatch. */
+	void store();
+
 	inline auto begin() const { return data_.begin(); }
 	inline auto end() const { return data_.end(); }
 
@@ -131,124 +233,245 @@ class Frame {
 	// onEndFlush
 	// onError
 
-	static void registerChannel(ftl::codecs::Channel, const ChannelConfig &config);
-	static void clearRegistry();
+	inline MUTEX &mutex();
+
+	protected:
+	std::any &createAnyChange(ftl::codecs::Channel c, ftl::data::ChangeType t);
+
+	std::any &createAnyChange(ftl::codecs::Channel c, ftl::data::ChangeType t, std::vector<uint8_t> &data);
 
-	static bool isPersistent(ftl::codecs::Channel);
-	static size_t getChannelType(ftl::codecs::Channel);
-	static std::string getChannelName(ftl::codecs::Channel);
-	static ftl::codecs::Channel getChannelByName(const std::string &name);
+	std::any &createAny(ftl::codecs::Channel c);
 
 	private:
-	std::map<ftl::codecs::Channel, std::any> data_;
-	std::unordered_set<ftl::codecs::Channel> changed_;
-	std::unordered_map<ftl::codecs::Channel, std::list<std::function<bool(Frame&,ftl::codecs::Channel)>>> triggers_;
-	std::list<std::function<bool(Frame&,ftl::codecs::Channel)>> any_triggers_;
+	struct ChannelData {
+		ChannelStatus status=ChannelStatus::INVALID;
+		std::any data;
+		std::vector<uint8_t> encoded={};
+	};
+
+	ChannelData &_getData(ftl::codecs::Channel);
+	const ChannelData &_getData(ftl::codecs::Channel) const;
+
+	std::map<ftl::codecs::Channel, ChannelData> data_;
+	std::unordered_map<ftl::codecs::Channel, ChangeType> changed_;
+	Pool *pool_;
 	Session *parent_;
+	FrameStatus status_;
+
+	inline void restart(int64_t ts) {
+		timestamp_ = ts;
+		status_ = FrameStatus::CREATED;
+	}
 };
 
-class Session : public Frame {};
+class Session : public Frame {
+	friend class Frame;
+
+	public:
+	Session() : Frame(nullptr, nullptr,0,0) {
+		status_ = FrameStatus::STORED;
+	}
+
+	~Session() {
+		// Sessions don't get memory pooled.
+		status_ = FrameStatus::RELEASED;
+	}
+
+	ftl::Handle onChange(uint32_t id, ftl::codecs::Channel c, const std::function<bool(Frame&,ftl::codecs::Channel)> &cb);
+
+	ftl::Handle onChange(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb);
+
+	ftl::Handle onFlush(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb);
+
+	void notifyChanges(Frame &f);
+
+	void flush(Frame &f);
+
+	void flush(Frame &f, ftl::codecs::Channel c);
+
+	inline MUTEX &mutex() { return mutex_; }
+	
+	private:
+	std::unordered_map<uint64_t, ftl::Handler<Frame&,ftl::codecs::Channel>> change_channel_;
+	ftl::Handler<Frame&,ftl::codecs::Channel> change_;
+	ftl::Handler<Frame&,ftl::codecs::Channel> flush_;
+
+	MUTEX mutex_;
+};
 
 }
 }
 
 // ==== Implementations ========================================================
 
-bool ftl::data::Frame::has(ftl::codecs::Channel c) {
-	return data_.find(c) != data_.end() || (parent_ && parent_->has(c));
+MUTEX &ftl::data::Frame::mutex() { return parent_->mutex(); }
+
+bool ftl::data::Frame::hasOwn(ftl::codecs::Channel c) const {
+	const auto &i = data_.find(c);
+	return (i != data_.end() && i->second.status != ftl::data::ChannelStatus::INVALID);
 }
 
-bool ftl::data::Frame::changed(ftl::codecs::Channel c) {
+bool ftl::data::Frame::changed(ftl::codecs::Channel c) const {
 	return changed_.find(c) != changed_.end();
 }
 
-void ftl::data::Frame::on(ftl::codecs::Channel c, const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
-	if (parent_) parent_->on(c, cb);
-	else triggers_[c].push_back(cb);  // TODO: Find better way to enable removal
+ftl::data::ChangeType ftl::data::Frame::getChangeType(ftl::codecs::Channel c) const {
+	const auto &i = changed_.find(c);
+	return (i == changed_.end()) ? ftl::data::ChangeType::UNCHANGED : i->second;
+}
+
+bool ftl::data::Frame::flushed(ftl::codecs::Channel c) const {
+	const auto &d = _getData(c);
+	return d.status == ChannelStatus::FLUSHED;
+}
+
+bool ftl::data::Frame::readonly(ftl::codecs::Channel c) const {
+	return flushed(c);
 }
 
-void ftl::data::Frame::on(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
-	any_triggers_.push_back(cb);
+ftl::Handle ftl::data::Frame::onChange(ftl::codecs::Channel c, const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
+	return parent_->onChange(id, c, cb);
+}
+
+ftl::Handle ftl::data::Frame::onChange(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
+	return parent_->onChange(cb);
+}
+
+ftl::Handle ftl::data::Frame::onFlush(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
+	return parent_->onFlush(cb);
 }
 
 template <typename T>
-bool ftl::data::Frame::isType(ftl::codecs::Channel c) {
-	auto i = data_.find(c);
-	if (i != data_.end()) {
-		return typeid(T) == i->second.type();
-	} else return false;
+bool ftl::data::Frame::isType(ftl::codecs::Channel c) const {
+	const auto &i = data_.find(c);
+	if (i != data_.end() && i->second.status != ftl::data::ChannelStatus::INVALID) {
+		return typeid(T) == i->second.data.type();
+	} else {
+		return (parent_ && parent_->isType<T>(c)); 
+	}
 }
 
 template <typename T>
 const T *ftl::data::Frame::getPtr(ftl::codecs::Channel c) const noexcept {
-	auto i = data_.find(c);
-	if (i != data_.end()) {
-		return std::any_cast<T>(&i->second);
-	} else if (parent_) {
-		return parent_->getPtr<T>(c);
+	const auto &d = _getData(c);
+	if (d.status != ftl::data::ChannelStatus::INVALID) {
+		return std::any_cast<T>(&d.data);
 	} else return nullptr;
 }
 
 template <typename T>
 const T &ftl::data::Frame::get(ftl::codecs::Channel c) const {
-	auto i = data_.find(c);
-	if (i != data_.end()) {
-		auto *p = std::any_cast<T>(&i->second);
+	const auto &d = _getData(c);
+	if (d.status != ftl::data::ChannelStatus::INVALID) {
+		auto *p = std::any_cast<T>(&d.data);
 		if (!p) throw FTL_Error("'get' wrong type for channel (" << static_cast<unsigned int>(c) << ")");
 		return *p;
-	} else if (parent_) {
-		return parent_->get<T>(c);
-	} else throw FTL_Error("'get' on missing channel (" << static_cast<unsigned int>(c) << ")");
+	} else throw FTL_Error("Missing channel (" << static_cast<unsigned int>(c) << ")");
 }
 
-template <typename T>
-T &ftl::data::Frame::create(ftl::codecs::Channel c, const T &value) {
-	touch(c);
-	size_t t = getChannelType(c);
-	if (t > 0 && t != typeid(T).hash_code()) throw FTL_Error("Incorrect type for channel " << static_cast<unsigned int>(c));
-	auto &d = data_[c];
-	d = value;
-	return *std::any_cast<T>(&d);
+// Non-list version
+template <typename T, std::enable_if_t<!is_list<T>::value,int> = 0>
+T &ftl::data::Frame::create(ftl::codecs::Channel c) {
+	if (isAggregate(c)) throw FTL_Error("Aggregate channels must be of list type");
+
+	ftl::data::verifyChannelType<T>(c);
+
+	std::any &a = createAny(c);
+	if (!isType<T>(c)) return a.emplace<T>();
+	else return *std::any_cast<T>(&a);
+}
+
+// List version
+template <typename T, std::enable_if_t<is_list<T>::value,int> = 0>
+ftl::data::Aggregator<T> ftl::data::Frame::create(ftl::codecs::Channel c) {
+	ftl::data::verifyChannelType<T>(c);
+
+	std::any &a = createAny(c);
+	if (!isType<T>(c)) a.emplace<T>();
+	return ftl::data::Aggregator<T>{*std::any_cast<T>(&a), isAggregate(c)};
 }
 
 template <typename T>
-T &ftl::data::Frame::create(ftl::codecs::Channel c) {
-	touch(c);
-	size_t t = getChannelType(c);
-	if (t > 0 && t != typeid(T).hash_code()) throw FTL_Error("Incorrect type for channel " << static_cast<unsigned int>(c));
-	if (!isType<T>(c)) return data_[c].emplace<T>();
-	else return *std::any_cast<T>(&data_[c]);
+T &ftl::data::Frame::createChange(ftl::codecs::Channel c, ftl::data::ChangeType type, std::vector<uint8_t> &data) {
+	if (!bool(is_list<T>{}) && isAggregate(c)) throw FTL_Error("Aggregate channels must be of list type");
+
+	ftl::data::verifyChannelType<T>(c);
+
+	std::any &a = createAnyChange(c, type, data);
+	if (!isType<T>(c)) return a.emplace<T>();
+	else return *std::any_cast<T>(&a);
+}
+
+// Non-list version
+template <typename T, std::enable_if_t<!is_list<T>::value,int> = 0>
+T &ftl::data::Frame::createChange(ftl::codecs::Channel c, ftl::data::ChangeType type) {
+	if (isAggregate(c)) throw FTL_Error("Aggregate channels must be of list type");
+
+	ftl::data::verifyChannelType<T>(c);
+
+	std::any &a = createAnyChange(c, type);
+	if (!isType<T>(c)) return a.emplace<T>();
+	else return *std::any_cast<T>(&a);
+}
+
+// List version
+template <typename T, std::enable_if_t<is_list<T>::value,int> = 0>
+ftl::data::Aggregator<T> ftl::data::Frame::createChange(ftl::codecs::Channel c, ftl::data::ChangeType type) {
+	ftl::data::verifyChannelType<T>(c);
+
+	std::any &a = createAnyChange(c, type);
+	if (!isType<T>(c)) a.emplace<T>();
+	return ftl::data::Aggregator<T>{*std::any_cast<T>(&a), isAggregate(c)};
 }
 
 template <typename T, typename ...ARGS>
 T &ftl::data::Frame::emplace(ftl::codecs::Channel c, ARGS... args) {
 	touch(c);
-	return data_[c].emplace<T>(std::forward<ARGS...>(args...));
+	return data_[c].data.emplace<T>(std::forward<ARGS...>(args...));
 }
 
-template <typename T>
-void ftl::data::Frame::push(ftl::codecs::Channel c, const T &v) {
+// Non-list version
+template <typename T, std::enable_if_t<!is_list<T>::value,int> = 0>
+T &ftl::data::Frame::set(ftl::codecs::Channel c) {
+	if (status_ != FrameStatus::STORED) throw FTL_Error("Cannot modify before store");
+
 	auto i = data_.find(c);
 	if (i != data_.end()) {
-		auto *p = std::any_cast<std::vector<T>>(&i->second);
-		p->push_back(v);
+		if (i->second.status != ftl::data::ChannelStatus::FLUSHED) {
+			i->second.encoded.clear();
+			touch(c);
+			return *std::any_cast<T>(&i->second.data);
+		} else {
+			throw FTL_Error("Channel is flushed and read-only: " << static_cast<unsigned int>(c));
+		}
+	} else if (parent_ && parent_->isType<T>(c)) {
+		touch(c);
+		return create<T>(c);
 	} else {
-		throw FTL_Error("Push on missing channel (" << static_cast<unsigned int>(c) << ")");
+		throw FTL_Error("Set on missing channel (" << static_cast<unsigned int>(c) << ")");
 	}
-	touch(c);
 }
 
-template <typename T>
-void ftl::data::Frame::set(ftl::codecs::Channel c, const T &v) {
+// List version
+template <typename T, std::enable_if_t<is_list<T>::value,int> = 0>
+ftl::data::Aggregator<T> ftl::data::Frame::set(ftl::codecs::Channel c) {
+	if (status_ != FrameStatus::STORED) throw FTL_Error("Cannot modify before store");
+
 	auto i = data_.find(c);
 	if (i != data_.end()) {
-		i->second = v;
+		if (i->second.status != ftl::data::ChannelStatus::FLUSHED) {
+			i->second.encoded.clear();
+			touch(c);
+			return ftl::data::Aggregator<T>{*std::any_cast<T>(&i->second.data), isAggregate(c)};
+		} else {
+			throw FTL_Error("Channel is flushed and read-only: " << static_cast<unsigned int>(c));
+		}
 	} else if (parent_ && parent_->isType<T>(c)) {
-		create<T>(c, v);
+		touch(c);
+		return create<T>(c);
 	} else {
 		throw FTL_Error("Set on missing channel (" << static_cast<unsigned int>(c) << ")");
 	}
-	touch(c);
 }
 
 #endif
\ No newline at end of file
diff --git a/components/structures/src/new_frame.cpp b/components/structures/src/new_frame.cpp
index 36a8b0c64..7ed685d0c 100644
--- a/components/structures/src/new_frame.cpp
+++ b/components/structures/src/new_frame.cpp
@@ -1,15 +1,18 @@
 #include <ftl/data/new_frame.hpp>
+#include <ftl/data/framepool.hpp>
 
 using ftl::data::Frame;
+using ftl::data::Session;
 using ftl::data::ChannelConfig;
-using ftl::data::ChannelMode;
+using ftl::data::StorageMode;
+using ftl::data::FrameStatus;
 
 #define LOGURU_REPLACE_GLOG 1
 #include <loguru.hpp>
 
 static std::unordered_map<ftl::codecs::Channel, ChannelConfig> reg_channels;
 
-void Frame::registerChannel(ftl::codecs::Channel c, const ChannelConfig &config) {
+void ftl::data::registerChannel(ftl::codecs::Channel c, const ChannelConfig &config) {
 	auto i = reg_channels.find(c);
 	if (i != reg_channels.end()) {
 		throw FTL_Error("Channel " << static_cast<unsigned int>(c) << " already registered");
@@ -18,87 +21,277 @@ void Frame::registerChannel(ftl::codecs::Channel c, const ChannelConfig &config)
 	reg_channels[c] = config;
 }
 
-void Frame::clearRegistry() {
+void ftl::data::clearRegistry() {
 	reg_channels.clear();
 }
 
-bool Frame::isPersistent(ftl::codecs::Channel c) {
+bool ftl::data::isPersistent(ftl::codecs::Channel c) {
 	auto i = reg_channels.find(c);
-	return (i != reg_channels.end()) ? i->second.mode == ChannelMode::PERSISTENT : true;
+	return (i != reg_channels.end()) ? i->second.mode == StorageMode::PERSISTENT : false;
 }
 
-size_t Frame::getChannelType(ftl::codecs::Channel c) {
+bool ftl::data::isAggregate(ftl::codecs::Channel c) {
+	auto i = reg_channels.find(c);
+	return (i != reg_channels.end()) ? i->second.mode == StorageMode::AGGREGATE : false;
+}
+
+size_t ftl::data::getChannelType(ftl::codecs::Channel c) {
 	auto i = reg_channels.find(c);
 	return (i != reg_channels.end()) ? i->second.type_id : 0;
 }
 
-std::string Frame::getChannelName(ftl::codecs::Channel c) {
+std::string ftl::data::getChannelName(ftl::codecs::Channel c) {
 	auto i = reg_channels.find(c);
 	return (i != reg_channels.end()) ? i->second.name : "";
 }
 
-ftl::codecs::Channel Frame::getChannelByName(const std::string &name) {
+ftl::codecs::Channel ftl::data::getChannelByName(const std::string &name) {
 	return ftl::codecs::Channel::Colour;
 }
 
+Frame::~Frame() {
+	if (status_ == FrameStatus::CREATED) store();
+	if (status_ == FrameStatus::STORED) flush();
+	if (status_ != FrameStatus::RELEASED && pool_) pool_->release(*this);
+};
+
+bool ftl::data::Frame::has(ftl::codecs::Channel c) const {
+	const auto &i = data_.find(c);
+	if (i != data_.end() && i->second.status != ftl::data::ChannelStatus::INVALID) {
+		return true;
+	} else {
+		return (parent_ && parent_->has(c)); 
+	}
+}
+
+Frame::ChannelData &Frame::_getData(ftl::codecs::Channel c) {
+	if (status_ == FrameStatus::RELEASED) throw FTL_Error("Reading a released frame");
+	const auto &i = data_.find(c);
+	if (i != data_.end()) {
+		return i->second;
+	} else if (parent_) {
+		return parent_->_getData(c);
+	} else throw FTL_Error("Missing channel (" << static_cast<unsigned int>(c) << ")");
+}
+
+const Frame::ChannelData &Frame::_getData(ftl::codecs::Channel c) const {
+	if (status_ == FrameStatus::RELEASED) throw FTL_Error("Reading a released frame");
+	const auto &i = data_.find(c);
+	if (i != data_.end()) {
+		return i->second;
+	} else if (parent_) {
+		return parent_->_getData(c);
+	} else throw FTL_Error("Missing channel (" << static_cast<unsigned int>(c) << ")");
+}
+
+std::any &Frame::createAnyChange(ftl::codecs::Channel c, ftl::data::ChangeType t) {
+	if (status_ != FrameStatus::CREATED) throw FTL_Error("Cannot apply change after store " << static_cast<int>(status_));
+
+	auto &d = data_[c];
+	if (d.status != ftl::data::ChannelStatus::FLUSHED) {
+		d.status = ftl::data::ChannelStatus::DISPATCHED;
+		d.encoded.clear();
+		touch(c, t);
+		return d.data;
+	} else {
+		throw FTL_Error("Channel is flushed and read-only: " << static_cast<unsigned int>(c));
+	}
+}
+
+std::any &Frame::createAnyChange(ftl::codecs::Channel c, ftl::data::ChangeType t, std::vector<uint8_t> &data) {
+	if (status_ != FrameStatus::CREATED) throw FTL_Error("Cannot apply change after store " << static_cast<int>(status_));
+
+	auto &d = data_[c];
+	if (d.status != ftl::data::ChannelStatus::FLUSHED) {
+		d.status = ftl::data::ChannelStatus::DISPATCHED;
+		d.encoded = std::move(data);
+		touch(c, t);
+		return d.data;
+	} else {
+		throw FTL_Error("Channel is flushed and read-only: " << static_cast<unsigned int>(c));
+	}
+}
+
+std::any &Frame::createAny(ftl::codecs::Channel c) {
+	if (status_ != FrameStatus::STORED) throw FTL_Error("Cannot create before store or after flush");
+
+	auto &d = data_[c];
+	if (d.status != ftl::data::ChannelStatus::FLUSHED) {
+		d.status = ftl::data::ChannelStatus::VALID;
+		d.encoded.clear();
+		touch(c);
+		return d.data;
+	} else {
+		throw FTL_Error("Channel is flushed and read-only: " << static_cast<unsigned int>(c));
+	}
+}
+
+std::any &Frame::getAnyMutable(ftl::codecs::Channel c) {
+	auto &d = _getData(c);
+	return d.data;
+}
+
+const std::vector<uint8_t> &ftl::data::Frame::getEncoded(ftl::codecs::Channel c) const {
+	const auto &d = _getData(c);
+	if (d.status != ftl::data::ChannelStatus::INVALID) {
+		return d.encoded;
+	} else throw FTL_Error("Missing channel (" << static_cast<unsigned int>(c) << ")");
+}
 
 bool Frame::flush() {
+	if (status_ == FrameStatus::CREATED) throw FTL_Error("Frame cannot be flushed before store");
+	if (status_ == FrameStatus::FLUSHED) throw FTL_Error("Frame cannot be flushed twice");
+	status_ = FrameStatus::FLUSHED;
+
 	if (parent_) {
-		for (auto c : changed_) {
-			parent_->changed_.emplace(c);
-			parent_->data_[c] = std::move(data_[c]);
-			data_.erase(c);
-		}
-		parent_->flush();
-	} else {
-		for (auto c : changed_) {
-			auto i = triggers_.find(c);
-			if (i != triggers_.end()) {
-				for (auto f : i->second) {
-					try {
-						f(*this, c);
-					} catch (const std::exception &e) {
-						LOG(ERROR) << "Exception in frame flush: " << e.what();
-					}
-				}
-			}
-		}
+		parent_->flush(*this);
+	}
+	for (auto c : changed_) {
+		_getData(c.first).status = ChannelStatus::FLUSHED;
 	}
-	changed_.clear();
 	return true;
 }
 
+bool Frame::flush(ftl::codecs::Channel c) {
+	if (status_ == FrameStatus::CREATED) throw FTL_Error("Frame cannot be flushed before store");
+	if (status_ == FrameStatus::FLUSHED) throw FTL_Error("Frame cannot be flushed twice");
+	//status_ = FrameStatus::FLUSHED;
+
+	if (parent_ && changed(c)) {
+		parent_->flush(*this, c);
+		_getData(c).status = ChannelStatus::FLUSHED;
+	}
+	return true;
+}
+
+void Frame::store() {
+	if (status_ != FrameStatus::CREATED) throw FTL_Error("Frame cannot be stored twice");
+	status_ = FrameStatus::STORED;
+
+	if (!parent_) return;
+
+	for (auto c : changed_) {
+		if (ftl::data::isPersistent(c.first) && hasOwn(c.first)) {
+			auto &d = data_[c.first];
+			auto &pd = parent_->data_[c.first];
+			pd.data = d.data;
+			//pd.encoded = std::move(d.encoded);
+			pd.status = ChannelStatus::VALID;
+			//data_.erase(c.first);
+		}
+
+		parent_->change_.trigger(*this, c.first);
+		uint64_t sig = (uint64_t(id) << 32) + static_cast<unsigned int>(c.first);
+		const auto &i = parent_->change_channel_.find(sig);
+		if (i != parent_->change_channel_.end()) i->second.trigger(*this, c.first);
+	}
+}
+
 void Frame::merge(Frame &f) {
-	for (auto x : f) {
-		data_[x.first] = std::move(x.second);
+	for (auto &x : f) {
+		auto &d = data_[x.first];
+		d.data = std::move(x.second.data);
+		d.encoded = std::move(x.second.encoded);
+		f.data_[x.first].status = ChannelStatus::INVALID;
+		d.status = ChannelStatus::VALID;
 		touch(x.first);
 	}
 }
 
-void Frame::swapTo(Frame &f) {
-	for (auto x : *this) {
-		f.data_[x.first].swap(x.second);
-		f.changed_.emplace(x.first);
-		changed_.emplace(x.first);
-	}
+void Frame::moveTo(Frame &f) {
+	if (status_ == FrameStatus::RELEASED) throw FTL_Error("Moving released frame");
+	f.status_ = status_;
+	f.parent_ = parent_;
+	f.pool_ = pool_;
+	f.data_ = std::move(data_);
+	f.changed_ = std::move(changed_);
+	status_ = FrameStatus::RELEASED;
 }
 
 void Frame::swapChanged(Frame &f) {
 	for (auto x : changed_) {
-		f.data_[x].swap(data_[x]);
-		f.changed_.emplace(x);
+		f.data_[x.first].data.swap(data_[x.first].data);
+		f.changed_[x.first] = ftl::data::ChangeType::LOCAL;
 	}
 }
 
 void Frame::swapChannel(ftl::codecs::Channel c, Frame &f) {
-	if (has(c)) {
-		f.data_[c].swap(data_[c]);
-		f.changed_.emplace(c);
-		changed_.emplace(c);
+	if (f.hasOwn(c)) {
+		auto &d = data_[c];
+		auto &fd = f.data_[c];
+		fd.data.swap(d.data);
+		d.status = ftl::data::ChannelStatus::VALID;
+		changed_[c] = f.changed_[c];
+		f.changed_[c] = ftl::data::ChangeType::LOCAL;
 	}
 }
 
-void Frame::clear() {
+void Frame::reset() {
+	for (auto &d : data_) {
+		d.second.status = ChannelStatus::INVALID;
+	}
+	changed_.clear();
+	status_ = FrameStatus::CREATED;
+}
+
+void Frame::hardReset() {
+	status_ = FrameStatus::CREATED;
 	changed_.clear();
 	data_.clear();
 }
+
+// ==== Session ================================================================
+
+ftl::Handle Session::onChange(uint32_t id, ftl::codecs::Channel c, const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
+	uint64_t sig = (uint64_t(id) << 32) + static_cast<unsigned int>(c);
+	return change_channel_[sig].on(cb);
+}
+
+ftl::Handle Session::onChange(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
+	return change_.on(cb);
+}
+
+ftl::Handle Session::onFlush(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
+	return flush_.on(cb);
+}
+
+void Session::notifyChanges(Frame &f) {
+	
+}
+
+void Session::flush(Frame &f) {
+	// TODO: Lock
+	for (auto c : f.changed()) {
+		if (c.second == ftl::data::ChangeType::LOCAL) {
+			auto &d = f._getData(c.first);
+			if (d.status == ftl::data::ChannelStatus::VALID) {
+				d.status = ftl::data::ChannelStatus::FLUSHED;
+				flush_.trigger(f, c.first);
+			}
+		} else if (c.second == ftl::data::ChangeType::FOREIGN) {
+			auto &d = f._getData(c.first);
+			if (d.status == ftl::data::ChannelStatus::DISPATCHED) {
+				d.status = ftl::data::ChannelStatus::FLUSHED;
+				flush_.trigger(f, c.first);
+			}
+		}
+	}
+}
+
+void Session::flush(Frame &f, ftl::codecs::Channel c) {
+	// TODO: Lock
+	auto cc = f.changed_[c];
+	if (cc == ftl::data::ChangeType::LOCAL) {
+		auto &d = f._getData(c);
+		if (d.status == ftl::data::ChannelStatus::VALID) {
+			d.status = ftl::data::ChannelStatus::FLUSHED;
+			flush_.trigger(f, c);
+		}
+	} else if (cc == ftl::data::ChangeType::FOREIGN) {
+		auto &d = f._getData(c);
+		if (d.status == ftl::data::ChannelStatus::DISPATCHED) {
+			d.status = ftl::data::ChannelStatus::FLUSHED;
+			flush_.trigger(f, c);
+		}
+	}
+}
diff --git a/components/structures/src/pool.cpp b/components/structures/src/pool.cpp
new file mode 100644
index 000000000..dd22c3b46
--- /dev/null
+++ b/components/structures/src/pool.cpp
@@ -0,0 +1,75 @@
+#include <ftl/data/framepool.hpp>
+
+using ftl::data::Pool;
+using ftl::data::Frame;
+using ftl::data::Session;
+
+Pool::Pool(size_t min_n, size_t max_n) : min_n_(min_n), max_n_(max_n) {
+	ideal_n_ = min_n + (max_n-min_n)/2;
+}
+
+Pool::~Pool() {
+	for (auto &p : pool_) {
+		for (auto *f : p.second.pool) {
+			f->status_ = FrameStatus::RELEASED;
+			delete f;
+		}
+	}
+}
+
+Frame Pool::allocate(uint32_t id, int64_t timestamp) {
+	auto &pool = _getPool(id);
+
+	if (timestamp < pool.last_timestamp) {
+		throw FTL_Error("New frame timestamp is older that previous: " << timestamp);
+	}
+
+	// Add items as required
+	if (pool.pool.size() < min_n_) {
+		while (pool.pool.size() < ideal_n_) {
+			pool.pool.push_back(new Frame(this, &pool.session, id, 0));
+		}
+	}
+
+	Frame *f = pool.pool.front();
+	Frame ff = std::move(*f);
+	delete f;
+	ff.restart(timestamp);
+	pool.pool.pop_front();
+	pool.last_timestamp = timestamp;
+	return ff;
+}
+
+void Pool::release(Frame &f) {
+	if (f.status() == FrameStatus::RELEASED) return;
+	f.reset();
+	auto &pool = _getPool(f.id);
+
+	if (pool.pool.size() < max_n_) {
+		Frame *pf = new Frame(this, &pool.session, f.id, 0);
+		f.moveTo(*pf);
+		pool.pool.push_back(pf);
+	}
+}
+
+ftl::data::Session &Pool::session(uint32_t id) {
+	auto &pool = _getPool(id);
+	return pool.session;
+}
+
+size_t Pool::size(uint32_t id) {
+	auto &pool = _getPool(id);
+	return pool.pool.size();
+}
+
+size_t Pool::size() {
+	size_t s = 0;
+	for (auto &p : pool_) {
+		s += p.second.pool.size();
+	}
+	return s;
+}
+
+ftl::data::Pool::PoolData &Pool::_getPool(uint32_t id) {
+	return pool_[id];
+}
diff --git a/components/structures/test/CMakeLists.txt b/components/structures/test/CMakeLists.txt
index 155634bc4..653cfd3c4 100644
--- a/components/structures/test/CMakeLists.txt
+++ b/components/structures/test/CMakeLists.txt
@@ -3,7 +3,23 @@ add_executable(nframe_unit
 	$<TARGET_OBJECTS:CatchTest>
 	./frame_unit.cpp
 	../src/new_frame.cpp
+	./frame_example_1.cpp
 )
 target_include_directories(nframe_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
 target_link_libraries(nframe_unit
-	ftlcommon ftlcodecs)
\ No newline at end of file
+	ftlcommon ftlcodecs)
+
+add_test(NFrameUnitTest nframe_unit)
+
+### Pool Unit ##################################################################
+add_executable(pool_unit
+	$<TARGET_OBJECTS:CatchTest>
+	./pool_unit.cpp
+	../src/new_frame.cpp
+	../src/pool.cpp
+)
+target_include_directories(pool_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
+target_link_libraries(pool_unit
+	ftlcommon ftlcodecs)
+
+add_test(MemPoolUnitTest pool_unit)
\ No newline at end of file
diff --git a/components/structures/test/frame_example_1.cpp b/components/structures/test/frame_example_1.cpp
new file mode 100644
index 000000000..8be10efaa
--- /dev/null
+++ b/components/structures/test/frame_example_1.cpp
@@ -0,0 +1,135 @@
+#include "catch.hpp"
+
+#include <ftl/data/new_frame.hpp>
+
+using ftl::data::Session;
+using ftl::data::Frame;
+using ftl::codecs::Channel;
+using ftl::data::ChangeType;
+using ftl::data::StorageMode;
+
+namespace ftl {
+namespace streams {
+
+/* Mock Feed class */
+class Feed {
+	public:
+	Feed() : buffer0_(nullptr, &session_,0,0), buffer1_(nullptr, &session_,0,0) {
+		flush_handle_ = session_.onFlush([this](Frame &f, Channel c) {
+			// Loop changes back to buffer.
+			// Normally transmitted somewhere first.
+			//buffer_.swapChannel(c, f);
+			ChangeType cc = f.getChangeType(c);
+			buffer0_.createAnyChange(c, (cc == ChangeType::LOCAL) ? ChangeType::FOREIGN : ChangeType::COMPLETED) = f.getAnyMutable(c);
+			return true;
+		});
+	}
+
+	inline Frame &buffer() { return buffer0_; }
+	inline Session &session() { return session_; }
+
+	inline void fakeDispatch() {
+		buffer0_.moveTo(buffer1_);
+		buffer0_ = Frame(nullptr, &session_,0,0);
+
+		// Save any persistent changes
+		buffer1_.store();
+		// Transmit any forwarding changes and prevent further changes
+		buffer1_.flush();  // TODO: use special dispatched function
+
+		// Call the onFrame handler.
+		// Would be in another thread in real version of this class.
+		frame_handler_.trigger(buffer1_);
+	}
+
+	inline ftl::Handle onFrame(const std::function<bool(Frame&)> &cb) {
+		return frame_handler_.on(cb);
+	}
+
+	Frame createFrame(int id) {
+		// TODO: Give it the id and a timestamp
+		Frame f(nullptr, &session_,0,0);
+		f.store();
+		return f;
+	}
+
+	private:
+	ftl::Handler<Frame&> frame_handler_;
+	Session session_;
+	Frame buffer0_;
+	Frame buffer1_;
+	ftl::Handle flush_handle_;
+};
+
+}
+}
+
+using ftl::streams::Feed;
+
+/* Test class */
+struct VideoFrame {
+	int gpudata;
+	int matdata;
+};
+
+TEST_CASE("ftl::data::Frame full non-owner example", "[example]") {
+	// Register channels somewhere at startup
+	ftl::data::make_channel<VideoFrame>(Channel::Colour, "colour", StorageMode::TRANSIENT);
+	ftl::data::make_channel<VideoFrame>(Channel::Depth, "depth", StorageMode::TRANSIENT);
+	ftl::data::make_channel<std::list<std::string>>(Channel::Messages, "messages", StorageMode::AGGREGATE);
+	ftl::data::make_channel<float>(Channel::Pose, "pose", StorageMode::PERSISTENT);
+
+	Feed feed;
+
+	int i=0;
+	int changed = 0;
+	ftl::Handle myhandle;
+
+	auto h = feed.onFrame([&i,&feed,&myhandle,&changed](Frame &f) {
+		// First frame received
+		if (i++ == 0 ) {
+			// User of Frame makes changes or reads values from state
+			REQUIRE( f.get<float>(Channel::Pose) == 6.0f );
+			REQUIRE( f.get<VideoFrame>(Channel::Depth).gpudata == 1 );
+
+			// Create a new frame for same source for some return state
+			Frame nf = feed.createFrame(f.id);
+			nf.create<std::list<std::string>>(Channel::Messages) = {"First Message"};
+			nf.create<std::list<std::string>>(Channel::Messages) = {"Second Message"};
+			nf.create<int>(Channel::Control) = 3456;
+			//nf.set<float>(Channel::Pose) = 7.0f;
+
+			// Listen for this `Control` change to be confirmed
+			myhandle = nf.onChange(Channel::Control, [&changed](Frame &f, Channel c) {
+				changed++;
+				return false;  // Call once only
+			});
+			
+			// Either by destruction or manually, final action is flush to send
+			nf.flush();
+		// Second frame received
+		} else {
+			REQUIRE( changed == 1 );  // Change notified before `onFrame`
+			REQUIRE( f.get<float>(Channel::Pose) == 6.0f );
+			REQUIRE( f.get<int>(Channel::Control) == 3456 );
+			REQUIRE( (*f.get<std::list<std::string>>(Channel::Messages).begin()) == "First Message" );
+		}
+		return true;
+	});
+
+	// Generate some incoming changes from network
+	// Usually this is done inside the Feed class...
+	feed.buffer().createChange<VideoFrame>(Channel::Colour, ChangeType::FOREIGN).gpudata = 1;
+	feed.buffer().createChange<VideoFrame>(Channel::Depth, ChangeType::COMPLETED).gpudata = 1;
+	feed.buffer().createChange<float>(Channel::Pose, ChangeType::FOREIGN) = 6.0f;
+
+	// Fake a frame being received on network or from file
+	feed.fakeDispatch();
+	// And dispatch the response frame also
+	feed.fakeDispatch();
+
+	REQUIRE( i == 2 );
+
+	// For testing only...
+	ftl::data::clearRegistry();
+}
diff --git a/components/structures/test/frame_unit.cpp b/components/structures/test/frame_unit.cpp
index 7cf289868..265fa6e9d 100644
--- a/components/structures/test/frame_unit.cpp
+++ b/components/structures/test/frame_unit.cpp
@@ -1,3 +1,8 @@
+/*
+ * These tests directly relate to the specification found at:
+ *     https://gitlab.utu.fi/nicolas.pope/ftl/-/wikis/Design/Frames
+ */
+
 #include "catch.hpp"
 
 #include <ftl/data/new_frame.hpp>
@@ -5,19 +10,122 @@
 using ftl::data::Session;
 using ftl::data::Frame;
 using ftl::codecs::Channel;
+using ftl::data::ChangeType;
+using ftl::data::StorageMode;
+
+namespace ftl {
+namespace data {
+
+class Pool {
+	public:
+	static Frame make(Session *s, int id, uint64_t ts) { return Frame(nullptr, s, id, ts); }
+
+	void release(Frame &f);
+};
+
+}
+
+namespace streams {
+
+// Only Pool can create frames so make a mock Feed.
+class Feed {
+	public:
+	static Frame make(Session *s, int id, uint64_t ts) { return ftl::data::Pool::make(s, id, ts); }
+};
+
+}
+}
+
+using ftl::streams::Feed;
+
+void ftl::data::Pool::release(Frame &f) {
+
+}
+
+/* #1.1.1 */
+static_assert(sizeof(ftl::codecs::Channel) >= 4, "Channel must be at least 32bit");
+
+/* #1.1.2 */
+static_assert(std::is_integral<decltype(ftl::data::Frame::id)>::value, "Integral ID requried in Frame");
+static_assert(std::is_member_function_pointer<decltype(&ftl::data::Frame::timestamp)>::value, "Timestamp is required");
+
+/* #1.1.3  */
+static_assert(std::is_member_function_pointer<decltype(&ftl::data::Frame::mutex)>::value, "Frame::mutex is not a member function.");
+
+/* #1.1.4 */
+TEST_CASE("ftl::data::Frame encoded data", "[1.1.4]") {
+	SECTION("provide encoded data") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		std::vector<uint8_t> data{44,22,33};
+
+		f.createChange<int>(Channel::Pose, ftl::data::ChangeType::FOREIGN, data) = 55;
+		const auto &x = f.get<int>(Channel::Pose);
+		REQUIRE( x == 55 );
+
+		// Data has been moved.
+		REQUIRE(data.size() == 0);
+	}
+
+	SECTION("get encoded data") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		std::vector<uint8_t> data{44,22,33};
+
+		f.createChange<int>(Channel::Pose, ftl::data::ChangeType::FOREIGN, data);
+
+		auto &data2 = f.getEncoded(Channel::Pose);
+		REQUIRE( data2.size() == 3 );
+		REQUIRE( data2[0] == 44 );
+	}
+}
+
+/* #1.1.5 */
+TEST_CASE("ftl::data::Frame clear encoded on change", "[1.1.5]") {
+	SECTION("change by set") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		std::vector<uint8_t> data{44,22,33};
 
+		f.createChange<int>(Channel::Pose, ftl::data::ChangeType::FOREIGN, data);
+		f.store();
+
+		auto &data2 = f.getEncoded(Channel::Pose);
+		REQUIRE( data2.size() == 3 );
+		
+		f.set<int>(Channel::Pose) = 66;
+		REQUIRE(f.getEncoded(Channel::Pose).size() == 0);
+	}
+
+	SECTION("change by create") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		std::vector<uint8_t> data{44,22,33};
+
+		f.createChange<int>(Channel::Pose, ftl::data::ChangeType::FOREIGN, data);
+		f.store();
+
+		auto &data2 = f.getEncoded(Channel::Pose);
+		REQUIRE( data2.size() == 3 );
+		
+		f.create<int>(Channel::Pose) = 66;
+		REQUIRE(f.getEncoded(Channel::Pose).size() == 0);
+	}
+}
+
+/* #1.2.1 */
 TEST_CASE("ftl::data::Frame create get", "[Frame]") {
 	SECTION("write and read integers") {
-		Frame f;
-		f.create<int>(Channel::Pose, 55);
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Pose) = 55;
 
 		const auto &x = f.get<int>(Channel::Pose);
 		REQUIRE( x == 55 );
 	}
 
 	SECTION("write and read floats") {
-		Frame f;
-		f.create<float>(Channel::Pose, 44.0f);
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<float>(Channel::Pose) = 44.0f;
 
 		const auto &x = f.get<float>(Channel::Pose);
 		REQUIRE( x == 44.0f );
@@ -28,38 +136,21 @@ TEST_CASE("ftl::data::Frame create get", "[Frame]") {
 			int a=44;
 			float b=33.0f;
 		};
-		Frame f;
-		f.create<Test>(Channel::Pose, {});
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<Test>(Channel::Pose) = {};
 
 		const auto &x = f.get<Test>(Channel::Pose);
 		REQUIRE( x.a == 44 );
 		REQUIRE( x.b == 33.0f );
 	}
 
-	SECTION("write and read fail") {
-		struct Test {
-			int a=44;
-			float b=33.0f;
-		};
-		Frame f;
-		f.create<Test>(Channel::Pose, {});
-
-		bool err = false;
-
-		try {
-			int x = f.get<int>(Channel::Pose);
-			REQUIRE(x);
-		} catch (...) {
-			err = true;
-		}
-		REQUIRE(err);
-	}
-}
-
-TEST_CASE("ftl::data::Frame isType", "[Frame]") {
 	SECTION("is int type") {
-		Frame f;
-		f.create<int>(Channel::Pose, 55);
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Pose) = 55;
 
 		REQUIRE( f.isType<int>(Channel::Pose) );
 		REQUIRE( !f.isType<float>(Channel::Pose) );
@@ -70,44 +161,93 @@ TEST_CASE("ftl::data::Frame isType", "[Frame]") {
 			int a; int b;
 		};
 
-		Frame f;
-		f.create<Test>(Channel::Pose, {3,4});
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<Test>(Channel::Pose) = {3,4};
 
 		REQUIRE( f.isType<Test>(Channel::Pose) );
 		REQUIRE( !f.isType<float>(Channel::Pose) );
 	}
 
 	SECTION("missing") {
-		Frame f;
+		Frame f = Feed::make(nullptr, 0, 0);
 
 		REQUIRE( !f.isType<float>(Channel::Pose) );
 	}
 }
 
-TEST_CASE("ftl::data::Frame changed", "[Frame]") {
-	SECTION("change on create") {
-		Frame f;
+/* #1.2.2 */
+TEST_CASE("ftl::data::registerChannel", "[Frame]") {
+	SECTION("register typed channel and valid create") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
 
-		REQUIRE( !f.changed(Channel::Pose) );
-		f.create<int>(Channel::Pose, 55);
-		REQUIRE( f.changed(Channel::Pose) );
+		ftl::data::make_channel<float>(Channel::Colour, "colour", ftl::data::StorageMode::PERSISTENT);
+		f.create<float>(Channel::Colour) = 5.0f;
+		REQUIRE( f.get<float>(Channel::Colour) == 5.0f );
+
+		ftl::data::clearRegistry();
 	}
 
-	SECTION("no change on untouch") {
-		Frame f;
+	SECTION("register typed channel and invalid create") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
 
-		f.create<int>(Channel::Pose, 55);
-		REQUIRE( f.changed(Channel::Pose) );
-		f.untouch(Channel::Pose);
-		REQUIRE( !f.changed(Channel::Pose) );
+		ftl::data::make_channel<float>(Channel::Colour, "colour", ftl::data::StorageMode::PERSISTENT);
+
+		bool err = false;
+		try {
+			f.create<int>(Channel::Colour) = 5;
+		} catch(const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+		REQUIRE( err );
+
+		ftl::data::clearRegistry();
+	}
+
+	SECTION("register void for any type") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		ftl::data::make_channel<void>(Channel::Colour, "colour", ftl::data::StorageMode::PERSISTENT);
+	
+		f.create<int>(Channel::Colour) = 5;
+		REQUIRE( f.get<int>(Channel::Colour) == 5 );
+
+		ftl::data::clearRegistry();
 	}
 }
 
-TEST_CASE("ftl::data::Frame create", "[Frame]") {
+/* #1.2.3 */
+TEST_CASE("ftl::data::Frame type failure") {
+	SECTION("write and read fail") {
+		struct Test {
+			int a=44;
+			float b=33.0f;
+		};
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+		f.create<Test>(Channel::Pose) = {};
+
+		bool err = false;
+
+		try {
+			f.get<int>(Channel::Pose);
+		} catch (const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+		REQUIRE(err);
+	}
+
 	SECTION("same value on create") {
-		Frame f;
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
 
-		f.create<int>(Channel::Pose, 55);
+		f.create<int>(Channel::Pose) = 55;
 		const auto &x = f.get<int>(Channel::Pose);
 		REQUIRE( x == 55 );
 
@@ -116,10 +256,11 @@ TEST_CASE("ftl::data::Frame create", "[Frame]") {
 		REQUIRE( y == 55 );
 	}
 
-	SECTION("change of type") {
-		Frame f;
+	SECTION("change of type by recreate") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
 
-		f.create<int>(Channel::Pose, 55);
+		f.create<int>(Channel::Pose) = 55;
 		auto x = f.getPtr<int>(Channel::Pose);
 		REQUIRE( x );
 		REQUIRE( *x == 55 );
@@ -131,12 +272,58 @@ TEST_CASE("ftl::data::Frame create", "[Frame]") {
 	}
 }
 
-TEST_CASE("ftl::data::Frame use of parent", "[Frame]") {
-	SECTION("get from parent") {
+/* #1.2.4 UNTESTED */
+
+/* #1.2.5 */
+TEST_CASE("ftl::data::Frame persistent data", "[1.2.5]") {
+	ftl::data::make_channel<int>(Channel::Density, "density", ftl::data::StorageMode::PERSISTENT);
+
+	SECTION("persistent through createChange") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		f.createChange<int>(Channel::Density, ChangeType::FOREIGN) = 44;
+		f.store();	
+		REQUIRE( p.get<int>(Channel::Density) == 44 );
+	}
+
+	// These are not valid as per #3.2.5
+	/*SECTION("persistent via create") {
 		Session p;
 		Frame f(&p);
 
-		p.create<int>(Channel::Pose, 55);
+		f.create<int>(Channel::Density, 44);
+		f.store();	
+		REQUIRE( p.get<int>(Channel::Density) == 44 );
+	}
+
+	SECTION("persistent via set") {
+		Session p;
+		Frame f(&p);
+
+		f.create<int>(Channel::Density, 44);
+		f.set<int>(Channel::Density, 45);
+		f.store();	
+		REQUIRE( p.get<int>(Channel::Density) == 45 );
+	}*/
+
+	SECTION("available in other frame") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		f.createChange<int>(Channel::Density, ChangeType::FOREIGN) = 44;	
+		f.store();	
+
+		Frame f2 = Feed::make(&p, 0, 0);
+		REQUIRE( f2.get<int>(Channel::Density) == 44 );
+	}
+
+	SECTION("get from parent") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		p.create<int>(Channel::Pose) = 55;
 
 		auto x = f.getPtr<int>(Channel::Pose);
 		REQUIRE( x );
@@ -148,23 +335,25 @@ TEST_CASE("ftl::data::Frame use of parent", "[Frame]") {
 
 	SECTION("has from parent") {
 		Session p;
-		Frame f(&p);
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
 
-		p.create<int>(Channel::Pose, 55);
+		p.create<int>(Channel::Pose) = 55;
 		REQUIRE( f.has(Channel::Pose) );
 	}
 
 	SECTION("no change in parent") {
 		Session p;
-		Frame f(&p);
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
 
-		p.create<int>(Channel::Pose, 55);
+		p.create<int>(Channel::Pose) = 55;
 		p.untouch(Channel::Pose);
 
 		REQUIRE( !f.changed(Channel::Pose) );
 		REQUIRE( !p.changed(Channel::Pose) );
 
-		f.set<int>(Channel::Pose, 66);
+		f.set<int>(Channel::Pose) = 66;
 
 		REQUIRE( f.changed(Channel::Pose) );
 		REQUIRE( !p.changed(Channel::Pose) );
@@ -177,168 +366,1042 @@ TEST_CASE("ftl::data::Frame use of parent", "[Frame]") {
 		REQUIRE( y );
 		REQUIRE( *y == 55 );
 	}
+
+	ftl::data::clearRegistry();
 }
 
-TEST_CASE("ftl::data::Frame flush", "[Frame]") {
-	SECTION("event on flush") {
-		Frame f;
+/* #1.2.6 */
+TEST_CASE("ftl::data::Frame transient data", "[1.2.6]") {
+	ftl::data::make_channel<int>(Channel::Density, "density", ftl::data::StorageMode::TRANSIENT);
 
-		int event = 0;
-		f.on(Channel::Pose, [&event](Frame &frame, Channel c) {
-			event++;
-			return true;
-		});
+	SECTION("not persistent after store") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
 
-		f.create<int>(Channel::Pose, 55);
-		REQUIRE( event == 0 );
+		f.createChange<int>(Channel::Density, ChangeType::FOREIGN) = 44;
+		f.store();	
+		
+		REQUIRE( !p.has(Channel::Density) );
+	}
 
-		f.flush();
-		REQUIRE( event == 1 );
+	ftl::data::clearRegistry();
+}
+
+/* #1.2.7 */
+TEST_CASE("ftl::data::Frame aggregate data", "[1.2.7]") {
+	ftl::data::make_channel<void>(Channel::Density, "density", ftl::data::StorageMode::AGGREGATE);
+
+	SECTION("not persistent after store") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		f.createChange<std::list<int>>(Channel::Density, ChangeType::FOREIGN) = {44};
+		f.store();	
+		
+		REQUIRE( !p.has(Channel::Density) );
 	}
 
-	SECTION("parent event on flush") {
+	// TODO: Check elsewhere that the changes are since last frame, not
+	// applicable as part of this unit test.
+
+	SECTION("aggregate channels actually aggregate with createChange") {
 		Session p;
-		Frame f(&p);
+		Frame f = Feed::make(&p, 0, 0);
+
+		f.createChange<std::list<int>>(Channel::Density, ChangeType::FOREIGN) = {34};
+		f.createChange<std::list<int>>(Channel::Density, ChangeType::FOREIGN) = {55};
+		f.createChange<std::list<int>>(Channel::Density, ChangeType::FOREIGN) = {12,89};
+		f.store();
+
+		auto list = f.get<std::list<int>>(Channel::Density).begin();
+		REQUIRE( *(list++) == 34 );
+		REQUIRE( *(list++) == 55 );
+		REQUIRE( *(list++) == 12 );
+		REQUIRE( *(list++) == 89 );
+	}
 
-		int event = 0;
-		p.on(Channel::Pose, [&event](Frame &frame, Channel c) {
-			event++;
-			return true;
-		});
+	SECTION("non aggregate channels do not aggregate with createChange") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
 
-		f.create<int>(Channel::Pose, 55);
-		REQUIRE( event == 0 );
+		f.createChange<std::list<int>>(Channel::Colour, ChangeType::FOREIGN) = {34};
+		f.createChange<std::list<int>>(Channel::Colour, ChangeType::FOREIGN) = {55};
+		f.createChange<std::list<int>>(Channel::Colour, ChangeType::FOREIGN) = {12,89};
+		f.store();
 
-		f.flush();
-		REQUIRE( event == 1 );
+		auto list = f.get<std::list<int>>(Channel::Colour).begin();
+		REQUIRE( *(list++) == 12 );
+		REQUIRE( *(list++) == 89 );
 	}
 
-	SECTION("parent change on flush") {
+	SECTION("aggregate channels allow move aggregate with createChange") {
 		Session p;
-		Frame f(&p);
+		Frame f = Feed::make(&p, 0, 0);
 
-		p.create<int>(Channel::Pose, 55);
-		p.flush();
+		std::list<int> data1 = {34};
+		std::list<int> data2 = {55};
 
-		f.set<int>(Channel::Pose, 66);
-		auto x = p.getPtr<int>(Channel::Pose);
-		REQUIRE( x );
-		REQUIRE( *x == 55 );
-		
-		f.flush();
-		x = p.getPtr<int>(Channel::Pose);
-		REQUIRE( x );
-		REQUIRE( *x == 66 );
+		f.createChange<std::list<int>>(Channel::Density, ChangeType::FOREIGN) = std::move(data1);
+		f.createChange<std::list<int>>(Channel::Density, ChangeType::FOREIGN) = std::move(data2);
+		f.store();
+
+		auto list = f.get<std::list<int>>(Channel::Density).begin();
+		REQUIRE( *(list++) == 34 );
+		REQUIRE( *(list++) == 55 );
+		REQUIRE( data1.size() == 0 );
+		REQUIRE( data2.size() == 0 );
+	}
+
+	SECTION("aggregate channels actually aggregate with create") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		f.create<std::list<int>>(Channel::Density) = {34};
+		f.create<std::list<int>>(Channel::Density) = {55};
+		f.create<std::list<int>>(Channel::Density) = {12,89};
+
+		auto list = f.get<std::list<int>>(Channel::Density).begin();
+		REQUIRE( *(list++) == 34 );
+		REQUIRE( *(list++) == 55 );
+		REQUIRE( *(list++) == 12 );
+		REQUIRE( *(list++) == 89 );
 	}
 
-	SECTION("untouched on flush") {
-		Frame f;
+	SECTION("non aggregate channels do not aggregate with create") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
 
-		f.create<int>(Channel::Pose, 55);
-		REQUIRE( f.changed(Channel::Pose) );
+		f.create<std::list<int>>(Channel::Colour) = {34};
+		f.create<std::list<int>>(Channel::Colour) = {55};
+		f.create<std::list<int>>(Channel::Colour) = {12,89};
 
-		f.flush();
-		REQUIRE( !f.changed(Channel::Pose) );
+		auto list = f.get<std::list<int>>(Channel::Colour).begin();
+		REQUIRE( *(list++) == 12 );
+		REQUIRE( *(list++) == 89 );
 	}
-}
 
-TEST_CASE("ftl::data::Frame register", "[Frame]") {
-	SECTION("register typed channel and valid create") {
-		Frame f;
+	SECTION("aggregate channels actually aggregate with set") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		f.create<std::list<int>>(Channel::Density) = {34};
+		f.set<std::list<int>>(Channel::Density) = {55};
+		f.set<std::list<int>>(Channel::Density) = {12,89};
+
+		auto list = f.get<std::list<int>>(Channel::Density).begin();
+		REQUIRE( *(list++) == 34 );
+		REQUIRE( *(list++) == 55 );
+		REQUIRE( *(list++) == 12 );
+		REQUIRE( *(list++) == 89 );
+	}
+
+	SECTION("non aggregate channels do not aggregate with set") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
 
-		Frame::registerChannel(Channel::Colour, ftl::data::make_channel<float>("colour", ftl::data::ChannelMode::PERSISTENT));
-		REQUIRE( f.create<float>(Channel::Colour, 5.0f) == 5.0f );
+		f.create<std::list<int>>(Channel::Colour) = {34};
+		f.set<std::list<int>>(Channel::Colour) = {55};
+		f.set<std::list<int>>(Channel::Colour) = {12,89};
 
-		Frame::clearRegistry();
+		auto list = f.get<std::list<int>>(Channel::Colour).begin();
+		REQUIRE( *(list++) == 12 );
+		REQUIRE( *(list++) == 89 );
 	}
 
-	SECTION("register typed channel and invalid create") {
-		Frame f;
+	ftl::data::clearRegistry();
+}
+
+/* #1.2.8 Not applicable as a unit test of Frame. */
+
+/* #1.2.9 */
+TEST_CASE("ftl::data::Frame aggregate lists", "[1.2.9]") {
+	ftl::data::make_channel<void>(Channel::Density, "density", ftl::data::StorageMode::AGGREGATE);
+
+	SECTION("only allow stl list container") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
 
-		Frame::registerChannel(Channel::Colour, ftl::data::make_channel<float>("colour", ftl::data::ChannelMode::PERSISTENT));
+		f.create<std::list<int>>(Channel::Density) = {44};
 
 		bool err = false;
+
 		try {
-			f.create<int>(Channel::Colour, 5);
-		} catch(const std::exception &e) {
+			f.create<int>(Channel::Density);
+		} catch (const ftl::exception &e) {
+			e.ignore();
 			err = true;
 		}
-		REQUIRE( err );
 
-		Frame::clearRegistry();
+		REQUIRE( err );
 	}
+
+	ftl::data::clearRegistry();
 }
 
-// ==== Complex type overload test =============================================
+/* #1.3 Not applicable as a unit test of Frame. */
 
-struct TestA {
-	int a=55;
-};
+/* #2.1.1 */
+static_assert(!std::is_default_constructible<Frame>::value, "Must not have default construction");
+// TODO: Check for privacy of actual constructor? Proposed for future feature of C++ but not yet.
 
-struct TestB {
-	int b=99;
-};
+/* #2.1.2 */
+static_assert(!std::is_copy_constructible<Frame>::value, "Must not have a copy constructor");
+static_assert(!std::is_copy_assignable<Frame>::value, "Must not allow copy assignment");
 
-struct TestC {
-	TestA a;
-	TestB b;
-};
+/* #2.1.3 */
+static_assert(std::is_move_constructible<Frame>::value, "Must have a move constructor");
+static_assert(std::is_move_assignable<Frame>::value, "Must allow move assignment");
 
-template <>
-TestA &ftl::data::Frame::create<TestA>(ftl::codecs::Channel c) {
-	return create<TestC>(c).a;
-}
+/* #2.1.4 Not applicable as a unit test of Frame. */
 
-template <>
-TestA &ftl::data::Frame::create<TestA>(ftl::codecs::Channel c, const TestA &a) {
-	TestC cc;
-	cc.a = a;
-	return create<TestC>(c, cc).a;
-}
+/* #2.1.5 Not applicable as a unit test of Frame. */
 
-template <>
-TestB &ftl::data::Frame::create<TestB>(ftl::codecs::Channel c, const TestB &b) {
-	TestC cc;
-	cc.b = b;
-	return create<TestC>(c, cc).b;
-}
+/* #2.1.6 Not applicable as a unit test of Frame. */
 
-template <>
-const TestA *ftl::data::Frame::getPtr<TestA>(ftl::codecs::Channel c) const noexcept {
-	auto *ptr = getPtr<TestC>(c);
-	return (ptr) ? &ptr->a : nullptr;
+/* #2.1.7 Not applicable as a unit test of Frame. */
+
+/* #2.1.8 */
+TEST_CASE("ftl::data::Frame merging", "[2.1.8]") {
+	SECTION("merge replaces data in destination") {
+		Frame f1 = Feed::make(nullptr, 0, 0);
+		Frame f2 = Feed::make(nullptr, 0, 0);
+		f1.store();
+		f2.store();
+
+		f1.create<int>(Channel::Colour) = 43;
+		f1.create<int>(Channel::Colour2) = 77;
+
+		f2.create<int>(Channel::Colour2) = 88;
+
+		f2.merge(f1);
+
+		REQUIRE( f2.get<int>(Channel::Colour2) == 77 );
+	}
+
+	SECTION("new items are created") {
+		Frame f1 = Feed::make(nullptr, 0, 0);
+		Frame f2 = Feed::make(nullptr, 0, 0);
+		f1.store();
+		f2.store();
+
+		f1.create<int>(Channel::Colour) = 43;
+		f1.create<int>(Channel::Colour2) = 77;
+
+		f2.create<int>(Channel::Colour2) = 88;
+
+		f2.merge(f1);
+
+		REQUIRE( f2.get<int>(Channel::Colour) == 43 );
+	}
+
+	SECTION("old items remain") {
+		Frame f1 = Feed::make(nullptr, 0, 0);
+		Frame f2 = Feed::make(nullptr, 0, 0);
+		f1.store();
+		f2.store();
+
+		f1.create<int>(Channel::Colour2) = 77;
+
+		f2.create<int>(Channel::Colour) = 43;
+		f2.create<int>(Channel::Colour2) = 88;
+
+		f2.merge(f1);
+
+		REQUIRE( f2.get<int>(Channel::Colour) == 43 );
+	}
+
+	SECTION("flushed status is removed") {
+		Frame f1 = Feed::make(nullptr, 0, 0);
+		Frame f2 = Feed::make(nullptr, 0, 0);
+		f1.store();
+		f2.store();
+
+		f1.create<int>(Channel::Colour) = 43;
+		f1.flush();
+
+		REQUIRE( f1.flushed(Channel::Colour) );
+
+		f2.merge(f1);
+
+		REQUIRE( !f2.flushed(Channel::Colour) );
+		REQUIRE( f2.has(Channel::Colour) );
+	}
 }
 
-template <>
-const TestB *ftl::data::Frame::getPtr<TestB>(ftl::codecs::Channel c) const noexcept {
-	auto *ptr = getPtr<TestC>(c);
-	return (ptr) ? &ptr->b : nullptr;
+/* #2.1.9 */
+TEST_CASE("ftl::data::Frame merge is change", "[2.1.9]") {
+	SECTION("merges are marked as changes") {
+		Frame f1 = Feed::make(nullptr, 0, 0);
+		Frame f2 = Feed::make(nullptr, 0, 0);
+		f1.store();
+		f2.store();
+
+		f1.create<int>(Channel::Colour) = 43;
+		f2.create<int>(Channel::Colour2) = 88;
+		f2.untouch(Channel::Colour2);
+		f2.merge(f1);
+
+		REQUIRE( f2.getChangeType(Channel::Colour) == ChangeType::LOCAL );
+		REQUIRE( !f2.changed(Channel::Colour2) );
+	}
 }
 
-TEST_CASE("ftl::data::Frame Complex Overload", "[Frame]") {
-	SECTION("Create and get first type with default") {
-		Frame f;
-		f.create<TestA>(Channel::Pose);
-		
-		auto *x = f.getPtr<TestA>(Channel::Pose);
-		REQUIRE( x );
-		REQUIRE( x->a == 55 );
+/* #2.1.10 Unimplemented, merge is move only. This tests for the move instead */
+TEST_CASE("ftl::data::Frame merge moves encoded", "[2.1.10]") {
+	SECTION("encoded data moved") {
+		Frame f1 = Feed::make(nullptr, 0, 0);
+		Frame f2 = Feed::make(nullptr, 0, 0);
 
-		auto *y = f.getPtr<TestB>(Channel::Pose);
-		REQUIRE( y );
-		REQUIRE( y->b == 99 );
+		std::vector<uint8_t> data{88,99,100};
+		f1.createChange<int>(Channel::Colour, ChangeType::FOREIGN, data);
+		f2.merge(f1);
+
+		REQUIRE( f2.getEncoded(Channel::Colour).size() == 3 );
+		REQUIRE( !f1.has(Channel::Colour) );
 	}
+}
 
-	SECTION("Create and get first type with value") {
-		Frame f;
-		f.create<TestA>(Channel::Pose, {77});
-		
-		auto *x = f.getPtr<TestA>(Channel::Pose);
-		REQUIRE( x );
-		REQUIRE( x->a == 77 );
+/* #2.2.1 */
+TEST_CASE("ftl::data::Frame modify after flush", "[2.2.1]") {
+	SECTION("create fails after flush") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
 
-		auto *y = f.getPtr<TestB>(Channel::Pose);
-		REQUIRE( y );
-		REQUIRE( y->b == 99 );
-	}
+		f.create<int>(Channel::Colour) = 89;
+		f.flush();
+
+		bool err = false;
+		try {
+			f.create<int>(Channel::Colour) = 90;
+		} catch (const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+
+		REQUIRE( err );
+	}
+
+	SECTION("set fails after flush") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Colour) = 89;
+		f.flush();
+
+		bool err = false;
+		try {
+			f.set<int>(Channel::Colour) = 90;
+		} catch (const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+
+		REQUIRE( err );
+	}
+
+	SECTION("createChange fails after flush") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Colour) = 89;
+		f.flush();
+
+		bool err = false;
+		try {
+			f.createChange<int>(Channel::Colour, ChangeType::FOREIGN) = 90;
+		} catch (const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+
+		REQUIRE( err );
+	}
+
+	SECTION("channel marked readonly after flush") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Colour) = 89;
+		f.flush();
+		REQUIRE( f.readonly(Channel::Colour) );
+	}
+}
+
+/* #2.2.2 FIXME: Specification needs review. */
+
+/* #2.2.3 */
+TEST_CASE("ftl::data::Frame multiple flush", "[Frame]") {
+	SECTION("fail on multiple frame flush") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Colour) = 89;
+		f.flush();
+
+		bool err = false;
+		try {
+			f.flush();
+		} catch (const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+
+		REQUIRE( err );
+	}
+}
+
+/* #2.2.4 */
+TEST_CASE("ftl::data::Frame locality of changes", "[2.2.4]") {
+	ftl::data::make_channel<int>(Channel::Density, "density", ftl::data::StorageMode::PERSISTENT);
+
+	SECTION("not persistent after flush only") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Density) = 44;
+		f.flush();
+
+		bool err=false;
+
+		try {		
+			p.get<int>(Channel::Density);
+		} catch(const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+		REQUIRE( err );
+	}
+
+	SECTION("not persistent without store") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Density) = 44;
+
+		bool err=false;
+
+		try {		
+			p.get<int>(Channel::Density);
+		} catch(const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+		REQUIRE( err );
+	}
+
+	ftl::data::clearRegistry();
+}
+
+/* #2.2.5 */
+TEST_CASE("ftl::data::Frame changed status", "[2.2.5]") {
+	SECTION("change on create") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		REQUIRE( !f.changed(Channel::Pose) );
+		f.create<int>(Channel::Pose) = 55;
+		REQUIRE( f.changed(Channel::Pose) );
+	}
+
+	SECTION("no change on untouch") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Pose) = 55;
+		REQUIRE( f.changed(Channel::Pose) );
+		f.untouch(Channel::Pose);
+		REQUIRE( !f.changed(Channel::Pose) );
+	}
+}
+
+/* #2.3.1 Not applicable as a unit test of Frame. */
+
+/* #2.3.2 Not applicable as a unit test of Frame. */
+
+/* #2.3.3 */
+TEST_CASE("ftl::data::Frame change type", "[2.3.3]") {
+	SECTION("changes are local type") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+
+		REQUIRE( !f.changed(Channel::Pose) );
+		f.create<int>(Channel::Pose) = 55;
+		REQUIRE( f.getChangeType(Channel::Pose) == ChangeType::LOCAL );
+	}
+
+	SECTION("local change overrides foreign change") {
+		Frame f = Feed::make(nullptr, 0, 0);
+
+		f.createChange<int>(Channel::Pose, ChangeType::FOREIGN) = 55;
+		REQUIRE( f.getChangeType(Channel::Pose) == ChangeType::FOREIGN );
+		f.store();
+
+		f.set<int>(Channel::Pose) = 66;
+		REQUIRE( f.getChangeType(Channel::Pose) == ChangeType::LOCAL );
+	}
+
+	SECTION("local change overrides completed change") {
+		Frame f = Feed::make(nullptr, 0, 0);
+
+		f.createChange<int>(Channel::Pose, ChangeType::COMPLETED) = 55;
+		REQUIRE( f.getChangeType(Channel::Pose) == ChangeType::COMPLETED );
+		f.store();
+		f.set<int>(Channel::Pose) = 66;
+		REQUIRE( f.getChangeType(Channel::Pose) == ChangeType::LOCAL );
+	}
+}
+
+/* #2.3.4 Not applicable as a unit test of Frame. */
+
+/* #2.3.5 Not applicable as a unit test of Frame. */
+
+/* #2.3.6 Not applicable as a unit test of Frame. */
+
+/* #2.3.7 Not applicable as a unit test of Frame. */
+
+/* #3.1.1 Not applicable as a unit test of Frame. */
+
+/* #3.1.2 Not applicable as a unit test of Frame. */
+
+/* #3.1.3 Not applicable as a unit test of Frame. */
+
+/* #3.1.4 */
+TEST_CASE("ftl::data::Frame override of persistent", "[3.1.4]") {
+	SECTION("local changes override persistent data") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		p.create<int>(Channel::Colour) = 44;
+
+		// Note: Get without local create
+		REQUIRE( f.get<int>(Channel::Colour) == 44 );
+
+		// Note: set without create when exists in session store
+		f.set<int>(Channel::Colour) = 66;
+		REQUIRE( f.get<int>(Channel::Colour) == 66 );
+	}
+}
+
+/* #3.1.5 Not applicable as a unit test of Frame. */
+
+/* #3.1.6 FIXME: Specification needs review */
+
+/* #3.1.7 Implicit in other tests. */
+
+/* #3.1.8 Not applicable as a unit test of Frame. */
+
+/* #3.2.1 Not applicable as a unit test of Frame. */
+
+/* #3.2.2 Not applicable as a unit test of Frame. */
+
+/* #3.2.3 Not applicable as a unit test of Frame. */
+
+/* #3.2.4 Not applicable as a unit test of Frame. */
+
+/* #3.2.5 */
+TEST_CASE("ftl::data::Frame initial store", "[3.2.5]") {
+	SECTION("cannot create before store") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		bool err = false;
+		try {
+			f.create<int>(Channel::Colour) = 55;
+		} catch (const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+		REQUIRE( err );
+	}
+
+	SECTION("can createChange before store") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		f.createChange<int>(Channel::Colour, ChangeType::FOREIGN) = 89;
+	}
+
+	SECTION("cannot createChange after store") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		f.store();
+
+		bool err = false;
+		try {
+			f.createChange<int>(Channel::Colour, ChangeType::FOREIGN);
+		} catch (const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+		REQUIRE( err );
+	}
+
+	SECTION("cannot store twice") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		f.store();
+
+		bool err = false;
+		try {
+			f.store();
+		} catch (const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+		REQUIRE( err );
+	}
+
+	SECTION("cannot flush before store") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		bool err = false;
+		try {
+			f.flush();
+		} catch (const ftl::exception &e) {
+			e.ignore();
+			err = true;
+		}
+		REQUIRE( err );
+	}
+}
+
+/* #3.3.1 Not applicable as a unit test of Frame. */
+
+/* #3.3.2 Not applicable as a unit test of Frame. */
+
+/* #3.3.3 See #3.2.5 */
+
+/* #3.3.2 Not applicable as a unit test of Frame. However, see #3.2.5 */
+
+/* #3.4.1 */
+TEST_CASE("ftl::data::Frame change events", "[3.4.1]") {
+	SECTION("event on store of foreign change") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		int event = 0;
+		auto h = f.onChange([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.createChange<int>(Channel::Pose, ChangeType::FOREIGN);
+		REQUIRE( event == 0 );
+
+		f.store();
+		REQUIRE( event == 1 );
+	}
+
+	SECTION("event on store of completed change") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		int event = 0;
+		auto h = f.onChange([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.createChange<int>(Channel::Pose, ChangeType::COMPLETED);
+		REQUIRE( event == 0 );
+
+		f.store();
+		REQUIRE( event == 1 );
+	}
+}
+
+/* #3.4.2 Not applicable as a unit test of Frame. See #3.2.5 */
+
+/* #3.4.3 Not applicable as a unit test of Frame. See #3.2.5 */
+
+/* #3.4.4 */
+TEST_CASE("ftl::data::Frame parallel change events", "[3.4.4]") {
+	SECTION("event for each of multiple changes") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		int event = 0;
+		auto h = f.onChange([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.createChange<int>(Channel::Pose, ChangeType::FOREIGN);
+		f.createChange<int>(Channel::Colour, ChangeType::FOREIGN);
+		f.createChange<int>(Channel::Depth, ChangeType::FOREIGN);
+		REQUIRE( event == 0 );
+
+		f.store();
+		REQUIRE( event == 3 );
+	}
+}
+
+/* #3.4.5 see above test, #3.4.4 */
+
+/* #3.4.6 */
+TEST_CASE("ftl::data::Frame aggregate changes", "[3.4.6]") {
+	ftl::data::make_channel<std::list<int>>(Channel::Density, "density", ftl::data::StorageMode::AGGREGATE);
+
+	SECTION("multiple changes cause single event") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		int event = 0;
+		auto h = f.onChange([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.createChange<std::list<int>>(Channel::Density, ChangeType::FOREIGN) = {34};
+		f.createChange<std::list<int>>(Channel::Density, ChangeType::FOREIGN) = {55};
+		f.createChange<std::list<int>>(Channel::Density, ChangeType::FOREIGN) = {12,89};
+		REQUIRE( event == 0 );
+
+		f.store();
+		REQUIRE( event == 1 );
+	}
+
+	ftl::data::clearRegistry();
+}
+
+/* #3.4.7 */
+//static_assert(std::is_same<decltype(Frame::onChange),ftl::Handle(ftl::codecs::Channel, const std::function<bool(Frame&,ftl::codecs::Channel)> &)>::value, "Wrong event handler type");
+
+/* #3.4.8 Not applicable as a unit test of Frame. */
+
+/* #4.1.1 */
+TEST_CASE("ftl::data::Frame flush events", "[4.1.1]") {
+	SECTION("event on flush") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		int event = 0;
+		auto h = f.onFlush([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.create<int>(Channel::Pose) = 55;
+		REQUIRE( event == 0 );
+
+		f.flush();
+		REQUIRE( event == 1 );
+	}
+
+	SECTION("parent event on flush") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		int event = 0;
+		auto h = p.onFlush([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.create<int>(Channel::Pose) = 55;
+		REQUIRE( event == 0 );
+
+		f.flush();
+		REQUIRE( event == 1 );
+	}
+}
+
+/* #4.1.2 */
+TEST_CASE("ftl::data::Frame flush per channel", "[4.1.2]") {
+	SECTION("event on flush of channel") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		int event = 0;
+		auto h = f.onFlush([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.create<int>(Channel::Pose) = 55;
+		f.create<int>(Channel::Colour) = 45;
+		REQUIRE( event == 0 );
+
+		f.flush(Channel::Pose);
+		REQUIRE( event == 1 );
+	}
+
+	SECTION("flushed channel readonly") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Pose) = 55;
+		f.create<int>(Channel::Colour) = 45;
+
+		f.flush(Channel::Pose);
+		REQUIRE( f.readonly(Channel::Pose) );
+		REQUIRE( !f.readonly(Channel::Colour) );
+	}
+}
+
+/* #4.1.3 Not applicable as a unit test of Frame. */
+
+/* #4.1.4 Not applicable as a unit test of Frame. */
+
+/* #4.1.5 Not applicable as a unit test of Frame. */
+
+/* #4.1.6 */
+TEST_CASE("ftl::data::Frame flush on destruct", "[4.1.6]") {
+	SECTION("flush a non-flushed frame on destruct") {
+		Session p;
+
+		int event = 0;
+		auto h = p.onFlush([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		{
+			Frame f = Feed::make(&p, 0, 0);
+			f.store();
+			f.create<int>(Channel::Pose) = 55;
+			REQUIRE( event == 0 );
+		}
+
+		REQUIRE( event == 1 );
+	}
+
+	SECTION("no flush of flushed frame on destruct") {
+		Session p;
+
+		int event = 0;
+		auto h = p.onFlush([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		{
+			Frame f = Feed::make(&p, 0, 0);
+			f.store();
+			f.create<int>(Channel::Pose) = 55;
+			f.flush();
+			REQUIRE( event == 1 );
+		}
+
+		REQUIRE( event == 1 );
+	}
+}
+
+/* #4.2.1 */
+TEST_CASE("ftl::data::Frame flush foreign", "[4.2.1]") {
+	// For local flush see #4.1.1
+
+	SECTION("event on foreign flush") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		f.createChange<int>(Channel::Colour, ChangeType::FOREIGN) = 55;
+		f.store();
+
+		int event = 0;
+		auto h = f.onFlush([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		REQUIRE( event == 0 );
+		f.flush();
+		REQUIRE( event == 1 );
+	}
+}
+
+/* #4.2.2 */
+TEST_CASE("ftl::data::Frame no flush of completed", "[4.2.2]") {
+	// For local flush see #4.1.1
+
+	SECTION("no event on completed flush") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+
+		f.createChange<int>(Channel::Colour, ChangeType::COMPLETED) = 55;
+		f.store();
+
+		int event = 0;
+		auto h = f.onFlush([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		REQUIRE( event == 0 );
+		f.flush();
+		REQUIRE( event == 0 );
+	}
+}
+
+/* #4.2.3 see #2.2.4 */
+
+/* #4.3.1 see #4.2.1 */
+
+/* #4.3.2 see #4.2.2 but also Feed class */
+
+/* #4.3.3 see #2.2.4 */
+
+/* #4.4.1 see #4.1.6 and #4.2.1 */
+
+/* #4.4.2 see #4.2.2 */
+
+/* #4.4.3 */
+TEST_CASE("ftl::data::Frame parallel flush events", "[4.4.3]") {
+	SECTION("event for each of multiple changes") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		int event = 0;
+		auto h = f.onFlush([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.create<int>(Channel::Pose);
+		f.create<int>(Channel::Colour);
+		f.create<int>(Channel::Depth);
+		REQUIRE( event == 0 );
+
+		f.flush();
+		REQUIRE( event == 3 );
+	}
+}
+
+/* #4.4.4 see #4.4.3 */
+
+/* #4.4.5 */
+TEST_CASE("ftl::data::Frame aggregate flush events", "[4.4.5]") {
+	ftl::data::make_channel<std::list<int>>(Channel::Density, "density", ftl::data::StorageMode::AGGREGATE);
+
+	SECTION("multiple changes cause single event") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		int event = 0;
+		auto h = f.onFlush([&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.create<std::list<int>>(Channel::Density) = {34};
+		f.create<std::list<int>>(Channel::Density) = {55};
+		f.create<std::list<int>>(Channel::Density) = {12,89};
+		REQUIRE( event == 0 );
+
+		f.flush();
+		REQUIRE( event == 1 );
+	}
+
+	ftl::data::clearRegistry();
+}
+
+/* #4.4.6 */
+// TODO: Check function signature
+
+/* #4.4.7 FIXME: Review specification */
+TEST_CASE("ftl::data::Frame status after flush", "[4.4.7]") {
+	SECTION("still changed after flush") {
+		Session p;
+		Frame f = Feed::make(&p, 0, 0);
+		f.store();
+
+		f.create<int>(Channel::Colour) = 55;
+		f.flush();
+
+		REQUIRE( f.changed(Channel::Colour) );
+	}
+}
+
+/* #5 FIXME: RPC not implemented. */
+
+/* #6 See pool unit tests */
+
+
+
+// ==== Complex type overload test =============================================
+
+struct TestA {
+	int a=55;
+};
+
+struct TestB {
+	int b=99;
+};
+
+struct TestC {
+	TestA a;
+	TestB b;
+};
+
+template <>
+TestA &ftl::data::Frame::create<TestA>(ftl::codecs::Channel c) {
+	return create<TestC>(c).a;
+}
+
+template <>
+TestB &ftl::data::Frame::create<TestB>(ftl::codecs::Channel c) {
+	return create<TestC>(c).b;
+}
+
+template <>
+const TestA *ftl::data::Frame::getPtr<TestA>(ftl::codecs::Channel c) const noexcept {
+	auto *ptr = getPtr<TestC>(c);
+	return (ptr) ? &ptr->a : nullptr;
+}
+
+template <>
+const TestB *ftl::data::Frame::getPtr<TestB>(ftl::codecs::Channel c) const noexcept {
+	auto *ptr = getPtr<TestC>(c);
+	return (ptr) ? &ptr->b : nullptr;
+}
+
+TEST_CASE("ftl::data::Frame Complex Overload", "[Frame]") {
+	ftl::data::make_channel<TestC>(Channel::Pose, "pose", ftl::data::StorageMode::PERSISTENT);
+
+	SECTION("Create and get first type with default") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+		f.create<TestA>(Channel::Pose);
+		
+		auto *x = f.getPtr<TestA>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( x->a == 55 );
+
+		auto *y = f.getPtr<TestB>(Channel::Pose);
+		REQUIRE( y );
+		REQUIRE( y->b == 99 );
+	}
+
+	SECTION("Create and get first type with value") {
+		Frame f = Feed::make(nullptr, 0, 0);
+		f.store();
+		f.create<TestA>(Channel::Pose) = {77};
+		
+		auto *x = f.getPtr<TestA>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( x->a == 77 );
+
+		auto *y = f.getPtr<TestB>(Channel::Pose);
+		REQUIRE( y );
+		REQUIRE( y->b == 99 );
+	}
+
+	ftl::data::clearRegistry();
 }
diff --git a/components/structures/test/pool_unit.cpp b/components/structures/test/pool_unit.cpp
new file mode 100644
index 000000000..fd96db8c8
--- /dev/null
+++ b/components/structures/test/pool_unit.cpp
@@ -0,0 +1,120 @@
+/*
+ * These tests directly relate to the specification found at:
+ *     https://gitlab.utu.fi/nicolas.pope/ftl/-/wikis/Design/Frames
+ * 
+ * Starting from section 5 on memory management.
+ */
+
+#include "catch.hpp"
+
+#include <ftl/data/framepool.hpp>
+
+using ftl::data::Session;
+using ftl::data::Frame;
+using ftl::data::Pool;
+using ftl::codecs::Channel;
+using ftl::data::ChangeType;
+using ftl::data::StorageMode;
+using ftl::data::FrameStatus;
+
+/* #5.1 */
+TEST_CASE("ftl::data::Pool create frames", "[5.1]") {
+	SECTION("can allocate valid frame from pool") {
+		Pool pool(5,5);
+
+		Frame f = pool.allocate(0, 0);
+		REQUIRE( f.status() == FrameStatus::CREATED );
+		REQUIRE( pool.size() == 4 );
+	}
+}
+
+/* #5.2 */
+TEST_CASE("ftl::data::Pool release frames on destruct", "[5.1]") {
+	SECTION("can destroy allocated frame") {
+		Pool pool(5,5);
+
+		{
+			Frame f = pool.allocate(0, 0);
+			REQUIRE( f.status() == FrameStatus::CREATED );
+			REQUIRE( pool.size() == 4 );
+		}
+
+		REQUIRE( pool.size() == 5 );
+	}
+
+	SECTION("data reused between allocations") {
+		Pool pool(1,1);
+
+		const int *ptr = nullptr;
+
+		{
+			Frame f = pool.allocate(0, 0);
+			f.store();
+			f.create<std::vector<int>>(Channel::Colour) = {44,55,66};
+			ptr = f.get<std::vector<int>>(Channel::Colour).data();
+		}
+
+		REQUIRE( pool.size() == 1 );
+
+		{
+			Frame f = pool.allocate(0, 0);
+			f.store();
+			auto &v = f.create<std::vector<int>>(Channel::Colour);
+
+			REQUIRE( v[0] == 44 );
+			REQUIRE( v[1] == 55 );
+			REQUIRE( v[2] == 66 );
+
+			REQUIRE( (ptr && ptr == v.data()) );
+		}
+	}
+}
+
+/* #5.3 */
+TEST_CASE("ftl::data::Pool reused frames are stale", "[5.3]") {
+	SECTION("data reused is stale") {
+		Pool pool(1,1);
+
+		{
+			Frame f = pool.allocate(0, 0);
+			f.store();
+			f.create<std::vector<int>>(Channel::Colour) = {44,55,66};
+		}
+
+		REQUIRE( pool.size() == 1 );
+
+		{
+			Frame f = pool.allocate(0, 0);
+			f.store();
+
+			REQUIRE( !f.has(Channel::Colour) );
+			REQUIRE( !f.changed(Channel::Colour) );
+
+			auto &v = f.create<std::vector<int>>(Channel::Colour);
+			REQUIRE( v[0] == 44 );
+		}
+	}
+}
+
+/* #5.4 */
+// Hard to test
+
+/* #5.5 */
+TEST_CASE("ftl::data::Pool excessive allocations", "[5.5]") {
+	SECTION("allocate far beyond pool size") {
+		Pool pool(10,20);
+
+		{
+			std::list<Frame> l;
+			for (int i=0; i<100; ++i) {
+				l.push_back(std::move(pool.allocate(0,0)));
+			}
+
+			REQUIRE( pool.size() >= 10 );
+		}
+
+		// 2*pool size is the chosen max
+		REQUIRE( pool.size() <= 20 );
+	}
+}
+
diff --git a/lib/libsgm/src/CMakeLists.txt b/lib/libsgm/src/CMakeLists.txt
index a338f8380..d1bc459e4 100644
--- a/lib/libsgm/src/CMakeLists.txt
+++ b/lib/libsgm/src/CMakeLists.txt
@@ -5,6 +5,8 @@ find_package(CUDA REQUIRED)
 include_directories(../include)
 
 if (CMAKE_COMPILER_IS_GNUCXX)
+	set(CMAKE_CUDA_HOST_COMPILER gcc-7)
+	set(CUDA_HOST_COMPILER gcc-7)
 	set(CMAKE_CXX_FLAGS "-O3 -Wall -fPIC")
 	set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -std=c++11")
 endif()
diff --git a/lib/libstereo/CMakeLists.txt b/lib/libstereo/CMakeLists.txt
index 64e19cd84..cc1b10aef 100644
--- a/lib/libstereo/CMakeLists.txt
+++ b/lib/libstereo/CMakeLists.txt
@@ -20,6 +20,7 @@ set(CMAKE_USE_RELATIVE_PATHS ON)
 set(CMAKE_CXX_FLAGS_RELEASE)
 
 if (CMAKE_COMPILER_IS_GNUCXX)
+	set(CMAKE_CUDA_HOST_COMPILER gcc-7)
     set(CMAKE_CUDA_FLAGS "--gpu-architecture=compute_61 -std=c++14 -Xcompiler -fPIC -Xcompiler ${OpenMP_CXX_FLAGS} --expt-relaxed-constexpr")
     set(CMAKE_CUDA_FLAGS_RELEASE "-O3")
 else()
-- 
GitLab