From 3f57020b009dd6a5c5559fef9bd614e838d15cd3 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Jan 2020 14:48:00 -0500
Subject: [PATCH 1/8] Resolves thread safety oversights in SDK kiosk mode.

---
 .../Clock Signal.xcodeproj/project.pbxproj    |   6 +
 OSBindings/SDL/main.cpp                       | 183 ++++++++++--------
 2 files changed, 106 insertions(+), 83 deletions(-)

diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 6fba9e0e8..feced1631 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -158,6 +158,8 @@
 		4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; };
 		4B2B3A4B1F9B8FA70062DABF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; };
 		4B2B3A4C1F9B8FA70062DABF /* MemoryFuzzer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A481F9B8FA70062DABF /* MemoryFuzzer.cpp */; };
+		4B2BF19123DCC6A200C3AD60 /* BD500.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA03523CEB86000B98D9E /* BD500.cpp */; };
+		4B2BF19223DCC6A800C3AD60 /* STX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA03323C58B1E00B98D9E /* STX.cpp */; };
 		4B2BFC5F1D613E0200BA3AA9 /* TapePRG.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */; };
 		4B2BFDB21DAEF5FF001A68B8 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFDB01DAEF5FF001A68B8 /* Video.cpp */; };
 		4B2C45421E3C3896002A2389 /* cartridge.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B2C45411E3C3896002A2389 /* cartridge.png */; };
@@ -4252,6 +4254,7 @@
 				4B055AD31FAE9B0B0060FFFF /* Microdisc.cpp in Sources */,
 				4B055AB41FAE860F0060FFFF /* OricTAP.cpp in Sources */,
 				4B055AB71FAE860F0060FFFF /* TZX.cpp in Sources */,
+				4B2BF19123DCC6A200C3AD60 /* BD500.cpp in Sources */,
 				4B055ADA1FAE9B460060FFFF /* 1770.cpp in Sources */,
 				4B055ADC1FAE9B460060FFFF /* AY38910.cpp in Sources */,
 				4B055AD71FAE9B180060FFFF /* Keyboard.cpp in Sources */,
@@ -4303,6 +4306,7 @@
 				4BEBFB4E2002C4BF000708CC /* MSXDSK.cpp in Sources */,
 				4B055ADD1FAE9B460060FFFF /* i8272.cpp in Sources */,
 				4B055A9C1FAE85DA0060FFFF /* CPCDSK.cpp in Sources */,
+				4B2BF19223DCC6A800C3AD60 /* STX.cpp in Sources */,
 				4B0ACC2723775819008902D0 /* AtariST.cpp in Sources */,
 				4B8318B922D3E56D006DB630 /* MemoryPacker.cpp in Sources */,
 				4B055ABA1FAE86170060FFFF /* Commodore.cpp in Sources */,
