From 9cc747b3e2f2566ef28204cc0806b28335f02d2b Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 24 Apr 2021 12:10:28 -0400
Subject: [PATCH 01/27] Resolves potential source of errors: specifying
 incorrect table size.

(Having made exactly this mistake with the ZX Spectrum)
---
 Machines/AmstradCPC/Keyboard.cpp        |  2 +-
 Machines/Commodore/Vic-20/Keyboard.cpp  |  2 +-
 Machines/Electron/Keyboard.cpp          |  2 +-
 Machines/Oric/Keyboard.cpp              |  2 +-
 Machines/Sinclair/Keyboard/Keyboard.cpp |  6 +++---
 Machines/Utility/Typer.cpp              | 10 ----------
 Machines/Utility/Typer.hpp              | 12 ++++++++----
 7 files changed, 15 insertions(+), 21 deletions(-)

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/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/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];
+		}
 };
 
 /*!

From d61f478a395563e665021b1b17f0ad217e5276ea Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 24 Apr 2021 23:17:47 -0400
Subject: [PATCH 02/27] Basic sketch for state snapshots: an extra field on
 Target.

I think it doesn't make sense for states to own a target as that complicates the concept of Media. Plus they're distinct because it makes sense to have only one per Target. Let's see how this pans out.
---
 Analyser/Static/StaticAnalyser.cpp            | 43 ++++++++++++++++---
 Analyser/Static/StaticAnalyser.hpp            | 12 ++++--
 Machines/MachineTypes.hpp                     |  1 +
 Machines/StateProducer.hpp                    | 26 +++++++++++
 .../Clock Signal.xcodeproj/project.pbxproj    | 18 ++++++++
 OSBindings/Mac/Clock Signal/Info.plist        | 20 +++++++++
 Storage/State/SNA.cpp                         | 36 ++++++++++++++++
 Storage/State/SNA.hpp                         | 24 +++++++++++
 8 files changed, 170 insertions(+), 10 deletions(-)
 create mode 100644 Machines/StateProducer.hpp
 create mode 100644 Storage/State/SNA.cpp
 create mode 100644 Storage/State/SNA.hpp

diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp
index e2c2e2823..e24897264 100644
--- a/Analyser/Static/StaticAnalyser.cpp
+++ b/Analyser/Static/StaticAnalyser.cpp
@@ -57,6 +57,9 @@
 #include "../../Storage/MassStorage/Formats/DAT.hpp"
 #include "../../Storage/MassStorage/Formats/HFV.hpp"
 
+// State Snapshots
+#include "../../Storage/State/SNA.hpp"
+
 // Tapes
 #include "../../Storage/Tape/Formats/CAS.hpp"
 #include "../../Storage/Tape/Formats/CommodoreTAP.hpp"
@@ -73,15 +76,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);\
@@ -189,6 +200,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
 #undef TryInsert
 #undef InsertInstance
 
+
 	return result;
 }
 
@@ -199,14 +211,31 @@ 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)	{									\
+		auto target = Storage::State::class::load(file_name);	\
+		if(target) {											\
+			targets.push_back(std::move(target));				\
+			return targets;										\
+		}														\
+	}
+
+	Format("sna", SNA);
+
+#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..9e7372cac 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::shared_ptr<Reflection::Struct> state;
+
 	Machine machine;
 	Media media;
 	float confidence = 0.0f;
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/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/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 24c509446..60002f0fd 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -479,6 +479,8 @@
 		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 */; };
 		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 +1431,9 @@
 		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>"; };
 		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>"; };
@@ -2842,6 +2847,7 @@
 				4B8805F81DCFF6CD003085B1 /* Data */,
 				4BAB62AA1D3272D200DF5BA0 /* Disk */,
 				4B6AAEA1230E3E1D0078E864 /* MassStorage */,
+				4B8DD3832634D37E00B3C866 /* State */,
 				4B69FB3A1C4D908A00B5F0AA /* Tape */,
 			);
 			name = Storage;
@@ -3248,6 +3254,15 @@
 			path = AmstradCPC;
 			sourceTree = "<group>";
 		};
