diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp
index e2c2e2823..97160fe37 100644
--- a/Analyser/Static/StaticAnalyser.cpp
+++ b/Analyser/Static/StaticAnalyser.cpp
@@ -57,6 +57,10 @@
 #include "../../Storage/MassStorage/Formats/DAT.hpp"
 #include "../../Storage/MassStorage/Formats/HFV.hpp"
 
+// State Snapshots
+#include "../../Storage/State/SNA.hpp"
+#include "../../Storage/State/Z80.hpp"
+
 // Tapes
 #include "../../Storage/Tape/Formats/CAS.hpp"
 #include "../../Storage/Tape/Formats/CommodoreTAP.hpp"
@@ -73,15 +77,23 @@
 
 using namespace Analyser::Static;
 
-static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::IntType &potential_platforms) {
-	Media result;
+namespace {
 
+std::string get_extension(const std::string &name) {
 	// Get the extension, if any; it will be assumed that extensions are reliable, so an extension is a broad-phase
 	// test as to file format.
-	std::string::size_type final_dot = file_name.find_last_of(".");
-	if(final_dot == std::string::npos) return result;
-	std::string extension = file_name.substr(final_dot + 1);
+	std::string::size_type final_dot = name.find_last_of(".");
+	if(final_dot == std::string::npos) return name;
+	std::string extension = name.substr(final_dot + 1);
 	std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
+	return extension;
+}
+
+}
+
+static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::IntType &potential_platforms) {
+	Media result;
+	const std::string extension = get_extension(file_name);
 
 #define InsertInstance(list, instance, platforms) \
 	list.emplace_back(instance);\
@@ -199,14 +211,34 @@ Media Analyser::Static::GetMedia(const std::string &file_name) {
 
 TargetList Analyser::Static::GetTargets(const std::string &file_name) {
 	TargetList targets;
+	const std::string extension = get_extension(file_name);
 
+	// Check whether the file directly identifies a target; if so then just return that.
+#define Format(ext, class) 											\
+	if(extension == ext)	{										\
+		try {														\
+			auto target = Storage::State::class::load(file_name);	\
+			if(target) {											\
+				targets.push_back(std::move(target));				\
+				return targets;										\
+			}														\
+		} catch(...) {}												\
+	}
+
+	Format("sna", SNA);
+	Format("z80", Z80);
+
+#undef TryInsert
+
+	// Otherwise:
+	//
 	// Collect all disks, tapes ROMs, etc as can be extrapolated from this file, forming the
 	// union of all platforms this file might be a target for.
 	TargetPlatform::IntType potential_platforms = 0;
 	Media media = GetMediaAndPlatforms(file_name, potential_platforms);
 
-	// Hand off to platform-specific determination of whether these things are actually compatible and,
-	// if so, how to load them.
+	// Hand off to platform-specific determination of whether these
+	// things are actually compatible and, if so, how to load them.
 #define Append(x) if(potential_platforms & TargetPlatform::x) {\
 	auto new_targets = x::GetTargets(media, file_name, potential_platforms);\
 	std::move(new_targets.begin(), new_targets.end(), std::back_inserter(targets));\
diff --git a/Analyser/Static/StaticAnalyser.hpp b/Analyser/Static/StaticAnalyser.hpp
index e806ae042..259f7cca9 100644
--- a/Analyser/Static/StaticAnalyser.hpp
+++ b/Analyser/Static/StaticAnalyser.hpp
@@ -15,6 +15,7 @@
 #include "../../Storage/Disk/Disk.hpp"
 #include "../../Storage/MassStorage/MassStorageDevice.hpp"
 #include "../../Storage/Tape/Tape.hpp"
+#include "../../Reflection/Struct.hpp"
 
 #include <memory>
 #include <string>
@@ -23,8 +24,10 @@
 namespace Analyser {
 namespace Static {
 
+struct State;
+
 /*!
-	A list of disks, tapes and cartridges.
+	A list of disks, tapes and cartridges, and possibly a state snapshot.
 */
 struct Media {
 	std::vector<std::shared_ptr<Storage::Disk::Disk>> disks;
@@ -48,13 +51,16 @@ struct Media {
 };
 
 /*!
-	A list of disks, tapes and cartridges plus information about the machine to which to attach them and its configuration,
-	and instructions on how to launch the software attached, plus a measure of confidence in this target's correctness.
+	Describes a machine and possibly its state; conventionally subclassed to add other machine-specific configuration fields and any
+	necessary instructions on how to launch any software provided, plus a measure of confidence in this target's correctness.
 */
 struct Target {
 	Target(Machine machine) : machine(machine) {}
 	virtual ~Target() {}
 
+	// This field is entirely optional.
+	std::unique_ptr<Reflection::Struct> state;
+
 	Machine machine;
 	Media media;
 	float confidence = 0.0f;
diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp
index f13a00810..cbd7e19b7 100644
--- a/Components/AY38910/AY38910.hpp
+++ b/Components/AY38910/AY38910.hpp
@@ -12,6 +12,8 @@
 #include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
 #include "../../Concurrency/AsyncTaskQueue.hpp"
 
+#include "../../Reflection/Struct.hpp"
+
 namespace GI {
 namespace AY38910 {
 
@@ -162,6 +164,8 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
 		uint8_t a_left_ = 255, a_right_ = 255;
 		uint8_t b_left_ = 255, b_right_ = 255;
 		uint8_t c_left_ = 255, c_right_ = 255;
+
+		friend struct State;
 };
 
 /*!
@@ -192,6 +196,26 @@ struct Utility {
 
 };
 
+struct State: public Reflection::StructImpl<State> {
+	uint8_t registers[16]{};
+
+	// TODO: all audio-production thread state.
+
+	State() {
+		if(needs_declare()) {
+			DeclareField(registers);
+		}
+	}
+
+	template <typename AY> void apply(AY &target) {
+		// Establish emulator-thread state
+		for(uint8_t c = 0; c < 16; c++) {
+			target.select_register(c);
+			target.set_register_value(registers[c]);
+		}
+	}
+};
+
 }
 }
 
diff --git a/Machines/AmstradCPC/Keyboard.cpp b/Machines/AmstradCPC/Keyboard.cpp
index 98b67e8ae..7473f942f 100644
--- a/Machines/AmstradCPC/Keyboard.cpp
+++ b/Machines/AmstradCPC/Keyboard.cpp
@@ -151,7 +151,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
 #undef SHIFT
 #undef X
 
-	return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
+	return table_lookup_sequence_for_character(key_sequences, character);
 }
 
 bool CharacterMapper::needs_pause_after_key(uint16_t key) const {
diff --git a/Machines/Commodore/Vic-20/Keyboard.cpp b/Machines/Commodore/Vic-20/Keyboard.cpp
index 1b79bd414..62493d0e1 100644
--- a/Machines/Commodore/Vic-20/Keyboard.cpp
+++ b/Machines/Commodore/Vic-20/Keyboard.cpp
@@ -151,5 +151,5 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
 #undef SHIFT
 #undef X
 
-	return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
+	return table_lookup_sequence_for_character(key_sequences, character);
 }
diff --git a/Machines/Electron/Keyboard.cpp b/Machines/Electron/Keyboard.cpp
index c554c2cbe..689e5707f 100644
--- a/Machines/Electron/Keyboard.cpp
+++ b/Machines/Electron/Keyboard.cpp
@@ -145,7 +145,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
 #undef SHIFT
 #undef X
 
-	return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
+	return table_lookup_sequence_for_character(key_sequences, character);
 }
 
 bool CharacterMapper::needs_pause_after_key(uint16_t key) const {
diff --git a/Machines/MachineTypes.hpp b/Machines/MachineTypes.hpp
index c96a02f10..f19e18d3a 100644
--- a/Machines/MachineTypes.hpp
+++ b/Machines/MachineTypes.hpp
@@ -20,6 +20,7 @@
 #include "MediaTarget.hpp"
 #include "MouseMachine.hpp"
 #include "ScanProducer.hpp"
+#include "StateProducer.hpp"
 #include "TimedMachine.hpp"
 
 #endif /* MachineTypes_h */
diff --git a/Machines/Oric/Keyboard.cpp b/Machines/Oric/Keyboard.cpp
index 2e373ad57..3b12bc7e2 100644
--- a/Machines/Oric/Keyboard.cpp
+++ b/Machines/Oric/Keyboard.cpp
@@ -127,5 +127,5 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
 #undef SHIFT
 #undef X
 
-	return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
+	return table_lookup_sequence_for_character(key_sequences, character);
 }
diff --git a/Machines/Sinclair/Keyboard/Keyboard.cpp b/Machines/Sinclair/Keyboard/Keyboard.cpp
index 1e9db2bf8..f0cd23b39 100644
--- a/Machines/Sinclair/Keyboard/Keyboard.cpp
+++ b/Machines/Sinclair/Keyboard/Keyboard.cpp
@@ -275,13 +275,13 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
 
 	switch(machine_) {
 		case Machine::ZX80:
-		return table_lookup_sequence_for_character(zx80_key_sequences, sizeof(zx80_key_sequences), character);
+		return table_lookup_sequence_for_character(zx80_key_sequences, character);
 
 		case Machine::ZX81:
-		return table_lookup_sequence_for_character(zx81_key_sequences, sizeof(zx81_key_sequences), character);
+		return table_lookup_sequence_for_character(zx81_key_sequences, character);
 
 		case Machine::ZXSpectrum:
-		return table_lookup_sequence_for_character(spectrum_key_sequences, sizeof(zx81_key_sequences), character);
+		return table_lookup_sequence_for_character(spectrum_key_sequences, character);
 	}
 }
 
diff --git a/Machines/Sinclair/ZXSpectrum/State.hpp b/Machines/Sinclair/ZXSpectrum/State.hpp
new file mode 100644
index 000000000..69d23c5f5
--- /dev/null
+++ b/Machines/Sinclair/ZXSpectrum/State.hpp
@@ -0,0 +1,55 @@
+//
+//  State.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 25/04/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#ifndef State_hpp
+#define State_hpp
+
+#include "../../../Reflection/Struct.hpp"
+#include "../../../Processors/Z80/State/State.hpp"
+
+#include "Video.hpp"
+#include "../../../Components/AY38910/AY38910.hpp"
+
+namespace Sinclair {
+namespace ZXSpectrum {
+
+
+struct State: public Reflection::StructImpl<State> {
+	CPU::Z80::State z80;
+	Video::State video;
+
+	// In 16kb or 48kb mode, RAM will be 16kb or 48kb and represent
+	// memory in standard linear order. In 128kb mode, RAM will be
+	// 128kb with the first 16kb representing bank 0, the next bank 1, etc.
+	std::vector<uint8_t> ram;
+
+	// Meaningful for 128kb machines only.
+	uint8_t last_7ffd = 0;
+	uint8_t last_fffd = 0;
+	GI::AY38910::State ay;
+
+	// Meaningful for the +2a and +3 only.
+	uint8_t last_1ffd = 0;
+
+	State() {
+		if(needs_declare()) {
+			DeclareField(z80);
+			DeclareField(video);
+			DeclareField(ram);
+			DeclareField(last_7ffd);
+			DeclareField(last_fffd);
+			DeclareField(last_1ffd);
+			DeclareField(ay);
+		}
+	}
+};
+
+}
+}
+
+#endif /* State_h */
diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp
index c48392a61..4bb96d883 100644
--- a/Machines/Sinclair/ZXSpectrum/Video.hpp
+++ b/Machines/Sinclair/ZXSpectrum/Video.hpp
@@ -12,12 +12,15 @@
 #include "../../../Outputs/CRT/CRT.hpp"
 #include "../../../ClockReceiver/ClockReceiver.hpp"
 
+#include "../../../Reflection/Struct.hpp"
+
 #include <algorithm>
 
 namespace Sinclair {
 namespace ZXSpectrum {
+namespace Video {
 
-enum class VideoTiming {
+enum class Timing {
 	FortyEightK,
 	OneTwoEightK,
 	Plus3,
@@ -47,7 +50,7 @@ enum class VideoTiming {
 
 */
 
-template <VideoTiming timing> class Video {
+template <Timing timing> class Video {
 	private:
 		struct Timings {
 			// Number of cycles per line. Will be 224 or 228.
@@ -78,17 +81,17 @@ template <VideoTiming timing> class Video {
 		};
 
 		static constexpr Timings get_timings() {
-			if constexpr (timing == VideoTiming::Plus3) {
+			if constexpr (timing == Timing::Plus3) {
 				constexpr int delays[] = {1, 0, 7, 6, 5, 4, 3, 2};
 				return Timings(228, 311, 6, 129, 14361, delays);
 			}
 
-			if constexpr (timing == VideoTiming::OneTwoEightK) {
+			if constexpr (timing == Timing::OneTwoEightK) {
 				constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
 				return Timings(228, 311, 4, 128, 14361, delays);
 			}
 
-			if constexpr (timing == VideoTiming::FortyEightK) {
+			if constexpr (timing == Timing::FortyEightK) {
 				constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
 				return Timings(224, 312, 4, 128, 14335, delays);
 			}
@@ -103,7 +106,7 @@ template <VideoTiming timing> class Video {
 
 			constexpr int sync_line = (timings.interrupt_time / timings.cycles_per_line) + 1;
 
-			constexpr int sync_position = (timing == VideoTiming::FortyEightK) ? 164 * 2 : 166 * 2;
+			constexpr int sync_position = (timing == Timing::FortyEightK) ? 164 * 2 : 166 * 2;
 			constexpr int sync_length = 17 * 2;
 			constexpr int burst_position = sync_position + 40;
 			constexpr int burst_length = 17;
@@ -220,7 +223,7 @@ template <VideoTiming timing> class Video {
 					if(offset >= burst_position && offset < burst_position+burst_length && end_offset > offset) {
 						const int burst_duration = std::min(burst_position + burst_length, end_offset) - offset;
 
-						if constexpr (timing >= VideoTiming::OneTwoEightK) {
+						if constexpr (timing >= Timing::OneTwoEightK) {
 							crt_.output_colour_burst(burst_duration, 116, is_alternate_line_);
 							// The colour burst phase above is an empirical guess. I need to research further.
 						} else {
@@ -248,7 +251,7 @@ template <VideoTiming timing> class Video {
 		}
 
 		static constexpr int half_cycles_per_line() {
-			if constexpr (timing == VideoTiming::FortyEightK) {
+			if constexpr (timing == Timing::FortyEightK) {
 				// TODO: determine real figure here, if one exists.
 				// The source I'm looking at now suggests that the theoretical
 				// ideal of 224*2 ignores the real-life effects of separate
@@ -267,6 +270,11 @@ template <VideoTiming timing> class Video {
 			crt_.set_display_type(Outputs::Display::DisplayType::RGB);
 			crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
 
+			// Get the CRT roughly into phase.
+			//
+			// TODO: this is coupled to an assumption about the initial CRT. Fix.
+			const auto timings = get_timings();
+			crt_.output_blank(timings.lines_per_frame*timings.cycles_per_line - timings.interrupt_time);
 		}
 
 		void set_video_source(const uint8_t *source) {
@@ -328,7 +336,7 @@ template <VideoTiming timing> class Video {
 		*/
 		uint8_t get_floating_value() const {
 			constexpr auto timings = get_timings();
-			const uint8_t out_of_bounds = (timing == VideoTiming::Plus3) ? last_contended_access_ : 0xff;
+			const uint8_t out_of_bounds = (timing == Timing::Plus3) ? last_contended_access_ : 0xff;
 
 			const int line = time_into_frame_ / timings.cycles_per_line;
 			if(line >= 192) {
@@ -342,7 +350,7 @@ template <VideoTiming timing> class Video {
 
 			// The +2a and +3 always return the low bit as set.
 			const uint8_t value = last_fetches_[(time_into_line >> 1) & 3];
-			if constexpr (timing == VideoTiming::Plus3) {
+			if constexpr (timing == Timing::Plus3) {
 				return value | 1;
 			}
 			return value;
@@ -354,7 +362,7 @@ template <VideoTiming timing> class Video {
 			bus is accessed when the gate array isn't currently reading.
 		*/
 		void set_last_contended_area_access([[maybe_unused]] uint8_t value) {
-			if constexpr (timing == VideoTiming::Plus3) {
+			if constexpr (timing == Timing::Plus3) {
 				last_contended_access_ = value | 1;
 			}
 		}
@@ -363,6 +371,7 @@ template <VideoTiming timing> class Video {
 			Sets the current border colour.
 		*/
 		void set_border_colour(uint8_t colour) {
+			border_byte_ = colour;
 			border_colour_ = palette[colour];
 		}
 
@@ -381,11 +390,17 @@ template <VideoTiming timing> class Video {
 			crt_.set_display_type(type);
 		}
 
+		/*! Gets the display type. */
+		Outputs::Display::DisplayType get_display_type() const {
+			return crt_.get_display_type();
+		}
+
 	private:
 		int time_into_frame_ = 0;
 		Outputs::CRT::CRT crt_;
 		const uint8_t *memory_ = nullptr;
 		uint8_t border_colour_ = 0;
+		uint8_t border_byte_ = 0;
 
 		uint8_t *pixel_target_ = nullptr;
 		int attribute_address_ = 0;
@@ -398,6 +413,8 @@ template <VideoTiming timing> class Video {
 		uint8_t last_fetches_[4] = {0xff, 0xff, 0xff, 0xff};
 		uint8_t last_contended_access_ = 0xff;
 
+		friend struct State;
+
 #define RGB(r, g, b)	(r << 4) | (g << 2) | b
 		static constexpr uint8_t palette[] = {
 			RGB(0, 0, 0),	RGB(0, 0, 2),	RGB(2, 0, 0),	RGB(2, 0, 2),
@@ -408,6 +425,41 @@ template <VideoTiming timing> class Video {
 #undef RGB
 };
 
+struct State: public Reflection::StructImpl<State> {
+	uint8_t border_colour = 0;
+	int time_into_frame = 0;
+	bool flash = 0;
+	int flash_counter = 0;
+	bool is_alternate_line = false;
+
+	State() {
+		if(needs_declare()) {
+			DeclareField(border_colour);
+			DeclareField(time_into_frame);
+			DeclareField(flash);
+			DeclareField(flash_counter);
+			DeclareField(is_alternate_line);
+		}
+	}
+
+	template <typename Video> State(const Video &source) : State() {
+		border_colour = source.border_byte_;
+		time_into_frame = source.time_into_frame_;
+		flash = source.flash_mask_;
+		flash_counter = source.flash_counter_;
+		is_alternate_line = source. is_alternate_line_;
+	}
+
+	template <typename Video> void apply(Video &target) {
+		target.set_border_colour(border_colour);
+		target.time_into_frame_ = time_into_frame;
+		target.flash_mask_ = flash ? 0xff : 0x00;
+		target.flash_counter_ = flash_counter;
+		target.is_alternate_line_ = is_alternate_line;
+	}
+};
+
+}
 }
 }
 
diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index 06812cdce..e3c242d81 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -8,9 +8,9 @@
 
 #include "ZXSpectrum.hpp"
 
+#include "State.hpp"
 #include "Video.hpp"
-
-#define LOG_PREFIX "[Spectrum] "
+#include "../Keyboard/Keyboard.hpp"
 
 #include "../../../Activity/Source.hpp"
 #include "../../MachineTypes.hpp"
@@ -24,7 +24,9 @@
 // just grab the CPC's version of an FDC.
 #include "../../AmstradCPC/FDC.hpp"
 
+#define LOG_PREFIX "[Spectrum] "
 #include "../../../Outputs/Log.hpp"
+
 #include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp"
 #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
 #include "../../../Outputs/Speaker/Implementation/SampleSource.hpp"
@@ -39,10 +41,6 @@
 
 #include "../../../ClockReceiver/JustInTime.hpp"
 
-#include "../../../Processors/Z80/State/State.hpp"
-
-#include "../Keyboard/Keyboard.hpp"
-
 #include <array>
 
 namespace Sinclair {
@@ -124,6 +122,31 @@ template<Model model> class ConcreteMachine:
 				duration_to_press_enter_ = Cycles(5 * clock_rate());
 				keyboard_.set_key_state(ZX::Keyboard::KeyEnter, true);
 			}
+
+			// Install state if supplied.
+			if(target.state) {
+				const auto state = static_cast<State *>(target.state.get());
+				state->z80.apply(z80_);
+				state->video.apply(*video_.last_valid());
+				state->ay.apply(ay_);
+
+				// If this is a 48k or 16k machine, remap source data from its original
+				// linear form to whatever the banks end up being; otherwise copy as is.
+				if(model <= Model::FortyEightK) {
+					const size_t num_banks = std::min(size_t(48*1024), state->ram.size()) >> 14;
+					for(size_t c = 0; c < num_banks; c++) {
+						memcpy(&write_pointers_[c + 1][(c+1) * 0x4000], &state->ram[c * 0x4000], 0x4000);
+					}
+				} else {
+					memcpy(ram_.data(), state->ram.data(), std::min(ram_.size(), state->ram.size()));
+
+					port1ffd_ = state->last_1ffd;
+					port7ffd_ = state->last_7ffd;
+					update_memory_map();
+
+					GI::AY38910::Utility::select_register(ay_, state->last_fffd);
+				}
+			}
 		}
 
 		~ConcreteMachine() {
@@ -202,6 +225,10 @@ template<Model model> class ConcreteMachine:
 			video_->set_display_type(display_type);
 		}
 
+		Outputs::Display::DisplayType get_display_type() const override {
+			return video_->get_display_type();
+		}
+
 		// MARK: - BusHandler.
 
 		forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
@@ -384,10 +411,6 @@ template<Model model> class ConcreteMachine:
 
 						// Set the proper video base pointer.
 						set_video_address();
-
-						// Potentially lock paging, _after_ the current
-						// port values have taken effect.
-						disable_paging_ |= *cycle.value & 0x20;
 					}
 
 					// Test for +2a/+3 paging (i.e. port 1ffd).
@@ -625,6 +648,7 @@ template<Model model> class ConcreteMachine:
 			auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);	// OptionsType is arbitrary, but not optional.
 			options->automatic_tape_motor_control = use_automatic_tape_motor_control_;
 			options->quickload = allow_fast_tape_hack_;
+			options->output = get_video_signal_configurable();
 			return options;
 		}
 
@@ -713,6 +737,10 @@ template<Model model> class ConcreteMachine:
 				set_memory(2, 2);
 				set_memory(3, port7ffd_ & 7);
 			}
+
+			// Potentially lock paging, _after_ the current
+			// port values have taken effect.
+			disable_paging_ = port7ffd_ & 0x20;
 		}
 
 		void set_memory(int bank, uint8_t source) {
@@ -758,10 +786,10 @@ template<Model model> class ConcreteMachine:
 		// MARK: - Video.
 		using VideoType =
 			std::conditional_t<
-				model <= Model::FortyEightK, Video<VideoTiming::FortyEightK>,
+				model <= Model::FortyEightK, Video::Video<Video::Timing::FortyEightK>,
 				std::conditional_t<
-					model <= Model::Plus2, Video<VideoTiming::OneTwoEightK>,
-					Video<VideoTiming::Plus3>
+					model <= Model::Plus2, Video::Video<Video::Timing::OneTwoEightK>,
+					Video::Video<Video::Timing::Plus3>
 				>
 			>;
 		JustInTimeActor<VideoType> video_;
diff --git a/Machines/StateProducer.hpp b/Machines/StateProducer.hpp
new file mode 100644
index 000000000..3a27e3cd0
--- /dev/null
+++ b/Machines/StateProducer.hpp
@@ -0,0 +1,26 @@
+//
+//  StateProducer.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 24/04/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#ifndef State_h
+#define State_h
+
+#include <memory>
+#include "../Analyser/Static/StaticAnalyser.hpp"
+
+namespace MachineTypes {
+
+struct StateProducer {
+	// TODO.
+//	virtual bool get_state(Analyser::Static::State *, [[maybe_unused]] bool advance_to_simple = false) {
+//		return false;
+//	}
+};
+
+};
+
+#endif /* State_h */
diff --git a/Machines/Utility/Typer.cpp b/Machines/Utility/Typer.cpp
index 9706d85bf..1e290bef5 100644
--- a/Machines/Utility/Typer.cpp
+++ b/Machines/Utility/Typer.cpp
@@ -131,13 +131,3 @@ bool Typer::type_next_character() {
 
 	return true;
 }
-
-// MARK: - Character mapper
-
-const uint16_t *CharacterMapper::table_lookup_sequence_for_character(const KeySequence *sequences, std::size_t length, char character) const {
-	std::size_t ucharacter = size_t((unsigned char)character);
-	if(ucharacter >= (length / sizeof(KeySequence))) return nullptr;
-	if(sequences[ucharacter][0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) return nullptr;
-	return sequences[ucharacter];
-}
-
diff --git a/Machines/Utility/Typer.hpp b/Machines/Utility/Typer.hpp
index 0e0a15b3a..79ac1b86a 100644
--- a/Machines/Utility/Typer.hpp
+++ b/Machines/Utility/Typer.hpp
@@ -44,11 +44,15 @@ class CharacterMapper {
 		typedef uint16_t KeySequence[16];
 
 		/*!
-			Provided in the base class as a convenience: given the lookup table of key sequences @c sequences,
-			with @c length entries, returns the sequence for character @c character if it exists; otherwise
-			returns @c nullptr.
+			Provided in the base class as a convenience: given the C array of key sequences @c sequences,
+			returns the sequence for character @c character if it exists; otherwise returns @c nullptr.
 		*/
-		const uint16_t *table_lookup_sequence_for_character(const KeySequence *sequences, std::size_t length, char character) const;
+		template <typename Collection> const uint16_t *table_lookup_sequence_for_character(const Collection &sequences, char character) const {
+			std::size_t ucharacter = size_t((unsigned char)character);
+			if(ucharacter >= sizeof(sequences) / sizeof(KeySequence)) return nullptr;
+			if(sequences[ucharacter][0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) return nullptr;
+			return sequences[ucharacter];
+		}
 };
 
 /*!
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 24c509446..7d908b13d 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -479,6 +479,10 @@
 		4B89453E201967B4007DE474 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B894517201967B4007DE474 /* StaticAnalyser.cpp */; };
 		4B89453F201967B4007DE474 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B894517201967B4007DE474 /* StaticAnalyser.cpp */; };
 		4B8DD3682633B2D400B3C866 /* SpectrumVideoContentionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DD3672633B2D400B3C866 /* SpectrumVideoContentionTests.mm */; };
+		4B8DD3862634D37E00B3C866 /* SNA.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DD3842634D37E00B3C866 /* SNA.cpp */; };
+		4B8DD3872634D37E00B3C866 /* SNA.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DD3842634D37E00B3C866 /* SNA.cpp */; };
+		4B8DD39726360DDF00B3C866 /* Z80.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DD39526360DDF00B3C866 /* Z80.cpp */; };
+		4B8DD39826360DDF00B3C866 /* Z80.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DD39526360DDF00B3C866 /* Z80.cpp */; };
 		4B8DF4D825465B7500F3433C /* IIgsMemoryMapTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DF4D725465B7500F3433C /* IIgsMemoryMapTests.mm */; };
 		4B8DF4F9254E36AE00F3433C /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DF4F7254E36AD00F3433C /* Video.cpp */; };
 		4B8DF4FA254E36AE00F3433C /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DF4F7254E36AD00F3433C /* Video.cpp */; };
@@ -1429,6 +1433,12 @@
 		4B8A7E85212F988200F2BBC6 /* DeferredQueue.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DeferredQueue.hpp; sourceTree = "<group>"; };
 		4B8D287E1F77207100645199 /* TrackSerialiser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TrackSerialiser.hpp; sourceTree = "<group>"; };
 		4B8DD3672633B2D400B3C866 /* SpectrumVideoContentionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SpectrumVideoContentionTests.mm; sourceTree = "<group>"; };
+		4B8DD375263481BB00B3C866 /* StateProducer.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = StateProducer.hpp; sourceTree = "<group>"; };
+		4B8DD3842634D37E00B3C866 /* SNA.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SNA.cpp; sourceTree = "<group>"; };
+		4B8DD3852634D37E00B3C866 /* SNA.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SNA.hpp; sourceTree = "<group>"; };
+		4B8DD3912635A72F00B3C866 /* State.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = State.hpp; sourceTree = "<group>"; };
+		4B8DD39526360DDF00B3C866 /* Z80.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Z80.cpp; sourceTree = "<group>"; };
+		4B8DD39626360DDF00B3C866 /* Z80.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Z80.hpp; sourceTree = "<group>"; };
 		4B8DF4D62546561300F3433C /* MemoryMap.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MemoryMap.hpp; sourceTree = "<group>"; };
 		4B8DF4D725465B7500F3433C /* IIgsMemoryMapTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = IIgsMemoryMapTests.mm; sourceTree = "<group>"; };
 		4B8DF4ED254B840B00F3433C /* AppleClock.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AppleClock.hpp; sourceTree = "<group>"; };
@@ -2210,6 +2220,7 @@
 				4B0F1BFA260300D900B85C66 /* ZXSpectrum.cpp */,
 				4B0F1BFB260300D900B85C66 /* ZXSpectrum.hpp */,
 				4B0F1C092603BA5F00B85C66 /* Video.hpp */,
+				4B8DD3912635A72F00B3C866 /* State.hpp */,
 			);
 			path = ZXSpectrum;
 			sourceTree = "<group>";
@@ -2842,6 +2853,7 @@
 				4B8805F81DCFF6CD003085B1 /* Data */,
 				4BAB62AA1D3272D200DF5BA0 /* Disk */,
 				4B6AAEA1230E3E1D0078E864 /* MassStorage */,
+				4B8DD3832634D37E00B3C866 /* State */,
 				4B69FB3A1C4D908A00B5F0AA /* Tape */,
 			);
 			name = Storage;
@@ -3248,6 +3260,17 @@
 			path = AmstradCPC;
 			sourceTree = "<group>";
 		};
+		4B8DD3832634D37E00B3C866 /* State */ = {
+			isa = PBXGroup;
+			children = (
+				4B8DD3842634D37E00B3C866 /* SNA.cpp */,
+				4B8DD3852634D37E00B3C866 /* SNA.hpp */,
+				4B8DD39526360DDF00B3C866 /* Z80.cpp */,
+				4B8DD39626360DDF00B3C866 /* Z80.hpp */,
+			);
+			path = State;
+			sourceTree = "<group>";
+		};
 		4B8DF4EC254B840B00F3433C /* AppleClock */ = {
 			isa = PBXGroup;
 			children = (
@@ -4018,6 +4041,7 @@
 				4B92294222B04A3D00A1458F /* MouseMachine.hpp */,
 				4BDCC5F81FB27A5E001220C5 /* ROMMachine.hpp */,
 				4B046DC31CFE651500E9E45E /* ScanProducer.hpp */,
+				4B8DD375263481BB00B3C866 /* StateProducer.hpp */,
 				4BC57CD32434282000FBC404 /* TimedMachine.hpp */,
 				4B38F3491F2EC12000D9235D /* AmstradCPC */,
 				4BCE0048227CE8CA000CA200 /* Apple */,
@@ -5216,6 +5240,7 @@
 				4BEDA43225B3C700000C2DBD /* Executor.cpp in Sources */,
 				4BC1317B2346DF2B00E4FF3D /* MSA.cpp in Sources */,
 				4B894533201967B4007DE474 /* 6502.cpp in Sources */,
+				4B8DD3872634D37E00B3C866 /* SNA.cpp in Sources */,
 				4B055AA91FAE85EF0060FFFF /* CommodoreGCR.cpp in Sources */,
 				4B055ADB1FAE9B460060FFFF /* 6560.cpp in Sources */,
 				4B17B58C20A8A9D9007CCA8F /* StringSerialiser.cpp in Sources */,
@@ -5249,6 +5274,7 @@
 				4B055AC81FAE9AFB0060FFFF /* C1540.cpp in Sources */,
 				4B055A8F1FAE85A90060FFFF /* FileHolder.cpp in Sources */,
 				4B055A911FAE85B50060FFFF /* Cartridge.cpp in Sources */,
+				4B8DD39826360DDF00B3C866 /* Z80.cpp in Sources */,
 				4B894525201967B4007DE474 /* Tape.cpp in Sources */,
 				4B055ACD1FAE9B030060FFFF /* Keyboard.cpp in Sources */,
 				4B055AB21FAE860F0060FFFF /* CommodoreTAP.cpp in Sources */,
@@ -5345,6 +5371,7 @@
 				4BE211FF253FC80900435408 /* StaticAnalyser.cpp in Sources */,
 				4B0F1BE22602FF9C00B85C66 /* ZX8081.cpp in Sources */,
 				4B8334951F5E25B60097E338 /* C1540.cpp in Sources */,
+				4B8DD39726360DDF00B3C866 /* Z80.cpp in Sources */,
 				4BEDA40C25B2844B000C2DBD /* Decoder.cpp in Sources */,
 				4B89453C201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
 				4B595FAD2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */,
@@ -5486,6 +5513,7 @@
 				4BEBFB4D2002C4BF000708CC /* MSXDSK.cpp in Sources */,
 				4BBFBB6C1EE8401E00C01E7A /* ZX8081.cpp in Sources */,
 				4B83348A1F5DB94B0097E338 /* IRQDelegatePortHandler.cpp in Sources */,
+				4B8DD3862634D37E00B3C866 /* SNA.cpp in Sources */,
 				4B4DEC06252BFA56004583AC /* 65816Base.cpp in Sources */,
 				4B894524201967B4007DE474 /* Tape.cpp in Sources */,
 				4B7136891F78725F008B8ED9 /* Shifter.cpp in Sources */,
diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist
index 5bb5dc8d1..b37475e53 100644
--- a/OSBindings/Mac/Clock Signal/Info.plist	
+++ b/OSBindings/Mac/Clock Signal/Info.plist	
@@ -592,6 +592,46 @@
 			<key>NSDocumentClass</key>
 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
 		</dict>
+		<dict>
+			<key>CFBundleTypeExtensions</key>
+			<array>
+				<string>sna</string>
+			</array>
+			<key>CFBundleTypeName</key>
+			<string>ZX Spectrum SNA snapshot</string>
+			<key>CFBundleTypeOSTypes</key>
+			<array>
+				<string>????</string>
+			</array>
+			<key>CFBundleTypeRole</key>
+			<string>Viewer</string>
+			<key>LSHandlerRank</key>
+			<string>Owner</string>
+			<key>LSTypeIsPackage</key>
+			<false/>
+			<key>NSDocumentClass</key>
+			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
+		</dict>
+		<dict>
+			<key>CFBundleTypeExtensions</key>
+			<array>
+				<string>z80</string>
+			</array>
+			<key>CFBundleTypeName</key>
+			<string>ZX Spectrum Z80 snapshot</string>
+			<key>CFBundleTypeOSTypes</key>
+			<array>
+				<string>????</string>
+			</array>
+			<key>CFBundleTypeRole</key>
+			<string>Viewer</string>
+			<key>LSHandlerRank</key>
+			<string>Owner</string>
+			<key>LSTypeIsPackage</key>
+			<false/>
+			<key>NSDocumentClass</key>
+			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
+		</dict>
 	</array>
 	<key>CFBundleExecutable</key>
 	<string>$(EXECUTABLE_NAME)</string>
diff --git a/OSBindings/Qt/ClockSignal.pro b/OSBindings/Qt/ClockSignal.pro
index a78500ab7..65055ae4d 100644
--- a/OSBindings/Qt/ClockSignal.pro
+++ b/OSBindings/Qt/ClockSignal.pro
@@ -129,6 +129,7 @@ SOURCES += \
 	$$SRC/Storage/MassStorage/Encodings/*.cpp \
 	$$SRC/Storage/MassStorage/Formats/*.cpp \
 	$$SRC/Storage/MassStorage/SCSI/*.cpp \
+	$$SRC/Storage/State/*.cpp \
 	$$SRC/Storage/Tape/*.cpp \
 	$$SRC/Storage/Tape/Formats/*.cpp \
 	$$SRC/Storage/Tape/Parsers/*.cpp \
@@ -269,6 +270,7 @@ HEADERS += \
 	$$SRC/Storage/MassStorage/Encodings/*.hpp \
 	$$SRC/Storage/MassStorage/Formats/*.hpp \
 	$$SRC/Storage/MassStorage/SCSI/*.hpp \
+	$$SRC/Storage/State/*.hpp \
 	$$SRC/Storage/Tape/*.hpp \
 	$$SRC/Storage/Tape/Formats/*.hpp \
 	$$SRC/Storage/Tape/Parsers/*.hpp \
diff --git a/OSBindings/SDL/SConstruct b/OSBindings/SDL/SConstruct
index 6be9e9e28..2511a5291 100644
--- a/OSBindings/SDL/SConstruct
+++ b/OSBindings/SDL/SConstruct
@@ -125,6 +125,7 @@ SOURCES += glob.glob('../../Storage/MassStorage/*.cpp')
 SOURCES += glob.glob('../../Storage/MassStorage/Encodings/*.cpp')
 SOURCES += glob.glob('../../Storage/MassStorage/Formats/*.cpp')
 SOURCES += glob.glob('../../Storage/MassStorage/SCSI/*.cpp')
+SOURCES += glob.glob('../../Storage/State/*.cpp')
 SOURCES += glob.glob('../../Storage/Tape/*.cpp')
 SOURCES += glob.glob('../../Storage/Tape/Formats/*.cpp')
 SOURCES += glob.glob('../../Storage/Tape/Parsers/*.cpp')
diff --git a/Processors/Z80/Implementation/Z80Base.cpp b/Processors/Z80/Implementation/Z80Base.cpp
index c1c10943a..1516220d5 100644
--- a/Processors/Z80/Implementation/Z80Base.cpp
+++ b/Processors/Z80/Implementation/Z80Base.cpp
@@ -33,18 +33,18 @@ uint16_t ProcessorBase::get_value_of_register(Register r) const {
 		case Register::L:						return hl_.halves.low;
 		case Register::HL:						return hl_.full;
 
-		case Register::ADash:					return afDash_.halves.high;
-		case Register::FlagsDash:				return afDash_.halves.low;
-		case Register::AFDash:					return afDash_.full;
-		case Register::BDash:					return bcDash_.halves.high;
-		case Register::CDash:					return bcDash_.halves.low;
-		case Register::BCDash:					return bcDash_.full;
-		case Register::DDash:					return deDash_.halves.high;
-		case Register::EDash:					return deDash_.halves.low;
-		case Register::DEDash:					return deDash_.full;
-		case Register::HDash:					return hlDash_.halves.high;
-		case Register::LDash:					return hlDash_.halves.low;
-		case Register::HLDash:					return hlDash_.full;
+		case Register::ADash:					return af_dash_.halves.high;
+		case Register::FlagsDash:				return af_dash_.halves.low;
+		case Register::AFDash:					return af_dash_.full;
+		case Register::BDash:					return bc_dash_.halves.high;
+		case Register::CDash:					return bc_dash_.halves.low;
+		case Register::BCDash:					return bc_dash_.full;
+		case Register::DDash:					return de_dash_.halves.high;
+		case Register::EDash:					return de_dash_.halves.low;
+		case Register::DEDash:					return de_dash_.full;
+		case Register::HDash:					return hl_dash_.halves.high;
+		case Register::LDash:					return hl_dash_.halves.low;
+		case Register::HLDash:					return hl_dash_.full;
 
 		case Register::IXh:						return ix_.halves.high;
 		case Register::IXl:						return ix_.halves.low;
@@ -86,18 +86,18 @@ void ProcessorBase::set_value_of_register(Register r, uint16_t value) {
 		case Register::L:				hl_.halves.low = uint8_t(value);	break;
 		case Register::HL:				hl_.full = value;					break;
 
-		case Register::ADash:			afDash_.halves.high = uint8_t(value);	break;
-		case Register::FlagsDash:		afDash_.halves.low = uint8_t(value);	break;
-		case Register::AFDash:			afDash_.full = value;					break;
-		case Register::BDash:			bcDash_.halves.high = uint8_t(value);	break;
-		case Register::CDash:			bcDash_.halves.low = uint8_t(value);	break;
-		case Register::BCDash:			bcDash_.full = value;					break;
-		case Register::DDash:			deDash_.halves.high = uint8_t(value);	break;
-		case Register::EDash:			deDash_.halves.low = uint8_t(value);	break;
-		case Register::DEDash:			deDash_.full = value;					break;
-		case Register::HDash:			hlDash_.halves.high = uint8_t(value);	break;
-		case Register::LDash:			hlDash_.halves.low = uint8_t(value);	break;
-		case Register::HLDash:			hlDash_.full = value;					break;
+		case Register::ADash:			af_dash_.halves.high = uint8_t(value);	break;
+		case Register::FlagsDash:		af_dash_.halves.low = uint8_t(value);	break;
+		case Register::AFDash:			af_dash_.full = value;					break;
+		case Register::BDash:			bc_dash_.halves.high = uint8_t(value);	break;
+		case Register::CDash:			bc_dash_.halves.low = uint8_t(value);	break;
+		case Register::BCDash:			bc_dash_.full = value;					break;
+		case Register::DDash:			de_dash_.halves.high = uint8_t(value);	break;
+		case Register::EDash:			de_dash_.halves.low = uint8_t(value);	break;
+		case Register::DEDash:			de_dash_.full = value;					break;
+		case Register::HDash:			hl_dash_.halves.high = uint8_t(value);	break;
+		case Register::LDash:			hl_dash_.halves.low = uint8_t(value);	break;
+		case Register::HLDash:			hl_dash_.full = value;					break;
 
 		case Register::IXh:				ix_.halves.high = uint8_t(value);		break;
 		case Register::IXl:				ix_.halves.low = uint8_t(value);		break;
diff --git a/Processors/Z80/Implementation/Z80Implementation.hpp b/Processors/Z80/Implementation/Z80Implementation.hpp
index 490e5f50f..56cf63fd9 100644
--- a/Processors/Z80/Implementation/Z80Implementation.hpp
+++ b/Processors/Z80/Implementation/Z80Implementation.hpp
@@ -461,17 +461,17 @@ template <	class T,
 				case MicroOp::ExAFAFDash: {
 					const uint8_t a = a_;
 					const uint8_t f = get_flags();
-					set_flags(afDash_.halves.low);
-					a_ = afDash_.halves.high;
-					afDash_.halves.high = a;
-					afDash_.halves.low = f;
+					set_flags(af_dash_.halves.low);
+					a_ = af_dash_.halves.high;
+					af_dash_.halves.high = a;
+					af_dash_.halves.low = f;
 				} break;
 
 				case MicroOp::EXX: {
 					uint16_t temp;
-					swap(de_, deDash_);
-					swap(bc_, bcDash_);
-					swap(hl_, hlDash_);
+					swap(de_, de_dash_);
+					swap(bc_, bc_dash_);
+					swap(hl_, hl_dash_);
 				} break;
 
 #undef swap
diff --git a/Processors/Z80/Implementation/Z80Storage.hpp b/Processors/Z80/Implementation/Z80Storage.hpp
index 613e9334a..b88bf2a84 100644
--- a/Processors/Z80/Implementation/Z80Storage.hpp
+++ b/Processors/Z80/Implementation/Z80Storage.hpp
@@ -129,7 +129,7 @@ class ProcessorStorage {
 
 		uint8_t a_;
 		RegisterPair16 bc_, de_, hl_;
-		RegisterPair16 afDash_, bcDash_, deDash_, hlDash_;
+		RegisterPair16 af_dash_, bc_dash_, de_dash_, hl_dash_;
 		RegisterPair16 ix_, iy_, pc_, sp_;
 		RegisterPair16 ir_, refresh_addr_;
 		bool iff1_ = false, iff2_ = false;
diff --git a/Processors/Z80/State/State.cpp b/Processors/Z80/State/State.cpp
index c9b90f2de..4f6f83fae 100644
--- a/Processors/Z80/State/State.cpp
+++ b/Processors/Z80/State/State.cpp
@@ -19,10 +19,10 @@ State::State(const ProcessorBase &src): State() {
 	registers.bc = src.bc_.full;
 	registers.de = src.de_.full;
 	registers.hl = src.hl_.full;
-	registers.afDash = src.afDash_.full;
-	registers.bcDash = src.bcDash_.full;
-	registers.deDash = src.deDash_.full;
-	registers.hlDash = src.hlDash_.full;
+	registers.af_dash = src.af_dash_.full;
+	registers.bc_dash = src.bc_dash_.full;
+	registers.de_dash = src.de_dash_.full;
+	registers.hl_dash = src.hl_dash_.full;
 	registers.ix = src.ix_.full;
 	registers.iy = src.iy_.full;
 	registers.ir = src.ir_.full;
@@ -108,10 +108,10 @@ void State::apply(ProcessorBase &target) {
 	target.bc_.full = registers.bc;
 	target.de_.full = registers.de;
 	target.hl_.full = registers.hl;
-	target.afDash_.full = registers.afDash;
-	target.bcDash_.full = registers.bcDash;
-	target.deDash_.full = registers.deDash;
-	target.hlDash_.full = registers.hlDash;
+	target.af_dash_.full = registers.af_dash;
+	target.bc_dash_.full = registers.bc_dash;
+	target.de_dash_.full = registers.de_dash;
+	target.hl_dash_.full = registers.hl_dash;
 	target.ix_.full = registers.ix;
 	target.iy_.full = registers.iy;
 	target.ir_.full = registers.ir;
@@ -179,10 +179,10 @@ State::Registers::Registers() {
 		DeclareField(bc);
 		DeclareField(de);
 		DeclareField(hl);
-		DeclareField(afDash);
-		DeclareField(bcDash);
-		DeclareField(deDash);
-		DeclareField(hlDash);
+		DeclareField(af_dash);	// TODO: is there any disadvantage to declaring these for reflective
+		DeclareField(bc_dash);	// purposes as AF', BC', etc?
+		DeclareField(de_dash);
+		DeclareField(hl_dash);
 		DeclareField(ix);
 		DeclareField(iy);
 		DeclareField(ir);
diff --git a/Processors/Z80/State/State.hpp b/Processors/Z80/State/State.hpp
index f5676aa60..b074e38bf 100644
--- a/Processors/Z80/State/State.hpp
+++ b/Processors/Z80/State/State.hpp
@@ -31,7 +31,7 @@ struct State: public Reflection::StructImpl<State> {
 		uint8_t a;
 		uint8_t flags;
 		uint16_t bc, de, hl;
-		uint16_t afDash, bcDash, deDash, hlDash;
+		uint16_t af_dash, bc_dash, de_dash, hl_dash;
 		uint16_t ix, iy, ir;
 		uint16_t program_counter, stack_pointer;
 		uint16_t memptr;
@@ -60,25 +60,25 @@ struct State: public Reflection::StructImpl<State> {
 		obviously doesn't.
 	*/
 	struct ExecutionState: public Reflection::StructImpl<ExecutionState> {
-		bool is_halted;
+		bool is_halted = false;
 
-		uint8_t requests;
-		uint8_t last_requests;
-		uint8_t temp8;
-		uint8_t operation;
-		uint16_t temp16;
-		unsigned int flag_adjustment_history;
-		uint16_t pc_increment;
-		uint16_t refresh_address;
+		uint8_t requests = 0;
+		uint8_t last_requests = 0;
+		uint8_t temp8 = 0;
+		uint8_t operation = 0;
+		uint16_t temp16 = 0;
+		unsigned int flag_adjustment_history = 0;
+		uint16_t pc_increment = 1;
+		uint16_t refresh_address = 0;
 
 		ReflectableEnum(Phase,
 			UntakenConditionalCall, Reset, IRQMode0, IRQMode1, IRQMode2,
 			NMI, FetchDecode, Operation
 		);
 
-		Phase phase;
-		int half_cycles_into_step;
-		int steps_into_phase;
+		Phase phase = Phase::FetchDecode;
+		int half_cycles_into_step = 0;
+		int steps_into_phase = 0;
 		uint16_t instruction_page = 0;
 
 		ExecutionState();
diff --git a/Storage/FileHolder.hpp b/Storage/FileHolder.hpp
index a78fce034..dfcfb4d29 100644
--- a/Storage/FileHolder.hpp
+++ b/Storage/FileHolder.hpp
@@ -43,7 +43,7 @@ class FileHolder final {
 				Rewrite		opens the file for rewriting; none of the original content is preserved; whatever
 							the caller outputs will replace the existing file.
 
-			@raises ErrorCantOpen if the file cannot be opened.
+			@throws ErrorCantOpen if the file cannot be opened.
 		*/
 		FileHolder(const std::string &file_name, FileMode ideal_mode = FileMode::ReadWrite);
 
diff --git a/Storage/State/SNA.cpp b/Storage/State/SNA.cpp
new file mode 100644
index 000000000..5daa42444
--- /dev/null
+++ b/Storage/State/SNA.cpp
@@ -0,0 +1,79 @@
+//
+//  SNA.cpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 24/04/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#include "SNA.hpp"
+
+#include "../FileHolder.hpp"
+
+#include "../../Analyser/Static/ZXSpectrum/Target.hpp"
+#include "../../Machines/Sinclair/ZXSpectrum/State.hpp"
+
+using namespace Storage::State;
+
+std::unique_ptr<Analyser::Static::Target> SNA::load(const std::string &file_name) {
+	// Make sure the file is accessible and appropriately sized.
+	FileHolder file(file_name);
+	if(file.stats().st_size != 48*1024 + 0x1b) {
+		return nullptr;
+	}
+
+	// SNAs are always for 48kb machines.
+	using Target = Analyser::Static::ZXSpectrum::Target;
+	auto result = std::make_unique<Target>();
+	result->model = Target::Model::FortyEightK;
+
+	// Prepare to populate ZX Spectrum state.
+	auto *const state = new Sinclair::ZXSpectrum::State();
+	result->state = std::unique_ptr<Reflection::Struct>(state);
+
+	// Comments below: [offset] [contents]
+
+	//	00	I
+	const uint8_t i = file.get8();
+
+	//	01	HL';	03	DE';	05	BC';	07	AF'
+	state->z80.registers.hl_dash = file.get16le();
+	state->z80.registers.de_dash = file.get16le();
+	state->z80.registers.bc_dash = file.get16le();
+	state->z80.registers.af_dash = file.get16le();
+
+	//	09	HL;		0B	DE;		0D	BC;		0F	IY;		11	IX
+	state->z80.registers.hl = file.get16le();
+	state->z80.registers.de = file.get16le();
+	state->z80.registers.bc = file.get16le();
+	state->z80.registers.iy = file.get16le();
+	state->z80.registers.ix = file.get16le();
+
+	//	13	IFF2 (in bit 2)
+	const uint8_t iff = file.get8();
+	state->z80.registers.iff1 = state->z80.registers.iff2 = iff & 4;
+
+	//	14	R
+	const uint8_t r = file.get8();
+	state->z80.registers.ir = uint16_t((i << 8) | r);
+
+	//	15	AF;		17	SP;		19	interrupt mode
+	state->z80.registers.flags = file.get8();
+	state->z80.registers.a = file.get8();
+	state->z80.registers.stack_pointer = file.get16le();
+	state->z80.registers.interrupt_mode = file.get8();
+
+	//	1A	border colour
+	state->video.border_colour = file.get8();
+
+	//	1B–	48kb RAM contents
+	state->ram = file.read(48*1024);
+
+	// To establish program counter, point it to a RET that
+	// I know is in the 16/48kb ROM. This avoids having to
+	// try to do a pop here, given that the true program counter
+	// might currently be in the ROM.
+	state->z80.registers.program_counter = 0x1d83;
+
+	return result;
+}
diff --git a/Storage/State/SNA.hpp b/Storage/State/SNA.hpp
new file mode 100644
index 000000000..2f2811d93
--- /dev/null
+++ b/Storage/State/SNA.hpp
@@ -0,0 +1,24 @@
+//
+//  SNA.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 24/04/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#ifndef Storage_State_SNA_hpp
+#define Storage_State_SNA_hpp
+
+#include "../../Analyser/Static/StaticAnalyser.hpp"
+
+namespace Storage {
+namespace State {
+
+struct SNA {
+	static std::unique_ptr<Analyser::Static::Target> load(const std::string &file_name);
+};
+
+}
+}
+
+#endif /* Storage_State_SNA_hpp */
diff --git a/Storage/State/Z80.cpp b/Storage/State/Z80.cpp
new file mode 100644
index 000000000..4f5c9d390
--- /dev/null
+++ b/Storage/State/Z80.cpp
@@ -0,0 +1,211 @@
+//
+//  Z80.cpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 25/04/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#include "Z80.hpp"
+
+#include "../FileHolder.hpp"
+
+#include "../../Analyser/Static/ZXSpectrum/Target.hpp"
+#include "../../Machines/Sinclair/ZXSpectrum/State.hpp"
+
+using namespace Storage::State;
+
+namespace {
+
+std::vector<uint8_t> read_memory(Storage::FileHolder &file, size_t size, bool is_compressed) {
+	if(!is_compressed) {
+		return file.read(size);
+	}
+
+	std::vector<uint8_t> result(size);
+	size_t cursor = 0;
+
+	while(cursor != size) {
+		const uint8_t next = file.get8();
+
+		// If the next byte definitely doesn't, or can't,
+		// start an ED ED sequence then just take it.
+		if(next != 0xed || cursor == size - 1) {
+			result[cursor] = next;
+			++cursor;
+			continue;
+		}
+
+		// Grab the next byte. If it's not ED then write
+		// both and continue.
+		const uint8_t after = file.get8();
+		if(after != 0xed) {
+			result[cursor] = next;
+			result[cursor+1] = after;
+			cursor += 2;
+			continue;
+		}
+
+		// An ED ED has begun, so grab the RLE sequence.
+		const uint8_t count = file.get8();
+		const uint8_t value = file.get8();
+
+		memset(&result[cursor], value, count);
+		cursor += count;
+	}
+
+	return result;
+}
+
+}
+
+std::unique_ptr<Analyser::Static::Target> Z80::load(const std::string &file_name) {
+	FileHolder file(file_name);
+
+	// Construct a target with a Spectrum state.
+	using Target = Analyser::Static::ZXSpectrum::Target;
+	auto result = std::make_unique<Target>();
+	auto *const state = new Sinclair::ZXSpectrum::State();
+	result->state = std::unique_ptr<Reflection::Struct>(state);
+
+	// Read version 1 header.
+	state->z80.registers.a = file.get8();
+	state->z80.registers.flags = file.get8();
+	state->z80.registers.bc = file.get16le();
+	state->z80.registers.hl = file.get16le();
+	state->z80.registers.program_counter = file.get16le();
+	state->z80.registers.stack_pointer = file.get16le();
+	state->z80.registers.ir = file.get16be();	// Stored I then R.
+
+	// Bit 7 of R is stored separately; likely this relates to an
+	// optimisation in the Z80 emulator that for some reason was
+	// exported into its file format.
+	const uint8_t raw_misc = file.get8();
+	const uint8_t misc = (raw_misc == 0xff) ? 1 : raw_misc;
+	state->z80.registers.ir = uint16_t((state->z80.registers.ir & ~0x80) | ((misc&1) << 7));
+
+	state->z80.registers.de = file.get16le();
+	state->z80.registers.bc_dash = file.get16le();
+	state->z80.registers.de_dash = file.get16le();
+	state->z80.registers.hl_dash = file.get16le();
+	state->z80.registers.af_dash = file.get16be();	// Stored A' then F'.
+	state->z80.registers.iy = file.get16le();
+	state->z80.registers.ix = file.get16le();
+	state->z80.registers.iff1 = bool(file.get8());
+	state->z80.registers.iff2 = bool(file.get8());
+
+	// Ignored from the next byte:
+	//
+	//	bit 2 = 1 	=> issue 2 emulation
+	//	bit 3 = 1	=> double interrupt frequency (?)
+	//	bit 4–5		=> video synchronisation (to do with emulation hackery?)
+	//	bit 6–7		=> joystick type
+	state->z80.registers.interrupt_mode = file.get8() & 3;
+
+	// If the program counter is non-0 then this is a version 1 snapshot,
+	// which means it's definitely a 48k image.
+	if(state->z80.registers.program_counter) {
+		result->model = Target::Model::FortyEightK;
+		state->ram = read_memory(file, 48*1024, misc & 0x20);
+		return result;
+	}
+
+	// This was a version 1 or 2 snapshot, so keep going...
+	const uint16_t bonus_header_size = file.get16le();
+	if(bonus_header_size != 23 && bonus_header_size != 54 && bonus_header_size != 55) {
+		return nullptr;
+	}
+
+	state->z80.registers.program_counter = file.get16le();
+	const uint8_t model = file.get8();
+	switch(model) {
+		default: return nullptr;
+		case 0:		result->model = Target::Model::FortyEightK;		break;
+		case 3:		result->model = Target::Model::OneTwoEightK;	break;
+		case 7:
+		case 8:		result->model = Target::Model::Plus3;			break;
+		case 12:	result->model = Target::Model::Plus2;			break;
+		case 13:	result->model = Target::Model::Plus2a;			break;
+	}
+
+	state->last_7ffd = file.get8();
+
+	file.seek(1, SEEK_CUR);
+	if(file.get8() & 0x80) {
+		// The 'hardware modify' bit, which inexplicably does this:
+		switch(result->model) {
+			default: break;
+			case Target::Model::FortyEightK:	result->model = Target::Model::SixteenK;	break;
+			case Target::Model::OneTwoEightK:	result->model = Target::Model::Plus2;		break;
+			case Target::Model::Plus3:			result->model = Target::Model::Plus2a;		break;
+		}
+	}
+
+	state->last_fffd = file.get8();
+	file.read(state->ay.registers, 16);
+
+	if(bonus_header_size != 23) {
+		// More Z80, the emulator, lack of encapsulation to deal with here.
+		const uint16_t low_t_state = file.get16le();
+		const uint16_t high_t_state = file.get8();
+		int time_since_interrupt;
+		switch(result->model) {
+			case Target::Model::SixteenK:
+			case Target::Model::FortyEightK:
+				time_since_interrupt = (17471 - low_t_state) + (high_t_state * 17472);
+			break;
+
+			default:
+				time_since_interrupt = (17726 - low_t_state) + (high_t_state * 17727);
+			break;
+		}
+		// TODO: map time_since_interrupt to time_into_frame, somehow.
+
+		// Skip: Spectator flag, MGT, Multiface and other ROM flags.
+		file.seek(5, SEEK_CUR);
+
+		// Skip: highly Z80-the-emulator-specific stuff about user-defined joystick.
+		file.seek(20, SEEK_CUR);
+
+		// Skip: Disciple/Plus D stuff.
+		file.seek(3, SEEK_CUR);
+
+		if(bonus_header_size == 55) {
+			state->last_1ffd = file.get8();
+		}
+	}
+
+	// Grab RAM.
+	switch(result->model) {
+		case Target::Model::SixteenK:		state->ram.resize(16 * 1024);	break;
+		case Target::Model::FortyEightK:	state->ram.resize(48 * 1024);	break;
+		default:							state->ram.resize(128 * 1024);	break;
+	}
+
+	while(true) {
+		const uint16_t block_size = file.get16le();
+		const uint8_t page = file.get8();
+		const auto location = file.tell();
+		if(file.eof()) break;
+
+		const auto data = read_memory(file, 16384, block_size != 0xffff);
+
+		if(result->model == Target::Model::SixteenK || result->model == Target::Model::FortyEightK) {
+			switch(page) {
+				default: break;
+				case 4:	memcpy(&state->ram[0x4000], data.data(), 16384);	break;
+				case 5:	memcpy(&state->ram[0x8000], data.data(), 16384);	break;
+				case 8:	memcpy(&state->ram[0x0000], data.data(), 16384);	break;
+			}
+		} else {
+			if(page >= 3 && page <= 10) {
+				memcpy(&state->ram[(page - 3) * 0x4000], data.data(), 16384);
+			}
+		}
+
+		assert(location + block_size == file.tell());
+		file.seek(location + block_size, SEEK_SET);
+	}
+
+	return result;
+}
diff --git a/Storage/State/Z80.hpp b/Storage/State/Z80.hpp
new file mode 100644
index 000000000..ef5a9af1e
--- /dev/null
+++ b/Storage/State/Z80.hpp
@@ -0,0 +1,24 @@
+//
+//  Z80.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 25/04/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#ifndef Storage_State_Z80_hpp
+#define Storage_State_Z80_hpp
+
+#include "../../Analyser/Static/StaticAnalyser.hpp"
+
+namespace Storage {
+namespace State {
+
+struct Z80 {
+	static std::unique_ptr<Analyser::Static::Target> load(const std::string &file_name);
+};
+
+}
+}
+
+#endif /* Storage_State_Z80_hpp */