@@ -5075,6 +5079,7 @@
 				GCC_WARN_UNUSED_LABEL = YES;
 				INFOPLIST_FILE = "Clock Signal/Info.plist";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 10.10;
 				OTHER_CPLUSPLUSFLAGS = (
 					"$(OTHER_CFLAGS)",
 					"-Wreorder",
@@ -5121,6 +5126,7 @@
 				GCC_WARN_UNUSED_LABEL = YES;
 				INFOPLIST_FILE = "Clock Signal/Info.plist";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 10.10;
 				OTHER_CPLUSPLUSFLAGS = (
 					"$(OTHER_CFLAGS)",
 					"-Wreorder",
diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp
index 6a70d98dd..49f95d269 100644
--- a/OSBindings/SDL/main.cpp
+++ b/OSBindings/SDL/main.cpp
@@ -35,9 +35,11 @@ namespace {
 
 struct BestEffortUpdaterDelegate: public Concurrency::BestEffortUpdater::Delegate {
 	Time::Seconds update(Concurrency::BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update, int flags) override {
+		std::lock_guard<std::mutex> lock_guard(*machine_mutex);
 		return machine->crt_machine()->run_until(duration, flags);
 	}
 
+	std::mutex *machine_mutex;
 	Machine::DynamicMachine *machine;
 };
 
@@ -103,6 +105,7 @@ class ActivityObserver: public Activity::Observer {
 		}
 
 		void set_aspect_ratio(float aspect_ratio) {
+			std::lock_guard<std::mutex> lock_guard(mutex);
 			lights_.clear();
 
 			// Generate a bunch of LEDs for connected drives.
@@ -129,6 +132,7 @@ class ActivityObserver: public Activity::Observer {
 		}
 
 		void draw() {
+			std::lock_guard<std::mutex> lock_guard(mutex);
 			for(const auto &lit_led: lit_leds_) {
 				if(blinking_leds_.find(lit_led) == blinking_leds_.end() && lights_.find(lit_led) != lights_.end())
 					lights_[lit_led]->draw(0.0, 0.8, 0.0);
@@ -139,26 +143,31 @@ class ActivityObserver: public Activity::Observer {
 	private:
 		std::vector<std::string> leds_;
 		void register_led(const std::string &name) override {
+			std::lock_guard<std::mutex> lock_guard(mutex);
 			leds_.push_back(name);
 		}
 
 		std::vector<std::string> drives_;
 		void register_drive(const std::string &name) override {
+			std::lock_guard<std::mutex> lock_guard(mutex);
 			drives_.push_back(name);
 		}
 
 		void set_led_status(const std::string &name, bool lit) override {
+			std::lock_guard<std::mutex> lock_guard(mutex);
 			if(lit) lit_leds_.insert(name);
 			else lit_leds_.erase(name);
 		}
 
 		void announce_drive_event(const std::string &name, DriveEvent event) override {
+			std::lock_guard<std::mutex> lock_guard(mutex);
 			blinking_leds_.insert(name);
 		}
 
 		std::map<std::string, std::unique_ptr<Outputs::Display::OpenGL::Rectangle>> lights_;
 		std::set<std::string> lit_leds_;
 		std::set<std::string> blinking_leds_;
+		std::mutex mutex;
 };
 
 bool KeyboardKeyForSDLScancode(SDL_Keycode scancode, Inputs::Keyboard::Key &key) {
@@ -442,6 +451,7 @@ int main(int argc, char *argv[]) {
 
 	// Create and configure a machine.
 	::Machine::Error error;
+	std::mutex machine_mutex;
 	std::unique_ptr<::Machine::DynamicMachine> machine(::Machine::MachineForTargets(targets, rom_fetcher, error));
 	if(!machine) {
 		switch(error) {
@@ -463,6 +473,7 @@ int main(int argc, char *argv[]) {
 	}
 
 	best_effort_updater_delegate.machine = machine.get();
+	best_effort_updater_delegate.machine_mutex = &machine_mutex;
 	speaker_delegate.updater = &updater;
 	updater.set_delegate(&best_effort_updater_delegate);
 
@@ -617,7 +628,14 @@ int main(int argc, char *argv[]) {
 	bool should_quit = false;
 	Uint32 fullscreen_mode = 0;
 	while(!should_quit) {
-		// Process all pending events.
+		// Wait for vsync and draw a new frame (NB: machine_mutex is *not* currently locked).
+		SDL_GL_SwapWindow(window);
+		scan_target.update(int(window_width), int(window_height));
+		scan_target.draw(int(window_width), int(window_height));
+		if(activity_observer) activity_observer->draw();
+
+		// Grab the machine lock and process all pending events.
+		std::lock_guard<std::mutex> lock_guard(machine_mutex);
 		SDL_Event event;
 		while(SDL_PollEvent(&event)) {
 			switch(event.type) {
@@ -643,87 +661,88 @@ int main(int argc, char *argv[]) {
 				} break;
 
 				case SDL_KEYDOWN:
-					// Syphon off the key-press if it's control+shift+V (paste).
-					if(event.key.keysym.sym == SDLK_v && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) {
-						const auto keyboard_machine = machine->keyboard_machine();
-						if(keyboard_machine) {
-							keyboard_machine->type_string(SDL_GetClipboardText());
+				case SDL_KEYUP: {
+					const auto keyboard_machine = machine->keyboard_machine();
+
+					if(event.type == SDL_KEYDOWN) {
+						// Syphon off the key-press if it's control+shift+V (paste).
+						if(event.key.keysym.sym == SDLK_v && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) {
+							if(keyboard_machine) {
+								keyboard_machine->type_string(SDL_GetClipboardText());
+								break;
+							}
+						}
+
+						// Use ctrl+escape to release the mouse (if captured).
+						if(event.key.keysym.sym == SDLK_ESCAPE && (SDL_GetModState()&KMOD_CTRL)) {
+							SDL_SetRelativeMouseMode(SDL_FALSE);
+							window_titler.set_mouse_is_captured(false);
+						}
+
+						// Capture ctrl+shift+d as a take-a-screenshot command.
+						if(event.key.keysym.sym == SDLK_d && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) {
+							// Grab the screen buffer.
+							Outputs::Display::OpenGL::Screenshot screenshot(4, 3);
+
+							// Pick the directory for images. Try `xdg-user-dir PICTURES` first.
+							std::string target_directory = system_get("xdg-user-dir PICTURES");
+
+							// Make sure there are no newlines.
+							target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\n'), target_directory.end());
+							target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\r'), target_directory.end());
+
+							// Fall back on the HOME directory if necessary.
+							if(target_directory.empty()) target_directory = getenv("HOME");
+
+							// Find the first available name of the form ~/clk-screenshot-<number>.bmp.
+							size_t index = 0;
+							std::string target;
+							while(true) {
+								target = target_directory + "/clk-screenshot-" + std::to_string(index) + ".bmp";
+
+								struct stat file_stats;
+								if(stat(target.c_str(), &file_stats))
+									break;
+
+								++index;
+							}
+
+							// Create a suitable SDL surface and save the thing.
+							const bool is_big_endian = SDL_BYTEORDER == SDL_BIG_ENDIAN;
+							SDL_Surface *const surface = SDL_CreateRGBSurfaceFrom(
+								screenshot.pixel_data.data(),
+								screenshot.width, screenshot.height,
+								8*4,
+								screenshot.width*4,
+								is_big_endian ? 0xff000000 : 0x000000ff,
+								is_big_endian ? 0x00ff0000 : 0x0000ff00,
+								is_big_endian ? 0x0000ff00 : 0x00ff0000,
+								0);
+							SDL_SaveBMP(surface, target.c_str());
+							SDL_FreeSurface(surface);
+							break;
+						}
+
+
+						// Syphon off alt+enter (toggle full-screen) upon key up only; this was previously a key down action,
+						// but the SDL_KEYDOWN announcement was found to be reposted after changing graphics mode on some
+						// systems so key up is safer.
+						if(event.type == SDL_KEYUP && event.key.keysym.sym == SDLK_RETURN && (SDL_GetModState()&KMOD_ALT)) {
+							fullscreen_mode ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
+							SDL_SetWindowFullscreen(window, fullscreen_mode);
+							SDL_ShowCursor((fullscreen_mode&SDL_WINDOW_FULLSCREEN_DESKTOP) ? SDL_DISABLE : SDL_ENABLE);
+
+							// Announce a potential discontinuity in keyboard input.
+							const auto keyboard_machine = machine->keyboard_machine();
+							if(keyboard_machine) {
+								keyboard_machine->get_keyboard().reset_all_keys();
+							}
 							break;
 						}
 					}
 
-					// Use ctrl+escape to release the mouse (if captured).
-					if(event.key.keysym.sym == SDLK_ESCAPE && (SDL_GetModState()&KMOD_CTRL)) {
-						SDL_SetRelativeMouseMode(SDL_FALSE);
-						window_titler.set_mouse_is_captured(false);
-					}
-
-					// Capture ctrl+shift+d as a take-a-screenshot command.
-					if(event.key.keysym.sym == SDLK_d && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) {
-						// Grab the screen buffer.
-						Outputs::Display::OpenGL::Screenshot screenshot(4, 3);
-
-						// Pick the directory for images. Try `xdg-user-dir PICTURES` first.
-						std::string target_directory = system_get("xdg-user-dir PICTURES");
-
-						// Make sure there are no newlines.
-						target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\n'), target_directory.end());
-						target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\r'), target_directory.end());
-
-						// Fall back on the HOME directory if necessary.
-						if(target_directory.empty()) target_directory = getenv("HOME");
-
-						// Find the first available name of the form ~/clk-screenshot-<number>.bmp.
-						size_t index = 0;
-						std::string target;
-						while(true) {
-							target = target_directory + "/clk-screenshot-" + std::to_string(index) + ".bmp";
-
-							struct stat file_stats;
-							if(stat(target.c_str(), &file_stats))
-								break;
-
-							++index;
-						}
-
-						// Create a suitable SDL surface and save the thing.
-						const bool is_big_endian = SDL_BYTEORDER == SDL_BIG_ENDIAN;
-						SDL_Surface *const surface = SDL_CreateRGBSurfaceFrom(
-							screenshot.pixel_data.data(),
-							screenshot.width, screenshot.height,
-							8*4,
-							screenshot.width*4,
-							is_big_endian ? 0xff000000 : 0x000000ff,
-							is_big_endian ? 0x00ff0000 : 0x0000ff00,
-							is_big_endian ? 0x0000ff00 : 0x00ff0000,
-							0);
-						SDL_SaveBMP(surface, target.c_str());
-						SDL_FreeSurface(surface);
-						break;
-					}
-
-				// deliberate fallthrough...
-				case SDL_KEYUP: {
-
-					// Syphon off alt+enter (toggle full-screen) upon key up only; this was previously a key down action,
-					// but the SDL_KEYDOWN announcement was found to be reposted after changing graphics mode on some
-					// systems so key up is safer.
-					if(event.type == SDL_KEYUP && event.key.keysym.sym == SDLK_RETURN && (SDL_GetModState()&KMOD_ALT)) {
-						fullscreen_mode ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
-						SDL_SetWindowFullscreen(window, fullscreen_mode);
-						SDL_ShowCursor((fullscreen_mode&SDL_WINDOW_FULLSCREEN_DESKTOP) ? SDL_DISABLE : SDL_ENABLE);
-
-						// Announce a potential discontinuity in keyboard input.
-						const auto keyboard_machine = machine->keyboard_machine();
-						if(keyboard_machine) {
-							keyboard_machine->get_keyboard().reset_all_keys();
-						}
-						break;
-					}
-
 					const bool is_pressed = event.type == SDL_KEYDOWN;
 
-					const auto keyboard_machine = machine->keyboard_machine();
 					if(keyboard_machine) {
 						Inputs::Keyboard::Key key = Inputs::Keyboard::Key::Space;
 						if(!KeyboardKeyForSDLScancode(event.key.keysym.scancode, key)) break;
@@ -760,12 +779,13 @@ int main(int argc, char *argv[]) {
 				} break;
 
 				case SDL_MOUSEBUTTONDOWN:
-					if(uses_mouse && !SDL_GetRelativeMouseMode()) {
+				case SDL_MOUSEBUTTONUP: {
+					if(uses_mouse && event.type == SDL_MOUSEBUTTONDOWN && !SDL_GetRelativeMouseMode()) {
 						SDL_SetRelativeMouseMode(SDL_TRUE);
 						window_titler.set_mouse_is_captured(true);
 						break;
 					}
-				case SDL_MOUSEBUTTONUP: {
+
 					const auto mouse_machine = machine->mouse_machine();
 					if(mouse_machine) {
 						mouse_machine->get_mouse().set_button_pressed(
@@ -835,15 +855,12 @@ int main(int argc, char *argv[]) {
 			}
 		}
 
-		// Display a new frame and wait for vsync.
+		// Request a machine update.
 		updater.update();
-		scan_target.update(int(window_width), int(window_height));
-		scan_target.draw(int(window_width), int(window_height));
-		if(activity_observer) activity_observer->draw();
-		SDL_GL_SwapWindow(window);
 	}
 
 	// Clean up.
+	updater.flush();	// Ensure no further updates will occur.
 	joysticks.clear();
 	SDL_DestroyWindow( window );
 	SDL_Quit();

From 9666193c67d4565277f8885616811ddd153d955f Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Jan 2020 14:50:28 -0500
Subject: [PATCH 2/8] Pulls the call to .update out of the critical section.

---
 OSBindings/SDL/main.cpp | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp
index 49f95d269..44bae33fb 100644
--- a/OSBindings/SDL/main.cpp
+++ b/OSBindings/SDL/main.cpp
@@ -628,11 +628,14 @@ int main(int argc, char *argv[]) {
 	bool should_quit = false;
 	Uint32 fullscreen_mode = 0;
 	while(!should_quit) {
-		// Wait for vsync and draw a new frame (NB: machine_mutex is *not* currently locked).
+		// Wait for vsync, draw a new frame and post a machine update.
+		// NB: machine_mutex is *not* currently locked, therefore it shouldn't
+		// be 'most' of the time.
 		SDL_GL_SwapWindow(window);
 		scan_target.update(int(window_width), int(window_height));
 		scan_target.draw(int(window_width), int(window_height));
 		if(activity_observer) activity_observer->draw();
+		updater.update();
 
 		// Grab the machine lock and process all pending events.
 		std::lock_guard<std::mutex> lock_guard(machine_mutex);
@@ -854,9 +857,6 @@ int main(int argc, char *argv[]) {
 				}
 			}
 		}
-
-		// Request a machine update.
-		updater.update();
 	}
 
 	// Clean up.

From 03d23aad410c89666e3995b42be7028f28808456 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Jan 2020 23:27:09 -0500
Subject: [PATCH 3/8] Corrects reported ZX80/81 scan status.

---
 Machines/ZX8081/Video.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Machines/ZX8081/Video.cpp b/Machines/ZX8081/Video.cpp
index 10507a864..e5241b924 100644
--- a/Machines/ZX8081/Video.cpp
+++ b/Machines/ZX8081/Video.cpp
@@ -111,5 +111,5 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
 }
 
 Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
-	return crt_.get_scaled_scan_status() / 0.5f;
+	return crt_.get_scaled_scan_status() / 2.0f;
 }

From 43bf6aca677f828e9c4ec3b1bfbaa54bd5a520b1 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Jan 2020 23:46:18 -0500
Subject: [PATCH 4/8] Corrects reported scan status for the Amstrad CPC.

---
 Machines/AmstradCPC/AmstradCPC.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp
index 19c68e772..ca3015451 100644
--- a/Machines/AmstradCPC/AmstradCPC.cpp
+++ b/Machines/AmstradCPC/AmstradCPC.cpp
@@ -337,7 +337,7 @@ class CRTCBusHandler {
 
 		/// @returns The current scan status.
 		Outputs::Display::ScanStatus get_scaled_scan_status() const {
-			return crt_.get_scaled_scan_status() / 64.0f;
+			return crt_.get_scaled_scan_status() / 4.0f;
 		}
 
 		/// Sets the type of display.

From 79bb0f822287d0cc8bb8f726c21c79338e1a95d6 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 26 Jan 2020 11:36:06 -0500
Subject: [PATCH 5/8] Updates comment.

---
 Machines/Apple/Macintosh/Macintosh.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/Machines/Apple/Macintosh/Macintosh.cpp b/Machines/Apple/Macintosh/Macintosh.cpp
index 75e57e65e..19d019ca4 100644
--- a/Machines/Apple/Macintosh/Macintosh.cpp
+++ b/Machines/Apple/Macintosh/Macintosh.cpp
@@ -399,7 +399,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
 			via_.flush();
 			audio_.queue.perform();
 
-			// Experimental?
+			// This avoids deferring IWM costs indefinitely, until
+			// they become artbitrarily large.
 			iwm_.flush();
 		}
 

From b5147562720803d2d354e7cb451731d34c3e9160 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 26 Jan 2020 13:25:23 -0500
Subject: [PATCH 6/8] Adds the option to run machines at a multiple of their
 real speeds.

Exposed to SDL users only, for now.
---
 .../Implementation/MultiSpeaker.cpp           |  4 ++--
 .../Implementation/MultiSpeaker.hpp           |  2 +-
 Machines/CRTMachine.hpp                       | 23 ++++++++++++++++++-
 .../xcschemes/Clock Signal.xcscheme           |  2 +-
 OSBindings/SDL/main.cpp                       | 21 +++++++++++++++--
 .../Speaker/Implementation/LowpassSpeaker.hpp |  4 ++--
 Outputs/Speaker/Speaker.hpp                   | 23 ++++++++++++++++++-
 7 files changed, 69 insertions(+), 10 deletions(-)

diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp
index c5a8fa724..a6f412ca4 100644
--- a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp
+++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp
@@ -37,9 +37,9 @@ float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum)
 	return ideal / static_cast<float>(speakers_.size());
 }
 
-void MultiSpeaker::set_output_rate(float cycles_per_second, int buffer_size) {
+void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size) {
 	for(const auto &speaker: speakers_) {
-		speaker->set_output_rate(cycles_per_second, buffer_size);
+		speaker->set_computed_output_rate(cycles_per_second, buffer_size);
 	}
 }
 
diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp
index a6f882fd0..890526118 100644
--- a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp
+++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp
@@ -39,7 +39,7 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker:
 
 		// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation.
 		float get_ideal_clock_rate_in_range(float minimum, float maximum) override;
-		void set_output_rate(float cycles_per_second, int buffer_size) override;
+		void set_computed_output_rate(float cycles_per_second, int buffer_size) override;
 		void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override;
 
 	private:
diff --git a/Machines/CRTMachine.hpp b/Machines/CRTMachine.hpp
index 62c1ff408..1e57575c1 100644
--- a/Machines/CRTMachine.hpp
+++ b/Machines/CRTMachine.hpp
@@ -54,11 +54,31 @@ class Machine {
 
 		/// Runs the machine for @c duration seconds.
 		virtual void run_for(Time::Seconds duration) {
-			const double cycles = (duration * clock_rate_) + clock_conversion_error_;
+			const double cycles = (duration * clock_rate_ * speed_multiplier_) + clock_conversion_error_;
 			clock_conversion_error_ = std::fmod(cycles, 1.0);
 			run_for(Cycles(static_cast<int>(cycles)));
 		}
 
+		/*!
+			Sets a speed multiplier to apply to this machine; e.g. a multiplier of 1.5 will cause the
+			emulated machine to run 50% faster than a real machine. This speed-up is an emulation
+			fiction: it will apply across the system, including to the CRT.
+		*/
+		virtual void set_speed_multiplier(double multiplier) {
+			speed_multiplier_ = multiplier;
+			auto speaker = get_speaker();
+			if(speaker) {
+				speaker->set_input_rate_multiplier(float(multiplier));
+			}
+		}
+
+		/*!
+			@returns The current speed multiplier.
+		*/
+		virtual double get_speed_multiplier() {
+			return speed_multiplier_;
+		}
+
 		/*!
 			Runs for the machine for at least @c duration seconds, and then until @c condition is true.
 
@@ -169,6 +189,7 @@ class Machine {
 	private:
 		double clock_rate_ = 1.0;
 		double clock_conversion_error_ = 0.0;
+		double speed_multiplier_ = 1.0;
 };
 
 }
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme
index 47f9c7286..1465a4f62 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme	
@@ -67,7 +67,7 @@
       </Testables>
    </TestAction>
    <LaunchAction
-      buildConfiguration = "Debug"
+      buildConfiguration = "Release"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       enableASanStackUseAfterReturn = "YES"
diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp
index 44bae33fb..799d944b0 100644
--- a/OSBindings/SDL/main.cpp
+++ b/OSBindings/SDL/main.cpp
@@ -345,7 +345,7 @@ int main(int argc, char *argv[]) {
 	ParsedArguments arguments = parse_arguments(argc, argv);
 
 	// This may be printed either as
-	const std::string usage_suffix = " [file] [OPTIONS] [--rompath={path to ROMs}]";
+	const std::string usage_suffix = " [file] [OPTIONS] [--rompath={path to ROMs}] [--speed={speed multiplier, e.g. 1.5}]";
 
 	// Print a help message if requested.
 	if(arguments.selections.find("help") != arguments.selections.end() || arguments.selections.find("h") != arguments.selections.end()) {
@@ -410,7 +410,7 @@ int main(int argc, char *argv[]) {
 				"/usr/share/CLK/"
 			};
 			if(arguments.selections.find("rompath") != arguments.selections.end()) {
-				std::string user_path = arguments.selections["rompath"]->list_selection()->value;
+				const std::string user_path = arguments.selections["rompath"]->list_selection()->value;
 				if(user_path.back() != '/') {
 					paths.push_back(user_path + "/");
 				} else {
@@ -472,6 +472,23 @@ int main(int argc, char *argv[]) {
 		return EXIT_FAILURE;
 	}
 
+	// Apply the speed multiplier, if one was requested.
+	if(arguments.selections.find("speed") != arguments.selections.end()) {
+		const char *speed_string = arguments.selections["speed"]->list_selection()->value.c_str();
+		char *end;
+		double speed = strtod(speed_string, &end);
+
+		if(end-speed_string != strlen(speed_string)) {
+			std::cerr << "Unable to parse speed: " << speed_string << std::endl;
+		} else if(speed <= 0.0) {
+			std::cerr << "Cannot run at speed " << speed_string << "; speeds must be positive." << std::endl;
+		} else {
+			machine->crt_machine()->set_speed_multiplier(speed);
+			// TODO: what if not a 'CRT' machine? Likely rests on resolving this project's machine naming policy.
+		}
+	}
+
+	// Wire up the best-effort updater, its delegate, and the speaker delegate.
 	best_effort_updater_delegate.machine = machine.get();
 	best_effort_updater_delegate.machine_mutex = &machine_mutex;
 	speaker_delegate.updater = &updater;
diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp
index ced763a56..686e00d25 100644
--- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp
+++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp
@@ -35,7 +35,7 @@ template <typename T> class LowpassSpeaker: public Speaker {
 		}
 
 		// Implemented as per Speaker.
-		float get_ideal_clock_rate_in_range(float minimum, float maximum) {
+		float get_ideal_clock_rate_in_range(float minimum, float maximum) final {
 			std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
 
 			// return twice the cut off, if applicable
@@ -58,7 +58,7 @@ template <typename T> class LowpassSpeaker: public Speaker {
 		}
 
 		// Implemented as per Speaker.
-		void set_output_rate(float cycles_per_second, int buffer_size) {
+		void set_computed_output_rate(float cycles_per_second, int buffer_size) final {
 			std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
 			filter_parameters_.output_cycles_per_second = cycles_per_second;
 			filter_parameters_.parameters_are_dirty = true;
diff --git a/Outputs/Speaker/Speaker.hpp b/Outputs/Speaker/Speaker.hpp
index 2f575880b..941621729 100644
--- a/Outputs/Speaker/Speaker.hpp
+++ b/Outputs/Speaker/Speaker.hpp
@@ -24,7 +24,15 @@ class Speaker {
 		virtual ~Speaker() {}
 
 		virtual float get_ideal_clock_rate_in_range(float minimum, float maximum) = 0;
-		virtual void set_output_rate(float cycles_per_second, int buffer_size) = 0;
+		void set_output_rate(float cycles_per_second, int buffer_size) {
+			output_cycles_per_second_ = cycles_per_second;
+			output_buffer_size_ = buffer_size;
+			compute_output_rate();
+		}
+		void set_input_rate_multiplier(float multiplier) {
+			input_rate_multiplier_ = multiplier;
+			compute_output_rate();
+		}
 
 		int completed_sample_sets() const { return completed_sample_sets_; }
 
@@ -36,13 +44,26 @@ class Speaker {
 			delegate_ = delegate;
 		}
 
+		virtual void set_computed_output_rate(float cycles_per_second, int buffer_size) = 0;
+
 	protected:
 		void did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) {
 			++completed_sample_sets_;
 			delegate_->speaker_did_complete_samples(this, buffer);
 		}
 		Delegate *delegate_ = nullptr;
+
+	private:
+		void compute_output_rate() {
+			// The input rate multiplier is actually used as an output rate divider,
+			// to confirm to the public interface of a generic speaker being output-centric.
+			set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_);
+		}
+
 		int completed_sample_sets_ = 0;
+		float input_rate_multiplier_ = 1.0f;
+		float output_cycles_per_second_ = 1.0f;
+		int output_buffer_size_ = 1;
 };
 
 }

From 6bcdd3177ddb721f51958fbe9cfa35f04fad6040 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 26 Jan 2020 18:04:25 -0500
Subject: [PATCH 7/8] Ensures that a change of screen issues a `reshape`. Just
 in case.

Thereby resolves display mis-sizing when dragging from a Retina display to a regular one, or vice versa.
---
 OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m
index 283f6e19b..5f4267c3a 100644
--- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m	
+++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m	
@@ -17,6 +17,7 @@
 @implementation CSOpenGLView {
 	CVDisplayLinkRef _displayLink;
 	CGSize _backingSize;
+	NSScreen *_currentScreen;
 
 	NSTrackingArea *_mouseTrackingArea;
 	NSTimer *_mouseHideTimer;
@@ -26,6 +27,9 @@
 - (void)prepareOpenGL {
 	[super prepareOpenGL];
 
+	// Note the initial screen.
+	_currentScreen = self.window.screen;
+
 	// Synchronize buffer swaps with vertical refresh rate
 	GLint swapInt = 1;
 	[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
@@ -56,6 +60,14 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
 }
 
 - (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
+	if(self.window.screen != _currentScreen) {
+		_currentScreen = self.window.screen;
+
+		// Issue a reshape, in case a switch to/from a Retina display has
+		// happened, changing the results of -convertSizeToBacking:, etc.
+		[self reshape];
+	}
+
 	[self redrawWithEvent:CSOpenGLViewRedrawEventTimer];
 }
 

From 52e49439a620cd81f3af7a1cd2e8837f9bedc2ac Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 26 Jan 2020 23:23:33 -0500
Subject: [PATCH 8/8] Recreates display link upon a screen change.

Different screens may have different refresh rates, and I can find no guarantees about how Apple handles that.
---
 .../Mac/Clock Signal/Views/CSOpenGLView.m     | 53 ++++++++++++++++---
 1 file changed, 46 insertions(+), 7 deletions(-)

diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m
index 5f4267c3a..c1a57dee0 100644
--- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m	
+++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m	
@@ -30,12 +30,35 @@
 	// Note the initial screen.
 	_currentScreen = self.window.screen;
 
-	// Synchronize buffer swaps with vertical refresh rate
+	// Synchronize buffer swaps with vertical refresh rate.
+	// TODO: discard this, once scheduling is sufficiently intelligent?
 	GLint swapInt = 1;
 	[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
 
+	// set the clear colour
+	[self.openGLContext makeCurrentContext];
+	glClearColor(0.0, 0.0, 0.0, 1.0);
+
+	// Setup the [initial] display link.
+	[self setupDisplayLink];
+}
+
+- (void)setupDisplayLink {
+	// Kill the existing link if there is one, then wait until its final shout is definitely done.
+	if(_displayLink) {
+		const double duration = CVDisplayLinkGetActualOutputVideoRefreshPeriod(_displayLink);
+		CVDisplayLinkStop(_displayLink);
+
+		// This is a workaround; I could find no way to ensure that a callback from the display
+		// link is not currently ongoing.
+		usleep((useconds_t)ceil(duration * 1000000.0));
+
+		CVDisplayLinkRelease(_displayLink);
+	}
+
 	// Create a display link capable of being used with all active displays
-	CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
+	NSNumber *const screenNumber = self.window.screen.deviceDescription[@"NSScreenNumber"];
+	CVDisplayLinkCreateWithCGDisplay(screenNumber.unsignedIntValue, &_displayLink);
 
 	// Set the renderer output callback function
 	CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self));
@@ -45,27 +68,42 @@
 	CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
 	CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat);
 
-	// set the clear colour
-	[self.openGLContext makeCurrentContext];
-	glClearColor(0.0, 0.0, 0.0, 1.0);
-
 	// Activate the display link
 	CVDisplayLinkStart(_displayLink);
 }
 
 static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
 	CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext;
+
 	[view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)];
+	/*
+		Do not touch the display link from after this call; there's a bit of a race condition with setupDisplayLink.
+		Specifically: Apple provides CVDisplayLinkStop but a call to that merely prevents future calls to the callback,
+		it doesn't wait for completion of any current calls. So I've set up a usleep for one callback's duration,
+		so code in here gets one callback's duration to access the display link.
+
+		In practice, it should do so only upon entry, and before calling into the view. The view promises not to
+		access the display link itself as part of -drawAtTime:frequency:.
+	*/
+
 	return kCVReturnSuccess;
 }
 
 - (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
+	// Test now whether the screen this view is on has changed since last time it was checked.
+	// There's likely a callback available for this, on NSWindow if nowhere else, or an NSNotification,
+	// but since this method is going to be called repeatedly anyway, and the test is cheap, polling
+	// feels fine.
 	if(self.window.screen != _currentScreen) {
 		_currentScreen = self.window.screen;
 
 		// Issue a reshape, in case a switch to/from a Retina display has
 		// happened, changing the results of -convertSizeToBacking:, etc.
 		[self reshape];
+
+		// Also switch display links, to make sure synchronisation is with the display
+		// the window is actually on, and at its rate.
+		[self setupDisplayLink];
 	}
 
 	[self redrawWithEvent:CSOpenGLViewRedrawEventTimer];
@@ -87,7 +125,8 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
 }
 
 - (void)dealloc {
-	// Release the display link
+	// Stop and release the display link
+	CVDisplayLinkStop(_displayLink);
 	CVDisplayLinkRelease(_displayLink);
 }