+		4B8DD3832634D37E00B3C866 /* State */ = {
+			isa = PBXGroup;
+			children = (
+				4B8DD3842634D37E00B3C866 /* SNA.cpp */,
+				4B8DD3852634D37E00B3C866 /* SNA.hpp */,
+			);
+			path = State;
+			sourceTree = "<group>";
+		};
 		4B8DF4EC254B840B00F3433C /* AppleClock */ = {
 			isa = PBXGroup;
 			children = (
@@ -4018,6 +4033,7 @@
 				4B92294222B04A3D00A1458F /* MouseMachine.hpp */,
 				4BDCC5F81FB27A5E001220C5 /* ROMMachine.hpp */,
 				4B046DC31CFE651500E9E45E /* ScanProducer.hpp */,
+				4B8DD375263481BB00B3C866 /* StateProducer.hpp */,
 				4BC57CD32434282000FBC404 /* TimedMachine.hpp */,
 				4B38F3491F2EC12000D9235D /* AmstradCPC */,
 				4BCE0048227CE8CA000CA200 /* Apple */,
@@ -5216,6 +5232,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 */,
@@ -5486,6 +5503,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..aac5bb5fd 100644
--- a/OSBindings/Mac/Clock Signal/Info.plist	
+++ b/OSBindings/Mac/Clock Signal/Info.plist	
@@ -592,6 +592,26 @@
 			<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 image</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/Storage/State/SNA.cpp b/Storage/State/SNA.cpp
new file mode 100644
index 000000000..7199a5c2f
--- /dev/null
+++ b/Storage/State/SNA.cpp
@@ -0,0 +1,36 @@
+//
+//  SNA.cpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 24/04/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#include "SNA.hpp"
+
+using namespace Storage::State;
+
+std::unique_ptr<Analyser::Static::Target> SNA::load(const std::string &file_name) {
+	// 0x1a byte header:
+	//
+	//	00	I
+	//	01	HL'
+	//	03	DE'
+	//	05	BC'
+	//	07	AF'
+	//	09	HL
+	//	0B	DE
+	//	0D	BC
+	//	0F	IY
+	//	11	IX
+	//	13	IFF2 (in bit 2)
+	//	14	R
+	//	15	AF
+	//	17	SP
+	//	19	interrupt mode
+	//	1A	border colour
+	//	1B–	48kb RAM contents
+
+	(void)file_name;
+	return nullptr;
+}
diff --git a/Storage/State/SNA.hpp b/Storage/State/SNA.hpp
new file mode 100644
index 000000000..9afa703b0
--- /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 SNA_hpp
+#define 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 /* SNA_hpp */

From e7a9ae18a1877751343798e6ea008c6fc6c2f493 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 24 Apr 2021 23:18:00 -0400
Subject: [PATCH 03/27] Introduce further default state.

---
 Processors/Z80/State/State.hpp | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/Processors/Z80/State/State.hpp b/Processors/Z80/State/State.hpp
index f5676aa60..adce4d7f6 100644
--- a/Processors/Z80/State/State.hpp
+++ b/Processors/Z80/State/State.hpp
@@ -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();

From 1c2ea0d7fe665d8e5be09230830a2a4b52b2f6b0 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 24 Apr 2021 23:19:30 -0400
Subject: [PATCH 04/27] `unique_ptr` makes more sense here.

---
 Analyser/Static/StaticAnalyser.hpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Analyser/Static/StaticAnalyser.hpp b/Analyser/Static/StaticAnalyser.hpp
index 9e7372cac..259f7cca9 100644
--- a/Analyser/Static/StaticAnalyser.hpp
+++ b/Analyser/Static/StaticAnalyser.hpp
@@ -59,7 +59,7 @@ struct Target {
 	virtual ~Target() {}
 
 	// This field is entirely optional.
-	std::shared_ptr<Reflection::Struct> state;
+	std::unique_ptr<Reflection::Struct> state;
 
 	Machine machine;
 	Media media;

From 14ae579fca5c9a9839dc9d096af5c4a52294f311 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 24 Apr 2021 23:19:41 -0400
Subject: [PATCH 05/27] Add further note to future self.

---
 Storage/State/SNA.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Storage/State/SNA.cpp b/Storage/State/SNA.cpp
index 7199a5c2f..15595cab2 100644
--- a/Storage/State/SNA.cpp
+++ b/Storage/State/SNA.cpp
@@ -30,6 +30,8 @@ std::unique_ptr<Analyser::Static::Target> SNA::load(const std::string &file_name
 	//	19	interrupt mode
 	//	1A	border colour
 	//	1B–	48kb RAM contents
+	//
+	// (perform a POP to get the PC)
 
 	(void)file_name;
 	return nullptr;

From 5b419ca5bf9c8089d861dd641617d4cdcd0fe665 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 24 Apr 2021 23:25:08 -0400
Subject: [PATCH 06/27] Add State folder to Scons and Qt projects.

---
 OSBindings/Qt/ClockSignal.pro | 2 ++
 OSBindings/SDL/SConstruct     | 1 +
 2 files changed, 3 insertions(+)

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')

From 485c2a866c67cc8309a0da90999912918492cd64 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 24 Apr 2021 23:38:00 -0400
Subject: [PATCH 07/27] Without yet a struct for Spectrum states, at least
 checks general wiring.

---
 Storage/State/SNA.cpp | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/Storage/State/SNA.cpp b/Storage/State/SNA.cpp
index 15595cab2..99af049d1 100644
--- a/Storage/State/SNA.cpp
+++ b/Storage/State/SNA.cpp
@@ -7,10 +7,17 @@
 //
 
 #include "SNA.hpp"
+#include "../../Analyser/Static/ZXSpectrum/Target.hpp"
 
 using namespace Storage::State;
 
 std::unique_ptr<Analyser::Static::Target> SNA::load(const std::string &file_name) {
+	using Target = Analyser::Static::ZXSpectrum::Target;
+	auto result = std::make_unique<Target>();
+
+	// SNAs are always for 48kb machines.
+	result->model = Target::Model::FortyEightK;
+
 	// 0x1a byte header:
 	//
 	//	00	I
@@ -34,5 +41,6 @@ std::unique_ptr<Analyser::Static::Target> SNA::load(const std::string &file_name
 	// (perform a POP to get the PC)
 
 	(void)file_name;
-	return nullptr;
+
+	return result;
 }

From cc78bfb229fd3213c335ccb4c0880d4245ec146c Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 13:00:43 -0400
Subject: [PATCH 08/27] Forwards most of the Z80 state.

---
 Analyser/Static/StaticAnalyser.cpp            | 16 +++--
 Machines/Sinclair/ZXSpectrum/State.hpp        | 32 +++++++++
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp   | 15 +++--
 .../Clock Signal.xcodeproj/project.pbxproj    |  2 +
 Storage/State/SNA.cpp                         | 65 +++++++++++++------
 5 files changed, 97 insertions(+), 33 deletions(-)
 create mode 100644 Machines/Sinclair/ZXSpectrum/State.hpp

diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp
index e24897264..8cc0954de 100644
--- a/Analyser/Static/StaticAnalyser.cpp
+++ b/Analyser/Static/StaticAnalyser.cpp
@@ -214,13 +214,15 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) {
 	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)	{									\
-		auto target = Storage::State::class::load(file_name);	\
-		if(target) {											\
-			targets.push_back(std::move(target));				\
-			return targets;										\
-		}														\
+#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);
diff --git a/Machines/Sinclair/ZXSpectrum/State.hpp b/Machines/Sinclair/ZXSpectrum/State.hpp
new file mode 100644
index 000000000..dc6a9bff2
--- /dev/null
+++ b/Machines/Sinclair/ZXSpectrum/State.hpp
@@ -0,0 +1,32 @@
+//
+//  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"
+
+namespace Sinclair {
+namespace ZXSpectrum {
+
+
+struct State: public Reflection::StructImpl<State> {
+	CPU::Z80::State z80;
+
+	State() {
+		if(needs_declare()) {
+			DeclareField(z80);
+		}
+	}
+};
+
+}
+}
+
+#endif /* State_h */
diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index 06812cdce..bf3142cfc 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,11 @@ 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) {
+				LOG("TODO: state");
+			}
 		}
 
 		~ConcreteMachine() {
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 60002f0fd..05d2ef9b4 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -1434,6 +1434,7 @@
 		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>"; };
 		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>"; };
@@ -2215,6 +2216,7 @@
 				4B0F1BFA260300D900B85C66 /* ZXSpectrum.cpp */,
 				4B0F1BFB260300D900B85C66 /* ZXSpectrum.hpp */,
 				4B0F1C092603BA5F00B85C66 /* Video.hpp */,
+				4B8DD3912635A72F00B3C866 /* State.hpp */,
 			);
 			path = ZXSpectrum;
 			sourceTree = "<group>";
diff --git a/Storage/State/SNA.cpp b/Storage/State/SNA.cpp
index 99af049d1..dc2ad8e2b 100644
--- a/Storage/State/SNA.cpp
+++ b/Storage/State/SNA.cpp
@@ -7,40 +7,65 @@
 //
 
 #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) {
-	using Target = Analyser::Static::ZXSpectrum::Target;
-	auto result = std::make_unique<Target>();
+	// 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;
 
-	// 0x1a byte header:
-	//
+	// 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
-	//	01	HL'
-	//	03	DE'
-	//	05	BC'
-	//	07	AF'
-	//	09	HL
-	//	0B	DE
-	//	0D	BC
-	//	0F	IY
-	//	11	IX
+	const uint8_t i = file.get8();
+
+	//	01	HL';	03	DE';	05	BC';	07	AF'
+	state->z80.registers.hlDash = file.get16le();
+	state->z80.registers.deDash = file.get16le();
+	state->z80.registers.bcDash = file.get16le();
+	state->z80.registers.afDash = 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
-	//	15	AF
-	//	17	SP
-	//	19	interrupt mode
+	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();
+
+	// TODO: border colour, RAM contents, then pop the program counter.
 	//	1A	border colour
 	//	1B–	48kb RAM contents
-	//
-	// (perform a POP to get the PC)
-
-	(void)file_name;
 
 	return result;
 }

From 7aeb17ac923c6b8f5c99e32637d8d582efafb629 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 13:01:23 -0400
Subject: [PATCH 09/27] Corrects HeaderDoc/etc directive.

---
 Storage/FileHolder.hpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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);
 

From 0ebd900e4081b17ba18467537229241f886a01ec Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 13:03:24 -0400
Subject: [PATCH 10/27] Baby steps: apply Z80 state.

As far as it currently is. Since SNA is leaving the PC at the default of 0x0000, this currently has no visible effect.
---
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index bf3142cfc..23996270e 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -125,7 +125,10 @@ template<Model model> class ConcreteMachine:
 
 			// Install state if supplied.
 			if(target.state) {
-				LOG("TODO: state");
+				const auto state = static_cast<State *>(target.state.get());
+				state->z80.apply(z80_);
+
+				LOG("TODO: apply rest of state");
 			}
 		}
 

