diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp
index 58685125f..c5ed4a245 100644
--- a/Machines/AmstradCPC/AmstradCPC.cpp
+++ b/Machines/AmstradCPC/AmstradCPC.cpp
@@ -55,8 +55,6 @@ class InterruptTimer {
 			trailing edge because it is active high.
 		*/
 		inline void signal_hsync() {
-//			printf("count h: %d/%d [%d]\n", timer_, reset_counter_, interrupt_request_);
-
 			// Increment the timer and if it has hit 52 then reset it and
 			// set the interrupt request line to true.
 			++timer_;
@@ -81,13 +79,11 @@ class InterruptTimer {
 
 		/// Indicates the leading edge of a new vertical sync.
 		inline void signal_vsync() {
-//			printf("count v\n");
 			reset_counter_ = 2;
 		}
 
 		/// Indicates that an interrupt acknowledge has been received from the Z80.
 		inline void signal_interrupt_acknowledge() {
-//			printf("count IRQA\n");
 			interrupt_request_ = false;
 			timer_ &= ~32;
 		}
@@ -104,7 +100,6 @@ class InterruptTimer {
 
 		/// Resets the timer.
 		inline void reset_count() {
-//			printf("count reset\n");
 			timer_ = 0;
 			interrupt_request_ = false;
 		}
@@ -190,6 +185,8 @@ class CRTCBusHandler {
 			bus state and determines what output to produce based on the current palette and mode.
 		*/
 		forceinline void perform_bus_cycle(const Motorola::CRTC::BusState &state) {
+			// TODO: there's a one-tick delay on pixel output; incorporate that.
+
 			// The gate array waits 2us to react to the CRTC's vsync signal, and then
 			// caps output at 4us. Since the clock rate is 1Mhz, that's 2 and 4 cycles,
 			// respectively.
@@ -240,10 +237,10 @@ class CRTCBusHandler {
 				previous_output_mode_ = output_mode;
 			}
 
-			// increment cycles since state changed
+			// Increment cycles since state changed.
 			cycles_++;
 
-			// collect some more pixels if output is ongoing
+			// Collect some more pixels if output is ongoing.
 			if(previous_output_mode_ == OutputMode::Pixels) {
 				if(!pixel_data_) {
 					pixel_pointer_ = pixel_data_ = crt_.begin_data(320, 8);
@@ -300,34 +297,28 @@ class CRTCBusHandler {
 				}
 			}
 
-			// Notify a leading hsync edge to the interrupt timer.
-			// Per Interrupts in the CPC: "to be confirmed: does gate array count positive or negative edge transitions of HSYNC signal?";
-			// if you take it as given that display mode is latched as a result of hsync then Pipe Mania seems to imply that the count
-			// occurs on a leading edge and the mode lock on a trailing.
-			if(!was_hsync_ && state.hsync) {
-				interrupt_timer_.signal_hsync();
-			}
-
-			// Check for a trailing CRTC hsync; if one occurred then that's the trigger potentially to change modes.
-			if(was_hsync_ && !state.hsync) {
-				if(mode_ != next_mode_) {
-					mode_ = next_mode_;
-					switch(mode_) {
-						default:
-						case 0:		pixel_divider_ = 4;	break;
-						case 1:		pixel_divider_ = 2;	break;
-						case 2:		pixel_divider_ = 1;	break;
-					}
-					build_mode_table();
+			// Latch mode four cycles after HSYNC was signalled, if still active.
+			if(cycles_into_hsync_ == 4 && mode_ != next_mode_) {
+				mode_ = next_mode_;
+				switch(mode_) {
+					default:
+					case 0:		pixel_divider_ = 4;	break;
+					case 1:		pixel_divider_ = 2;	break;
+					case 2:		pixel_divider_ = 1;	break;
 				}
+				build_mode_table();
 			}
 
-			// check for a leading vsync; that also needs to be communicated to the interrupt timer
+			// For the interrupt timer: notify the leading edge of vertical sync and the
+			// trailing edge of horizontal sync.
 			if(!was_vsync_ && state.vsync) {
 				interrupt_timer_.signal_vsync();
 			}
+			if(was_hsync_ && !state.hsync) {
+				interrupt_timer_.signal_hsync();
+			}
 
-			// update current state for edge detection next time around
+			// Update current state for edge detection next time around.
 			was_vsync_ = state.vsync;
 			was_hsync_ = state.hsync;
 		}
@@ -858,223 +849,229 @@ class ConcreteMachine:
 			clock_offset_ = (clock_offset_ + cycle.length) & HalfCycles(7);
 			z80_.set_wait_line(clock_offset_ >= HalfCycles(2));
 
-			// Update the CRTC once every eight half cycles; aiming for half-cycle 4 as
-			// per the initial seed to the crtc_counter_, but any time in the final four
-			// will do as it's safe to conclude that nobody else has touched video RAM
-			// during that whole window.
-			crtc_counter_ += cycle.length;
-			const Cycles crtc_cycles = crtc_counter_.divide_cycles(Cycles(4));
-			if(crtc_cycles > Cycles(0)) crtc_.run_for(crtc_cycles);
+			// Float this out as a lambda to allow easy repositioning relative to the CPU activity;
+			// for now this is largely experimental.
+			const auto update_subsystems = [&] {
+				// Update the CRTC once every eight half cycles; aiming for half-cycle 4 as
+				// per the initial seed to the crtc_counter_, but any time in the final four
+				// will do as it's safe to conclude that nobody else has touched video RAM
+				// during that whole window.
+				crtc_counter_ += cycle.length;
+				const Cycles crtc_cycles = crtc_counter_.divide_cycles(Cycles(4));
+				if(crtc_cycles > Cycles(0)) crtc_.run_for(crtc_cycles);
 
-			// Check whether that prompted a change in the interrupt line. If so then date
-			// it to whenever the cycle was triggered.
-			if(interrupt_timer_.request_has_changed()) z80_.set_interrupt_line(interrupt_timer_.get_request(), -crtc_counter_);
+				// Check whether that prompted a change in the interrupt line. If so then date
+				// it to whenever the cycle was triggered.
+				if(interrupt_timer_.request_has_changed()) z80_.set_interrupt_line(interrupt_timer_.get_request(), -crtc_counter_);
 
-			// TODO (in the player, not here): adapt it to accept an input clock rate and
-			// run_for as HalfCycles.
-			if(!tape_player_is_sleeping_) tape_player_.run_for(cycle.length.as_integral());
+				// TODO (in the player, not here): adapt it to accept an input clock rate and
+				// run_for as HalfCycles.
+				if(!tape_player_is_sleeping_) tape_player_.run_for(cycle.length.as_integral());
 
-			// Pump the AY.
-			ay_.run_for(cycle.length);
+				// Pump the AY.
+				ay_.run_for(cycle.length);
 
-			if constexpr (has_fdc) {
-				// Clock the FDC, if connected, using a lazy scale by two.
-				time_since_fdc_update_ += cycle.length;
-			}
+				if constexpr (has_fdc) {
+					// Clock the FDC, if connected, using a lazy scale by two.
+					time_since_fdc_update_ += cycle.length;
+				}
 
-			// Update typing activity.
-			if(typer_) typer_->run_for(cycle.length);
+				// Update typing activity.
+				if(typer_) typer_->run_for(cycle.length);
+			};
 
-			// Stop now if no action is strictly required.
-			if(!cycle.is_terminal()) return HalfCycles(0);
+			// Continue only if action strictly required.
+			if(cycle.is_terminal()) {
+				uint16_t address = cycle.address ? *cycle.address : 0x0000;
+				switch(cycle.operation) {
+					case CPU::Z80::PartialMachineCycle::ReadOpcode:
 
-			uint16_t address = cycle.address ? *cycle.address : 0x0000;
-			switch(cycle.operation) {
-				case CPU::Z80::PartialMachineCycle::ReadOpcode:
+						// TODO: just capturing byte reads as below doesn't seem to do that much in terms of acceleration;
+						// I'm not immediately clear whether that's just because the machine still has to sit through
+						// pilot tone in real time, or just that almost no software uses the ROM loader.
+						if(use_fast_tape_hack_ && address == tape_read_byte_address && read_pointers_[0] == roms_[ROMType::OS].data()) {
+							using Parser = Storage::Tape::ZXSpectrum::Parser;
+							Parser parser(Parser::MachineType::AmstradCPC);
 
-					// TODO: just capturing byte reads as below doesn't seem to do that much in terms of acceleration;
-					// I'm not immediately clear whether that's just because the machine still has to sit through
-					// pilot tone in real time, or just that almost no software uses the ROM loader.
-					if(use_fast_tape_hack_ && address == tape_read_byte_address && read_pointers_[0] == roms_[ROMType::OS].data()) {
-						using Parser = Storage::Tape::ZXSpectrum::Parser;
-						Parser parser(Parser::MachineType::AmstradCPC);
+							const auto speed = read_pointers_[tape_speed_value_address >> 14][tape_speed_value_address & 16383];
+							parser.set_cpc_read_speed(speed);
 
-						const auto speed = read_pointers_[tape_speed_value_address >> 14][tape_speed_value_address & 16383];
-						parser.set_cpc_read_speed(speed);
+							// Seed with the current pulse; the CPC will have finished the
+							// preceding symbol and be a short way into the pulse that should determine the
+							// first bit of this byte.
+							parser.process_pulse(tape_player_.get_current_pulse());
+							const auto byte = parser.get_byte(tape_player_.get_tape());
+							auto flags = z80_.value_of(CPU::Z80::Register::Flags);
 
-						// Seed with the current pulse; the CPC will have finished the
-						// preceding symbol and be a short way into the pulse that should determine the
-						// first bit of this byte.
-						parser.process_pulse(tape_player_.get_current_pulse());
-						const auto byte = parser.get_byte(tape_player_.get_tape());
-						auto flags = z80_.value_of(CPU::Z80::Register::Flags);
+							if(byte) {
+								// In A ROM-esque fashion, begin the first pulse after the final one
+								// that was just consumed.
+								tape_player_.complete_pulse();
 
-						if(byte) {
-							// In A ROM-esque fashion, begin the first pulse after the final one
-							// that was just consumed.
-							tape_player_.complete_pulse();
+								// Update in-memory CRC.
+								auto crc_value =
+									uint16_t(
+										read_pointers_[tape_crc_address >> 14][tape_crc_address & 16383] |
+										(read_pointers_[(tape_crc_address+1) >> 14][(tape_crc_address+1) & 16383] << 8)
+									);
 
-							// Update in-memory CRC.
-							auto crc_value =
-								uint16_t(
-									read_pointers_[tape_crc_address >> 14][tape_crc_address & 16383] |
-									(read_pointers_[(tape_crc_address+1) >> 14][(tape_crc_address+1) & 16383] << 8)
-								);
+								tape_crc_.set_value(crc_value);
+								tape_crc_.add(*byte);
+								crc_value = tape_crc_.get_value();
 
-							tape_crc_.set_value(crc_value);
-							tape_crc_.add(*byte);
-							crc_value = tape_crc_.get_value();
+								write_pointers_[tape_crc_address >> 14][tape_crc_address & 16383] = uint8_t(crc_value);
+								write_pointers_[(tape_crc_address+1) >> 14][(tape_crc_address+1) & 16383] = uint8_t(crc_value >> 8);
 
-							write_pointers_[tape_crc_address >> 14][tape_crc_address & 16383] = uint8_t(crc_value);
-							write_pointers_[(tape_crc_address+1) >> 14][(tape_crc_address+1) & 16383] = uint8_t(crc_value >> 8);
+								// Indicate successful byte read.
+								z80_.set_value_of(CPU::Z80::Register::A, *byte);
+								flags |= CPU::Z80::Flag::Carry;
+							} else {
+								// TODO: return tape player to previous state and decline to serve.
+								z80_.set_value_of(CPU::Z80::Register::A, 0);
+								flags &= ~CPU::Z80::Flag::Carry;
+							}
+							z80_.set_value_of(CPU::Z80::Register::Flags, flags);
 
-							// Indicate successful byte read.
-							z80_.set_value_of(CPU::Z80::Register::A, *byte);
-							flags |= CPU::Z80::Flag::Carry;
-						} else {
-							// TODO: return tape player to previous state and decline to serve.
-							z80_.set_value_of(CPU::Z80::Register::A, 0);
-							flags &= ~CPU::Z80::Flag::Carry;
+							// RET.
+							*cycle.value = 0xc9;
+							break;
 						}
-						z80_.set_value_of(CPU::Z80::Register::Flags, flags);
 
-						// RET.
-						*cycle.value = 0xc9;
-						break;
-					}
+						if constexpr (catches_ssm) {
+							ssm_code_ = (ssm_code_ << 8) | read_pointers_[address >> 14][address & 16383];
+							if(ssm_delegate_) {
+								if((ssm_code_ & 0xff00ff00) == 0xed00ed00) {
+									const auto code = uint16_t(
+										((ssm_code_ << 8) & 0xff00) | ((ssm_code_ >> 16) & 0x00ff)
+									);
 
-					if constexpr (catches_ssm) {
-						ssm_code_ = (ssm_code_ << 8) | read_pointers_[address >> 14][address & 16383];
-						if(ssm_delegate_) {
-							if((ssm_code_ & 0xff00ff00) == 0xed00ed00) {
-								const auto code = uint16_t(
-									((ssm_code_ << 8) & 0xff00) | ((ssm_code_ >> 16) & 0x00ff)
-								);
+									const auto is_valid = [](uint8_t digit) {
+										return
+											(digit <= 0x3f) ||
+											(digit >= 0x7f && digit <= 0x9f) ||
+											(digit >= 0xa4 && digit <= 0xa7) ||
+											(digit >= 0xac && digit <= 0xaf) ||
+											(digit >= 0xb4 && digit <= 0xb7) ||
+											(digit >= 0xbc && digit <= 0xbf) ||
+											(digit >= 0xc0 && digit <= 0xfd);
+									};
 
-								const auto is_valid = [](uint8_t digit) {
-									return
-										(digit <= 0x3f) ||
-										(digit >= 0x7f && digit <= 0x9f) ||
-										(digit >= 0xa4 && digit <= 0xa7) ||
-										(digit >= 0xac && digit <= 0xaf) ||
-										(digit >= 0xb4 && digit <= 0xb7) ||
-										(digit >= 0xbc && digit <= 0xbf) ||
-										(digit >= 0xc0 && digit <= 0xfd);
-								};
-
-								if(
-									is_valid(static_cast<uint8_t>(code)) && is_valid(static_cast<uint8_t>(code >> 8))
-								) {
-									ssm_delegate_->perform(code);
-									ssm_code_ = 0;
+									if(
+										is_valid(static_cast<uint8_t>(code)) && is_valid(static_cast<uint8_t>(code >> 8))
+									) {
+										ssm_delegate_->perform(code);
+										ssm_code_ = 0;
+									}
+								} else if((ssm_code_ & 0xffff) == 0xedfe) {
+									ssm_delegate_->perform(0xfffe);
+								} else if((ssm_code_ & 0xffff) == 0xedff) {
+									ssm_delegate_->perform(0xffff);
 								}
-							} else if((ssm_code_ & 0xffff) == 0xedfe) {
-								ssm_delegate_->perform(0xfffe);
-							} else if((ssm_code_ & 0xffff) == 0xedff) {
-								ssm_delegate_->perform(0xffff);
 							}
 						}
-					}
-				[[fallthrough]];
+					[[fallthrough]];
 
-				case CPU::Z80::PartialMachineCycle::Read:
-					*cycle.value = read_pointers_[address >> 14][address & 16383];
-				break;
+					case CPU::Z80::PartialMachineCycle::Read:
+						*cycle.value = read_pointers_[address >> 14][address & 16383];
+					break;
 
-				case CPU::Z80::PartialMachineCycle::Write:
-					write_pointers_[address >> 14][address & 16383] = *cycle.value;
-				break;
+					case CPU::Z80::PartialMachineCycle::Write:
+						write_pointers_[address >> 14][address & 16383] = *cycle.value;
+					break;
 
-				case CPU::Z80::PartialMachineCycle::Output:
-					// Check for a gate array access.
-					if((address & 0xc000) == 0x4000) {
-						write_to_gate_array(*cycle.value);
-					}
-
-					// Check for an upper ROM selection
-					if constexpr (has_fdc) {
-						if(!(address&0x2000)) {
-							upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC;
-							if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data();
+					case CPU::Z80::PartialMachineCycle::Output:
+						// Check for a gate array access.
+						if((address & 0xc000) == 0x4000) {
+							write_to_gate_array(*cycle.value);
 						}
-					}
 
-					// Check for a CRTC access
-					if(!(address & 0x4000)) {
-						switch((address >> 8) & 3) {
-							case 0:	crtc_.select_register(*cycle.value);	break;
-							case 1:	crtc_.set_register(*cycle.value);		break;
-							default: break;
+						// Check for an upper ROM selection
+						if constexpr (has_fdc) {
+							if(!(address&0x2000)) {
+								upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC;
+								if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data();
+							}
 						}
-					}
 
-					// Check for an 8255 PIO access
-					if(!(address & 0x800)) {
-						i8255_.write((address >> 8) & 3, *cycle.value);
-					}
+						// Check for a CRTC access
+						if(!(address & 0x4000)) {
+							switch((address >> 8) & 3) {
+								case 0:	crtc_.select_register(*cycle.value);	break;
+								case 1:	crtc_.set_register(*cycle.value);		break;
+								default: break;
+							}
+						}
+
+						// Check for an 8255 PIO access
+						if(!(address & 0x800)) {
+							i8255_.write((address >> 8) & 3, *cycle.value);
+						}
+
+						if constexpr (has_fdc) {
+							// Check for an FDC access
+							if((address & 0x580) == 0x100) {
+								flush_fdc();
+								fdc_.write(address & 1, *cycle.value);
+							}
+
+							// Check for a disk motor access
+							if(!(address & 0x580)) {
+								flush_fdc();
+								fdc_.set_motor_on(!!(*cycle.value));
+							}
+						}
+					break;
+
+					case CPU::Z80::PartialMachineCycle::Input:
+						// Default to nothing answering
+						*cycle.value = 0xff;
+
+						// Check for a PIO access
+						if(!(address & 0x800)) {
+							*cycle.value &= i8255_.read((address >> 8) & 3);
+						}
 
-					if constexpr (has_fdc) {
 						// Check for an FDC access
-						if((address & 0x580) == 0x100) {
-							flush_fdc();
-							fdc_.write(address & 1, *cycle.value);
+						if constexpr (has_fdc) {
+							if((address & 0x580) == 0x100) {
+								flush_fdc();
+								*cycle.value &= fdc_.read(address & 1);
+							}
 						}
 
-						// Check for a disk motor access
-						if(!(address & 0x580)) {
-							flush_fdc();
-							fdc_.set_motor_on(!!(*cycle.value));
+						// Check for a CRTC access; the below is not a typo, the CRTC can be selected
+						// for writing via an input, and will sample whatever happens to be available
+						if(!(address & 0x4000)) {
+							switch((address >> 8) & 3) {
+								case 0:	crtc_.select_register(*cycle.value);	break;
+								case 1:	crtc_.set_register(*cycle.value);		break;
+								case 2: *cycle.value &= crtc_.get_status();		break;
+								case 3:	*cycle.value &= crtc_.get_register();	break;
+							}
 						}
-					}
-				break;
 
-				case CPU::Z80::PartialMachineCycle::Input:
-					// Default to nothing answering
-					*cycle.value = 0xff;
-
-					// Check for a PIO access
-					if(!(address & 0x800)) {
-						*cycle.value &= i8255_.read((address >> 8) & 3);
-					}
-
-					// Check for an FDC access
-					if constexpr (has_fdc) {
-						if((address & 0x580) == 0x100) {
-							flush_fdc();
-							*cycle.value &= fdc_.read(address & 1);
+						// As with the CRTC, the gate array will sample the bus if the address decoding
+						// implies that it should, unaware of data direction
+						if((address & 0xc000) == 0x4000) {
+							write_to_gate_array(*cycle.value);
 						}
-					}
+					break;
 
-					// Check for a CRTC access; the below is not a typo, the CRTC can be selected
-					// for writing via an input, and will sample whatever happens to be available
-					if(!(address & 0x4000)) {
-						switch((address >> 8) & 3) {
-							case 0:	crtc_.select_register(*cycle.value);	break;
-							case 1:	crtc_.set_register(*cycle.value);		break;
-							case 2: *cycle.value &= crtc_.get_status();		break;
-							case 3:	*cycle.value &= crtc_.get_register();	break;
-						}
-					}
+					case CPU::Z80::PartialMachineCycle::Interrupt:
+						// Nothing is loaded onto the bus during an interrupt acknowledge, but
+						// the fact of the acknowledge needs to be posted on to the interrupt timer.
+						*cycle.value = 0xff;
+						interrupt_timer_.signal_interrupt_acknowledge();
+					break;
 
-					// As with the CRTC, the gate array will sample the bus if the address decoding
-					// implies that it should, unaware of data direction
-					if((address & 0xc000) == 0x4000) {
-						write_to_gate_array(*cycle.value);
-					}
-				break;
+					default: break;
+				}
 
-				case CPU::Z80::PartialMachineCycle::Interrupt:
-					// Nothing is loaded onto the bus during an interrupt acknowledge, but
-					// the fact of the acknowledge needs to be posted on to the interrupt timer.
-					*cycle.value = 0xff;
-					interrupt_timer_.signal_interrupt_acknowledge();
-				break;
-
-				default: break;
+				// Check whether the interrupt signal has changed due to CPU intervention.
+				if(interrupt_timer_.request_has_changed()) z80_.set_interrupt_line(interrupt_timer_.get_request());
 			}
 
-			// Check whether the interrupt signal has changed the other way.
-			if(interrupt_timer_.request_has_changed()) z80_.set_interrupt_line(interrupt_timer_.get_request());
+			update_subsystems();
 
 			// This implementation doesn't use time-stuffing; once in-phase waits won't be longer
 			// than a single cycle so there's no real performance benefit to trying to find the
diff --git a/OSBindings/Mac/Clock SignalTests/CPCShakerTests.mm b/OSBindings/Mac/Clock SignalTests/CPCShakerTests.mm
index 4173d0ee3..06e0283d9 100644
--- a/OSBindings/Mac/Clock SignalTests/CPCShakerTests.mm	
+++ b/OSBindings/Mac/Clock SignalTests/CPCShakerTests.mm	
@@ -43,9 +43,6 @@ struct ScanTarget: public Outputs::Display::ScanTarget {
 		const int src_pixels = scan_.end_points[1].data_offset - scan_.end_points[0].data_offset;
 		const int dst_pixels = (scan_.end_points[1].x - scan_.end_points[0].x) / WidthDivider;
 
-		const int step = (src_pixels << 16) / dst_pixels;
-		int position = 0;
-
 		const auto x1 = scan_.end_points[0].x / WidthDivider;
 		const auto x2 = scan_.end_points[1].x / WidthDivider;
 
@@ -53,9 +50,15 @@ struct ScanTarget: public Outputs::Display::ScanTarget {
 		if(x_ < x1) {
 			std::fill(&line[x_], &line[x1], 0);
 		}
-		for(int x = x1; x < x2; x++) {
-			line[x] = data_[position >> 16];
-			position += step;
+
+		if(x2 != x1) {
+			const int step = (src_pixels << 16) / dst_pixels;
+			int position = 0;
+
+			for(int x = x1; x < x2; x++) {
+				line[x] = data_[position >> 16];
+				position += step;
+			}
 		}
 		x_ = x2;
 	}