From a5098a60ec950e1379859959b3931525eef1a211 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 13:18:26 -0400
Subject: [PATCH 11/27] Attempts to get in-SNA software to start.

---
 Machines/Sinclair/ZXSpectrum/State.hpp      |  2 ++
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp |  1 +
 Storage/State/SNA.cpp                       | 12 +++++++++++-
 3 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/Machines/Sinclair/ZXSpectrum/State.hpp b/Machines/Sinclair/ZXSpectrum/State.hpp
index dc6a9bff2..60b6db78f 100644
--- a/Machines/Sinclair/ZXSpectrum/State.hpp
+++ b/Machines/Sinclair/ZXSpectrum/State.hpp
@@ -18,10 +18,12 @@ namespace ZXSpectrum {
 
 struct State: public Reflection::StructImpl<State> {
 	CPU::Z80::State z80;
+	std::vector<uint8_t> ram;
 
 	State() {
 		if(needs_declare()) {
 			DeclareField(z80);
+			DeclareField(ram);
 		}
 	}
 };
diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index 23996270e..469e74fd8 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -127,6 +127,7 @@ template<Model model> class ConcreteMachine:
 			if(target.state) {
 				const auto state = static_cast<State *>(target.state.get());
 				state->z80.apply(z80_);
+				memcpy(ram_.data(), state->ram.data(), std::min(ram_.size(), state->ram.size()));
 
 				LOG("TODO: apply rest of state");
 			}
diff --git a/Storage/State/SNA.cpp b/Storage/State/SNA.cpp
index dc2ad8e2b..0f5c6a978 100644
--- a/Storage/State/SNA.cpp
+++ b/Storage/State/SNA.cpp
@@ -63,9 +63,19 @@ std::unique_ptr<Analyser::Static::Target> SNA::load(const std::string &file_name
 	state->z80.registers.stack_pointer = file.get16le();
 	state->z80.registers.interrupt_mode = file.get8();
 
-	// TODO: border colour, RAM contents, then pop the program counter.
 	//	1A	border colour
+	const uint8_t border_colour = file.get8();
+	(void)border_colour;	// TODO.
+
 	//	1B–	48kb RAM contents
+	state->ram = file.read(48*1024);
+
+	// Establish program counter.
+	state->z80.registers.program_counter = state->ram[state->z80.registers.stack_pointer];
+	state->ram[state->z80.registers.stack_pointer] = 0;
+	state->z80.registers.program_counter |= state->ram[state->z80.registers.stack_pointer+1] << 8;
+	state->ram[state->z80.registers.stack_pointer+1] = 0;
+	state->z80.registers.stack_pointer += 2;
 
 	return result;
 }

From 9b65d56ed0effa9aae175253366d29a277e1a048 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 13:26:53 -0400
Subject: [PATCH 12/27] Resolves potential flaw in POPping here.

---
 Storage/State/SNA.cpp | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/Storage/State/SNA.cpp b/Storage/State/SNA.cpp
index 0f5c6a978..2dce6657f 100644
--- a/Storage/State/SNA.cpp
+++ b/Storage/State/SNA.cpp
@@ -70,12 +70,11 @@ std::unique_ptr<Analyser::Static::Target> SNA::load(const std::string &file_name
 	//	1B–	48kb RAM contents
 	state->ram = file.read(48*1024);
 
-	// Establish program counter.
-	state->z80.registers.program_counter = state->ram[state->z80.registers.stack_pointer];
-	state->ram[state->z80.registers.stack_pointer] = 0;
-	state->z80.registers.program_counter |= state->ram[state->z80.registers.stack_pointer+1] << 8;
-	state->ram[state->z80.registers.stack_pointer+1] = 0;
-	state->z80.registers.stack_pointer += 2;
+	// 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;
 }

From 2bbf8bc9faea5b121f6936e2b731890fa9f7386c Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 13:27:11 -0400
Subject: [PATCH 13/27] Ensures 16/48kb snapshots are properly copied into
 place.

---
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index 469e74fd8..7e026dfdb 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -127,7 +127,17 @@ template<Model model> class ConcreteMachine:
 			if(target.state) {
 				const auto state = static_cast<State *>(target.state.get());
 				state->z80.apply(z80_);
-				memcpy(ram_.data(), state->ram.data(), std::min(ram_.size(), state->ram.size()));
+
+				// 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) {
+					for(size_t c = 0; c < 48*1024 && c < state->ram.size(); c++) {
+						const auto address = c + 0x4000;
+						write_pointers_[address >> 14][address] = state->ram[c];
+					}
+				} else {
+					memcpy(ram_.data(), state->ram.data(), std::min(ram_.size(), state->ram.size()));
+				}
 
 				LOG("TODO: apply rest of state");
 			}

From fd271d920bd1acfa799e003c2e23c88ec4326cfe Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 14:00:12 -0400
Subject: [PATCH 14/27] Adds capture and forwarding of border colour.

---
 Machines/Sinclair/ZXSpectrum/State.hpp      |  3 ++
 Machines/Sinclair/ZXSpectrum/Video.hpp      | 36 ++++++++++++++-------
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp |  6 ++--
 Storage/State/SNA.cpp                       |  3 +-
 4 files changed, 32 insertions(+), 16 deletions(-)

diff --git a/Machines/Sinclair/ZXSpectrum/State.hpp b/Machines/Sinclair/ZXSpectrum/State.hpp
index 60b6db78f..1333c8d62 100644
--- a/Machines/Sinclair/ZXSpectrum/State.hpp
+++ b/Machines/Sinclair/ZXSpectrum/State.hpp
@@ -11,6 +11,7 @@
 
 #include "../../../Reflection/Struct.hpp"
 #include "../../../Processors/Z80/State/State.hpp"
+#include "Video.hpp"
 
 namespace Sinclair {
 namespace ZXSpectrum {
@@ -18,11 +19,13 @@ namespace ZXSpectrum {
 
 struct State: public Reflection::StructImpl<State> {
 	CPU::Z80::State z80;
+	Video::State video;
 	std::vector<uint8_t> ram;
 
 	State() {
 		if(needs_declare()) {
 			DeclareField(z80);
+			DeclareField(video);
 			DeclareField(ram);
 		}
 	}
diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp
index c48392a61..8363d0ad3 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
@@ -328,7 +331,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 +345,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 +357,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;
 			}
 		}
@@ -408,6 +411,17 @@ template <VideoTiming timing> class Video {
 #undef RGB
 };
 
+struct State: public Reflection::StructImpl<State> {
+	uint8_t border_colour;
+
+	State() {
+		if(needs_declare()) {
+			DeclareField(border_colour);
+		}
+	}
+};
+
+}
 }
 }
 
diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index 7e026dfdb..83a07ab98 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -775,10 +775,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/Storage/State/SNA.cpp b/Storage/State/SNA.cpp
index 2dce6657f..7d29faf62 100644
--- a/Storage/State/SNA.cpp
+++ b/Storage/State/SNA.cpp
@@ -64,8 +64,7 @@ std::unique_ptr<Analyser::Static::Target> SNA::load(const std::string &file_name
 	state->z80.registers.interrupt_mode = file.get8();
 
 	//	1A	border colour
-	const uint8_t border_colour = file.get8();
-	(void)border_colour;	// TODO.
+	state->video.border_colour = file.get8();
 
 	//	1B–	48kb RAM contents
 	state->ram = file.read(48*1024);

From d80f03e36954e3303efa498105969e7c486bf6a8 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 14:11:36 -0400
Subject: [PATCH 15/27] Corrects longstanding deviation from naming convention.

---
 Processors/Z80/Implementation/Z80Base.cpp     | 48 +++++++++----------
 .../Z80/Implementation/Z80Implementation.hpp  | 14 +++---
 Processors/Z80/Implementation/Z80Storage.hpp  |  2 +-
 Processors/Z80/State/State.cpp                | 24 +++++-----
 Processors/Z80/State/State.hpp                |  2 +-
 Storage/State/SNA.cpp                         |  8 ++--
 6 files changed, 49 insertions(+), 49 deletions(-)

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 adce4d7f6..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;
diff --git a/Storage/State/SNA.cpp b/Storage/State/SNA.cpp
index 7d29faf62..5daa42444 100644
--- a/Storage/State/SNA.cpp
+++ b/Storage/State/SNA.cpp
@@ -37,10 +37,10 @@ std::unique_ptr<Analyser::Static::Target> SNA::load(const std::string &file_name
 	const uint8_t i = file.get8();
 
 	//	01	HL';	03	DE';	05	BC';	07	AF'
-	state->z80.registers.hlDash = file.get16le();
-	state->z80.registers.deDash = file.get16le();
-	state->z80.registers.bcDash = file.get16le();
-	state->z80.registers.afDash = file.get16le();
+	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();

From 0ef2806970e10b327fb98b398a9d93f99605d2f8 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 14:16:35 -0400
Subject: [PATCH 16/27] Adds just enough to ensure that border state gets
 through.

---
 Machines/Sinclair/ZXSpectrum/Video.hpp      | 20 +++++++++++++++++++-
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp |  3 +--
 2 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp
index 8363d0ad3..a6bc38a14 100644
--- a/Machines/Sinclair/ZXSpectrum/Video.hpp
+++ b/Machines/Sinclair/ZXSpectrum/Video.hpp
@@ -401,6 +401,8 @@ template <Timing 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),
@@ -412,13 +414,29 @@ template <Timing timing> class Video {
 };
 
 struct State: public Reflection::StructImpl<State> {
-	uint8_t border_colour;
+	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> 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 83a07ab98..7a4bdd9c7 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -127,6 +127,7 @@ template<Model model> class ConcreteMachine:
 			if(target.state) {
 				const auto state = static_cast<State *>(target.state.get());
 				state->z80.apply(z80_);
+				state->video.apply(*video_.last_valid());
 
 				// 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.
@@ -138,8 +139,6 @@ template<Model model> class ConcreteMachine:
 				} else {
 					memcpy(ram_.data(), state->ram.data(), std::min(ram_.size(), state->ram.size()));
 				}
-
-				LOG("TODO: apply rest of state");
 			}
 		}
 

From 25100642186d2746cc5868b199ee1f1927b2d112 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 14:20:40 -0400
Subject: [PATCH 17/27] Completes state object.

Subject to not yet dealing with last_fetches_ and last_contended_access_ correctly. Thought required.
---
 Machines/Sinclair/ZXSpectrum/Video.hpp | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp
index a6bc38a14..4304c5542 100644
--- a/Machines/Sinclair/ZXSpectrum/Video.hpp
+++ b/Machines/Sinclair/ZXSpectrum/Video.hpp
@@ -366,6 +366,7 @@ template <Timing timing> class Video {
 			Sets the current border colour.
 		*/
 		void set_border_colour(uint8_t colour) {
+			border_byte_ = colour;
 			border_colour_ = palette[colour];
 		}
 
@@ -389,6 +390,7 @@ template <Timing timing> class Video {
 		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;
@@ -430,6 +432,14 @@ struct State: public Reflection::StructImpl<State> {
 		}
 	}
 
+	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;

From 205518ba75506b428310cd70cd50e7e80802b43c Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 16:51:07 -0400
Subject: [PATCH 18/27] Switch to more efficient copy.

---
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index 7a4bdd9c7..bde983d91 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -132,9 +132,9 @@ template<Model model> class ConcreteMachine:
 				// 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) {
-					for(size_t c = 0; c < 48*1024 && c < state->ram.size(); c++) {
-						const auto address = c + 0x4000;
-						write_pointers_[address >> 14][address] = state->ram[c];
+					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()));

From 03577de675e3cd05d4c22face688408cfc28e6b4 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 16:54:34 -0400
Subject: [PATCH 19/27] Adds an empty vessel for .z80 support.

---
 Analyser/Static/StaticAnalyser.cpp            |  3 ++-
 .../Clock Signal.xcodeproj/project.pbxproj    |  8 +++++++
 OSBindings/Mac/Clock Signal/Info.plist        | 22 ++++++++++++++++-
 Storage/State/Z80.cpp                         | 15 ++++++++++++
 Storage/State/Z80.hpp                         | 24 +++++++++++++++++++
 5 files changed, 70 insertions(+), 2 deletions(-)
 create mode 100644 Storage/State/Z80.cpp
 create mode 100644 Storage/State/Z80.hpp

diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp
index 8cc0954de..97160fe37 100644
--- a/Analyser/Static/StaticAnalyser.cpp
+++ b/Analyser/Static/StaticAnalyser.cpp
@@ -59,6 +59,7 @@
 
 // State Snapshots
 #include "../../Storage/State/SNA.hpp"
+#include "../../Storage/State/Z80.hpp"
 
 // Tapes
 #include "../../Storage/Tape/Formats/CAS.hpp"
@@ -200,7 +201,6 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
 #undef TryInsert
 #undef InsertInstance
 
-
 	return result;
 }
 
@@ -226,6 +226,7 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) {
 	}
 
 	Format("sna", SNA);
+	Format("z80", Z80);
 
 #undef TryInsert
 
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 05d2ef9b4..7d908b13d 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -481,6 +481,8 @@
 		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 */; };
@@ -1435,6 +1437,8 @@
 		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>"; };
@@ -3261,6 +3265,8 @@
 			children = (
 				4B8DD3842634D37E00B3C866 /* SNA.cpp */,
 				4B8DD3852634D37E00B3C866 /* SNA.hpp */,
+				4B8DD39526360DDF00B3C866 /* Z80.cpp */,
+				4B8DD39626360DDF00B3C866 /* Z80.hpp */,
 			);
 			path = State;
 			sourceTree = "<group>";
@@ -5268,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 */,
@@ -5364,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 */,
diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist
index aac5bb5fd..b37475e53 100644
--- a/OSBindings/Mac/Clock Signal/Info.plist	
+++ b/OSBindings/Mac/Clock Signal/Info.plist	
@@ -598,7 +598,27 @@
 				<string>sna</string>
 			</array>
 			<key>CFBundleTypeName</key>
-			<string>ZX Spectrum SNA image</string>
+			<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>
diff --git a/Storage/State/Z80.cpp b/Storage/State/Z80.cpp
new file mode 100644
index 000000000..16aeaf81a
--- /dev/null
+++ b/Storage/State/Z80.cpp
@@ -0,0 +1,15 @@
+//
+//  Z80.cpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 25/04/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#include "Z80.hpp"
+
+using namespace Storage::State;
+
+std::unique_ptr<Analyser::Static::Target> Z80::load(const std::string &file_name) {
+	return nullptr;
+}
diff --git a/Storage/State/Z80.hpp b/Storage/State/Z80.hpp
new file mode 100644
index 000000000..062089fca
--- /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 Z80_hpp
+#define 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 /* Z80_hpp */

From e6252fe0edaf855e55a401d3c7b50387262b9755 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 17:34:43 -0400
Subject: [PATCH 20/27] Sneaks up towards loading RAM.

---
 Storage/State/SNA.hpp |   6 +-
 Storage/State/Z80.cpp | 173 +++++++++++++++++++++++++++++++++++++++++-
 Storage/State/Z80.hpp |   6 +-
 3 files changed, 177 insertions(+), 8 deletions(-)

diff --git a/Storage/State/SNA.hpp b/Storage/State/SNA.hpp
index 9afa703b0..2f2811d93 100644
--- a/Storage/State/SNA.hpp
+++ b/Storage/State/SNA.hpp
@@ -6,8 +6,8 @@
 //  Copyright © 2021 Thomas Harte. All rights reserved.
 //
 
-#ifndef SNA_hpp
-#define SNA_hpp
+#ifndef Storage_State_SNA_hpp
+#define Storage_State_SNA_hpp
 
 #include "../../Analyser/Static/StaticAnalyser.hpp"
 
@@ -21,4 +21,4 @@ struct SNA {
 }
 }
 
-#endif /* SNA_hpp */
+#endif /* Storage_State_SNA_hpp */
diff --git a/Storage/State/Z80.cpp b/Storage/State/Z80.cpp
index 16aeaf81a..c7323aba1 100644
--- a/Storage/State/Z80.cpp
+++ b/Storage/State/Z80.cpp
@@ -8,8 +8,177 @@
 
 #include "Z80.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> Z80::load(const std::string &file_name) {
-	return nullptr;
+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;
+
+	uint8_t incoming[2] = { file.get8(), file.get8()};
+	while(true) {
+		if(incoming[0] == 0xed && incoming[1] == 0xed) {
+			const uint8_t count = file.get8();
+			const uint8_t value = file.get8();
+
+			memset(&result[cursor], value, count);
+			cursor += count;
+			if(cursor == size) break;
+			incoming[0] = file.get8();
+			incoming[1] = file.get8();
+		} else {
+			result[cursor] = incoming[0];
+			++cursor;
+			if(cursor == size) break;
+			incoming[0] = incoming[1];
+			incoming[1] = file.get8();
+		}
+	}
+
+	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();
+	switch(file.get8()) {
+		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;
+	}
+
+	const uint8_t last7ffd = file.get8();	(void)last7ffd;	// TODO
+
+	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;
+		}
+	}
+
+	const uint8_t lastfffd = file.get8();	(void)lastfffd;	// TODO
+	file.seek(16, SEEK_CUR);	// Sound chip registers: TODO.
+
+	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) {
+			const uint8_t last1ffd = file.get8();	(void)last1ffd;	// TODO
+		}
+	}
+
+	// Grab RAM.
+	state->ram.resize(128 * 1024);
+	while(true) {
+		const uint16_t block_size = file.get16le();
+		const uint8_t page = file.get8();
+		const auto location = file.tell();
+		if(file.eof()) break;
+
+		switch(page) {
+			default: break;
+		}
+
+		file.seek(location + block_size, SEEK_SET);
+	}
+
+	return result;
 }
diff --git a/Storage/State/Z80.hpp b/Storage/State/Z80.hpp
index 062089fca..ef5a9af1e 100644
--- a/Storage/State/Z80.hpp
+++ b/Storage/State/Z80.hpp
@@ -6,8 +6,8 @@
 //  Copyright © 2021 Thomas Harte. All rights reserved.
 //
 
-#ifndef Z80_hpp
-#define Z80_hpp
+#ifndef Storage_State_Z80_hpp
+#define Storage_State_Z80_hpp
 
 #include "../../Analyser/Static/StaticAnalyser.hpp"
 
@@ -21,4 +21,4 @@ struct Z80 {
 }
 }
 
-#endif /* Z80_hpp */
+#endif /* Storage_State_Z80_hpp */

From cc41ccc5f1f50c7908b0f5a6cf18324602f26944 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 17:55:52 -0400
Subject: [PATCH 21/27] Adds RAM deserialisation.

---
 Storage/State/Z80.cpp | 24 ++++++++++++++++++++----
 1 file changed, 20 insertions(+), 4 deletions(-)

diff --git a/Storage/State/Z80.cpp b/Storage/State/Z80.cpp
index c7323aba1..7a8ca8a21 100644
--- a/Storage/State/Z80.cpp
+++ b/Storage/State/Z80.cpp
@@ -25,7 +25,7 @@ std::vector<uint8_t> read_memory(Storage::FileHolder &file, size_t size, bool is
 	std::vector<uint8_t> result(size);
 	size_t cursor = 0;
 
-	uint8_t incoming[2] = { file.get8(), file.get8()};
+	uint8_t incoming[2] = { file.get8(), file.get8() };
 	while(true) {
 		if(incoming[0] == 0xed && incoming[1] == 0xed) {
 			const uint8_t count = file.get8();
@@ -166,15 +166,31 @@ std::unique_ptr<Analyser::Static::Target> Z80::load(const std::string &file_name
 	}
 
 	// Grab RAM.
-	state->ram.resize(128 * 1024);
+	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;
 
-		switch(page) {
-			default: 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);
+			}
 		}
 
 		file.seek(location + block_size, SEEK_SET);

From 8d86aa69bc9270338b4fa0e3d1e508e3d6df40a9 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 18:02:31 -0400
Subject: [PATCH 22/27] Adds an assert to check handling of compressed data.

---
 Storage/State/Z80.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Storage/State/Z80.cpp b/Storage/State/Z80.cpp
index 7a8ca8a21..d59b8a7d4 100644
--- a/Storage/State/Z80.cpp
+++ b/Storage/State/Z80.cpp
@@ -193,6 +193,7 @@ std::unique_ptr<Analyser::Static::Target> Z80::load(const std::string &file_name
 			}
 		}
 
+		assert(location + block_size == file.tell());
 		file.seek(location + block_size, SEEK_SET);
 	}
 

From c34cb310a806ff52385926dcdeae132e0cbcf61f Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 18:07:36 -0400
Subject: [PATCH 23/27] Switches to more straightforward handler for .z80-style
 compression.

---
 Storage/State/Z80.cpp | 34 +++++++++++++++++++---------------
 1 file changed, 19 insertions(+), 15 deletions(-)

diff --git a/Storage/State/Z80.cpp b/Storage/State/Z80.cpp
index d59b8a7d4..d676b6248 100644
--- a/Storage/State/Z80.cpp
+++ b/Storage/State/Z80.cpp
@@ -25,24 +25,28 @@ std::vector<uint8_t> read_memory(Storage::FileHolder &file, size_t size, bool is
 	std::vector<uint8_t> result(size);
 	size_t cursor = 0;
 
-	uint8_t incoming[2] = { file.get8(), file.get8() };
-	while(true) {
-		if(incoming[0] == 0xed && incoming[1] == 0xed) {
-			const uint8_t count = file.get8();
-			const uint8_t value = file.get8();
+	while(cursor != size) {
+		const uint8_t next = file.get8();
 
-			memset(&result[cursor], value, count);
-			cursor += count;
-			if(cursor == size) break;
-			incoming[0] = file.get8();
-			incoming[1] = file.get8();
-		} else {
-			result[cursor] = incoming[0];
+		if(next != 0xed) {
+			result[cursor] = next;
 			++cursor;
-			if(cursor == size) break;
-			incoming[0] = incoming[1];
-			incoming[1] = file.get8();
+			continue;
 		}
+
+		const uint8_t after = file.get8();
+		if(after != 0xed) {
+			result[cursor] = next;
+			++cursor;
+			file.seek(-1, SEEK_CUR);
+			continue;
+		}
+
+		const uint8_t count = file.get8();
+		const uint8_t value = file.get8();
+
+		memset(&result[cursor], value, count);
+		cursor += count;
 	}
 
 	return result;

From 5e08d7db3975d6dca5fadd255dd7dede9d10a09c Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 20:46:49 -0400
Subject: [PATCH 24/27] Carries through paging state; avoids file rereads.

---
 Machines/Sinclair/ZXSpectrum/State.hpp      | 14 ++++++++++++++
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 14 ++++++++++----
 Storage/State/Z80.cpp                       | 20 +++++++++++++-------
 3 files changed, 37 insertions(+), 11 deletions(-)

diff --git a/Machines/Sinclair/ZXSpectrum/State.hpp b/Machines/Sinclair/ZXSpectrum/State.hpp
index 1333c8d62..ae9632ecd 100644
--- a/Machines/Sinclair/ZXSpectrum/State.hpp
+++ b/Machines/Sinclair/ZXSpectrum/State.hpp
@@ -20,13 +20,27 @@ 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;
+
+	// 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);
 		}
 	}
 };
diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index bde983d91..2632f5295 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -138,6 +138,12 @@ template<Model model> class ConcreteMachine:
 					}
 				} 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);
 				}
 			}
 		}
@@ -400,10 +406,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).
@@ -729,6 +731,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) {
diff --git a/Storage/State/Z80.cpp b/Storage/State/Z80.cpp
index d676b6248..9cf63eeee 100644
--- a/Storage/State/Z80.cpp
+++ b/Storage/State/Z80.cpp
@@ -28,20 +28,25 @@ std::vector<uint8_t> read_memory(Storage::FileHolder &file, size_t size, bool is
 	while(cursor != size) {
 		const uint8_t next = file.get8();
 
-		if(next != 0xed) {
+		// 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;
-			++cursor;
-			file.seek(-1, SEEK_CUR);
+			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();
 
@@ -112,7 +117,8 @@ std::unique_ptr<Analyser::Static::Target> Z80::load(const std::string &file_name
 	}
 
 	state->z80.registers.program_counter = file.get16le();
-	switch(file.get8()) {
+	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;
@@ -122,7 +128,7 @@ std::unique_ptr<Analyser::Static::Target> Z80::load(const std::string &file_name
 		case 13:	result->model = Target::Model::Plus2a;			break;
 	}
 
-	const uint8_t last7ffd = file.get8();	(void)last7ffd;	// TODO
+	state->last_7ffd = file.get8();
 
 	file.seek(1, SEEK_CUR);
 	if(file.get8() & 0x80) {
@@ -135,7 +141,7 @@ std::unique_ptr<Analyser::Static::Target> Z80::load(const std::string &file_name
 		}
 	}
 
-	const uint8_t lastfffd = file.get8();	(void)lastfffd;	// TODO
+	state->last_fffd = file.get8();
 	file.seek(16, SEEK_CUR);	// Sound chip registers: TODO.
 
 	if(bonus_header_size != 23) {
@@ -165,7 +171,7 @@ std::unique_ptr<Analyser::Static::Target> Z80::load(const std::string &file_name
 		file.seek(3, SEEK_CUR);
 
 		if(bonus_header_size == 55) {
-			const uint8_t last1ffd = file.get8();	(void)last1ffd;	// TODO
+			state->last_1ffd = file.get8();
 		}
 	}
 

From d403036d865cc8db55e6f278e503a7d91a5f8b27 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 20:56:57 -0400
Subject: [PATCH 25/27] Reduce bounce at Spectrum startup.

---
 Machines/Sinclair/ZXSpectrum/Video.hpp | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp
index 4304c5542..a82ea61dd 100644
--- a/Machines/Sinclair/ZXSpectrum/Video.hpp
+++ b/Machines/Sinclair/ZXSpectrum/Video.hpp
@@ -270,6 +270,11 @@ template <Timing 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) {

From 700c5059744e78bbb0ce2f1b3a48502884f393e5 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 25 Apr 2021 21:16:22 -0400
Subject: [PATCH 26/27] Ensures the ZX Spectrum properly reports its display
 type.

---
 Machines/Sinclair/ZXSpectrum/Video.hpp      | 5 +++++
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 5 +++++
 2 files changed, 10 insertions(+)

diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp
index a82ea61dd..4bb96d883 100644
--- a/Machines/Sinclair/ZXSpectrum/Video.hpp
+++ b/Machines/Sinclair/ZXSpectrum/Video.hpp
@@ -390,6 +390,11 @@ template <Timing 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_;
diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index 2632f5295..1a2f4ed0f 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -224,6 +224,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) {
@@ -643,6 +647,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;
 		}
 

From 3348167c4640563951d3bde139d212cf63f80e6a Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Mon, 26 Apr 2021 17:39:11 -0400
Subject: [PATCH 27/27] Ensures AY registers are conveyed.

---
 Components/AY38910/AY38910.hpp              | 24 +++++++++++++++++++++
 Machines/Sinclair/ZXSpectrum/State.hpp      |  4 ++++
 Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp |  1 +
 Storage/State/Z80.cpp                       |  2 +-
 4 files changed, 30 insertions(+), 1 deletion(-)

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/Sinclair/ZXSpectrum/State.hpp b/Machines/Sinclair/ZXSpectrum/State.hpp
index ae9632ecd..69d23c5f5 100644
--- a/Machines/Sinclair/ZXSpectrum/State.hpp
+++ b/Machines/Sinclair/ZXSpectrum/State.hpp
@@ -11,7 +11,9 @@
 
 #include "../../../Reflection/Struct.hpp"
 #include "../../../Processors/Z80/State/State.hpp"
+
 #include "Video.hpp"
+#include "../../../Components/AY38910/AY38910.hpp"
 
 namespace Sinclair {
 namespace ZXSpectrum {
@@ -29,6 +31,7 @@ struct State: public Reflection::StructImpl<State> {
 	// 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;
@@ -41,6 +44,7 @@ struct State: public Reflection::StructImpl<State> {
 			DeclareField(last_7ffd);
 			DeclareField(last_fffd);
 			DeclareField(last_1ffd);
+			DeclareField(ay);
 		}
 	}
 };
diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
index 1a2f4ed0f..e3c242d81 100644
--- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
+++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
@@ -128,6 +128,7 @@ template<Model model> class ConcreteMachine:
 				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.
diff --git a/Storage/State/Z80.cpp b/Storage/State/Z80.cpp
index 9cf63eeee..4f5c9d390 100644
--- a/Storage/State/Z80.cpp
+++ b/Storage/State/Z80.cpp
@@ -142,7 +142,7 @@ std::unique_ptr<Analyser::Static::Target> Z80::load(const std::string &file_name
 	}
 
 	state->last_fffd = file.get8();
-	file.seek(16, SEEK_CUR);	// Sound chip registers: TODO.
+	file.read(state->ay.registers, 16);
 
 	if(bonus_header_size != 23) {
 		// More Z80, the emulator, lack of encapsulation to deal with here.