mirror of
				https://github.com/TomHarte/CLK.git
				synced 2025-10-31 05:16:08 +00:00 
			
		
		
		
	Compare commits
	
		
			188 Commits
		
	
	
		
			2020-07-20
			...
			2020-09-17
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8fc3496cc9 | ||
|  | e807a462a1 | ||
|  | 18790a90ae | ||
|  | 21afc70261 | ||
|  | 7bb74af478 | ||
|  | 894269aa06 | ||
|  | 8b16da9695 | ||
|  | f783ec6269 | ||
|  | 22c9734874 | ||
|  | a17d0e428f | ||
|  | bb57f0bcc7 | ||
|  | b1aefbfe85 | ||
|  | 061288f5a7 | ||
|  | 5a53474536 | ||
|  | 18d0fff8da | ||
|  | 0ac2145740 | ||
|  | bc8787ded6 | ||
|  | 69d21daaa3 | ||
|  | 5651ef606d | ||
|  | b831b31382 | ||
|  | 2fd5cc056c | ||
|  | 82dbdf7dfc | ||
|  | eb9903cd10 | ||
|  | 227e98d6d7 | ||
|  | 35476063b7 | ||
|  | 8557bb2136 | ||
|  | c0c7818d5d | ||
|  | ceeadd6a33 | ||
|  | 1a2545fdea | ||
|  | c5e9a74c88 | ||
|  | d7972a7b86 | ||
|  | 7dd4c67304 | ||
|  | e113780fd1 | ||
|  | e32ae6c191 | ||
|  | bcaceff378 | ||
|  | d7b405c6f8 | ||
|  | edf8cf4dc6 | ||
|  | dfcc8e9822 | ||
|  | 016e96e6f8 | ||
|  | e7ce03c418 | ||
|  | 3d392dd81d | ||
|  | 42d810db7f | ||
|  | 18571e8351 | ||
|  | dda1649ab7 | ||
|  | c82e0df071 | ||
|  | 06b7ea5a6e | ||
|  | c49fcb9ec9 | ||
|  | 0e44d6d214 | ||
|  | 6adad7fbf5 | ||
|  | de6ed7b615 | ||
|  | 07dcb4dbb1 | ||
|  | e99896eadc | ||
|  | 489701afcb | ||
|  | 55e576cc57 | ||
|  | 6bd8ec9545 | ||
|  | 5cd8d86eef | ||
|  | 74d0acdaec | ||
|  | 0288a1974b | ||
|  | 6efd8782fe | ||
|  | 8bab9d5d60 | ||
|  | 6ef1dfd8be | ||
|  | 7e58648743 | ||
|  | 0f0c3e616d | ||
|  | c7ce65ea4c | ||
|  | c36247b609 | ||
|  | 15296e43a4 | ||
|  | f2929230a2 | ||
|  | bf252b8061 | ||
|  | 9e2bf2af7e | ||
|  | 245f2654f0 | ||
|  | 67ca298a72 | ||
|  | 67d4dbf91a | ||
|  | b344269140 | ||
|  | bb547610f2 | ||
|  | 1e1f007bb7 | ||
|  | c40d858f02 | ||
|  | 3d564d85fd | ||
|  | 02cea40ffa | ||
|  | e502d336db | ||
|  | 807cb99f6d | ||
|  | 8b6879a782 | ||
|  | 7ca0362f23 | ||
|  | 56c7bd242a | ||
|  | 5c6112415a | ||
|  | bf6a0c9fc4 | ||
|  | d54b937ab6 | ||
|  | 7c23c32e44 | ||
|  | 4e21d24b5f | ||
|  | ad6fb85fda | ||
|  | 5dc39a5d24 | ||
|  | 3597f687de | ||
|  | 8811506adf | ||
|  | 11dec6fc0f | ||
|  | 59c4c8233f | ||
|  | 9da79d2d81 | ||
|  | 246b474a25 | ||
|  | 27e8a3a1b5 | ||
|  | 745797b596 | ||
|  | 940e9e037e | ||
|  | 512c0079a9 | ||
|  | 645c29f853 | ||
|  | e55945674d | ||
|  | 7ac88536dd | ||
|  | 230b9fc9e6 | ||
|  | 27ca782cac | ||
|  | a136a00a2f | ||
|  | 637ec35d6a | ||
|  | 4b55df1cb4 | ||
|  | b9309268ba | ||
|  | 8fa89baf54 | ||
|  | 8374a5e579 | ||
|  | 525233e10b | ||
|  | eadda6a967 | ||
|  | 3d6590af89 | ||
|  | 28d933d5d6 | ||
|  | c1dc42a094 | ||
|  | 6384ff3ee7 | ||
|  | a118594c8b | ||
|  | 93c6105442 | ||
|  | ced4a75a1a | ||
|  | 57fecdc09e | ||
|  | cd491bb6e0 | ||
|  | f16ad8f71d | ||
|  | e340685a99 | ||
|  | df89a8771c | ||
|  | bdcf266e45 | ||
|  | edf41b06fd | ||
|  | 38960a08d6 | ||
|  | fbda7aab23 | ||
|  | c575aa0640 | ||
|  | 583f6b1ba2 | ||
|  | bb55ecc101 | ||
|  | 4421acef34 | ||
|  | 4c9418f59a | ||
|  | 219923bd63 | ||
|  | 7551782a25 | ||
|  | 5c836604c0 | ||
|  | eff24a8726 | ||
|  | 72df6e52cd | ||
|  | e235a45abb | ||
|  | d20c11e401 | ||
|  | 693b889fdd | ||
|  | 671f48dc10 | ||
|  | 7b1708f0bc | ||
|  | f34a9b4346 | ||
|  | 1e6d03246b | ||
|  | cdde57fcf2 | ||
|  | c0a61ac1ee | ||
|  | 9c97c0a906 | ||
|  | 8cacab196d | ||
|  | b14bedbe29 | ||
|  | 6bc66d8b96 | ||
|  | 23f381f381 | ||
|  | 51ad423eca | ||
|  | 72a8fef989 | ||
|  | 02f41ee513 | ||
|  | 9410594486 | ||
|  | 1c6223cc11 | ||
|  | 82d6a5387f | ||
|  | 5165e65021 | ||
|  | 1942742d73 | ||
|  | b7760bb052 | ||
|  | 2470055d90 | ||
|  | 62be2a2eec | ||
|  | b1e062945e | ||
|  | 3db4a8c312 | ||
|  | db8e1b0edf | ||
|  | 71c3f58c99 | ||
|  | 7c05b1788e | ||
|  | 77c5b86acc | ||
|  | bc6426313e | ||
|  | 8bef7ff4c5 | ||
|  | a2db6ddea5 | ||
|  | f9f500c194 | ||
|  | 6ad1e3e17e | ||
|  | e097a841d2 | ||
|  | fa95a17af5 | ||
|  | b961665985 | ||
|  | 8af35bc6bb | ||
|  | 9b75287a52 | ||
|  | 84d5316aa7 | ||
|  | 89acb70091 | ||
|  | 66165a6dea | ||
|  | 84dcf9925b | ||
|  | ee1d7eb61f | ||
|  | e260f92988 | ||
|  | 74788ccf8e | ||
|  | 0da5c07942 | 
| @@ -10,12 +10,12 @@ | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) : | ||||
| 	keyboard_(machines_) { | ||||
| MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	for(const auto &machine: machines) { | ||||
| 		auto keyboard_machine = machine->keyboard_machine(); | ||||
| 		if(keyboard_machine) machines_.push_back(keyboard_machine); | ||||
| 	} | ||||
| 	keyboard_ = std::make_unique<MultiKeyboard>(machines_); | ||||
| } | ||||
|  | ||||
| void MultiKeyboardMachine::clear_all_keys() { | ||||
| @@ -45,7 +45,7 @@ bool MultiKeyboardMachine::can_type(char c) const { | ||||
| } | ||||
|  | ||||
| Inputs::Keyboard &MultiKeyboardMachine::get_keyboard() { | ||||
| 	return keyboard_; | ||||
| 	return *keyboard_; | ||||
| } | ||||
|  | ||||
| MultiKeyboardMachine::MultiKeyboard::MultiKeyboard(const std::vector<::MachineTypes::KeyboardMachine *> &machines) | ||||
|   | ||||
| @@ -42,7 +42,7 @@ class MultiKeyboardMachine: public MachineTypes::KeyboardMachine { | ||||
| 				std::set<Key> observed_keys_; | ||||
| 				bool is_exclusive_ = false; | ||||
| 		}; | ||||
| 		MultiKeyboard keyboard_; | ||||
| 		std::unique_ptr<MultiKeyboard> keyboard_; | ||||
|  | ||||
| 	public: | ||||
| 		MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|   | ||||
| @@ -60,12 +60,9 @@ void MultiSpeaker::set_output_volume(float volume) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) { | ||||
| 	delegate_ = delegate; | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) { | ||||
| 	if(!delegate_) return; | ||||
| 	auto delegate = delegate_.load(std::memory_order::memory_order_relaxed); | ||||
| 	if(!delegate) return; | ||||
| 	{ | ||||
| 		std::lock_guard lock_guard(front_speaker_mutex_); | ||||
| 		if(speaker != front_speaker_) return; | ||||
| @@ -74,12 +71,13 @@ void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vec | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::speaker_did_change_input_clock(Speaker *speaker) { | ||||
| 	if(!delegate_) return; | ||||
| 	auto delegate = delegate_.load(std::memory_order::memory_order_relaxed); | ||||
| 	if(!delegate) return; | ||||
| 	{ | ||||
| 		std::lock_guard lock_guard(front_speaker_mutex_); | ||||
| 		if(speaker != front_speaker_) return; | ||||
| 	} | ||||
| 	delegate_->speaker_did_change_input_clock(this); | ||||
| 	delegate->speaker_did_change_input_clock(this); | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::set_new_front_machine(::Machine::DynamicMachine *machine) { | ||||
| @@ -87,7 +85,8 @@ void MultiSpeaker::set_new_front_machine(::Machine::DynamicMachine *machine) { | ||||
| 		std::lock_guard lock_guard(front_speaker_mutex_); | ||||
| 		front_speaker_ = machine->audio_producer()->get_speaker(); | ||||
| 	} | ||||
| 	if(delegate_) { | ||||
| 		delegate_->speaker_did_change_input_clock(this); | ||||
| 	auto delegate = delegate_.load(std::memory_order::memory_order_relaxed); | ||||
| 	if(delegate) { | ||||
| 		delegate->speaker_did_change_input_clock(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -40,7 +40,6 @@ 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_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) override; | ||||
| 		void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override; | ||||
| 		bool get_is_stereo() override; | ||||
| 		void set_output_volume(float) override; | ||||
|  | ||||
| @@ -51,7 +50,6 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker: | ||||
|  | ||||
| 		std::vector<Outputs::Speaker::Speaker *> speakers_; | ||||
| 		Outputs::Speaker::Speaker *front_speaker_ = nullptr; | ||||
| 		Outputs::Speaker::Speaker::Delegate *delegate_ = nullptr; | ||||
| 		std::mutex front_speaker_mutex_; | ||||
|  | ||||
| 		bool stereo_output_ = false; | ||||
|   | ||||
| @@ -89,9 +89,28 @@ void MultiMachine::did_run_machines(MultiTimedMachine *) { | ||||
|  | ||||
| void MultiMachine::pick_first() { | ||||
| 	has_picked_ = true; | ||||
|  | ||||
| 	// Ensure output rate specifics are properly copied; these may be set only once by the owner, | ||||
| 	// but rather than being propagated directly by the MultiSpeaker only the derived computed | ||||
| 	// output rate is propagated. So this ensures that if a new derivation is made, it's made correctly. | ||||
| 	if(machines_[0]->audio_producer()) { | ||||
| 		auto multi_speaker = audio_producer_.get_speaker(); | ||||
| 		auto specific_speaker = machines_[0]->audio_producer()->get_speaker(); | ||||
|  | ||||
| 		if(specific_speaker && multi_speaker) { | ||||
| 			specific_speaker->copy_output_rate(*multi_speaker); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// TODO: because it is not invalid for a caller to keep a reference to anything previously returned, | ||||
| 	// this erase can be added only once the Multi machines that take static copies of the machines list | ||||
| 	// are updated. | ||||
| 	// | ||||
| 	// Example failing use case otherwise: a caller still has reference to the MultiJoystickMachine, and | ||||
| 	// it has dangling references to the various JoystickMachines. | ||||
| 	// | ||||
| 	// This gets into particularly long grass with the MultiConfigurable and its MultiStruct. | ||||
| //	machines_.erase(machines_.begin() + 1, machines_.end()); | ||||
| 	// TODO: this isn't quite correct, because it may leak OpenGL/etc resources through failure to | ||||
| 	// request a close_output while the context is active. | ||||
| } | ||||
|  | ||||
| void *MultiMachine::raw_pointer() { | ||||
|   | ||||
| @@ -63,6 +63,13 @@ class VSyncPredictor { | ||||
| 			frame_duration_ = Nanos(1'000'000'000.0f / rate); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			@returns The time this class currently believes a whole frame occupies. | ||||
| 		*/ | ||||
| 		Time::Nanos frame_duration() { | ||||
| 			return frame_duration_; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Adds a record of how much jitter was experienced in scheduling; these values will be | ||||
| 			factored into the @c suggested_draw_time if supplied. | ||||
| @@ -87,15 +94,13 @@ class VSyncPredictor { | ||||
| 			(if those figures are being supplied). | ||||
| 		*/ | ||||
| 		Nanos suggested_draw_time() { | ||||
| 			const auto mean = redraw_period_.mean() - timer_jitter_.mean() - vsync_jitter_.mean(); | ||||
| 			const auto mean = redraw_period_.mean() + timer_jitter_.mean() + vsync_jitter_.mean(); | ||||
| 			const auto variance = redraw_period_.variance() + timer_jitter_.variance() + vsync_jitter_.variance(); | ||||
|  | ||||
| 			// Permit three standard deviations from the mean, to cover 99.9% of cases. | ||||
| 			const auto period = mean - Nanos(3.0f * sqrt(float(variance))); | ||||
| 			const auto period = mean + Nanos(3.0f * sqrt(float(variance))); | ||||
|  | ||||
| 			assert(abs(period) < 10'000'000'000); | ||||
|  | ||||
| 			return last_vsync_ + period; | ||||
| 			return last_vsync_ + frame_duration_ - period; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| @@ -109,7 +114,6 @@ class VSyncPredictor { | ||||
| 				} | ||||
|  | ||||
| 				void post(Time::Nanos value) { | ||||
| 					assert(abs(value) < 10'000'000'000);	// 10 seconds is a very liberal maximum. | ||||
| 					sum_ -= history_[write_pointer_]; | ||||
| 					sum_ += value; | ||||
| 					history_[write_pointer_] = value; | ||||
|   | ||||
| @@ -8,6 +8,10 @@ | ||||
|  | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| // As-yet unimplemented (incomplete list): | ||||
| // | ||||
| //	PB6 count-down mode for timer 2. | ||||
|  | ||||
| namespace MOS { | ||||
| namespace MOS6522 { | ||||
|  | ||||
| @@ -34,7 +38,7 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) { | ||||
| 	address &= 0xf; | ||||
| 	access(address); | ||||
| 	switch(address) { | ||||
| 		case 0x0:	// Write Port B. | ||||
| 		case 0x0:	// Write Port B. ('ORB') | ||||
| 			// Store locally and communicate outwards. | ||||
| 			registers_.output[1] = value; | ||||
|  | ||||
| @@ -45,7 +49,7 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) { | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
| 		case 0xf: | ||||
| 		case 0x1:	// Write Port A. | ||||
| 		case 0x1:	// Write Port A. ('ORA') | ||||
| 			registers_.output[0] = value; | ||||
|  | ||||
| 			bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| @@ -59,28 +63,38 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) { | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
|  | ||||
| 		case 0x2:	// Port B direction. | ||||
| 		case 0x2:	// Port B direction ('DDRB'). | ||||
| 			registers_.data_direction[1] = value; | ||||
| 		break; | ||||
| 		case 0x3:	// Port A direction. | ||||
| 		case 0x3:	// Port A direction ('DDRA'). | ||||
| 			registers_.data_direction[0] = value; | ||||
| 		break; | ||||
|  | ||||
| 		// Timer 1 | ||||
| 		case 0x6:	case 0x4:	registers_.timer_latch[0] = (registers_.timer_latch[0]&0xff00) | value;	break; | ||||
| 		case 0x5:	case 0x7: | ||||
| 		case 0x6:	case 0x4:	// ('T1L-L' and 'T1C-L') | ||||
| 			registers_.timer_latch[0] = (registers_.timer_latch[0]&0xff00) | value; | ||||
| 		break; | ||||
| 		case 0x7:	// Timer 1 latch, high ('T1L-H'). | ||||
| 			registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | uint16_t(value << 8); | ||||
| 		break; | ||||
| 		case 0x5:	// Timer 1 counter, high ('T1C-H'). | ||||
| 			// Fill latch. | ||||
| 			registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | uint16_t(value << 8); | ||||
|  | ||||
| 			// Restart timer. | ||||
| 			registers_.next_timer[0] = registers_.timer_latch[0]; | ||||
| 			timer_is_running_[0] = true; | ||||
|  | ||||
| 			// Clear existing interrupt flag. | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer1; | ||||
| 			if(address == 0x05) { | ||||
| 				registers_.next_timer[0] = registers_.timer_latch[0]; | ||||
| 				timer_is_running_[0] = true; | ||||
| 			} | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
|  | ||||
| 		// Timer 2 | ||||
| 		case 0x8:	registers_.timer_latch[1] = value;	break; | ||||
| 		case 0x9: | ||||
| 		case 0x8:	// ('T2C-L') | ||||
| 			registers_.timer_latch[1] = value; | ||||
| 		break; | ||||
| 		case 0x9:	// ('T2C-H') | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer2; | ||||
| 			registers_.next_timer[1] = registers_.timer_latch[1] | uint16_t(value << 8); | ||||
| 			timer_is_running_[1] = true; | ||||
| @@ -88,7 +102,7 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) { | ||||
| 		break; | ||||
|  | ||||
| 		// Shift | ||||
| 		case 0xa: | ||||
| 		case 0xa:	// ('SR') | ||||
| 			registers_.shift = value; | ||||
| 			shift_bits_remaining_ = 8; | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister; | ||||
| @@ -96,11 +110,11 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) { | ||||
| 		break; | ||||
|  | ||||
| 		// Control | ||||
| 		case 0xb: | ||||
| 		case 0xb:	// Auxiliary control ('ACR'). | ||||
| 			registers_.auxiliary_control = value; | ||||
| 			evaluate_cb2_output(); | ||||
| 		break; | ||||
| 		case 0xc: { | ||||
| 		case 0xc: {	// Peripheral control ('PCR'). | ||||
| //			const auto old_peripheral_control = registers_.peripheral_control; | ||||
| 			registers_.peripheral_control = value; | ||||
|  | ||||
| @@ -141,11 +155,11 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) { | ||||
| 		} break; | ||||
|  | ||||
| 		// Interrupt control | ||||
| 		case 0xd: | ||||
| 		case 0xd:	// Interrupt flag regiser ('IFR'). | ||||
| 			registers_.interrupt_flags &= ~value; | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
| 		case 0xe: | ||||
| 		case 0xe:	// Interrupt enable register ('IER'). | ||||
| 			if(value&0x80) | ||||
| 				registers_.interrupt_enable |= value; | ||||
| 			else | ||||
| @@ -159,46 +173,46 @@ template <typename T> uint8_t MOS6522<T>::read(int address) { | ||||
| 	address &= 0xf; | ||||
| 	access(address); | ||||
| 	switch(address) { | ||||
| 		case 0x0: | ||||
| 		case 0x0:	// Read Port B ('IRB'). | ||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge); | ||||
| 			reevaluate_interrupts(); | ||||
| 		return get_port_input(Port::B, registers_.data_direction[1], registers_.output[1]); | ||||
| 		case 0xf: | ||||
| 		case 0x1: | ||||
| 		case 0x1:	// Read Port A ('IRA'). | ||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge); | ||||
| 			reevaluate_interrupts(); | ||||
| 		return get_port_input(Port::A, registers_.data_direction[0], registers_.output[0]); | ||||
|  | ||||
| 		case 0x2:	return registers_.data_direction[1]; | ||||
| 		case 0x3:	return registers_.data_direction[0]; | ||||
| 		case 0x2:	return registers_.data_direction[1];	// Port B direction ('DDRB'). | ||||
| 		case 0x3:	return registers_.data_direction[0];	// Port A direction ('DDRA'). | ||||
|  | ||||
| 		// Timer 1 | ||||
| 		case 0x4: | ||||
| 		case 0x4:	// Timer 1 low-order latches ('T1L-L'). | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer1; | ||||
| 			reevaluate_interrupts(); | ||||
| 		return registers_.timer[0] & 0x00ff; | ||||
| 		case 0x5:	return registers_.timer[0] >> 8; | ||||
| 		case 0x6:	return registers_.timer_latch[0] & 0x00ff; | ||||
| 		case 0x7:	return registers_.timer_latch[0] >> 8; | ||||
| 		case 0x5:	return registers_.timer[0] >> 8;			// Timer 1 high-order counter ('T1C-H') | ||||
| 		case 0x6:	return registers_.timer_latch[0] & 0x00ff;	// Timer 1 low-order latches ('T1L-L'). | ||||
| 		case 0x7:	return registers_.timer_latch[0] >> 8;		// Timer 1 high-order latches ('T1L-H'). | ||||
|  | ||||
| 		// Timer 2 | ||||
| 		case 0x8: | ||||
| 		case 0x8:	// Timer 2 low-order counter ('T2C-L'). | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer2; | ||||
| 			reevaluate_interrupts(); | ||||
| 		return registers_.timer[1] & 0x00ff; | ||||
| 		case 0x9:	return registers_.timer[1] >> 8; | ||||
| 		case 0x9:	return registers_.timer[1] >> 8;	// Timer 2 high-order counter ('T2C-H'). | ||||
|  | ||||
| 		case 0xa: | ||||
| 		case 0xa:	// Shift register ('SR'). | ||||
| 			shift_bits_remaining_ = 8; | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister; | ||||
| 			reevaluate_interrupts(); | ||||
| 		return registers_.shift; | ||||
|  | ||||
| 		case 0xb:	return registers_.auxiliary_control; | ||||
| 		case 0xc:	return registers_.peripheral_control; | ||||
| 		case 0xb:	return registers_.auxiliary_control;	// Auxiliary control ('ACR'). | ||||
| 		case 0xc:	return registers_.peripheral_control;	// Peripheral control ('PCR'). | ||||
|  | ||||
| 		case 0xd:	return registers_.interrupt_flags | (get_interrupt_line() ? 0x80 : 0x00); | ||||
| 		case 0xe:	return registers_.interrupt_enable | 0x80; | ||||
| 		case 0xd:	return registers_.interrupt_flags | (get_interrupt_line() ? 0x80 : 0x00);	// Interrupt flag register ('IFR'). | ||||
| 		case 0xe:	return registers_.interrupt_enable | 0x80;									// Interrupt enable register ('IER'). | ||||
| 	} | ||||
|  | ||||
| 	return 0xff; | ||||
| @@ -276,10 +290,13 @@ template <typename T> void MOS6522<T>::do_phase2() { | ||||
| 		registers_.timer_needs_reload = false; | ||||
| 		registers_.timer[0] = registers_.timer_latch[0]; | ||||
| 	} else { | ||||
| 		registers_.timer[0] --; | ||||
| 		-- registers_.timer[0]; | ||||
| 	} | ||||
|  | ||||
| 	registers_.timer[1] --; | ||||
| 	// Count down timer 2 if it is in timed interrupt mode (i.e. auxiliary control bit 5 is clear). | ||||
| 	// TODO: implement count down on PB6 if this bit isn't set. | ||||
| 	registers_.timer[1] -= 1 ^ ((registers_.auxiliary_control >> 5)&1); | ||||
|  | ||||
| 	if(registers_.next_timer[0] >= 0) { | ||||
| 		registers_.timer[0] = uint16_t(registers_.next_timer[0]); | ||||
| 		registers_.next_timer[0] = -1; | ||||
|   | ||||
| @@ -445,20 +445,20 @@ template <class BusHandler> class MOS6560 { | ||||
| 		// register state | ||||
| 		struct { | ||||
| 			bool interlaced = false, tall_characters = false; | ||||
| 			uint8_t first_column_location, first_row_location; | ||||
| 			uint8_t number_of_columns, number_of_rows; | ||||
| 			uint16_t character_cell_start_address, video_matrix_start_address; | ||||
| 			uint16_t backgroundColour, borderColour, auxiliary_colour; | ||||
| 			uint8_t first_column_location = 0, first_row_location = 0; | ||||
| 			uint8_t number_of_columns = 0, number_of_rows = 0; | ||||
| 			uint16_t character_cell_start_address = 0, video_matrix_start_address = 0; | ||||
| 			uint16_t backgroundColour = 0, borderColour = 0, auxiliary_colour = 0; | ||||
| 			bool invertedCells = false; | ||||
|  | ||||
| 			uint8_t direct_values[16]; | ||||
| 			uint8_t direct_values[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; | ||||
| 		} registers_; | ||||
|  | ||||
| 		// output state | ||||
| 		enum State { | ||||
| 			Sync, ColourBurst, Border, Pixels | ||||
| 		} this_state_, output_state_; | ||||
| 		int cycles_in_state_; | ||||
| 		} this_state_ = State::Sync, output_state_ = State::Sync; | ||||
| 		int cycles_in_state_ = 0; | ||||
|  | ||||
| 		// counters that cover an entire field | ||||
| 		int horizontal_counter_ = 0, vertical_counter_ = 0; | ||||
| @@ -487,23 +487,23 @@ template <class BusHandler> class MOS6560 { | ||||
|  | ||||
| 		// latches dictating start and length of drawing | ||||
| 		bool vertical_drawing_latch_ = false, horizontal_drawing_latch_ = false; | ||||
| 		int rows_this_field_, columns_this_line_; | ||||
| 		int rows_this_field_ = 0, columns_this_line_ = 0; | ||||
|  | ||||
| 		// current drawing position counter | ||||
| 		int pixel_line_cycle_, column_counter_; | ||||
| 		int current_row_; | ||||
| 		uint16_t current_character_row_; | ||||
| 		uint16_t video_matrix_address_counter_, base_video_matrix_address_counter_; | ||||
| 		int pixel_line_cycle_ = 0, column_counter_ = 0; | ||||
| 		int current_row_ = 0; | ||||
| 		uint16_t current_character_row_ = 0; | ||||
| 		uint16_t video_matrix_address_counter_ = 0, base_video_matrix_address_counter_ = 0; | ||||
|  | ||||
| 		// data latched from the bus | ||||
| 		uint8_t character_code_, character_colour_, character_value_; | ||||
| 		uint8_t character_code_ = 0, character_colour_ = 0, character_value_ = 0; | ||||
|  | ||||
| 		bool is_odd_frame_ = false, is_odd_line_ = false; | ||||
|  | ||||
| 		// lookup table from 6560 colour index to appropriate PAL/NTSC value | ||||
| 		uint16_t colours_[16]; | ||||
| 		uint16_t colours_[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; | ||||
|  | ||||
| 		uint16_t *pixel_pointer; | ||||
| 		uint16_t *pixel_pointer = nullptr; | ||||
| 		void output_border(int number_of_cycles) { | ||||
| 			uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1)); | ||||
| 			if(colour_pointer) *colour_pointer = registers_.borderColour; | ||||
| @@ -511,13 +511,13 @@ template <class BusHandler> class MOS6560 { | ||||
| 		} | ||||
|  | ||||
| 		struct { | ||||
| 			int cycles_per_line; | ||||
| 			int line_counter_increment_offset; | ||||
| 			int final_line_increment_position; | ||||
| 			int lines_per_progressive_field; | ||||
| 			bool supports_interlacing; | ||||
| 			int cycles_per_line = 0; | ||||
| 			int line_counter_increment_offset = 0; | ||||
| 			int final_line_increment_position = 0; | ||||
| 			int lines_per_progressive_field = 0; | ||||
| 			bool supports_interlacing = 0; | ||||
| 		} timing_; | ||||
| 		OutputMode output_mode_; | ||||
| 		OutputMode output_mode_ = OutputMode::NTSC; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -316,6 +316,8 @@ void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) { | ||||
| 				} | ||||
| 				LOG("Receive bit count: " << receive_bit_count); | ||||
|  | ||||
| 				(void)receive_bit_count; | ||||
|  | ||||
| 				/* | ||||
| 					b7,b6: | ||||
| 						00 = 5 receive bits per character | ||||
|   | ||||
| @@ -190,7 +190,9 @@ void TMS9918::run_for(const HalfCycles cycles) { | ||||
| 	int read_cycles_pool = int_cycles; | ||||
|  | ||||
| 	while(write_cycles_pool || read_cycles_pool) { | ||||
| #ifndef NDEBUG | ||||
| 		LineBufferPointer backup = read_pointer_; | ||||
| #endif | ||||
|  | ||||
| 		if(write_cycles_pool) { | ||||
| 			// Determine how much writing to do. | ||||
| @@ -329,8 +331,10 @@ void TMS9918::run_for(const HalfCycles cycles) { | ||||
| 		} | ||||
|  | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 		assert(backup.row == read_pointer_.row && backup.column == read_pointer_.column); | ||||
| 		backup = write_pointer_; | ||||
| #endif | ||||
|  | ||||
|  | ||||
| 		if(read_cycles_pool) { | ||||
|   | ||||
| @@ -46,6 +46,8 @@ class Keyboard { | ||||
| 		/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys. | ||||
| 		Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers); | ||||
|  | ||||
| 		virtual ~Keyboard() {} | ||||
|  | ||||
| 		// Host interface. | ||||
|  | ||||
| 		/// @returns @c true if the key press affects the machine; @c false otherwise. | ||||
|   | ||||
| @@ -541,7 +541,17 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 						const int colour_burst_start = std::max(first_sync_column + sync_length + 1, column_); | ||||
| 						const int colour_burst_end = std::min(first_sync_column + sync_length + 4, ending_column); | ||||
| 						if(colour_burst_end > colour_burst_start) { | ||||
| 							crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, 0); | ||||
| 							// UGLY HACK AHOY! | ||||
| 							// The OpenGL scan target introduces a phase error of 1/8th of a wave. The Metal one does not. | ||||
| 							// Supply the real phase value if this is an Apple build. | ||||
| 							// TODO: eliminate UGLY HACK. | ||||
| #ifdef __APPLE__ | ||||
| 							constexpr int phase = 224; | ||||
| #else | ||||
| 							constexpr int phase = 0; | ||||
| #endif | ||||
|  | ||||
| 							crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, phase); | ||||
| 						} | ||||
|  | ||||
| 						second_blank_start = std::max(first_sync_column + sync_length + 3, column_); | ||||
|   | ||||
| @@ -25,7 +25,7 @@ Audio::Audio(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(tas | ||||
| void Audio::post_sample(uint8_t sample) { | ||||
| 	// Store sample directly indexed by current write pointer; this ensures that collected samples | ||||
| 	// directly map to volume and enabled/disabled states. | ||||
| 	sample_queue_.buffer[sample_queue_.write_pointer] = sample; | ||||
| 	sample_queue_.buffer[sample_queue_.write_pointer].store(sample, std::memory_order::memory_order_relaxed); | ||||
| 	sample_queue_.write_pointer = (sample_queue_.write_pointer + 1) % sample_queue_.buffer.size(); | ||||
| } | ||||
|  | ||||
| @@ -80,7 +80,7 @@ void Audio::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
|  | ||||
| 		// Determine the output level, and output that many samples. | ||||
| 		// (Hoping that the copiler substitutes an effective memset16-type operation here). | ||||
| 		const int16_t output_level = volume_multiplier_ * (int16_t(sample_queue_.buffer[sample_queue_.read_pointer]) - 128); | ||||
| 		const int16_t output_level = volume_multiplier_ * (int16_t(sample_queue_.buffer[sample_queue_.read_pointer].load(std::memory_order::memory_order_relaxed)) - 128); | ||||
| 		for(size_t c = 0; c < cycles_left_in_sample; ++c) { | ||||
| 			target[c] = output_level; | ||||
| 		} | ||||
|   | ||||
| @@ -63,7 +63,7 @@ class Audio: public ::Outputs::Speaker::SampleSource { | ||||
| 		// A queue of fetched samples; read from by one thread, | ||||
| 		// written to by another. | ||||
| 		struct { | ||||
| 			std::array<uint8_t, 740> buffer; | ||||
| 			std::array<std::atomic<uint8_t>, 740> buffer; | ||||
| 			size_t read_pointer = 0, write_pointer = 0; | ||||
| 		} sample_queue_; | ||||
|  | ||||
|   | ||||
| @@ -29,7 +29,17 @@ Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulato | ||||
|  	crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) { | ||||
|  | ||||
|  	crt_.set_display_type(Outputs::Display::DisplayType::RGB); | ||||
|  | ||||
| 	// UGLY HACK. UGLY, UGLY HACK. UGLY! | ||||
| 	// The OpenGL scan target fails properly to place visible areas which are not 4:3. | ||||
| 	// The [newer] Metal scan target has no such issue. So assume that Apple => Metal, | ||||
| 	// and set a visible area to work around the OpenGL issue if required. | ||||
| 	// TODO: eliminate UGLY HACK. | ||||
| #ifdef __APPLE__ | ||||
| 	crt_.set_visible_area(Outputs::Display::Rect(0.08f, 10.0f / 368.0f, 0.82f, 344.0f / 368.0f)); | ||||
| #else | ||||
| 	crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f)); | ||||
| #endif | ||||
| 	crt_.set_aspect_ratio(1.73f);	// The Mac uses a non-standard scanning area. | ||||
| } | ||||
|  | ||||
| @@ -105,10 +115,13 @@ void Video::run_for(HalfCycles duration) { | ||||
|  | ||||
| 							pixel_buffer_ += 16; | ||||
| 						} | ||||
| 					} else { | ||||
| 						video_address_ += size_t(final_pixel_word - first_word); | ||||
| 					} | ||||
|  | ||||
| 					if(final_pixel_word == 32) { | ||||
| 						crt_.output_data(512); | ||||
| 						pixel_buffer_ = nullptr; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,11 @@ | ||||
|  | ||||
| namespace Oric { | ||||
|  | ||||
| /*! | ||||
| 	Emulates a Byte Drive 500, at least to some extent. Very little is known about this interface, | ||||
| 	and I'm in possession of only a single disk image. So much of the below is community guesswork; | ||||
| 	see the thread at https://forum.defence-force.org/viewtopic.php?f=25&t=2055 | ||||
| */ | ||||
| class BD500: public DiskController { | ||||
| 	public: | ||||
| 		BD500(); | ||||
| @@ -36,6 +41,16 @@ class BD500: public DiskController { | ||||
|  | ||||
| 		void access(int address); | ||||
| 		void set_head_loaded(bool loaded); | ||||
|  | ||||
| 		bool enable_overlay_ram_ = false; | ||||
| 		bool disable_basic_rom_ = false; | ||||
| 		void select_paged_item() { | ||||
| 			PagedItem item = PagedItem::RAM; | ||||
| 			if(!enable_overlay_ram_) { | ||||
| 				item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC; | ||||
| 			} | ||||
| 			set_paged_item(item); | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| }; | ||||
|   | ||||
| @@ -44,28 +44,18 @@ class DiskController: public WD::WD1770 { | ||||
| 	protected: | ||||
| 		Delegate *delegate_ = nullptr; | ||||
|  | ||||
| 		bool enable_overlay_ram_ = false; | ||||
| 		bool disable_basic_rom_ = false; | ||||
| 		void select_paged_item() { | ||||
| 			PagedItem item = PagedItem::RAM; | ||||
| 			if(!enable_overlay_ram_) { | ||||
| 				item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC; | ||||
| 			} | ||||
| 			set_paged_item(item); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		PagedItem paged_item_ = PagedItem::DiskROM; | ||||
| 		int clock_rate_; | ||||
| 		Storage::Disk::Drive::ReadyType ready_type_; | ||||
|  | ||||
| 		inline void set_paged_item(PagedItem item) { | ||||
| 		void set_paged_item(PagedItem item) { | ||||
| 			if(paged_item_ == item) return; | ||||
| 			paged_item_ = item; | ||||
| 			if(delegate_) { | ||||
| 				delegate_->disk_controller_did_change_paged_item(this); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		PagedItem paged_item_ = PagedItem::DiskROM; | ||||
| 		int clock_rate_; | ||||
| 		Storage::Disk::Drive::ReadyType ready_type_; | ||||
| }; | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,16 @@ class Jasmin: public DiskController { | ||||
| 		uint8_t selected_drives_ = 0; | ||||
|  | ||||
| 		Activity::Observer *observer_ = nullptr; | ||||
|  | ||||
| 		bool enable_overlay_ram_ = false; | ||||
| 		bool disable_basic_rom_ = false; | ||||
| 		void select_paged_item() { | ||||
| 			PagedItem item = PagedItem::RAM; | ||||
| 			if(!enable_overlay_ram_) { | ||||
| 				item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC; | ||||
| 			} | ||||
| 			set_paged_item(item); | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| }; | ||||
|   | ||||
| @@ -38,7 +38,7 @@ void Microdisc::set_control_register(uint8_t control, uint8_t changes) { | ||||
|  | ||||
| 	// b4: side select | ||||
| 	if(changes & 0x10) { | ||||
| 		const int head = (control & 0x10) ? 1 : 0; | ||||
| 		const int head = (control & 0x10) >> 4; | ||||
| 		for_all_drives([head] (Storage::Disk::Drive &drive, size_t) { | ||||
| 			drive.set_head(head); | ||||
| 		}); | ||||
| @@ -52,7 +52,7 @@ void Microdisc::set_control_register(uint8_t control, uint8_t changes) { | ||||
| 	// b0: IRQ enable | ||||
| 	if(changes & 0x01) { | ||||
| 		const bool had_irq = get_interrupt_request_line(); | ||||
| 		irq_enable_ = !!(control & 0x01); | ||||
| 		irq_enable_ = bool(control & 0x01); | ||||
| 		const bool has_irq = get_interrupt_request_line(); | ||||
| 		if(has_irq != had_irq && delegate_) { | ||||
| 			delegate_->wd1770_did_change_output(this); | ||||
| @@ -62,9 +62,14 @@ void Microdisc::set_control_register(uint8_t control, uint8_t changes) { | ||||
| 	// b7: EPROM select (0 = select) | ||||
| 	// b1: ROM disable (0 = disable) | ||||
| 	if(changes & 0x82) { | ||||
| 		enable_overlay_ram_ = control & 0x80; | ||||
| 		disable_basic_rom_ = !(control & 0x02); | ||||
| 		select_paged_item(); | ||||
| 		PagedItem item; | ||||
| 		if(control & 0x02) item = PagedItem::BASIC; | ||||
| 		else if(control & 0x80) { | ||||
| 			item = PagedItem::RAM; | ||||
| 		} else { | ||||
| 			item = PagedItem::DiskROM; | ||||
| 		} | ||||
| 		set_paged_item(item); | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -98,7 +98,6 @@ | ||||
| 		4B055AED1FAE9BA20060FFFF /* Z80Storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8334831F5DA0360097E338 /* Z80Storage.cpp */; }; | ||||
| 		4B055AEE1FAE9BBF0060FFFF /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B86E2591F8C628F006FAA45 /* Keyboard.cpp */; }; | ||||
| 		4B055AEF1FAE9BF00060FFFF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; }; | ||||
| 		4B055AF11FAE9C160060FFFF /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; }; | ||||
| 		4B055AF21FAE9C1C0060FFFF /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B055AF01FAE9C080060FFFF /* OpenGL.framework */; }; | ||||
| 		4B08A2751EE35D56008B7065 /* Z80InterruptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B08A2741EE35D56008B7065 /* Z80InterruptTests.swift */; }; | ||||
| 		4B08A2781EE39306008B7065 /* TestMachine.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B08A2771EE39306008B7065 /* TestMachine.mm */; }; | ||||
| @@ -155,6 +154,9 @@ | ||||
| 		4B1D08061E0F7A1100763741 /* TimeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B1D08051E0F7A1100763741 /* TimeTests.mm */; }; | ||||
| 		4B1E85811D176468001EF87D /* 6532Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E85801D176468001EF87D /* 6532Tests.swift */; }; | ||||
| 		4B1EDB451E39A0AC009D6819 /* chip.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B1EDB431E39A0AC009D6819 /* chip.png */; }; | ||||
| 		4B228CD524D773B40077EF25 /* CSScanTarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B228CD424D773B30077EF25 /* CSScanTarget.mm */; }; | ||||
| 		4B228CD924DA12C60077EF25 /* CSScanTargetView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B228CD824DA12C60077EF25 /* CSScanTargetView.m */; }; | ||||
| 		4B228CDB24DA41890077EF25 /* ScanTarget.metal in Sources */ = {isa = PBXBuildFile; fileRef = 4B228CDA24DA41880077EF25 /* ScanTarget.metal */; }; | ||||
| 		4B2530F4244E6774007980BF /* fm.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B2530F3244E6773007980BF /* fm.json */; }; | ||||
| 		4B2A332D1DB86821002876E3 /* OricOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B2A332B1DB86821002876E3 /* OricOptions.xib */; }; | ||||
| 		4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; }; | ||||
| @@ -215,7 +217,6 @@ | ||||
| 		4B54C0C51F8D91D90050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0C41F8D91D90050900F /* Keyboard.cpp */; }; | ||||
| 		4B54C0C81F8D91E50050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0C61F8D91E50050900F /* Keyboard.cpp */; }; | ||||
| 		4B54C0CB1F8D92590050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0CA1F8D92580050900F /* Keyboard.cpp */; }; | ||||
| 		4B55CE5D1C3B7D6F0093A61B /* CSOpenGLView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */; }; | ||||
| 		4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */; }; | ||||
| 		4B55DD8320DF06680043F2E5 /* MachinePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B55DD8020DF06680043F2E5 /* MachinePicker.swift */; }; | ||||
| 		4B55DD8420DF06680043F2E5 /* MachinePicker.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B55DD8120DF06680043F2E5 /* MachinePicker.xib */; }; | ||||
| @@ -370,7 +371,6 @@ | ||||
| 		4B778F6123A5F3560000D260 /* Disk.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8944FC201967B4007DE474 /* Disk.cpp */; }; | ||||
| 		4B778F6223A5F35F0000D260 /* File.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B894500201967B4007DE474 /* File.cpp */; }; | ||||
| 		4B778F6323A5F3630000D260 /* Tape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B894501201967B4007DE474 /* Tape.cpp */; }; | ||||
| 		4B778F6423A5F3730000D260 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; }; | ||||
| 		4B7913CC1DFCD80E00175A82 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7913CA1DFCD80E00175A82 /* Video.cpp */; }; | ||||
| 		4B79A5011FC913C900EEDAD5 /* MSX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B79A4FF1FC913C900EEDAD5 /* MSX.cpp */; }; | ||||
| 		4B79E4441E3AF38600141F11 /* cassette.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B79E4411E3AF38600141F11 /* cassette.png */; }; | ||||
| @@ -768,6 +768,10 @@ | ||||
| 		4BB73EAC1B587A5100552FC2 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BB73EAA1B587A5100552FC2 /* MainMenu.xib */; }; | ||||
| 		4BB73EB71B587A5100552FC2 /* AllSuiteATests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EB61B587A5100552FC2 /* AllSuiteATests.swift */; }; | ||||
| 		4BB73EC21B587A5100552FC2 /* Clock_SignalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EC11B587A5100552FC2 /* Clock_SignalUITests.swift */; }; | ||||
| 		4BB8616E24E22DC500A00E03 /* BufferingScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB8616D24E22DC500A00E03 /* BufferingScanTarget.cpp */; }; | ||||
| 		4BB8616F24E22DC500A00E03 /* BufferingScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB8616D24E22DC500A00E03 /* BufferingScanTarget.cpp */; }; | ||||
| 		4BB8617124E22F5700A00E03 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BB8617024E22F4900A00E03 /* Accelerate.framework */; }; | ||||
| 		4BB8617224E22F5A00A00E03 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BB8617024E22F4900A00E03 /* Accelerate.framework */; }; | ||||
| 		4BBB70A4202011C2002FE009 /* MultiMediaTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A3202011C2002FE009 /* MultiMediaTarget.cpp */; }; | ||||
| 		4BBB70A5202011C2002FE009 /* MultiMediaTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A3202011C2002FE009 /* MultiMediaTarget.cpp */; }; | ||||
| 		4BBB70A8202014E2002FE009 /* MultiProducer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A6202014E2002FE009 /* MultiProducer.cpp */; }; | ||||
| @@ -792,7 +796,6 @@ | ||||
| 		4BC5FC3020CDDDEF00410AA0 /* AppleIIOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BC5FC2E20CDDDEE00410AA0 /* AppleIIOptions.xib */; }; | ||||
| 		4BC751B21D157E61006C31D9 /* 6522Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC751B11D157E61006C31D9 /* 6522Tests.swift */; }; | ||||
| 		4BC76E691C98E31700E6EF73 /* FIRFilter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */; }; | ||||
| 		4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; }; | ||||
| 		4BC890D3230F86020025A55A /* DirectAccessDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC890D1230F86020025A55A /* DirectAccessDevice.cpp */; }; | ||||
| 		4BC890D4230F86020025A55A /* DirectAccessDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC890D1230F86020025A55A /* DirectAccessDevice.cpp */; }; | ||||
| 		4BC91B831D1F160E00884B76 /* CommodoreTAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC91B811D1F160E00884B76 /* CommodoreTAP.cpp */; }; | ||||
| @@ -809,19 +812,14 @@ | ||||
| 		4BCE0060227D39AB000CA200 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE005E227D39AB000CA200 /* Video.cpp */; }; | ||||
| 		4BCF1FA41DADC3DD0039D2E7 /* Oric.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */; }; | ||||
| 		4BD0FBC3233706A200148981 /* CSApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BD0FBC2233706A200148981 /* CSApplication.m */; }; | ||||
| 		4BD191F42191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; }; | ||||
| 		4BD191F52191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; }; | ||||
| 		4BD388882239E198002D14B5 /* 68000Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD388872239E198002D14B5 /* 68000Tests.mm */; }; | ||||
| 		4BD3A30B1EE755C800B5B501 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD3A3091EE755C800B5B501 /* Video.cpp */; }; | ||||
| 		4BD424DF2193B5340097291A /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424DD2193B5340097291A /* TextureTarget.cpp */; }; | ||||
| 		4BD424E02193B5340097291A /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424DD2193B5340097291A /* TextureTarget.cpp */; }; | ||||
| 		4BD424E52193B5830097291A /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E12193B5820097291A /* Shader.cpp */; }; | ||||
| 		4BD424E62193B5830097291A /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E12193B5820097291A /* Shader.cpp */; }; | ||||
| 		4BD424E72193B5830097291A /* Rectangle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E22193B5820097291A /* Rectangle.cpp */; }; | ||||
| 		4BD424E82193B5830097291A /* Rectangle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E22193B5820097291A /* Rectangle.cpp */; }; | ||||
| 		4BD468F71D8DF41D0084958B /* 1770.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD468F51D8DF41D0084958B /* 1770.cpp */; }; | ||||
| 		4BD4A8D01E077FD20020D856 /* PCMTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */; }; | ||||
| 		4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; }; | ||||
| 		4BD5D2692199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; }; | ||||
| 		4BD61664206B2AC800236112 /* QuickLoadOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BD61662206B2AC700236112 /* QuickLoadOptions.xib */; }; | ||||
| 		4BD67DCB209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; }; | ||||
| @@ -1002,6 +1000,11 @@ | ||||
| 		4B1E857B1D174DEC001EF87D /* 6532.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = 6532.hpp; sourceTree = "<group>"; }; | ||||
| 		4B1E85801D176468001EF87D /* 6532Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6532Tests.swift; sourceTree = "<group>"; }; | ||||
| 		4B1EDB431E39A0AC009D6819 /* chip.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = chip.png; sourceTree = "<group>"; }; | ||||
| 		4B228CD424D773B30077EF25 /* CSScanTarget.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSScanTarget.mm; sourceTree = "<group>"; }; | ||||
| 		4B228CD624D773CA0077EF25 /* CSScanTarget.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSScanTarget.h; sourceTree = "<group>"; }; | ||||
| 		4B228CD724DA12C50077EF25 /* CSScanTargetView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSScanTargetView.h; sourceTree = "<group>"; }; | ||||
| 		4B228CD824DA12C60077EF25 /* CSScanTargetView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSScanTargetView.m; sourceTree = "<group>"; }; | ||||
| 		4B228CDA24DA41880077EF25 /* ScanTarget.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ScanTarget.metal; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.metal; }; | ||||
| 		4B24095A1C45DF85004DA684 /* Stepper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Stepper.hpp; sourceTree = "<group>"; }; | ||||
| 		4B2530F3244E6773007980BF /* fm.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fm.json; sourceTree = "<group>"; }; | ||||
| 		4B2A332C1DB86821002876E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/OricOptions.xib"; sourceTree = SOURCE_ROOT; }; | ||||
| @@ -1113,6 +1116,7 @@ | ||||
| 		4B4DC8271D2C2470003C5BF8 /* C1540.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = C1540.hpp; sourceTree = "<group>"; }; | ||||
| 		4B4DC8291D2C27A4003C5BF8 /* SerialBus.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SerialBus.cpp; sourceTree = "<group>"; }; | ||||
| 		4B4DC82A1D2C27A4003C5BF8 /* SerialBus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SerialBus.hpp; sourceTree = "<group>"; }; | ||||
| 		4B4F2B7024DF99D4000DA6B0 /* CSScanTarget+CppScanTarget.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CSScanTarget+CppScanTarget.h"; sourceTree = "<group>"; }; | ||||
| 		4B50AF7F242817F40099BBD7 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; | ||||
| 		4B51F70920A521D700AFA2C1 /* Source.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Source.hpp; sourceTree = "<group>"; }; | ||||
| 		4B51F70A20A521D700AFA2C1 /* Observer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Observer.hpp; sourceTree = "<group>"; }; | ||||
| @@ -1127,8 +1131,6 @@ | ||||
| 		4B54C0C71F8D91E50050900F /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Keyboard.hpp; path = Electron/Keyboard.hpp; sourceTree = "<group>"; }; | ||||
| 		4B54C0C91F8D92580050900F /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Keyboard.hpp; path = ZX8081/Keyboard.hpp; sourceTree = "<group>"; }; | ||||
| 		4B54C0CA1F8D92580050900F /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Keyboard.cpp; path = ZX8081/Keyboard.cpp; sourceTree = "<group>"; }; | ||||
| 		4B55CE5B1C3B7D6F0093A61B /* CSOpenGLView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSOpenGLView.h; sourceTree = "<group>"; }; | ||||
| 		4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSOpenGLView.m; sourceTree = "<group>"; }; | ||||
| 		4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachineDocument.swift; sourceTree = "<group>"; }; | ||||
| 		4B55DD8020DF06680043F2E5 /* MachinePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachinePicker.swift; sourceTree = "<group>"; }; | ||||
| 		4B55DD8220DF06680043F2E5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MachinePicker.xib; sourceTree = "<group>"; }; | ||||
| @@ -1636,6 +1638,9 @@ | ||||
| 		4BB73EC11B587A5100552FC2 /* Clock_SignalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clock_SignalUITests.swift; sourceTree = "<group>"; }; | ||||
| 		4BB73EC31B587A5100552FC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; | ||||
| 		4BB73ECF1B587A6700552FC2 /* Clock Signal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "Clock Signal.entitlements"; sourceTree = "<group>"; }; | ||||
| 		4BB8616C24E22DC500A00E03 /* BufferingScanTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = BufferingScanTarget.hpp; sourceTree = "<group>"; }; | ||||
| 		4BB8616D24E22DC500A00E03 /* BufferingScanTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BufferingScanTarget.cpp; sourceTree = "<group>"; }; | ||||
| 		4BB8617024E22F4900A00E03 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; | ||||
| 		4BBB709C2020109C002FE009 /* DynamicMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DynamicMachine.hpp; sourceTree = "<group>"; }; | ||||
| 		4BBB70A2202011C2002FE009 /* MultiMediaTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MultiMediaTarget.hpp; sourceTree = "<group>"; }; | ||||
| 		4BBB70A3202011C2002FE009 /* MultiMediaTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MultiMediaTarget.cpp; sourceTree = "<group>"; }; | ||||
| @@ -1678,7 +1683,6 @@ | ||||
| 		4BC751B11D157E61006C31D9 /* 6522Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6522Tests.swift; sourceTree = "<group>"; }; | ||||
| 		4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FIRFilter.cpp; sourceTree = "<group>"; }; | ||||
| 		4BC76E681C98E31700E6EF73 /* FIRFilter.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FIRFilter.hpp; sourceTree = "<group>"; }; | ||||
| 		4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; | ||||
| 		4BC890D1230F86020025A55A /* DirectAccessDevice.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DirectAccessDevice.cpp; sourceTree = "<group>"; }; | ||||
| 		4BC890D2230F86020025A55A /* DirectAccessDevice.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DirectAccessDevice.hpp; sourceTree = "<group>"; }; | ||||
| 		4BC91B811D1F160E00884B76 /* CommodoreTAP.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CommodoreTAP.cpp; sourceTree = "<group>"; }; | ||||
| @@ -1805,7 +1809,7 @@ | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				4B055AF21FAE9C1C0060FFFF /* OpenGL.framework in Frameworks */, | ||||
| 				4B055AF11FAE9C160060FFFF /* Accelerate.framework in Frameworks */, | ||||
| 				4BB8617224E22F5A00A00E03 /* Accelerate.framework in Frameworks */, | ||||
| 				4B055ABD1FAE86530060FFFF /* libz.tbd in Frameworks */, | ||||
| 				4B055A7A1FAE78A00060FFFF /* SDL2.framework in Frameworks */, | ||||
| 			); | ||||
| @@ -1815,8 +1819,8 @@ | ||||
| 			isa = PBXFrameworksBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				4BB8617124E22F5700A00E03 /* Accelerate.framework in Frameworks */, | ||||
| 				4B50AF80242817F40099BBD7 /* QuartzCore.framework in Frameworks */, | ||||
| 				4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */, | ||||
| 				4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| @@ -1826,7 +1830,6 @@ | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				4B9F11CA2272433900701480 /* libz.tbd in Frameworks */, | ||||
| 				4B778F6423A5F3730000D260 /* Accelerate.framework in Frameworks */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| @@ -1843,6 +1846,7 @@ | ||||
| 		4B055A761FAE78210060FFFF /* Frameworks */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				4BB8617024E22F4900A00E03 /* Accelerate.framework */, | ||||
| 				4B50AF7F242817F40099BBD7 /* QuartzCore.framework */, | ||||
| 				4B055AF01FAE9C080060FFFF /* OpenGL.framework */, | ||||
| 				4B055A771FAE78210060FFFF /* SDL2.framework */, | ||||
| @@ -2056,6 +2060,17 @@ | ||||
| 			path = Icons; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		4B228CD324D773B30077EF25 /* ScanTarget */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				4B228CDA24DA41880077EF25 /* ScanTarget.metal */, | ||||
| 				4B228CD424D773B30077EF25 /* CSScanTarget.mm */, | ||||
| 				4B228CD624D773CA0077EF25 /* CSScanTarget.h */, | ||||
| 				4B4F2B7024DF99D4000DA6B0 /* CSScanTarget+CppScanTarget.h */, | ||||
| 			); | ||||
| 			path = ScanTarget; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		4B2409591C45DF85004DA684 /* SignalProcessing */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| @@ -2219,6 +2234,7 @@ | ||||
| 				4BF52672218E752E00313227 /* ScanTarget.hpp */, | ||||
| 				4B0CCC411C62D0B3001CAC5F /* CRT */, | ||||
| 				4BD191D5219113B80042E144 /* OpenGL */, | ||||
| 				4BB8616B24E22DC500A00E03 /* ScanTargets */, | ||||
| 				4BD060A41FE49D3C006E14BE /* Speaker */, | ||||
| 			); | ||||
| 			name = Outputs; | ||||
| @@ -2467,8 +2483,8 @@ | ||||
| 		4B55CE5A1C3B7D6F0093A61B /* Views */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				4B55CE5B1C3B7D6F0093A61B /* CSOpenGLView.h */, | ||||
| 				4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */, | ||||
| 				4B228CD724DA12C50077EF25 /* CSScanTargetView.h */, | ||||
| 				4B228CD824DA12C60077EF25 /* CSScanTargetView.m */, | ||||
| 			); | ||||
| 			path = Views; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -3307,7 +3323,6 @@ | ||||
| 		4BB73E951B587A5100552FC2 = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */, | ||||
| 				4B51F70820A521D700AFA2C1 /* Activity */, | ||||
| 				4B8944E2201967B4007DE474 /* Analyser */, | ||||
| 				4BB73EA01B587A5100552FC2 /* Clock Signal */, | ||||
| @@ -3365,6 +3380,7 @@ | ||||
| 				4BB73EAA1B587A5100552FC2 /* MainMenu.xib */, | ||||
| 				4BE5F85A1C3E1C2500C43F01 /* Resources */, | ||||
| 				4BDA00DB22E60EE900AC3CD0 /* ROMRequester */, | ||||
| 				4B228CD324D773B30077EF25 /* ScanTarget */, | ||||
| 				4B55CE5A1C3B7D6F0093A61B /* Views */, | ||||
| 			); | ||||
| 			path = "Clock Signal"; | ||||
| @@ -3474,6 +3490,16 @@ | ||||
| 			path = ../../Processors; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		4BB8616B24E22DC500A00E03 /* ScanTargets */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				4BB8616C24E22DC500A00E03 /* BufferingScanTarget.hpp */, | ||||
| 				4BB8616D24E22DC500A00E03 /* BufferingScanTarget.cpp */, | ||||
| 			); | ||||
| 			name = ScanTargets; | ||||
| 			path = ../../Outputs/ScanTargets; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		4BBB70A1202011C2002FE009 /* Implementation */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| @@ -4485,6 +4511,7 @@ | ||||
| 				4BEBFB522002DB30000708CC /* DiskROM.cpp in Sources */, | ||||
| 				4BC23A2D2467600F001A6030 /* OPLL.cpp in Sources */, | ||||
| 				4B055AA11FAE85DA0060FFFF /* OricMFMDSK.cpp in Sources */, | ||||
| 				4BB8616F24E22DC500A00E03 /* BufferingScanTarget.cpp in Sources */, | ||||
| 				4B0ACC2923775819008902D0 /* DMAController.cpp in Sources */, | ||||
| 				4B055A951FAE85BB0060FFFF /* BitReverse.cpp in Sources */, | ||||
| 				4B055ACE1FAE9B030060FFFF /* Plus3.cpp in Sources */, | ||||
| @@ -4567,11 +4594,11 @@ | ||||
| 				4B0E04EA1FC9E5DA00F43484 /* CAS.cpp in Sources */, | ||||
| 				4B7A90ED20410A85008514A2 /* StaticAnalyser.cpp in Sources */, | ||||
| 				4B58601E1F806AB200AEE2E3 /* MFMSectorDump.cpp in Sources */, | ||||
| 				4B228CD924DA12C60077EF25 /* CSScanTargetView.m in Sources */, | ||||
| 				4B6AAEAD230E40250078E864 /* Target.cpp in Sources */, | ||||
| 				4B448E841F1C4C480009ABD6 /* PulseQueuedTape.cpp in Sources */, | ||||
| 				4B0E61071FF34737002A9DBD /* MSX.cpp in Sources */, | ||||
| 				4B4518A01F75FD1C00926311 /* CPCDSK.cpp in Sources */, | ||||
| 				4BD424DF2193B5340097291A /* TextureTarget.cpp in Sources */, | ||||
| 				4B0CCC451C62D0B3001CAC5F /* CRT.cpp in Sources */, | ||||
| 				4BC23A2C2467600F001A6030 /* OPLL.cpp in Sources */, | ||||
| 				4B322E041F5A2E3C004EB04C /* Z80Base.cpp in Sources */, | ||||
| @@ -4597,6 +4624,7 @@ | ||||
| 				4B1497921EE4B5A800CE2596 /* ZX8081.cpp in Sources */, | ||||
| 				4B643F3F1D77B88000D431D6 /* DocumentController.swift in Sources */, | ||||
| 				4BDA00E422E663B900AC3CD0 /* NSData+CRC32.m in Sources */, | ||||
| 				4BB8616E24E22DC500A00E03 /* BufferingScanTarget.cpp in Sources */, | ||||
| 				4BB4BFB022A42F290069048D /* MacintoshIMG.cpp in Sources */, | ||||
| 				4B05401E219D1618001BF69C /* ScanTarget.cpp in Sources */, | ||||
| 				4B4518861F75E91A00926311 /* MFMDiskController.cpp in Sources */, | ||||
| @@ -4609,13 +4637,11 @@ | ||||
| 				4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */, | ||||
| 				4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */, | ||||
| 				4BAE49582032881E004BE78E /* CSZX8081.mm in Sources */, | ||||
| 				4BD424E52193B5830097291A /* Shader.cpp in Sources */, | ||||
| 				4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */, | ||||
| 				4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */, | ||||
| 				4BCE005A227CFFCA000CA200 /* Macintosh.cpp in Sources */, | ||||
| 				4B6AAEA4230E3E1D0078E864 /* MassStorageDevice.cpp in Sources */, | ||||
| 				4B89452E201967B4007DE474 /* StaticAnalyser.cpp in Sources */, | ||||
| 				4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */, | ||||
| 				4BC890D3230F86020025A55A /* DirectAccessDevice.cpp in Sources */, | ||||
| 				4B7BA03723CEB86000B98D9E /* BD500.cpp in Sources */, | ||||
| 				4B38F3481F2EC11D00D9235D /* AmstradCPC.cpp in Sources */, | ||||
| @@ -4664,7 +4690,8 @@ | ||||
| 				4B8334841F5DA0360097E338 /* Z80Storage.cpp in Sources */, | ||||
| 				4BA61EB01D91515900B3C876 /* NSData+StdVector.mm in Sources */, | ||||
| 				4BDA00E022E644AF00AC3CD0 /* CSROMReceiverView.m in Sources */, | ||||
| 				4BD191F42191180E0042E144 /* ScanTarget.cpp in Sources */, | ||||
| 				4B228CDB24DA41890077EF25 /* ScanTarget.metal in Sources */, | ||||
| 				4B228CD524D773B40077EF25 /* CSScanTarget.mm in Sources */, | ||||
| 				4BCD634922D6756400F567F1 /* MacintoshDoubleDensityDrive.cpp in Sources */, | ||||
| 				4B0F94FE208C1A1600FE41D9 /* NIB.cpp in Sources */, | ||||
| 				4B89452A201967B4007DE474 /* File.cpp in Sources */, | ||||
| @@ -4691,7 +4718,6 @@ | ||||
| 				4B0E04FA1FC9FA3100F43484 /* 9918.cpp in Sources */, | ||||
| 				4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */, | ||||
| 				4B4518841F75E91A00926311 /* UnformattedTrack.cpp in Sources */, | ||||
| 				4B55CE5D1C3B7D6F0093A61B /* CSOpenGLView.m in Sources */, | ||||
| 				4B65086022F4CF8D009C1100 /* Keyboard.cpp in Sources */, | ||||
| 				4B894528201967B4007DE474 /* Disk.cpp in Sources */, | ||||
| 				4BBB70A4202011C2002FE009 /* MultiMediaTarget.cpp in Sources */, | ||||
| @@ -4748,7 +4774,6 @@ | ||||
| 				4B37EE821D7345A6006A09A4 /* BinaryDump.cpp in Sources */, | ||||
| 				4BCE0053227CE8CA000CA200 /* AppleII.cpp in Sources */, | ||||
| 				4B8334821F5D9FF70097E338 /* PartialMachineCycle.cpp in Sources */, | ||||
| 				4BD424E72193B5830097291A /* Rectangle.cpp in Sources */, | ||||
| 				4B1B88C0202E3DB200B67DFF /* MultiConfigurable.cpp in Sources */, | ||||
| 				4BFF1D3922337B0300838EA1 /* 68000Storage.cpp in Sources */, | ||||
| 				4B54C0BC1F8D8E790050900F /* KeyboardMachine.cpp in Sources */, | ||||
| @@ -5088,6 +5113,7 @@ | ||||
| 				); | ||||
| 				GCC_C_LANGUAGE_STANDARD = gnu11; | ||||
| 				GCC_OPTIMIZATION_LEVEL = 2; | ||||
| 				GCC_PREPROCESSOR_DEFINITIONS = NDEBUG; | ||||
| 				MACOSX_DEPLOYMENT_TARGET = 10.10; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 			}; | ||||
| @@ -5141,8 +5167,9 @@ | ||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||
| 				GCC_WARN_UNUSED_PARAMETER = YES; | ||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||
| 				MACOSX_DEPLOYMENT_TARGET = 10.10; | ||||
| 				MTL_ENABLE_DEBUG_INFO = YES; | ||||
| 				MACOSX_DEPLOYMENT_TARGET = 10.13; | ||||
| 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; | ||||
| 				MTL_FAST_MATH = YES; | ||||
| 				ONLY_ACTIVE_ARCH = YES; | ||||
| 				SDKROOT = macosx; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||
| @@ -5192,8 +5219,9 @@ | ||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||
| 				GCC_WARN_UNUSED_PARAMETER = YES; | ||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||
| 				MACOSX_DEPLOYMENT_TARGET = 10.10; | ||||
| 				MACOSX_DEPLOYMENT_TARGET = 10.13; | ||||
| 				MTL_ENABLE_DEBUG_INFO = NO; | ||||
| 				MTL_FAST_MATH = YES; | ||||
| 				SDKROOT = macosx; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; | ||||
| 				SWIFT_SWIFT3_OBJC_INFERENCE = Default; | ||||
| @@ -5232,7 +5260,7 @@ | ||||
| 				GCC_WARN_UNUSED_LABEL = YES; | ||||
| 				INFOPLIST_FILE = "Clock Signal/Info.plist"; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; | ||||
| 				MACOSX_DEPLOYMENT_TARGET = 10.12.2; | ||||
| 				MTL_TREAT_WARNINGS_AS_ERRORS = YES; | ||||
| 				OTHER_CPLUSPLUSFLAGS = ( | ||||
| 					"$(OTHER_CFLAGS)", | ||||
| 					"-Wreorder", | ||||
| @@ -5280,7 +5308,7 @@ | ||||
| 				GCC_WARN_UNUSED_LABEL = YES; | ||||
| 				INFOPLIST_FILE = "Clock Signal/Info.plist"; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; | ||||
| 				MACOSX_DEPLOYMENT_TARGET = 10.12.2; | ||||
| 				MTL_TREAT_WARNINGS_AS_ERRORS = YES; | ||||
| 				OTHER_CPLUSPLUSFLAGS = ( | ||||
| 					"$(OTHER_CFLAGS)", | ||||
| 					"-Wreorder", | ||||
|   | ||||
| @@ -31,7 +31,7 @@ | ||||
|       </Testables> | ||||
|    </TestAction> | ||||
|    <LaunchAction | ||||
|       buildConfiguration = "Debug" | ||||
|       buildConfiguration = "Release" | ||||
|       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||||
|       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||||
|       disableMainThreadChecker = "YES" | ||||
| @@ -57,9 +57,13 @@ | ||||
|             isEnabled = "NO"> | ||||
|          </CommandLineArgument> | ||||
|          <CommandLineArgument | ||||
|             argument = "--volume=0.001" | ||||
|             argument = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Macintosh/MusicWorks 0.42.image"" | ||||
|             isEnabled = "YES"> | ||||
|          </CommandLineArgument> | ||||
|          <CommandLineArgument | ||||
|             argument = "--volume=0.001" | ||||
|             isEnabled = "NO"> | ||||
|          </CommandLineArgument> | ||||
|          <CommandLineArgument | ||||
|             argument = "--new=amstradcpc" | ||||
|             isEnabled = "NO"> | ||||
| @@ -86,7 +90,7 @@ | ||||
|          </CommandLineArgument> | ||||
|          <CommandLineArgument | ||||
|             argument = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Amstrad CPC/Robocop.dsk"" | ||||
|             isEnabled = "YES"> | ||||
|             isEnabled = "NO"> | ||||
|          </CommandLineArgument> | ||||
|          <CommandLineArgument | ||||
|             argument = "--speed=5" | ||||
| @@ -94,7 +98,7 @@ | ||||
|          </CommandLineArgument> | ||||
|          <CommandLineArgument | ||||
|             argument = "--rompath=/Users/thomasharte/Projects/CLK/ROMImages" | ||||
|             isEnabled = "NO"> | ||||
|             isEnabled = "YES"> | ||||
|          </CommandLineArgument> | ||||
|          <CommandLineArgument | ||||
|             argument = "--help" | ||||
|   | ||||
| @@ -67,7 +67,7 @@ | ||||
|       </Testables> | ||||
|    </TestAction> | ||||
|    <LaunchAction | ||||
|       buildConfiguration = "Debug" | ||||
|       buildConfiguration = "Release" | ||||
|       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||||
|       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||||
|       enableASanStackUseAfterReturn = "YES" | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="15705" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> | ||||
| <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> | ||||
|     <dependencies> | ||||
|         <deployment identifier="macosx"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15705"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16097.2"/> | ||||
|         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||||
|     </dependencies> | ||||
|     <objects> | ||||
|         <customObject id="-2" userLabel="File's Owner" customClass="MachineDocument" customModule="Clock_Signal" customModuleProvider="target"> | ||||
|             <connections> | ||||
|                 <outlet property="openGLView" destination="DEG-fq-cjd" id="Gxs-2u-n7B"/> | ||||
|                 <outlet property="scanTargetView" destination="DEG-fq-cjd" id="5aX-3R-eXQ"/> | ||||
|                 <outlet property="volumeSlider" destination="zaz-lB-Iyt" id="flY-Th-oG4"/> | ||||
|                 <outlet property="volumeView" destination="4ap-Gi-2AO" id="v4e-k6-Fqf"/> | ||||
|                 <outlet property="window" destination="xOd-HO-29H" id="JIz-fz-R2o"/> | ||||
| @@ -27,7 +27,7 @@ | ||||
|                 <rect key="frame" x="0.0" y="0.0" width="600" height="450"/> | ||||
|                 <autoresizingMask key="autoresizingMask"/> | ||||
|                 <subviews> | ||||
|                     <openGLView hidden="YES" wantsLayer="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSOpenGLView"> | ||||
|                     <openGLView hidden="YES" wantsLayer="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSScanTargetView"> | ||||
|                         <rect key="frame" x="0.0" y="0.0" width="600" height="450"/> | ||||
|                     </openGLView> | ||||
|                     <box hidden="YES" boxType="custom" cornerRadius="4" title="Box" titlePosition="noTitle" translatesAutoresizingMaskIntoConstraints="NO" id="4ap-Gi-2AO"> | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
| #import "CSStaticAnalyser.h" | ||||
|  | ||||
| #import "CSAudioQueue.h" | ||||
| #import	"CSOpenGLView.h" | ||||
| #import	"CSScanTargetView.h" | ||||
| #import "CSROMReceiverView.h" | ||||
|  | ||||
| #import "CSJoystickManager.h" | ||||
|   | ||||
| @@ -14,8 +14,7 @@ class MachineDocument: | ||||
| 	NSDocument, | ||||
| 	NSWindowDelegate, | ||||
| 	CSMachineDelegate, | ||||
| 	CSOpenGLViewDelegate, | ||||
| 	CSOpenGLViewResponderDelegate, | ||||
| 	CSScanTargetViewResponderDelegate, | ||||
| 	CSAudioQueueDelegate, | ||||
| 	CSROMReciverViewDelegate | ||||
| { | ||||
| @@ -45,7 +44,7 @@ class MachineDocument: | ||||
| 	// MARK: - Main NIB connections. | ||||
|  | ||||
| 	/// The OpenGL view to receive this machine's display. | ||||
| 	@IBOutlet weak var openGLView: CSOpenGLView! | ||||
| 	@IBOutlet weak var scanTargetView: CSScanTargetView! | ||||
|  | ||||
| 	/// The options panel, if any. | ||||
| 	@IBOutlet var optionsPanel: MachinePanel! | ||||
| @@ -100,8 +99,7 @@ class MachineDocument: | ||||
| 		actionLock.lock() | ||||
| 		drawLock.lock() | ||||
| 		machine = nil | ||||
| 		openGLView.delegate = nil | ||||
| 		openGLView.invalidate() | ||||
| 		scanTargetView.invalidate() | ||||
| 		actionLock.unlock() | ||||
| 		drawLock.unlock() | ||||
|  | ||||
| @@ -181,10 +179,10 @@ class MachineDocument: | ||||
| 	// MARK: - Connections Between Machine and the Outside World | ||||
|  | ||||
| 	private func setupMachineOutput() { | ||||
| 		if let machine = self.machine, let openGLView = self.openGLView, machine.view != openGLView { | ||||
| 		if let machine = self.machine, let scanTargetView = self.scanTargetView, machine.view != scanTargetView { | ||||
| 			// Establish the output aspect ratio and audio. | ||||
| 			let aspectRatio = self.aspectRatio() | ||||
| 			machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height)) | ||||
| 			machine.setView(scanTargetView, aspectRatio: Float(aspectRatio.width / aspectRatio.height)) | ||||
|  | ||||
| 			// Attach an options panel if one is available. | ||||
| 			if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName { | ||||
| @@ -198,20 +196,20 @@ class MachineDocument: | ||||
|  | ||||
| 			// Callbacks from the OpenGL may come on a different thread, immediately following the .delegate set; | ||||
| 			// hence the full setup of the best-effort updater prior to setting self as a delegate. | ||||
| 			openGLView.delegate = self | ||||
| 			openGLView.responderDelegate = self | ||||
| //			scanTargetView.delegate = self | ||||
| 			scanTargetView.responderDelegate = self | ||||
|  | ||||
| 			// If this machine has a mouse, enable mouse capture; also indicate whether usurption | ||||
| 			// of the command key is desired. | ||||
| 			openGLView.shouldCaptureMouse = machine.hasMouse | ||||
| 			openGLView.shouldUsurpCommand = machine.shouldUsurpCommand | ||||
| 			scanTargetView.shouldCaptureMouse = machine.hasMouse | ||||
| 			scanTargetView.shouldUsurpCommand = machine.shouldUsurpCommand | ||||
|  | ||||
| 			setupAudioQueueClockRate() | ||||
|  | ||||
| 			// Bring OpenGL view-holding window on top of the options panel and show the content. | ||||
| 			openGLView.isHidden = false | ||||
| 			openGLView.window!.makeKeyAndOrderFront(self) | ||||
| 			openGLView.window!.makeFirstResponder(openGLView) | ||||
| 			scanTargetView.isHidden = false | ||||
| 			scanTargetView.window!.makeKeyAndOrderFront(self) | ||||
| 			scanTargetView.window!.makeFirstResponder(scanTargetView) | ||||
|  | ||||
| 			// Start forwarding best-effort updates. | ||||
| 			machine.start() | ||||
| @@ -252,18 +250,6 @@ class MachineDocument: | ||||
| 	final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) { | ||||
| 	} | ||||
|  | ||||
| 	/// Responds to the CSOpenGLViewDelegate redraw message by requesting a machine update if this is a timed | ||||
| 	/// request, and ordering a redraw regardless of the motivation. | ||||
| 	final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) { | ||||
| 		if drawLock.try() { | ||||
| 			if redrawEvent == .timer { | ||||
| 				machine.updateView(forPixelSize: view.backingSize) | ||||
| 			} | ||||
| 			machine.drawView(forPixelSize: view.backingSize) | ||||
| 			drawLock.unlock() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: - Pasteboard Forwarding. | ||||
|  | ||||
| 	/// Forwards any text currently on the pasteboard into the active machine. | ||||
| @@ -277,7 +263,7 @@ class MachineDocument: | ||||
| 	// MARK: - Runtime Media Insertion. | ||||
|  | ||||
| 	/// Delegate message to receive drag and drop files. | ||||
| 	final func openGLView(_ view: CSOpenGLView, didReceiveFileAt URL: URL) { | ||||
| 	final func scanTargetView(_ view: CSScanTargetView, didReceiveFileAt URL: URL) { | ||||
| 		let mediaSet = CSMediaSet(fileAt: URL) | ||||
| 		if let mediaSet = mediaSet { | ||||
| 			mediaSet.apply(to: self.machine) | ||||
| @@ -310,7 +296,7 @@ class MachineDocument: | ||||
| 			machine.clearAllKeys() | ||||
| 			machine.joystickManager = nil | ||||
| 		} | ||||
| 		self.openGLView.releaseMouse() | ||||
| 		self.scanTargetView.releaseMouse() | ||||
| 	} | ||||
|  | ||||
| 	/// Upon becoming key, attaches joystick input to the machine. | ||||
| @@ -608,23 +594,20 @@ class MachineDocument: | ||||
| 		let url = pictursURL.appendingPathComponent(filename) | ||||
|  | ||||
| 		// Obtain the machine's current display. | ||||
| 		var imageRepresentation: NSBitmapImageRep? = nil | ||||
| 		self.openGLView.perform { | ||||
| 			imageRepresentation = self.machine.imageRepresentation | ||||
| 		} | ||||
| 		let imageRepresentation = self.machine.imageRepresentation | ||||
|  | ||||
| 		// Encode as a PNG and save. | ||||
| 		let pngData = imageRepresentation!.representation(using: .png, properties: [:]) | ||||
| 		let pngData = imageRepresentation.representation(using: .png, properties: [:]) | ||||
| 		try! pngData?.write(to: url) | ||||
| 	} | ||||
|  | ||||
| 	// MARK: - Window Title Updates. | ||||
| 	private var unadornedWindowTitle = "" | ||||
| 	func openGLViewDidCaptureMouse(_ view: CSOpenGLView) { | ||||
| 	internal func scanTargetViewDidCaptureMouse(_ view: CSScanTargetView) { | ||||
| 		self.windowControllers[0].window?.title = self.unadornedWindowTitle + " (press ⌘+control to release mouse)" | ||||
| 	} | ||||
|  | ||||
| 	func openGLViewDidReleaseMouse(_ view: CSOpenGLView) { | ||||
| 	internal func scanTargetViewDidReleaseMouse(_ view: CSScanTargetView) { | ||||
| 		self.windowControllers[0].window?.title = self.unadornedWindowTitle | ||||
| 	} | ||||
|  | ||||
| @@ -750,7 +733,7 @@ class MachineDocument: | ||||
| 	} | ||||
| 	fileprivate var animationFader: ViewFader? = nil | ||||
|  | ||||
| 	func openGLViewDidShowOSMouseCursor(_ view: CSOpenGLView) { | ||||
| 	internal func scanTargetViewDidShowOSMouseCursor(_ view: CSScanTargetView) { | ||||
| 		// The OS mouse cursor became visible, so show the volume controls. | ||||
| 		animationFader = nil | ||||
| 		volumeView.layer?.removeAllAnimations() | ||||
| @@ -758,7 +741,7 @@ class MachineDocument: | ||||
| 		volumeView.layer?.opacity = 1.0 | ||||
| 	} | ||||
|  | ||||
| 	func openGLViewWillHideOSMouseCursor(_ view: CSOpenGLView) { | ||||
| 	internal func scanTargetViewWillHideOSMouseCursor(_ view: CSScanTargetView) { | ||||
| 		// The OS mouse cursor will be hidden, so hide the volume controls. | ||||
| 		if !volumeView.isHidden && volumeView.layer?.animation(forKey: "opacity") == nil { | ||||
| 			let fadeAnimation = CABasicAnimation(keyPath: "opacity") | ||||
|   | ||||
| @@ -9,9 +9,9 @@ | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| #import "CSAudioQueue.h" | ||||
| #import "CSOpenGLView.h" | ||||
| #import "CSStaticAnalyser.h" | ||||
| #import "CSJoystickManager.h" | ||||
| #import "CSScanTargetView.h" | ||||
| #import "CSStaticAnalyser.h" | ||||
|  | ||||
| @class CSMachine; | ||||
| @protocol CSMachineDelegate | ||||
| @@ -62,14 +62,11 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { | ||||
| - (BOOL)isStereo; | ||||
| - (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo; | ||||
|  | ||||
| - (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio; | ||||
| - (void)setView:(nullable CSScanTargetView *)view aspectRatio:(float)aspectRatio; | ||||
|  | ||||
| - (void)start; | ||||
| - (void)stop; | ||||
|  | ||||
| - (void)updateViewForPixelSize:(CGSize)pixelSize; | ||||
| - (void)drawViewForPixelSize:(CGSize)pixelSize; | ||||
|  | ||||
| - (void)setKey:(uint16_t)key characters:(nullable NSString *)characters isPressed:(BOOL)isPressed; | ||||
| - (void)clearAllKeys; | ||||
|  | ||||
| @@ -77,7 +74,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { | ||||
| - (void)addMouseMotionX:(CGFloat)deltaX y:(CGFloat)deltaY; | ||||
|  | ||||
| @property (atomic, strong, nullable) CSAudioQueue *audioQueue; | ||||
| @property (nonatomic, readonly, nonnull) CSOpenGLView *view; | ||||
| @property (nonatomic, readonly, nonnull) CSScanTargetView *view; | ||||
| @property (nonatomic, weak, nullable) id<CSMachineDelegate> delegate; | ||||
|  | ||||
| @property (nonatomic, readonly, nonnull) NSString *userDefaultsPrefix; | ||||
|   | ||||
| @@ -9,8 +9,9 @@ | ||||
| #import "CSMachine.h" | ||||
| #import "CSMachine+Target.h" | ||||
|  | ||||
| #include "CSROMFetcher.hpp" | ||||
| #import "CSHighPrecisionTimer.h" | ||||
| #include "CSROMFetcher.hpp" | ||||
| #import "CSScanTarget+CppScanTarget.h" | ||||
|  | ||||
| #include "MediaTarget.hpp" | ||||
| #include "JoystickMachine.hpp" | ||||
| @@ -31,13 +32,7 @@ | ||||
| #include <atomic> | ||||
| #include <bitset> | ||||
|  | ||||
| #import <OpenGL/OpenGL.h> | ||||
| #include <OpenGL/gl3.h> | ||||
|  | ||||
| #include "../../../../Outputs/OpenGL/ScanTarget.hpp" | ||||
| #include "../../../../Outputs/OpenGL/Screenshot.hpp" | ||||
|  | ||||
| @interface CSMachine() <CSOpenGLViewDisplayLinkDelegate> | ||||
| @interface CSMachine() <CSScanTargetViewDisplayLinkDelegate> | ||||
| - (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length; | ||||
| - (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker; | ||||
| - (void)addLED:(NSString *)led; | ||||
| @@ -154,7 +149,6 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 	NSMutableArray<NSString *> *_leds; | ||||
|  | ||||
| 	CSHighPrecisionTimer *_timer; | ||||
| 	CGSize _pixelSize; | ||||
| 	std::atomic_flag _isUpdating; | ||||
| 	Time::Nanos _syncTime; | ||||
| 	Time::Nanos _timeDiff; | ||||
| @@ -165,7 +159,11 @@ struct ActivityObserver: public Activity::Observer { | ||||
|  | ||||
| 	NSTimer *_joystickTimer; | ||||
|  | ||||
| 	std::unique_ptr<Outputs::Display::OpenGL::ScanTarget> _scanTarget; | ||||
| 	// This array exists to reduce blocking on the main queue; anything that would otherwise need | ||||
| 	// to synchronise on self in order to post input to the machine can instead synchronise on | ||||
| 	// _inputEvents and add a block to it. The main machine execution loop promises to synchronise | ||||
| 	// on _inputEvents very briefly at the start of every tick and execute all enqueued blocks. | ||||
| 	NSMutableArray<dispatch_block_t> *_inputEvents; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableArray<CSMissingROM *> *)missingROMs { | ||||
| @@ -217,6 +215,8 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 		_speakerDelegate.machine = self; | ||||
| 		_speakerDelegate.machineAccessLock = _delegateMachineAccessLock; | ||||
|  | ||||
| 		_inputEvents = [[NSMutableArray alloc] init]; | ||||
|  | ||||
| 		_joystickMachine = _machine->joystick_machine(); | ||||
| 		[self updateJoystickTimer]; | ||||
| 		_isUpdating.clear(); | ||||
| @@ -245,11 +245,11 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 	_speakerDelegate.machine = nil; | ||||
| 	[_delegateMachineAccessLock unlock]; | ||||
|  | ||||
| 	[_view performWithGLContext:^{ | ||||
| 		@synchronized(self) { | ||||
| 			self->_scanTarget.reset(); | ||||
| 		} | ||||
| 	}]; | ||||
| //	[_view performWithGLContext:^{ | ||||
| //		@synchronized(self) { | ||||
| //			self->_scanTarget.reset(); | ||||
| //		} | ||||
| //	}]; | ||||
| } | ||||
|  | ||||
| - (float)idealSamplingRateFromRange:(NSRange)range { | ||||
| @@ -351,30 +351,10 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio { | ||||
| - (void)setView:(CSScanTargetView *)view aspectRatio:(float)aspectRatio { | ||||
| 	_view = view; | ||||
| 	_view.displayLinkDelegate = self; | ||||
| 	[view performWithGLContext:^{ | ||||
| 		[self setupOutputWithAspectRatio:aspectRatio]; | ||||
| 	} flushDrawable:NO]; | ||||
| } | ||||
|  | ||||
| - (void)setupOutputWithAspectRatio:(float)aspectRatio { | ||||
| 	_scanTarget = std::make_unique<Outputs::Display::OpenGL::ScanTarget>(); | ||||
| 	_machine->scan_producer()->set_scan_target(_scanTarget.get()); | ||||
| } | ||||
|  | ||||
| - (void)updateViewForPixelSize:(CGSize)pixelSize { | ||||
| //	_pixelSize = pixelSize; | ||||
|  | ||||
| //	@synchronized(self) { | ||||
| //		const auto scan_status = _machine->crt_machine()->get_scan_status(); | ||||
| //		NSLog(@"FPS (hopefully): %0.2f [retrace: %0.4f]", 1.0f / scan_status.field_duration, scan_status.retrace_duration); | ||||
| //	} | ||||
| } | ||||
|  | ||||
| - (void)drawViewForPixelSize:(CGSize)pixelSize { | ||||
| 	_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); | ||||
| 	_machine->scan_producer()->set_scan_target(_view.scanTarget.scanTarget); | ||||
| } | ||||
|  | ||||
| - (void)paste:(NSString *)paste { | ||||
| @@ -384,26 +364,7 @@ struct ActivityObserver: public Activity::Observer { | ||||
| } | ||||
|  | ||||
| - (NSBitmapImageRep *)imageRepresentation { | ||||
| 	// Grab a screenshot. | ||||
| 	Outputs::Display::OpenGL::Screenshot screenshot(4, 3); | ||||
|  | ||||
| 	// Generate an NSBitmapImageRep containing the screenshot's data. | ||||
| 	NSBitmapImageRep *const result = | ||||
| 		[[NSBitmapImageRep alloc] | ||||
| 			initWithBitmapDataPlanes:NULL | ||||
| 			pixelsWide:screenshot.width | ||||
| 			pixelsHigh:screenshot.height | ||||
| 			bitsPerSample:8 | ||||
| 			samplesPerPixel:4 | ||||
| 			hasAlpha:YES | ||||
| 			isPlanar:NO | ||||
| 			colorSpaceName:NSDeviceRGBColorSpace | ||||
| 			bytesPerRow:4 * screenshot.width | ||||
| 			bitsPerPixel:0]; | ||||
|  | ||||
| 	memcpy(result.bitmapData, screenshot.pixel_data.data(), size_t(screenshot.width*screenshot.height*4)); | ||||
|  | ||||
| 	return result; | ||||
| 	return self.view.imageRepresentation; | ||||
| } | ||||
|  | ||||
| - (void)applyMedia:(const Analyser::Static::Media &)media { | ||||
| @@ -428,84 +389,84 @@ struct ActivityObserver: public Activity::Observer { | ||||
| } | ||||
|  | ||||
| - (void)setKey:(uint16_t)key characters:(NSString *)characters isPressed:(BOOL)isPressed { | ||||
| 	auto keyboard_machine = _machine->keyboard_machine(); | ||||
| 	if(keyboard_machine && (self.inputMode != CSMachineKeyboardInputModeJoystick || !keyboard_machine->get_keyboard().is_exclusive())) { | ||||
| 		Inputs::Keyboard::Key mapped_key = Inputs::Keyboard::Key::Help;	// Make an innocuous default guess. | ||||
| 	[self applyInputEvent:^{ | ||||
| 		auto keyboard_machine = self->_machine->keyboard_machine(); | ||||
| 		if(keyboard_machine && (self.inputMode != CSMachineKeyboardInputModeJoystick || !keyboard_machine->get_keyboard().is_exclusive())) { | ||||
| 			Inputs::Keyboard::Key mapped_key = Inputs::Keyboard::Key::Help;	// Make an innocuous default guess. | ||||
| #define BIND(source, dest) case source: mapped_key = Inputs::Keyboard::Key::dest; break; | ||||
| 		// Connect the Carbon-era Mac keyboard scancodes to Clock Signal's 'universal' enumeration in order | ||||
| 		// to pass into the platform-neutral realm. | ||||
| 		switch(key) { | ||||
| 			BIND(VK_ANSI_0, k0);	BIND(VK_ANSI_1, k1);	BIND(VK_ANSI_2, k2);	BIND(VK_ANSI_3, k3);	BIND(VK_ANSI_4, k4); | ||||
| 			BIND(VK_ANSI_5, k5);	BIND(VK_ANSI_6, k6);	BIND(VK_ANSI_7, k7);	BIND(VK_ANSI_8, k8);	BIND(VK_ANSI_9, k9); | ||||
| 			// Connect the Carbon-era Mac keyboard scancodes to Clock Signal's 'universal' enumeration in order | ||||
| 			// to pass into the platform-neutral realm. | ||||
| 			switch(key) { | ||||
| 				BIND(VK_ANSI_0, k0);	BIND(VK_ANSI_1, k1);	BIND(VK_ANSI_2, k2);	BIND(VK_ANSI_3, k3);	BIND(VK_ANSI_4, k4); | ||||
| 				BIND(VK_ANSI_5, k5);	BIND(VK_ANSI_6, k6);	BIND(VK_ANSI_7, k7);	BIND(VK_ANSI_8, k8);	BIND(VK_ANSI_9, k9); | ||||
|  | ||||
| 			BIND(VK_ANSI_Q, Q);		BIND(VK_ANSI_W, W);		BIND(VK_ANSI_E, E);		BIND(VK_ANSI_R, R);		BIND(VK_ANSI_T, T); | ||||
| 			BIND(VK_ANSI_Y, Y);		BIND(VK_ANSI_U, U);		BIND(VK_ANSI_I, I);		BIND(VK_ANSI_O, O);		BIND(VK_ANSI_P, P); | ||||
| 				BIND(VK_ANSI_Q, Q);		BIND(VK_ANSI_W, W);		BIND(VK_ANSI_E, E);		BIND(VK_ANSI_R, R);		BIND(VK_ANSI_T, T); | ||||
| 				BIND(VK_ANSI_Y, Y);		BIND(VK_ANSI_U, U);		BIND(VK_ANSI_I, I);		BIND(VK_ANSI_O, O);		BIND(VK_ANSI_P, P); | ||||
|  | ||||
| 			BIND(VK_ANSI_A, A);		BIND(VK_ANSI_S, S);		BIND(VK_ANSI_D, D);		BIND(VK_ANSI_F, F);		BIND(VK_ANSI_G, G); | ||||
| 			BIND(VK_ANSI_H, H);		BIND(VK_ANSI_J, J);		BIND(VK_ANSI_K, K);		BIND(VK_ANSI_L, L); | ||||
| 				BIND(VK_ANSI_A, A);		BIND(VK_ANSI_S, S);		BIND(VK_ANSI_D, D);		BIND(VK_ANSI_F, F);		BIND(VK_ANSI_G, G); | ||||
| 				BIND(VK_ANSI_H, H);		BIND(VK_ANSI_J, J);		BIND(VK_ANSI_K, K);		BIND(VK_ANSI_L, L); | ||||
|  | ||||
| 			BIND(VK_ANSI_Z, Z);		BIND(VK_ANSI_X, X);		BIND(VK_ANSI_C, C);		BIND(VK_ANSI_V, V); | ||||
| 			BIND(VK_ANSI_B, B);		BIND(VK_ANSI_N, N);		BIND(VK_ANSI_M, M); | ||||
| 				BIND(VK_ANSI_Z, Z);		BIND(VK_ANSI_X, X);		BIND(VK_ANSI_C, C);		BIND(VK_ANSI_V, V); | ||||
| 				BIND(VK_ANSI_B, B);		BIND(VK_ANSI_N, N);		BIND(VK_ANSI_M, M); | ||||
|  | ||||
| 			BIND(VK_F1, F1);		BIND(VK_F2, F2);		BIND(VK_F3, F3);		BIND(VK_F4, F4); | ||||
| 			BIND(VK_F5, F5);		BIND(VK_F6, F6);		BIND(VK_F7, F7);		BIND(VK_F8, F8); | ||||
| 			BIND(VK_F9, F9);		BIND(VK_F10, F10);		BIND(VK_F11, F11);		BIND(VK_F12, F12); | ||||
| 				BIND(VK_F1, F1);		BIND(VK_F2, F2);		BIND(VK_F3, F3);		BIND(VK_F4, F4); | ||||
| 				BIND(VK_F5, F5);		BIND(VK_F6, F6);		BIND(VK_F7, F7);		BIND(VK_F8, F8); | ||||
| 				BIND(VK_F9, F9);		BIND(VK_F10, F10);		BIND(VK_F11, F11);		BIND(VK_F12, F12); | ||||
|  | ||||
| 			BIND(VK_ANSI_Keypad0, Keypad0);		BIND(VK_ANSI_Keypad1, Keypad1);		BIND(VK_ANSI_Keypad2, Keypad2); | ||||
| 			BIND(VK_ANSI_Keypad3, Keypad3);		BIND(VK_ANSI_Keypad4, Keypad4);		BIND(VK_ANSI_Keypad5, Keypad5); | ||||
| 			BIND(VK_ANSI_Keypad6, Keypad6);		BIND(VK_ANSI_Keypad7, Keypad7);		BIND(VK_ANSI_Keypad8, Keypad8); | ||||
| 			BIND(VK_ANSI_Keypad9, Keypad9); | ||||
| 				BIND(VK_ANSI_Keypad0, Keypad0);		BIND(VK_ANSI_Keypad1, Keypad1);		BIND(VK_ANSI_Keypad2, Keypad2); | ||||
| 				BIND(VK_ANSI_Keypad3, Keypad3);		BIND(VK_ANSI_Keypad4, Keypad4);		BIND(VK_ANSI_Keypad5, Keypad5); | ||||
| 				BIND(VK_ANSI_Keypad6, Keypad6);		BIND(VK_ANSI_Keypad7, Keypad7);		BIND(VK_ANSI_Keypad8, Keypad8); | ||||
| 				BIND(VK_ANSI_Keypad9, Keypad9); | ||||
|  | ||||
| 			BIND(VK_ANSI_Equal, Equals);						BIND(VK_ANSI_Minus, Hyphen); | ||||
| 			BIND(VK_ANSI_RightBracket, CloseSquareBracket);		BIND(VK_ANSI_LeftBracket, OpenSquareBracket); | ||||
| 			BIND(VK_ANSI_Quote, Quote);							BIND(VK_ANSI_Grave, BackTick); | ||||
| 				BIND(VK_ANSI_Equal, Equals);						BIND(VK_ANSI_Minus, Hyphen); | ||||
| 				BIND(VK_ANSI_RightBracket, CloseSquareBracket);		BIND(VK_ANSI_LeftBracket, OpenSquareBracket); | ||||
| 				BIND(VK_ANSI_Quote, Quote);							BIND(VK_ANSI_Grave, BackTick); | ||||
|  | ||||
| 			BIND(VK_ANSI_Semicolon, Semicolon); | ||||
| 			BIND(VK_ANSI_Backslash, Backslash);					BIND(VK_ANSI_Slash, ForwardSlash); | ||||
| 			BIND(VK_ANSI_Comma, Comma);							BIND(VK_ANSI_Period, FullStop); | ||||
| 				BIND(VK_ANSI_Semicolon, Semicolon); | ||||
| 				BIND(VK_ANSI_Backslash, Backslash);					BIND(VK_ANSI_Slash, ForwardSlash); | ||||
| 				BIND(VK_ANSI_Comma, Comma);							BIND(VK_ANSI_Period, FullStop); | ||||
|  | ||||
| 			BIND(VK_ANSI_KeypadDecimal, KeypadDecimalPoint);	BIND(VK_ANSI_KeypadEquals, KeypadEquals); | ||||
| 			BIND(VK_ANSI_KeypadMultiply, KeypadAsterisk);		BIND(VK_ANSI_KeypadDivide, KeypadSlash); | ||||
| 			BIND(VK_ANSI_KeypadPlus, KeypadPlus);				BIND(VK_ANSI_KeypadMinus, KeypadMinus); | ||||
| 			BIND(VK_ANSI_KeypadClear, KeypadDelete);			BIND(VK_ANSI_KeypadEnter, KeypadEnter); | ||||
| 				BIND(VK_ANSI_KeypadDecimal, KeypadDecimalPoint);	BIND(VK_ANSI_KeypadEquals, KeypadEquals); | ||||
| 				BIND(VK_ANSI_KeypadMultiply, KeypadAsterisk);		BIND(VK_ANSI_KeypadDivide, KeypadSlash); | ||||
| 				BIND(VK_ANSI_KeypadPlus, KeypadPlus);				BIND(VK_ANSI_KeypadMinus, KeypadMinus); | ||||
| 				BIND(VK_ANSI_KeypadClear, KeypadDelete);			BIND(VK_ANSI_KeypadEnter, KeypadEnter); | ||||
|  | ||||
| 			BIND(VK_Return, Enter);					BIND(VK_Tab, Tab); | ||||
| 			BIND(VK_Space, Space);					BIND(VK_Delete, Backspace); | ||||
| 			BIND(VK_Control, LeftControl);			BIND(VK_Option, LeftOption); | ||||
| 			BIND(VK_Command, LeftMeta);				BIND(VK_Shift, LeftShift); | ||||
| 			BIND(VK_RightControl, RightControl);	BIND(VK_RightOption, RightOption); | ||||
| 			BIND(VK_Escape, Escape);				BIND(VK_CapsLock, CapsLock); | ||||
| 			BIND(VK_Home, Home);					BIND(VK_End, End); | ||||
| 			BIND(VK_PageUp, PageUp);				BIND(VK_PageDown, PageDown); | ||||
| 				BIND(VK_Return, Enter);					BIND(VK_Tab, Tab); | ||||
| 				BIND(VK_Space, Space);					BIND(VK_Delete, Backspace); | ||||
| 				BIND(VK_Control, LeftControl);			BIND(VK_Option, LeftOption); | ||||
| 				BIND(VK_Command, LeftMeta);				BIND(VK_Shift, LeftShift); | ||||
| 				BIND(VK_RightControl, RightControl);	BIND(VK_RightOption, RightOption); | ||||
| 				BIND(VK_Escape, Escape);				BIND(VK_CapsLock, CapsLock); | ||||
| 				BIND(VK_Home, Home);					BIND(VK_End, End); | ||||
| 				BIND(VK_PageUp, PageUp);				BIND(VK_PageDown, PageDown); | ||||
|  | ||||
| 			BIND(VK_RightShift, RightShift); | ||||
| 			BIND(VK_Help, Help); | ||||
| 			BIND(VK_ForwardDelete, Delete); | ||||
| 				BIND(VK_RightShift, RightShift); | ||||
| 				BIND(VK_Help, Help); | ||||
| 				BIND(VK_ForwardDelete, Delete); | ||||
|  | ||||
| 			BIND(VK_LeftArrow, Left);		BIND(VK_RightArrow, Right); | ||||
| 			BIND(VK_DownArrow, Down);		BIND(VK_UpArrow, Up); | ||||
| 		} | ||||
| 				BIND(VK_LeftArrow, Left);		BIND(VK_RightArrow, Right); | ||||
| 				BIND(VK_DownArrow, Down);		BIND(VK_UpArrow, Up); | ||||
| 			} | ||||
| #undef BIND | ||||
|  | ||||
| 		// Pick an ASCII code, if any. | ||||
| 		char pressedKey = '\0'; | ||||
| 		if(characters.length) { | ||||
| 			unichar firstCharacter = [characters characterAtIndex:0]; | ||||
| 			if(firstCharacter < 128) { | ||||
| 				pressedKey = (char)firstCharacter; | ||||
| 			// Pick an ASCII code, if any. | ||||
| 			char pressedKey = '\0'; | ||||
| 			if(characters.length) { | ||||
| 				unichar firstCharacter = [characters characterAtIndex:0]; | ||||
| 				if(firstCharacter < 128) { | ||||
| 					pressedKey = (char)firstCharacter; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			@synchronized(self) { | ||||
| 				if(keyboard_machine->apply_key(mapped_key, pressedKey, isPressed, self.inputMode == CSMachineKeyboardInputModeKeyboardLogical)) { | ||||
| 					return; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		@synchronized(self) { | ||||
| 			if(keyboard_machine->apply_key(mapped_key, pressedKey, isPressed, self.inputMode == CSMachineKeyboardInputModeKeyboardLogical)) { | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	auto joystick_machine = _machine->joystick_machine(); | ||||
| 	if(self.inputMode == CSMachineKeyboardInputModeJoystick && joystick_machine) { | ||||
| 		@synchronized(self) { | ||||
| 		auto joystick_machine = self->_machine->joystick_machine(); | ||||
| 		if(self.inputMode == CSMachineKeyboardInputModeJoystick && joystick_machine) { | ||||
| 			auto &joysticks = joystick_machine->get_joysticks(); | ||||
| 			if(!joysticks.empty()) { | ||||
| 				// Convert to a C++ bool so that the following calls are resolved correctly even if overloaded. | ||||
| @@ -530,49 +491,55 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}]; | ||||
| } | ||||
|  | ||||
| - (void)applyInputEvent:(dispatch_block_t)event { | ||||
| 	@synchronized(_inputEvents) { | ||||
| 		[_inputEvents addObject:event]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)clearAllKeys { | ||||
| 	const auto keyboard_machine = _machine->keyboard_machine(); | ||||
| 	if(keyboard_machine) { | ||||
| 		@synchronized(self) { | ||||
| 		[self applyInputEvent:^{ | ||||
| 			keyboard_machine->get_keyboard().reset_all_keys(); | ||||
| 		} | ||||
| 		}]; | ||||
| 	} | ||||
|  | ||||
| 	const auto joystick_machine = _machine->joystick_machine(); | ||||
| 	if(joystick_machine) { | ||||
| 		@synchronized(self) { | ||||
| 		[self applyInputEvent:^{ | ||||
| 			for(auto &joystick : joystick_machine->get_joysticks()) { | ||||
| 				joystick->reset_all_inputs(); | ||||
| 			} | ||||
| 		} | ||||
| 		}]; | ||||
| 	} | ||||
|  | ||||
| 	const auto mouse_machine = _machine->mouse_machine(); | ||||
| 	if(mouse_machine) { | ||||
| 		@synchronized(self) { | ||||
| 		[self applyInputEvent:^{ | ||||
| 			mouse_machine->get_mouse().reset_all_buttons(); | ||||
| 		} | ||||
| 		}]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)setMouseButton:(int)button isPressed:(BOOL)isPressed { | ||||
| 	auto mouse_machine = _machine->mouse_machine(); | ||||
| 	if(mouse_machine) { | ||||
| 		@synchronized(self) { | ||||
| 		[self applyInputEvent:^{ | ||||
| 			mouse_machine->get_mouse().set_button_pressed(button % mouse_machine->get_mouse().get_number_of_buttons(), isPressed); | ||||
| 		} | ||||
| 		}]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)addMouseMotionX:(CGFloat)deltaX y:(CGFloat)deltaY { | ||||
| 	auto mouse_machine = _machine->mouse_machine(); | ||||
| 	if(mouse_machine) { | ||||
| 		@synchronized(self) { | ||||
| 		[self applyInputEvent:^{ | ||||
| 			mouse_machine->get_mouse().move(int(deltaX), int(deltaY)); | ||||
| 		} | ||||
| 		}]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -737,11 +704,10 @@ struct ActivityObserver: public Activity::Observer { | ||||
|  | ||||
| #pragma mark - Timer | ||||
|  | ||||
| - (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime { | ||||
| - (void)scanTargetViewDisplayLinkDidFire:(CSScanTargetView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime { | ||||
| 	// First order of business: grab a timestamp. | ||||
| 	const auto timeNow = Time::nanos_now(); | ||||
|  | ||||
| 	CGSize pixelSize = view.backingSize; | ||||
| 	BOOL isSyncLocking; | ||||
| 	@synchronized(self) { | ||||
| 		// Store a means to map from CVTimeStamp.hostTime to Time::Nanos; | ||||
| @@ -753,9 +719,6 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 		// Store the next end-of-frame time. TODO: and start of next and implied visible duration, if raster racing? | ||||
| 		_syncTime = int64_t(now->hostTime) + _timeDiff; | ||||
|  | ||||
| 		// Also crib the current view pixel size. | ||||
| 		_pixelSize = pixelSize; | ||||
|  | ||||
| 		// Set the current refresh period. | ||||
| 		_refreshPeriod = double(now->videoRefreshPeriod) / double(now->videoTimeScale); | ||||
|  | ||||
| @@ -765,9 +728,7 @@ struct ActivityObserver: public Activity::Observer { | ||||
|  | ||||
| 	// Draw the current output. (TODO: do this within the timer if either raster racing or, at least, sync matching). | ||||
| 	if(!isSyncLocking) { | ||||
| 		[self.view performWithGLContext:^{ | ||||
| 			self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); | ||||
| 		} flushDrawable:YES]; | ||||
| 		[self.view draw]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -783,9 +744,16 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 		lastTime = std::max(timeNow - Time::Nanos(10'000'000'000 / TICKS), lastTime); | ||||
| 		const auto duration = timeNow - lastTime; | ||||
|  | ||||
| 		CGSize pixelSize; | ||||
| 		BOOL splitAndSync = NO; | ||||
| 		@synchronized(self) { | ||||
| 			// Post on input events. | ||||
| 			@synchronized(self->_inputEvents) { | ||||
| 				for(dispatch_block_t action: self->_inputEvents) { | ||||
| 					action(); | ||||
| 				} | ||||
| 				[self->_inputEvents removeAllObjects]; | ||||
| 			} | ||||
|  | ||||
| 			// If this tick includes vsync then inspect the machine. | ||||
| 			if(timeNow >= self->_syncTime && lastTime < self->_syncTime) { | ||||
| 				splitAndSync = self->_isSyncLocking = self->_scanSynchroniser.can_synchronise(self->_machine->scan_producer()->get_scan_status(), self->_refreshPeriod); | ||||
| @@ -806,7 +774,6 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 			if(!splitAndSync) { | ||||
| 				self->_machine->timed_machine()->run_for((double)duration / 1e9); | ||||
| 			} | ||||
| 			pixelSize = self->_pixelSize; | ||||
| 		} | ||||
|  | ||||
| 		// If this was not a split-and-sync then dispatch the update request asynchronously, unless | ||||
| @@ -822,13 +789,10 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 		} | ||||
| 		if(!wasUpdating) { | ||||
| 			dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ | ||||
| 				[self.view performWithGLContext:^{ | ||||
| 					self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); | ||||
|  | ||||
| 					if(splitAndSync) { | ||||
| 						self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); | ||||
| 					} | ||||
| 				} flushDrawable:splitAndSync]; | ||||
| 				[self.view updateBacking]; | ||||
| 				if(splitAndSync) { | ||||
| 					[self.view draw]; | ||||
| 				} | ||||
| 				self->_isUpdating.clear(); | ||||
| 			}); | ||||
| 		} | ||||
|   | ||||
| @@ -0,0 +1,16 @@ | ||||
| // | ||||
| //  CSScanTarget+C__ScanTarget.h | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 08/08/2020. | ||||
| //  Copyright © 2020 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "CSScanTarget.h" | ||||
| #include "ScanTarget.hpp" | ||||
|  | ||||
| @interface CSScanTarget (CppScanTarget) | ||||
|  | ||||
| @property (nonatomic, readonly, nonnull) Outputs::Display::ScanTarget *scanTarget; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										25
									
								
								OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // | ||||
| //  ScanTarget.h | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 02/08/2020. | ||||
| //  Copyright © 2020 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
| #import <MetalKit/MetalKit.h> | ||||
|  | ||||
| /*! | ||||
| 	Provides a ScanTarget that uses Metal as its back-end. | ||||
| */ | ||||
| @interface CSScanTarget : NSObject <MTKViewDelegate> | ||||
|  | ||||
| - (nonnull instancetype)initWithView:(nonnull MTKView *)view; | ||||
|  | ||||
| // Draws all scans currently residing at the scan target to the backing store, | ||||
| // ready for output when next requested. | ||||
| - (void)updateFrameBuffer; | ||||
|  | ||||
| - (nonnull NSBitmapImageRep *)imageRepresentation; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										1185
									
								
								OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1185
									
								
								OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										588
									
								
								OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										588
									
								
								OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,588 @@ | ||||
| // | ||||
| //  ScanTarget.metal | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 04/08/2020. | ||||
| //  Copyright © 2020 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include <metal_stdlib> | ||||
|  | ||||
| using namespace metal; | ||||
|  | ||||
| struct Uniforms { | ||||
| 	// This is used to scale scan positions, i.e. it provides the range | ||||
| 	// for mapping from scan-style integer positions into eye space. | ||||
| 	int2 scale; | ||||
|  | ||||
| 	// Applies a multiplication to all cyclesSinceRetrace values. | ||||
| 	float cycleMultiplier; | ||||
|  | ||||
| 	// This provides the intended height of a scan, in eye-coordinate terms. | ||||
| 	float lineWidth; | ||||
|  | ||||
| 	// Provides zoom and offset to scale the source data. | ||||
| 	float3x3 sourceToDisplay; | ||||
|  | ||||
| 	// Provides conversions to and from RGB for the active colour space. | ||||
| 	half3x3 toRGB; | ||||
| 	half3x3 fromRGB; | ||||
|  | ||||
| 	// Describes the filter in use for chroma filtering; it'll be | ||||
| 	// 15 coefficients but they're symmetrical around the centre. | ||||
| 	half3 chromaKernel[8]; | ||||
|  | ||||
| 	// Describes the filter in use for luma filtering; 15 coefficients | ||||
| 	// symmetrical around the centre. | ||||
| 	half lumaKernel[8]; | ||||
|  | ||||
| 	// Sets the opacity at which output strips are drawn. | ||||
| 	half outputAlpha; | ||||
|  | ||||
| 	// Sets the gamma power to which output colours are raised. | ||||
| 	half outputGamma; | ||||
|  | ||||
| 	// Sets a brightness multiplier for output colours. | ||||
| 	half outputMultiplier; | ||||
| }; | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| constexpr sampler standardSampler(	coord::pixel, | ||||
| 									address::clamp_to_edge,	// Although arbitrary, stick with this address mode for compatibility all the way to MTLFeatureSet_iOS_GPUFamily1_v1. | ||||
| 									filter::nearest); | ||||
|  | ||||
| constexpr sampler linearSampler(	coord::pixel, | ||||
| 									address::clamp_to_edge,	// Although arbitrary, stick with this address mode for compatibility all the way to MTLFeatureSet_iOS_GPUFamily1_v1. | ||||
| 									filter::linear); | ||||
|  | ||||
| } | ||||
|  | ||||
| // MARK: - Structs used for receiving data from the emulation. | ||||
|  | ||||
| // This is intended to match the net effect of `Scan` as defined by the BufferingScanTarget. | ||||
| struct Scan { | ||||
| 	struct EndPoint { | ||||
| 		uint16_t position[2]; | ||||
| 		uint16_t dataOffset; | ||||
| 		int16_t compositeAngle; | ||||
| 		uint16_t cyclesSinceRetrace; | ||||
| 	} endPoints[2]; | ||||
|  | ||||
| 	uint8_t compositeAmplitude; | ||||
| 	uint16_t dataY; | ||||
| 	uint16_t line; | ||||
| }; | ||||
|  | ||||
| // This matches the BufferingScanTarget's `Line`. | ||||
| struct Line { | ||||
| 	struct EndPoint { | ||||
| 		uint16_t position[2]; | ||||
| 		int16_t compositeAngle; | ||||
| 		uint16_t cyclesSinceRetrace; | ||||
| 	} endPoints[2]; | ||||
|  | ||||
| 	uint8_t compositeAmplitude; | ||||
| 	uint16_t line; | ||||
| }; | ||||
|  | ||||
| // MARK: - Intermediate structs. | ||||
|  | ||||
| struct SourceInterpolator { | ||||
| 	float4 position [[position]]; | ||||
| 	float2 textureCoordinates; | ||||
| 	half unitColourPhase;		// i.e. one unit per circle. | ||||
| 	half colourPhase;			// i.e. 2*pi units per circle, just regular radians. | ||||
| 	half colourAmplitude [[flat]]; | ||||
| }; | ||||
|  | ||||
| struct CopyInterpolator { | ||||
| 	float4 position [[position]]; | ||||
| 	float2 textureCoordinates; | ||||
| }; | ||||
|  | ||||
| // MARK: - Vertex shaders. | ||||
|  | ||||
| float2 textureLocation(constant Line *line, float offset, constant Uniforms &uniforms) { | ||||
| 	return float2( | ||||
| 		uniforms.cycleMultiplier * mix(line->endPoints[0].cyclesSinceRetrace, line->endPoints[1].cyclesSinceRetrace, offset), | ||||
| 		line->line + 0.5f); | ||||
| } | ||||
|  | ||||
| float2 textureLocation(constant Scan *scan, float offset, constant Uniforms &) { | ||||
| 	return float2( | ||||
| 		mix(scan->endPoints[0].dataOffset, scan->endPoints[1].dataOffset, offset), | ||||
| 		scan->dataY + 0.5f); | ||||
| } | ||||
|  | ||||
| template <typename Input> SourceInterpolator toDisplay( | ||||
| 	constant Uniforms &uniforms [[buffer(1)]], | ||||
| 	constant Input *inputs [[buffer(0)]], | ||||
| 	uint instanceID [[instance_id]], | ||||
| 	uint vertexID [[vertex_id]]) { | ||||
| 	SourceInterpolator output; | ||||
|  | ||||
| 	// Get start and end vertices in regular float2 form. | ||||
| 	const float2 start = float2( | ||||
| 		float(inputs[instanceID].endPoints[0].position[0]) / float(uniforms.scale.x), | ||||
| 		float(inputs[instanceID].endPoints[0].position[1]) / float(uniforms.scale.y) | ||||
| 	); | ||||
| 	const float2 end = float2( | ||||
| 		float(inputs[instanceID].endPoints[1].position[0]) / float(uniforms.scale.x), | ||||
| 		float(inputs[instanceID].endPoints[1].position[1]) / float(uniforms.scale.y) | ||||
| 	); | ||||
|  | ||||
| 	// Calculate the tangent and normal. | ||||
| 	const float2 tangent = (end - start); | ||||
| 	const float2 normal = float2(tangent.y, -tangent.x) / length(tangent); | ||||
|  | ||||
| 	// Load up the colour details. | ||||
| 	output.colourAmplitude = float(inputs[instanceID].compositeAmplitude) / 255.0f; | ||||
| 	output.unitColourPhase = mix( | ||||
| 		float(inputs[instanceID].endPoints[0].compositeAngle), | ||||
| 		float(inputs[instanceID].endPoints[1].compositeAngle), | ||||
| 		float((vertexID&2) >> 1) | ||||
| 	) / 64.0f; | ||||
| 	output.colourPhase = 2.0f * 3.141592654f * output.unitColourPhase; | ||||
|  | ||||
| 	// Hence determine this quad's real shape, using vertexID to pick a corner. | ||||
|  | ||||
| 	// position2d is now in the range [0, 1]. | ||||
| 	const float2 sourcePosition = start + (float(vertexID&2) * 0.5f) * tangent + (float(vertexID&1) - 0.5f) * normal * uniforms.lineWidth; | ||||
| 	const float2 position2d = (uniforms.sourceToDisplay * float3(sourcePosition, 1.0f)).xy; | ||||
|  | ||||
| 	output.position = float4( | ||||
| 		position2d, | ||||
| 		0.0f, | ||||
| 		1.0f | ||||
| 	); | ||||
| 	output.textureCoordinates = textureLocation(&inputs[instanceID], float((vertexID&2) >> 1), uniforms); | ||||
|  | ||||
| 	return output; | ||||
| } | ||||
|  | ||||
| // These next two assume the incoming geometry to be a four-vertex triangle strip; each instance will therefore | ||||
| // produce a quad. | ||||
|  | ||||
| vertex SourceInterpolator scanToDisplay(	constant Uniforms &uniforms [[buffer(1)]], | ||||
| 											constant Scan *scans [[buffer(0)]], | ||||
| 											uint instanceID [[instance_id]], | ||||
| 											uint vertexID [[vertex_id]]) { | ||||
| 	return toDisplay(uniforms, scans, instanceID, vertexID); | ||||
| } | ||||
|  | ||||
| vertex SourceInterpolator lineToDisplay(	constant Uniforms &uniforms [[buffer(1)]], | ||||
| 											constant Line *lines [[buffer(0)]], | ||||
| 											uint instanceID [[instance_id]], | ||||
| 											uint vertexID [[vertex_id]]) { | ||||
| 	return toDisplay(uniforms, lines, instanceID, vertexID); | ||||
| } | ||||
|  | ||||
| // This assumes that it needs to generate endpoints for a line segment. | ||||
|  | ||||
| vertex SourceInterpolator scanToComposition(	constant Uniforms &uniforms [[buffer(1)]], | ||||
| 												constant Scan *scans [[buffer(0)]], | ||||
| 												uint instanceID [[instance_id]], | ||||
| 												uint vertexID [[vertex_id]], | ||||
| 												texture2d<float> texture [[texture(0)]]) { | ||||
| 	SourceInterpolator result; | ||||
|  | ||||
| 	// Populate result as if direct texture access were available. | ||||
| 	result.position.x = uniforms.cycleMultiplier * mix(scans[instanceID].endPoints[0].cyclesSinceRetrace, scans[instanceID].endPoints[1].cyclesSinceRetrace, float(vertexID)); | ||||
| 	result.position.y = scans[instanceID].line; | ||||
| 	result.position.zw = float2(0.0f, 1.0f); | ||||
|  | ||||
| 	result.textureCoordinates.x = mix(scans[instanceID].endPoints[0].dataOffset, scans[instanceID].endPoints[1].dataOffset, float(vertexID)); | ||||
| 	result.textureCoordinates.y = scans[instanceID].dataY; | ||||
|  | ||||
| 	result.unitColourPhase = mix( | ||||
| 		float(scans[instanceID].endPoints[0].compositeAngle), | ||||
| 		float(scans[instanceID].endPoints[1].compositeAngle), | ||||
| 		float(vertexID) | ||||
| 	) / 64.0f; | ||||
| 	result.colourPhase = 2.0f * 3.141592654f * result.unitColourPhase; | ||||
| 	result.colourAmplitude = float(scans[instanceID].compositeAmplitude) / 255.0f; | ||||
|  | ||||
| 	// Map position into eye space, allowing for target texture dimensions. | ||||
| 	const float2 textureSize = float2(texture.get_width(), texture.get_height()); | ||||
| 	result.position.xy = | ||||
| 		((result.position.xy + float2(0.0f, 0.5f)) / textureSize) | ||||
| 		* float2(2.0f, -2.0f) + float2(-1.0f, 1.0f); | ||||
|  | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| vertex CopyInterpolator copyVertex(uint vertexID [[vertex_id]], texture2d<float> texture [[texture(0)]]) { | ||||
| 	CopyInterpolator vert; | ||||
|  | ||||
| 	const uint x = vertexID & 1; | ||||
| 	const uint y = (vertexID >> 1) & 1; | ||||
|  | ||||
| 	vert.textureCoordinates = float2( | ||||
| 		x * texture.get_width(), | ||||
| 		y * texture.get_height() | ||||
| 	); | ||||
| 	vert.position = float4( | ||||
| 		float(x) * 2.0 - 1.0, | ||||
| 		1.0 - float(y) * 2.0, | ||||
| 		0.0, | ||||
| 		1.0 | ||||
| 	); | ||||
|  | ||||
| 	return vert; | ||||
| } | ||||
|  | ||||
| // MARK: - Various input format conversion samplers. | ||||
|  | ||||
| half2 quadrature(float phase) { | ||||
| 	return half2(cos(phase), sin(phase)); | ||||
| } | ||||
|  | ||||
| half4 composite(half level, half2 quadrature, half amplitude) { | ||||
| 	return half4( | ||||
| 		level, | ||||
| 		half2(0.5f) + quadrature*half(0.5f), | ||||
| 		amplitude | ||||
| 	); | ||||
| } | ||||
|  | ||||
| // The luminance formats can be sampled either in their natural format, or to the intermediate | ||||
| // composite format used for composition. Direct sampling is always for final output, so the two | ||||
| // 8-bit formats also provide a gamma option. | ||||
|  | ||||
| half convertLuminance1(SourceInterpolator vert [[stage_in]], texture2d<ushort> texture [[texture(0)]]) { | ||||
| 	return clamp(half(texture.sample(standardSampler, vert.textureCoordinates).r), half(0.0f), half(1.0f)); | ||||
| } | ||||
|  | ||||
| half convertLuminance8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) { | ||||
| 	return texture.sample(standardSampler, vert.textureCoordinates).r; | ||||
| } | ||||
|  | ||||
| half convertPhaseLinkedLuminance8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) { | ||||
| 	const int offset = int(vert.unitColourPhase * 4.0f) & 3; | ||||
| 	auto sample = texture.sample(standardSampler, vert.textureCoordinates); | ||||
| 	return sample[offset]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #define CompositeSet(name, type)	\ | ||||
| 	fragment half4 sample##name(SourceInterpolator vert [[stage_in]], texture2d<type> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {	\ | ||||
| 		const half luminance = convert##name(vert, texture) * uniforms.outputMultiplier;	\ | ||||
| 		return half4(half3(luminance), uniforms.outputAlpha);	\ | ||||
| 	}	\ | ||||
| 	\ | ||||
| 	fragment half4 sample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d<type> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {	\ | ||||
| 		const half luminance = pow(convert##name(vert, texture) * uniforms.outputMultiplier, uniforms.outputGamma);	\ | ||||
| 		return half4(half3(luminance), uniforms.outputAlpha);	\ | ||||
| 	}	\ | ||||
| 	\ | ||||
| 	fragment half4 compositeSample##name(SourceInterpolator vert [[stage_in]], texture2d<type> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {	\ | ||||
| 		const half luminance = convert##name(vert, texture) * uniforms.outputMultiplier;	\ | ||||
| 		return composite(luminance, quadrature(vert.colourPhase), vert.colourAmplitude);	\ | ||||
| 	} | ||||
|  | ||||
| CompositeSet(Luminance1, ushort); | ||||
| CompositeSet(Luminance8, half); | ||||
| CompositeSet(PhaseLinkedLuminance8, half); | ||||
|  | ||||
| #undef CompositeSet | ||||
|  | ||||
| // The luminance/phase format can produce either composite or S-Video. | ||||
|  | ||||
| /// @returns A 2d vector comprised where .x = luminance; .y = chroma. | ||||
| half2 convertLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) { | ||||
| 	const auto luminancePhase = texture.sample(standardSampler, vert.textureCoordinates).rg; | ||||
| 	const half phaseOffset = 3.141592654 * 4.0 * luminancePhase.g; | ||||
| 	const half rawChroma = step(luminancePhase.g, half(0.75f)) * cos(vert.colourPhase + phaseOffset); | ||||
| 	return half2(luminancePhase.r, rawChroma); | ||||
| } | ||||
|  | ||||
| fragment half4 compositeSampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) { | ||||
| 	const half2 luminanceChroma = convertLuminance8Phase8(vert, texture); | ||||
| 	const half luminance = mix(luminanceChroma.r, luminanceChroma.g, vert.colourAmplitude); | ||||
| 	return composite(luminance, quadrature(vert.colourPhase), vert.colourAmplitude); | ||||
| } | ||||
|  | ||||
| fragment half4 sampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) { | ||||
| 	const half2 luminanceChroma = convertLuminance8Phase8(vert, texture); | ||||
| 	const half2 qam = quadrature(vert.colourPhase) * half(0.5f); | ||||
| 	return half4(luminanceChroma.r, | ||||
| 			half2(0.5f) + luminanceChroma.g*qam, | ||||
| 			half(1.0f)); | ||||
| } | ||||
|  | ||||
| fragment half4 directCompositeSampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { | ||||
| 	const half2 luminanceChroma = convertLuminance8Phase8(vert, texture); | ||||
| 	const half luminance = mix(luminanceChroma.r * uniforms.outputMultiplier, luminanceChroma.g, vert.colourAmplitude); | ||||
| 	return half4(half3(luminance), uniforms.outputAlpha); | ||||
| } | ||||
|  | ||||
| fragment half4 directCompositeSampleLuminance8Phase8WithGamma(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { | ||||
| 	const half2 luminanceChroma = convertLuminance8Phase8(vert, texture); | ||||
| 	const half luminance = mix(pow(luminanceChroma.r * uniforms.outputMultiplier, uniforms.outputGamma), luminanceChroma.g, vert.colourAmplitude); | ||||
| 	return half4(half3(luminance), uniforms.outputAlpha); | ||||
| } | ||||
|  | ||||
|  | ||||
| // All the RGB formats can produce RGB, composite or S-Video. | ||||
|  | ||||
| half3 convertRed8Green8Blue8(SourceInterpolator vert, texture2d<half> texture) { | ||||
| 	return texture.sample(standardSampler, vert.textureCoordinates).rgb; | ||||
| } | ||||
|  | ||||
| half3 convertRed4Green4Blue4(SourceInterpolator vert, texture2d<ushort> texture) { | ||||
| 	const auto sample = texture.sample(standardSampler, vert.textureCoordinates).rg; | ||||
| 	return clamp(half3(sample.r&15, (sample.g >> 4)&15, sample.g&15), half(0.0f), half(1.0f)); | ||||
| } | ||||
|  | ||||
| half3 convertRed2Green2Blue2(SourceInterpolator vert, texture2d<ushort> texture) { | ||||
| 	const auto sample = texture.sample(standardSampler, vert.textureCoordinates).r; | ||||
| 	return clamp(half3((sample >> 4)&3, (sample >> 2)&3, sample&3), half(0.0f), half(1.0f)); | ||||
| } | ||||
|  | ||||
| half3 convertRed1Green1Blue1(SourceInterpolator vert, texture2d<ushort> texture) { | ||||
| 	const auto sample = texture.sample(standardSampler, vert.textureCoordinates).r; | ||||
| 	return clamp(half3(sample&4, sample&2, sample&1), half(0.0f), half(1.0f)); | ||||
| } | ||||
|  | ||||
| #define DeclareShaders(name, pixelType)	\ | ||||
| 	fragment half4 sample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {	\ | ||||
| 		return half4(convert##name(vert, texture), uniforms.outputAlpha);	\ | ||||
| 	}	\ | ||||
| 	\ | ||||
| 	fragment half4 sample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {	\ | ||||
| 		return half4(pow(convert##name(vert, texture), uniforms.outputGamma), uniforms.outputAlpha);	\ | ||||
| 	}	\ | ||||
| 	\ | ||||
| 	fragment half4 svideoSample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {	\ | ||||
| 		const auto colour = uniforms.fromRGB * convert##name(vert, texture);	\ | ||||
| 		const half2 qam = quadrature(vert.colourPhase);	\ | ||||
| 		const half chroma = dot(colour.gb, qam);	\ | ||||
| 		return half4(	\ | ||||
| 			colour.r,	\ | ||||
| 			half2(0.5f) + chroma*qam*half(0.5f),	\ | ||||
| 			half(1.0f)		\ | ||||
| 		);	\ | ||||
| 	}	\ | ||||
| 	\ | ||||
| 	half composite##name(SourceInterpolator vert, texture2d<pixelType> texture, constant Uniforms &uniforms, half2 colourSubcarrier) {	\ | ||||
| 		const auto colour = uniforms.fromRGB * convert##name(vert, texture);	\ | ||||
| 		return mix(colour.r, dot(colour.gb, colourSubcarrier), half(vert.colourAmplitude));	\ | ||||
| 	}	\ | ||||
| 	\ | ||||
| 	fragment half4 compositeSample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {	\ | ||||
| 		const half2 colourSubcarrier = quadrature(vert.colourPhase);	\ | ||||
| 		return composite(composite##name(vert, texture, uniforms, colourSubcarrier), colourSubcarrier, vert.colourAmplitude);	\ | ||||
| 	}	\ | ||||
| 	\ | ||||
| 	fragment half4 directCompositeSample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {	\ | ||||
| 		const half level = composite##name(vert, texture, uniforms, quadrature(vert.colourPhase)); 	\ | ||||
| 		return half4(half3(level), uniforms.outputAlpha);	\ | ||||
| 	}	\ | ||||
| 	\ | ||||
| 	fragment half4 directCompositeSample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {	\ | ||||
| 		const half level = pow(composite##name(vert, texture, uniforms, quadrature(vert.colourPhase)), uniforms.outputGamma); 	\ | ||||
| 		return half4(half3(level), uniforms.outputAlpha);	\ | ||||
| 	} | ||||
|  | ||||
| DeclareShaders(Red8Green8Blue8, half) | ||||
| DeclareShaders(Red4Green4Blue4, ushort) | ||||
| DeclareShaders(Red2Green2Blue2, ushort) | ||||
| DeclareShaders(Red1Green1Blue1, ushort) | ||||
|  | ||||
| fragment half4 copyFragment(CopyInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) { | ||||
| 	return texture.sample(standardSampler, vert.textureCoordinates); | ||||
| } | ||||
|  | ||||
| fragment half4 interpolateFragment(CopyInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) { | ||||
| 	return texture.sample(linearSampler, vert.textureCoordinates); | ||||
| } | ||||
|  | ||||
| fragment half4 clearFragment(constant Uniforms &uniforms [[buffer(0)]]) { | ||||
| 	return half4(0.0, 0.0, 0.0, uniforms.outputAlpha); | ||||
| } | ||||
|  | ||||
| // MARK: - Compute kernels | ||||
|  | ||||
| /// Given input pixels of the form (luminance, 0.5 + 0.5*chrominance*cos(phase), 0.5 + 0.5*chrominance*sin(phase)), applies a lowpass | ||||
| /// filter to the two chrominance parts, then uses the toRGB matrix to convert to RGB and stores. | ||||
| template <bool applyGamma> void filterChromaKernel(	texture2d<half, access::read> inTexture [[texture(0)]], | ||||
| 													texture2d<half, access::write> outTexture [[texture(1)]], | ||||
| 													uint2 gid [[thread_position_in_grid]], | ||||
| 													constant Uniforms &uniforms [[buffer(0)]], | ||||
| 													constant int &offset [[buffer(1)]]) { | ||||
| 	constexpr half4 moveToZero(0.0f, 0.5f, 0.5f, 0.0f); | ||||
| 	const half4 rawSamples[] = { | ||||
| 		inTexture.read(gid + uint2(0, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(1, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(2, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(3, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(4, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(5, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(6, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(7, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(8, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(9, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(10, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(11, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(12, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(13, offset)) - moveToZero, | ||||
| 		inTexture.read(gid + uint2(14, offset)) - moveToZero, | ||||
| 	}; | ||||
|  | ||||
| #define Sample(x, y) uniforms.chromaKernel[y] * rawSamples[x].rgb | ||||
| 	const half3 colour = | ||||
| 		Sample(0, 0) + Sample(1, 1) + Sample(2, 2) + Sample(3, 3) + Sample(4, 4) + Sample(5, 5) + Sample(6, 6) + | ||||
| 		Sample(7, 7) + | ||||
| 		Sample(8, 6) + Sample(9, 5) + Sample(10, 4) + Sample(11, 3) + Sample(12, 2) + Sample(13, 1) + Sample(14, 0); | ||||
| #undef Sample | ||||
|  | ||||
| 	const half4 output = half4(uniforms.toRGB * colour * uniforms.outputMultiplier, uniforms.outputAlpha); | ||||
| 	if(applyGamma) { | ||||
| 		outTexture.write(pow(output, uniforms.outputGamma), gid + uint2(7, offset)); | ||||
| 	} else { | ||||
| 		outTexture.write(output, gid + uint2(7, offset)); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| kernel void filterChromaKernelNoGamma(	texture2d<half, access::read> inTexture [[texture(0)]], | ||||
| 										texture2d<half, access::write> outTexture [[texture(1)]], | ||||
| 										uint2 gid [[thread_position_in_grid]], | ||||
| 										constant Uniforms &uniforms [[buffer(0)]], | ||||
| 										constant int &offset [[buffer(1)]]) { | ||||
| 	filterChromaKernel<false>(inTexture, outTexture, gid, uniforms, offset); | ||||
| } | ||||
|  | ||||
| kernel void filterChromaKernelWithGamma(	texture2d<half, access::read> inTexture [[texture(0)]], | ||||
| 											texture2d<half, access::write> outTexture [[texture(1)]], | ||||
| 											uint2 gid [[thread_position_in_grid]], | ||||
| 											constant Uniforms &uniforms [[buffer(0)]], | ||||
| 											constant int &offset [[buffer(1)]]) { | ||||
| 	filterChromaKernel<true>(inTexture, outTexture, gid, uniforms, offset); | ||||
| } | ||||
|  | ||||
| void setSeparatedLumaChroma(half luminance, half4 centreSample, texture2d<half, access::write> outTexture, uint2 gid, int offset) { | ||||
| 	// The mix/steps below ensures that the absence of a colour burst leads the colour subcarrier to be discarded. | ||||
| 	const half isColour = step(half(0.01f), centreSample.a); | ||||
| 	const half chroma = (centreSample.r - luminance) / mix(half(1.0f), centreSample.a, isColour); | ||||
| 	outTexture.write(half4( | ||||
| 			luminance / mix(half(1.0f), (half(1.0f) - centreSample.a), isColour), | ||||
| 			isColour * (centreSample.gb - half2(0.5f)) * chroma + half2(0.5f), | ||||
| 			1.0f | ||||
| 		), | ||||
| 		gid + uint2(7, offset)); | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Given input pixels of the form: | ||||
| /// | ||||
| ///	(composite sample, cos(phase), sin(phase), colour amplitude), applies a lowpass | ||||
| /// | ||||
| /// Filters to separate luminance, subtracts that and scales and maps the remaining chrominance in order to output | ||||
| /// pixels in the form: | ||||
| /// | ||||
| ///	(luminance, 0.5 + 0.5*chrominance*cos(phase), 0.5 + 0.5*chrominance*sin(phase)) | ||||
| /// | ||||
| /// i.e. the input form for the filterChromaKernel, above]. | ||||
| kernel void separateLumaKernel15(	texture2d<half, access::read> inTexture [[texture(0)]], | ||||
| 									texture2d<half, access::write> outTexture [[texture(1)]], | ||||
| 									uint2 gid [[thread_position_in_grid]], | ||||
| 									constant Uniforms &uniforms [[buffer(0)]], | ||||
| 									constant int &offset [[buffer(1)]]) { | ||||
| 	const half4 centreSample = inTexture.read(gid + uint2(7, offset)); | ||||
| 	const half rawSamples[] = { | ||||
| 		inTexture.read(gid + uint2(0, offset)).r,	inTexture.read(gid + uint2(1, offset)).r, | ||||
| 		inTexture.read(gid + uint2(2, offset)).r,	inTexture.read(gid + uint2(3, offset)).r, | ||||
| 		inTexture.read(gid + uint2(4, offset)).r,	inTexture.read(gid + uint2(5, offset)).r, | ||||
| 		inTexture.read(gid + uint2(6, offset)).r, | ||||
| 		centreSample.r, | ||||
| 		inTexture.read(gid + uint2(8, offset)).r, | ||||
| 		inTexture.read(gid + uint2(9, offset)).r,	inTexture.read(gid + uint2(10, offset)).r, | ||||
| 		inTexture.read(gid + uint2(11, offset)).r,	inTexture.read(gid + uint2(12, offset)).r, | ||||
| 		inTexture.read(gid + uint2(13, offset)).r,	inTexture.read(gid + uint2(14, offset)).r, | ||||
| 	}; | ||||
|  | ||||
| #define Sample(x, y) uniforms.lumaKernel[y] * rawSamples[x] | ||||
| 	const half luminance = | ||||
| 		Sample(0, 0) + Sample(1, 1) + Sample(2, 2) + Sample(3, 3) + Sample(4, 4) + Sample(5, 5) + Sample(6, 6) + | ||||
| 		Sample(7, 7) + | ||||
| 		Sample(8, 6) + Sample(9, 5) + Sample(10, 4) + Sample(11, 3) + Sample(12, 2) + Sample(13, 1) + Sample(14, 0); | ||||
| #undef Sample | ||||
|  | ||||
| 	return setSeparatedLumaChroma(luminance, centreSample, outTexture, gid, offset); | ||||
| } | ||||
|  | ||||
| kernel void separateLumaKernel9(	texture2d<half, access::read> inTexture [[texture(0)]], | ||||
| 									texture2d<half, access::write> outTexture [[texture(1)]], | ||||
| 									uint2 gid [[thread_position_in_grid]], | ||||
| 									constant Uniforms &uniforms [[buffer(0)]], | ||||
| 									constant int &offset [[buffer(1)]]) { | ||||
| 	const half4 centreSample = inTexture.read(gid + uint2(7, offset)); | ||||
| 	const half rawSamples[] = { | ||||
| 		inTexture.read(gid + uint2(3, offset)).r,	inTexture.read(gid + uint2(4, offset)).r, | ||||
| 		inTexture.read(gid + uint2(5, offset)).r,	inTexture.read(gid + uint2(6, offset)).r, | ||||
| 		centreSample.r, | ||||
| 		inTexture.read(gid + uint2(8, offset)).r,	inTexture.read(gid + uint2(9, offset)).r, | ||||
| 		inTexture.read(gid + uint2(10, offset)).r,	inTexture.read(gid + uint2(11, offset)).r | ||||
| 	}; | ||||
|  | ||||
| #define Sample(x, y) uniforms.lumaKernel[y] * rawSamples[x] | ||||
| 	const half luminance = | ||||
| 		Sample(0, 3) + Sample(1, 4) + Sample(2, 5) + Sample(3, 6) + | ||||
| 		Sample(4, 7) + | ||||
| 		Sample(5, 6) + Sample(6, 5) + Sample(7, 4) + Sample(8, 3); | ||||
| #undef Sample | ||||
|  | ||||
| 	return setSeparatedLumaChroma(luminance, centreSample, outTexture, gid, offset); | ||||
| } | ||||
|  | ||||
| kernel void separateLumaKernel7(	texture2d<half, access::read> inTexture [[texture(0)]], | ||||
| 									texture2d<half, access::write> outTexture [[texture(1)]], | ||||
| 									uint2 gid [[thread_position_in_grid]], | ||||
| 									constant Uniforms &uniforms [[buffer(0)]], | ||||
| 									constant int &offset [[buffer(1)]]) { | ||||
| 	const half4 centreSample = inTexture.read(gid + uint2(7, offset)); | ||||
| 	const half rawSamples[] = { | ||||
| 		inTexture.read(gid + uint2(4, offset)).r, | ||||
| 		inTexture.read(gid + uint2(5, offset)).r,	inTexture.read(gid + uint2(6, offset)).r, | ||||
| 		centreSample.r, | ||||
| 		inTexture.read(gid + uint2(8, offset)).r,	inTexture.read(gid + uint2(9, offset)).r, | ||||
| 		inTexture.read(gid + uint2(10, offset)).r | ||||
| 	}; | ||||
|  | ||||
| #define Sample(x, y) uniforms.lumaKernel[y] * rawSamples[x] | ||||
| 	const half luminance = | ||||
| 		Sample(0, 4) + Sample(1, 5) + Sample(2, 6) + | ||||
| 		Sample(3, 7) + | ||||
| 		Sample(4, 6) + Sample(5, 5) + Sample(6, 4); | ||||
| #undef Sample | ||||
|  | ||||
| 	return setSeparatedLumaChroma(luminance, centreSample, outTexture, gid, offset); | ||||
| } | ||||
|  | ||||
| kernel void separateLumaKernel5(	texture2d<half, access::read> inTexture [[texture(0)]], | ||||
| 									texture2d<half, access::write> outTexture [[texture(1)]], | ||||
| 									uint2 gid [[thread_position_in_grid]], | ||||
| 									constant Uniforms &uniforms [[buffer(0)]], | ||||
| 									constant int &offset [[buffer(1)]]) { | ||||
| 	const half4 centreSample = inTexture.read(gid + uint2(7, offset)); | ||||
| 	const half rawSamples[] = { | ||||
| 		inTexture.read(gid + uint2(5, offset)).r,	inTexture.read(gid + uint2(6, offset)).r, | ||||
| 		centreSample.r, | ||||
| 		inTexture.read(gid + uint2(8, offset)).r,	inTexture.read(gid + uint2(9, offset)).r, | ||||
| 	}; | ||||
|  | ||||
| #define Sample(x, y) uniforms.lumaKernel[y] * rawSamples[x] | ||||
| 	const half luminance = | ||||
| 		Sample(0, 5) + Sample(1, 6) + | ||||
| 		Sample(2, 7) + | ||||
| 		Sample(3, 6) + Sample(4, 5); | ||||
| #undef Sample | ||||
|  | ||||
| 	return setSeparatedLumaChroma(luminance, centreSample, outTexture, gid, offset); | ||||
| } | ||||
|  | ||||
| kernel void clearKernel(	texture2d<half, access::write> outTexture [[texture(1)]], | ||||
| 							uint2 gid [[thread_position_in_grid]]) { | ||||
| 	outTexture.write(half4(0.0f, 0.0f, 0.0f, 1.0f), gid); | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| //
 | ||||
| //  CSOpenGLView.h
 | ||||
| //  CSScanTargetView.h
 | ||||
| //  Clock Signal
 | ||||
| //
 | ||||
| //  Created by Thomas Harte on 16/07/2015.
 | ||||
| @@ -8,63 +8,12 @@ | ||||
| 
 | ||||
| #import <Foundation/Foundation.h> | ||||
| #import <AppKit/AppKit.h> | ||||
| #import <MetalKit/MetalKit.h> | ||||
| 
 | ||||
| @class CSOpenGLView; | ||||
| @class CSScanTargetView; | ||||
| @class CSScanTarget; | ||||
| 
 | ||||
| typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { | ||||
| 	/// Indicates that AppKit requested a redraw for some reason (mostly likely, the window is being resized). So,
 | ||||
| 	/// if the delegate doesn't redraw the view, the user is likely to see a graphical flaw.
 | ||||
| 	CSOpenGLViewRedrawEventAppKit, | ||||
| 	/// Indicates that the view's display-linked timer has triggered a redraw request. So, if the delegate doesn't
 | ||||
| 	/// redraw the view, the user will just see the previous drawing without interruption.
 | ||||
| 	CSOpenGLViewRedrawEventTimer | ||||
| }; | ||||
| 
 | ||||
| @protocol CSOpenGLViewDelegate | ||||
| /*!
 | ||||
| 	Requests that the delegate produce an image of its current output state. May be called on | ||||
| 	any queue or thread. | ||||
| 	@param view The view making the request. | ||||
| 	@param redrawEvent If @c YES then the delegate may decline to redraw if its output would be | ||||
| 	identical to the previous frame. If @c NO then the delegate must draw. | ||||
| */ | ||||
| - (void)openGLViewRedraw:(nonnull CSOpenGLView *)view event:(CSOpenGLViewRedrawEvent)redrawEvent; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces receipt of a file by drag and drop to the delegate. | ||||
| 	@param view The view making the request. | ||||
| 	@param URL The file URL of the received file. | ||||
| */ | ||||
| - (void)openGLView:(nonnull CSOpenGLView *)view didReceiveFileAtURL:(nonnull NSURL *)URL; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces 'capture' of the mouse — i.e. that the view is now preventing the mouse from exiting | ||||
| 	the window, in order to forward continuous mouse motion. | ||||
| 	@param view The view making the announcement. | ||||
| */ | ||||
| - (void)openGLViewDidCaptureMouse:(nonnull CSOpenGLView *)view; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces that the mouse is no longer captured. | ||||
| 	@param view The view making the announcement. | ||||
| */ | ||||
| - (void)openGLViewDidReleaseMouse:(nonnull CSOpenGLView *)view; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces that the OS mouse cursor is now being displayed again, after having been invisible. | ||||
| 	@param view The view making the announcement. | ||||
| */ | ||||
| - (void)openGLViewDidShowOSMouseCursor:(nonnull CSOpenGLView *)view; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces that the OS mouse cursor will now be hidden. | ||||
| 	@param view The view making the announcement. | ||||
| */ | ||||
| - (void)openGLViewWillHideOSMouseCursor:(nonnull CSOpenGLView *)view; | ||||
| 
 | ||||
| @end | ||||
| 
 | ||||
| @protocol CSOpenGLViewResponderDelegate <NSObject> | ||||
| @protocol CSScanTargetViewResponderDelegate <NSObject> | ||||
| /*!
 | ||||
| 	Supplies a keyDown event to the delegate. | ||||
| 	@param event The @c NSEvent describing the keyDown. | ||||
| @@ -111,41 +60,72 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { | ||||
| */ | ||||
| - (void)mouseUp:(nonnull NSEvent *)event; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces 'capture' of the mouse — i.e. that the view is now preventing the mouse from exiting | ||||
| 	the window, in order to forward continuous mouse motion. | ||||
| 	@param view The view making the announcement. | ||||
| */ | ||||
| - (void)scanTargetViewDidCaptureMouse:(nonnull CSScanTargetView *)view; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces that the mouse is no longer captured. | ||||
| 	@param view The view making the announcement. | ||||
| */ | ||||
| - (void)scanTargetViewDidReleaseMouse:(nonnull CSScanTargetView *)view; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces that the OS mouse cursor is now being displayed again, after having been invisible. | ||||
| 	@param view The view making the announcement. | ||||
| */ | ||||
| - (void)scanTargetViewDidShowOSMouseCursor:(nonnull CSScanTargetView *)view; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces that the OS mouse cursor will now be hidden. | ||||
| 	@param view The view making the announcement. | ||||
| */ | ||||
| - (void)scanTargetViewWillHideOSMouseCursor:(nonnull CSScanTargetView *)view; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Announces receipt of a file by drag and drop to the delegate. | ||||
| 	@param view The view making the request. | ||||
| 	@param URL The file URL of the received file. | ||||
| */ | ||||
| - (void)scanTargetView:(nonnull CSScanTargetView *)view didReceiveFileAtURL:(nonnull NSURL *)URL; | ||||
| 
 | ||||
| @end | ||||
| 
 | ||||
| /*!
 | ||||
| 	Although I'm still on the fence about this as a design decision, CSOpenGLView  is itself responsible | ||||
| 	Although I'm still on the fence about this as a design decision, CSScanTargetView  is itself responsible | ||||
| 	for creating and destroying a CVDisplayLink. There's a practical reason for this: you'll get real synchronisation | ||||
| 	only if a link is explicitly tied to a particular display, and the CSOpenGLView therefore owns the knowledge | ||||
| 	only if a link is explicitly tied to a particular display, and the CSScanTargetView therefore owns the knowledge | ||||
| 	necessary to decide when to create and modify them. It doesn't currently just propagate "did change screen"-type | ||||
| 	messages because I haven't yet found a way to track that other than polling, in which case I might as well put | ||||
| 	that into the display link callback. | ||||
| */ | ||||
| @protocol CSOpenGLViewDisplayLinkDelegate | ||||
| @protocol CSScanTargetViewDisplayLinkDelegate | ||||
| 
 | ||||
| /*!
 | ||||
| 	Informs the delegate that the display link has fired. | ||||
| */ | ||||
| - (void)openGLViewDisplayLinkDidFire:(nonnull CSOpenGLView *)view now:(nonnull const CVTimeStamp *)now outputTime:(nonnull const CVTimeStamp *)outputTime; | ||||
| - (void)scanTargetViewDisplayLinkDidFire:(nonnull CSScanTargetView *)view now:(nonnull const CVTimeStamp *)now outputTime:(nonnull const CVTimeStamp *)outputTime; | ||||
| 
 | ||||
| @end | ||||
| 
 | ||||
| /*!
 | ||||
| 	Provides an OpenGL canvas with a refresh-linked update timer that can forward a subset | ||||
| 	Provides a visible scan target with a refresh-linked update timer that can forward a subset | ||||
| 	of typical first-responder actions. | ||||
| */ | ||||
| @interface CSOpenGLView : NSOpenGLView | ||||
| @interface CSScanTargetView : MTKView | ||||
| 
 | ||||
| @property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate; | ||||
| @property (nonatomic, weak, nullable) id <CSOpenGLViewResponderDelegate> responderDelegate; | ||||
| @property (atomic, weak, nullable) id <CSOpenGLViewDisplayLinkDelegate> displayLinkDelegate; | ||||
| @property (nonatomic, weak, nullable) id <CSScanTargetViewResponderDelegate> responderDelegate; | ||||
| @property (atomic, weak, nullable) id <CSScanTargetViewDisplayLinkDelegate> displayLinkDelegate; | ||||
| 
 | ||||
| /// Determines whether the view offers mouse capturing — i.e. if the user clicks on the view then
 | ||||
| /// then the system cursor is disabled and the mouse events defined by CSOpenGLViewResponderDelegate
 | ||||
| /// then the system cursor is disabled and the mouse events defined by CSScanTargetViewResponderDelegate
 | ||||
| /// are forwarded, unless and until the user releases the mouse using the control+command shortcut.
 | ||||
| @property (nonatomic, assign) BOOL shouldCaptureMouse; | ||||
| 
 | ||||
| /// Determines whether the CSOpenGLViewResponderDelegate of this window expects to use the command
 | ||||
| /// Determines whether the CSScanTargetViewResponderDelegate of this window expects to use the command
 | ||||
| /// key as though it were any other key — i.e. all command combinations should be forwarded to the delegate,
 | ||||
| /// not being allowed to trigger regular application shortcuts such as command+q or command+h.
 | ||||
| ///
 | ||||
| @@ -162,19 +142,24 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { | ||||
| */ | ||||
| - (void)invalidate; | ||||
| 
 | ||||
| /// The size in pixels of the OpenGL canvas, factoring in screen pixel density and view size in points.
 | ||||
| @property (nonatomic, readonly) CGSize backingSize; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Locks this view's OpenGL context and makes it current, performs @c action and then unlocks | ||||
| 	the context. @c action is performed on the calling queue. | ||||
| 	Ensures output begins on all pending scans. | ||||
| */ | ||||
| - (void)performWithGLContext:(nonnull dispatch_block_t)action flushDrawable:(BOOL)flushDrawable; | ||||
| - (void)performWithGLContext:(nonnull dispatch_block_t)action; | ||||
| - (void)updateBacking; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Instructs that the mouse cursor, if currently captured, should be released. | ||||
| */ | ||||
| - (void)releaseMouse; | ||||
| 
 | ||||
| /*!
 | ||||
| 	@returns An image of the view's current contents. | ||||
| */ | ||||
| - (nonnull NSBitmapImageRep *)imageRepresentation; | ||||
| 
 | ||||
| /*!
 | ||||
| 	@returns The CSScanTarget being used for this display. | ||||
| */ | ||||
| @property(nonatomic, readonly, nonnull) CSScanTarget *scanTarget; | ||||
| 
 | ||||
| @end | ||||
| @@ -1,24 +1,24 @@ | ||||
| // | ||||
| //  CSOpenGLView | ||||
| //  CSScanTargetView | ||||
| //  CLK | ||||
| // | ||||
| //  Created by Thomas Harte on 16/07/2015. | ||||
| //  Copyright 2015 Thomas Harte. All rights reserved. | ||||
| // | ||||
| 
 | ||||
| #import "CSOpenGLView.h" | ||||
| #import "CSScanTargetView.h" | ||||
| #import "CSApplication.h" | ||||
| #import "CSScanTarget.h" | ||||
| @import CoreVideo; | ||||
| @import GLKit; | ||||
| 
 | ||||
| #include <stdatomic.h> | ||||
| 
 | ||||
| @interface CSOpenGLView () <NSDraggingDestination, CSApplicationEventDelegate> | ||||
| @interface CSScanTargetView () <NSDraggingDestination, CSApplicationEventDelegate> | ||||
| @end | ||||
| 
 | ||||
| @implementation CSOpenGLView { | ||||
| @implementation CSScanTargetView { | ||||
| 	CVDisplayLinkRef _displayLink; | ||||
| 	CGSize _backingSize; | ||||
| 	NSNumber *_currentScreenNumber; | ||||
| 
 | ||||
| 	NSTrackingArea *_mouseTrackingArea; | ||||
| @@ -27,20 +27,8 @@ | ||||
| 
 | ||||
| 	atomic_int _isDrawingFlag; | ||||
| 	BOOL _isInvalid; | ||||
| } | ||||
| 
 | ||||
| - (void)prepareOpenGL { | ||||
| 	[super prepareOpenGL]; | ||||
| 
 | ||||
| 	// Prepare the atomic int. | ||||
| 	atomic_init(&_isDrawingFlag, 0); | ||||
| 
 | ||||
| 	// Set the clear colour. | ||||
| 	[self.openGLContext makeCurrentContext]; | ||||
| 	glClearColor(0.0, 0.0, 0.0, 1.0); | ||||
| 
 | ||||
| 	// Setup the [initial] display link. | ||||
| 	[self setupDisplayLink]; | ||||
| 	CSScanTarget *_scanTarget; | ||||
| } | ||||
| 
 | ||||
| - (void)setupDisplayLink { | ||||
| @@ -58,17 +46,12 @@ | ||||
| 	// Set the renderer output callback function. | ||||
| 	CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self)); | ||||
| 
 | ||||
| 	// Set the display link for the current renderer. | ||||
| 	CGLContextObj cglContext = [[self openGLContext] CGLContextObj]; | ||||
| 	CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj]; | ||||
| 	CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat); | ||||
| 
 | ||||
| 	// Activate the display link. | ||||
| 	CVDisplayLinkStart(_displayLink); | ||||
| } | ||||
| 
 | ||||
| static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, __unused CVOptionFlags flagsIn, __unused CVOptionFlags *flagsOut, void *displayLinkContext) { | ||||
| 	CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext; | ||||
| 	CSScanTargetView *const view = (__bridge CSScanTargetView *)displayLinkContext; | ||||
| 
 | ||||
| 	// Schedule an opportunity to check that the display link is still linked to the correct display. | ||||
| 	dispatch_async(dispatch_get_main_queue(), ^{ | ||||
| @@ -78,7 +61,7 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 	// Ensure _isDrawingFlag has value 1 when drawing, 0 otherwise. | ||||
| 	atomic_store(&view->_isDrawingFlag, 1); | ||||
| 
 | ||||
| 	[view.displayLinkDelegate openGLViewDisplayLinkDidFire:view now:now outputTime:outputTime]; | ||||
| 	[view.displayLinkDelegate scanTargetViewDisplayLinkDidFire:view now:now outputTime:outputTime]; | ||||
| 	/* | ||||
| 		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, | ||||
| @@ -106,30 +89,12 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 	// feels fine. | ||||
| 	NSNumber *const screenNumber = self.window.screen.deviceDescription[@"NSScreenNumber"]; | ||||
| 	if(![_currentScreenNumber isEqual:screenNumber]) { | ||||
| 		// 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]; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| - (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency { | ||||
| 	[self redrawWithEvent:CSOpenGLViewRedrawEventTimer]; | ||||
| } | ||||
| 
 | ||||
| - (void)drawRect:(NSRect)dirtyRect { | ||||
| 	[self redrawWithEvent:CSOpenGLViewRedrawEventAppKit]; | ||||
| } | ||||
| 
 | ||||
| - (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event  { | ||||
| 	[self performWithGLContext:^{ | ||||
| 		[self.delegate openGLViewRedraw:self event:event]; | ||||
| 	} flushDrawable:YES]; | ||||
| } | ||||
| 
 | ||||
| - (void)invalidate { | ||||
| 	_isInvalid = YES; | ||||
| 	[self stopDisplayLink]; | ||||
| @@ -160,65 +125,35 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 	CVDisplayLinkRelease(_displayLink); | ||||
| } | ||||
| 
 | ||||
| - (CGSize)backingSize { | ||||
| 	@synchronized(self) { | ||||
| 		return _backingSize; | ||||
| 	} | ||||
| - (CSScanTarget *)scanTarget { | ||||
| 	return _scanTarget; | ||||
| } | ||||
| 
 | ||||
| - (void)reshape { | ||||
| 	[super reshape]; | ||||
| 	@synchronized(self) { | ||||
| 		_backingSize = [self convertSizeToBacking:self.bounds.size]; | ||||
| 	} | ||||
| 
 | ||||
| 	[self performWithGLContext:^{ | ||||
| 		CGSize viewSize = [self backingSize]; | ||||
| 		glViewport(0, 0, (GLsizei)viewSize.width, (GLsizei)viewSize.height); | ||||
| 	} flushDrawable:NO]; | ||||
| - (void)updateBacking { | ||||
| 	[_scanTarget updateFrameBuffer]; | ||||
| } | ||||
| 
 | ||||
| - (void)awakeFromNib { | ||||
| 	NSOpenGLPixelFormatAttribute attributes[] = { | ||||
| 		NSOpenGLPFADoubleBuffer, | ||||
| 		NSOpenGLPFAOpenGLProfile,	NSOpenGLProfileVersion3_2Core, | ||||
| //		NSOpenGLPFAMultisample, | ||||
| //		NSOpenGLPFASampleBuffers,	1, | ||||
| //		NSOpenGLPFASamples,			2, | ||||
| 		0 | ||||
| 	}; | ||||
| 	// Use the preferred device if available. | ||||
| 	if(@available(macOS 10.15, *)) { | ||||
| 		self.device = self.preferredDevice; | ||||
| 	} else { | ||||
| 		self.device = MTLCreateSystemDefaultDevice(); | ||||
| 	} | ||||
| 
 | ||||
| 	NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes]; | ||||
| 	NSOpenGLContext *context = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil]; | ||||
| 	// Configure for explicit drawing. | ||||
| 	self.paused = YES; | ||||
| 	self.enableSetNeedsDisplay = NO; | ||||
| 
 | ||||
| #ifdef DEBUG | ||||
| 	// When we're using a CoreProfile context, crash if we call a legacy OpenGL function | ||||
| 	// This will make it much more obvious where and when such a function call is made so | ||||
| 	// that we can remove such calls. | ||||
| 	// Without this we'd simply get GL_INVALID_OPERATION error for calling legacy functions | ||||
| 	// but it would be more difficult to see where that function was called. | ||||
| 	CGLEnable([context CGLContextObj], kCGLCECrashOnRemovedFunctions); | ||||
| #endif | ||||
| 
 | ||||
| 	self.pixelFormat = pixelFormat; | ||||
| 	self.openGLContext = context; | ||||
| 	self.wantsBestResolutionOpenGLSurface = YES; | ||||
| 	// Create the scan target. | ||||
| 	_scanTarget = [[CSScanTarget alloc] initWithView:self]; | ||||
| 	self.delegate = _scanTarget; | ||||
| 
 | ||||
| 	// Register to receive dragged and dropped file URLs. | ||||
| 	[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]]; | ||||
| } | ||||
| 
 | ||||
| - (void)performWithGLContext:(dispatch_block_t)action flushDrawable:(BOOL)flushDrawable { | ||||
| 	CGLLockContext([[self openGLContext] CGLContextObj]); | ||||
| 	[self.openGLContext makeCurrentContext]; | ||||
| 	action(); | ||||
| 	CGLUnlockContext([[self openGLContext] CGLContextObj]); | ||||
| 
 | ||||
| 	if(flushDrawable) CGLFlushDrawable([[self openGLContext] CGLContextObj]); | ||||
| } | ||||
| 
 | ||||
| - (void)performWithGLContext:(nonnull dispatch_block_t)action { | ||||
| 	[self performWithGLContext:action flushDrawable:NO]; | ||||
| 	// Setup the [initial] display link. | ||||
| 	[self setupDisplayLink]; | ||||
| } | ||||
| 
 | ||||
| #pragma mark - NSResponder | ||||
| @@ -259,12 +194,16 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 	[self.responderDelegate paste:sender]; | ||||
| } | ||||
| 
 | ||||
| - (NSBitmapImageRep *)imageRepresentation { | ||||
| 	return self.scanTarget.imageRepresentation; | ||||
| } | ||||
| 
 | ||||
| #pragma mark - NSDraggingDestination | ||||
| 
 | ||||
| - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender { | ||||
| 	for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems]) { | ||||
| 		NSURL *URL = [NSURL URLWithString:[item stringForType:(__bridge NSString *)kUTTypeFileURL]]; | ||||
| 		[self.delegate openGLView:self didReceiveFileAtURL:URL]; | ||||
| 		[self.responderDelegate scanTargetView:self didReceiveFileAtURL:URL]; | ||||
| 	} | ||||
| 	return YES; | ||||
| } | ||||
| @@ -300,13 +239,13 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 
 | ||||
| 		_mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(__unused NSTimer * _Nonnull timer) { | ||||
| 			[NSCursor setHiddenUntilMouseMoves:YES]; | ||||
| 			[self.delegate openGLViewWillHideOSMouseCursor:self]; | ||||
| 			[self.responderDelegate scanTargetViewWillHideOSMouseCursor:self]; | ||||
| 		}]; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| - (void)mouseEntered:(NSEvent *)event { | ||||
| 	[self.delegate openGLViewDidShowOSMouseCursor:self]; | ||||
| 	[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self]; | ||||
| 	[super mouseEntered:event]; | ||||
| 	[self scheduleMouseHide]; | ||||
| } | ||||
| @@ -315,7 +254,7 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 	[super mouseExited:event]; | ||||
| 	[_mouseHideTimer invalidate]; | ||||
| 	_mouseHideTimer = nil; | ||||
| 	[self.delegate openGLViewWillHideOSMouseCursor:self]; | ||||
| 	[self.responderDelegate scanTargetViewWillHideOSMouseCursor:self]; | ||||
| } | ||||
| 
 | ||||
| - (void)releaseMouse { | ||||
| @@ -323,8 +262,8 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 		_mouseIsCaptured = NO; | ||||
| 		CGAssociateMouseAndMouseCursorPosition(true); | ||||
| 		[NSCursor unhide]; | ||||
| 		[self.delegate openGLViewDidReleaseMouse:self]; | ||||
| 		[self.delegate openGLViewDidShowOSMouseCursor:self]; | ||||
| 		[self.responderDelegate scanTargetViewDidReleaseMouse:self]; | ||||
| 		[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self]; | ||||
| 		((CSApplication *)[NSApplication sharedApplication]).eventDelegate = nil; | ||||
| 	} | ||||
| } | ||||
| @@ -336,7 +275,7 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 		// Mouse capture is off, so don't play games with the cursor, just schedule it to | ||||
| 		// hide in the near future. | ||||
| 		[self scheduleMouseHide]; | ||||
| 		[self.delegate openGLViewDidShowOSMouseCursor:self]; | ||||
| 		[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self]; | ||||
| 	} else { | ||||
| 		if(_mouseIsCaptured) { | ||||
| 			// Mouse capture is on, so move the cursor back to the middle of the window, and | ||||
| @@ -354,7 +293,7 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 
 | ||||
| 			[self.responderDelegate mouseMoved:event]; | ||||
| 		} else { | ||||
| 			[self.delegate openGLViewDidShowOSMouseCursor:self]; | ||||
| 			[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self]; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -387,8 +326,8 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const | ||||
| 			_mouseIsCaptured = YES; | ||||
| 			[NSCursor hide]; | ||||
| 			CGAssociateMouseAndMouseCursorPosition(false); | ||||
| 			[self.delegate openGLViewWillHideOSMouseCursor:self]; | ||||
| 			[self.delegate openGLViewDidCaptureMouse:self]; | ||||
| 			[self.responderDelegate scanTargetViewWillHideOSMouseCursor:self]; | ||||
| 			[self.responderDelegate scanTargetViewDidCaptureMouse:self]; | ||||
| 			if(self.shouldUsurpCommand) { | ||||
| 				((CSApplication *)[NSApplication sharedApplication]).eventDelegate = self; | ||||
| 			} | ||||
| @@ -7,7 +7,6 @@ | ||||
| // | ||||
|  | ||||
| #import <XCTest/XCTest.h> | ||||
| #import <OpenGL/OpenGL.h> | ||||
|  | ||||
| #include "9918.hpp" | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| QT += core gui multimedia widgets | ||||
|  | ||||
| # Be specific about C++17 but also try the vaguer C++1z for older | ||||
| # versions of Qt. | ||||
| CONFIG += c++17 | ||||
| CONFIG += c++1z | ||||
|  | ||||
| # Permit multiple source files in different directories to have the same file name. | ||||
| CONFIG += object_parallel_to_source | ||||
| @@ -82,6 +85,7 @@ SOURCES += \ | ||||
| \ | ||||
| 	$$SRC/Outputs/*.cpp \ | ||||
| 	$$SRC/Outputs/CRT/*.cpp \ | ||||
| 	$$SRC/Outputs/ScanTargets/*.cpp \ | ||||
| 	$$SRC/Outputs/OpenGL/*.cpp \ | ||||
| 	$$SRC/Outputs/OpenGL/Primitives/*.cpp \ | ||||
| \ | ||||
| @@ -201,6 +205,7 @@ HEADERS += \ | ||||
| 	$$SRC/Outputs/*.hpp \ | ||||
| 	$$SRC/Outputs/CRT/*.hpp \ | ||||
| 	$$SRC/Outputs/CRT/Internals/*.hpp \ | ||||
| 	$$SRC/Outputs/ScanTargets/*.hpp \ | ||||
| 	$$SRC/Outputs/OpenGL/*.hpp \ | ||||
| 	$$SRC/Outputs/OpenGL/Primitives/*.hpp \ | ||||
| 	$$SRC/Outputs/Speaker/*.hpp \ | ||||
|   | ||||
| @@ -45,7 +45,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { | ||||
|  | ||||
| MainWindow::MainWindow(const QString &fileName) { | ||||
| 	init(); | ||||
| 	launchFile(fileName); | ||||
| 	if(!launchFile(fileName)) { | ||||
| 		setUIPhase(UIPhase::SelectingMachine); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MainWindow::deleteMachine() { | ||||
| @@ -210,11 +212,17 @@ void MainWindow::insertFile(const QString &fileName) { | ||||
| 	mediaTarget->insert_media(media); | ||||
| } | ||||
|  | ||||
| void MainWindow::launchFile(const QString &fileName) { | ||||
| bool MainWindow::launchFile(const QString &fileName) { | ||||
| 	targets = Analyser::Static::GetTargets(fileName.toStdString()); | ||||
| 	if(!targets.empty()) { | ||||
| 		openFileName = QFileInfo(fileName).fileName(); | ||||
| 		launchMachine(); | ||||
| 		return true; | ||||
| 	} else { | ||||
| 		QMessageBox msgBox; | ||||
| 		msgBox.setText("Unable to open file: " + fileName); | ||||
| 		msgBox.exec(); | ||||
| 		return false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -707,6 +715,7 @@ void MainWindow::dropEvent(QDropEvent* event) { | ||||
| 			bool foundROM = false; | ||||
| 			const auto appDataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation).toStdString(); | ||||
|  | ||||
| 			QString unusedRoms; | ||||
| 			for(const auto &url: event->mimeData()->urls()) { | ||||
| 				const char *const name = url.toLocalFile().toUtf8(); | ||||
| 				FILE *const file = fopen(name, "rb"); | ||||
| @@ -716,6 +725,7 @@ void MainWindow::dropEvent(QDropEvent* event) { | ||||
| 				CRC::CRC32 generator; | ||||
| 				const uint32_t crc = generator.compute_crc(*contents); | ||||
|  | ||||
| 				bool wasUsed = false; | ||||
| 				for(const auto &rom: missingRoms) { | ||||
| 					if(std::find(rom.crc32s.begin(), rom.crc32s.end(), crc) != rom.crc32s.end()) { | ||||
| 						foundROM = true; | ||||
| @@ -731,10 +741,22 @@ void MainWindow::dropEvent(QDropEvent* event) { | ||||
| 						FILE *const target = fopen(destination.c_str(), "wb"); | ||||
| 						fwrite(contents->data(), 1, contents->size(), target); | ||||
| 						fclose(target); | ||||
|  | ||||
| 						wasUsed = true; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if(!wasUsed) { | ||||
| 					if(!unusedRoms.isEmpty()) unusedRoms += ", "; | ||||
| 					unusedRoms += url.fileName(); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if(!unusedRoms.isEmpty()) { | ||||
| 				QMessageBox msgBox; | ||||
| 				msgBox.setText("Couldn't identify ROMs: " + unusedRoms); | ||||
| 				msgBox.exec(); | ||||
| 			} | ||||
| 			if(foundROM) launchMachine(); | ||||
| 		} break; | ||||
| 	} | ||||
| @@ -1338,24 +1360,25 @@ void MainWindow::addActivityObserver() { | ||||
| } | ||||
|  | ||||
| void MainWindow::register_led(const std::string &name) { | ||||
| 	std::lock_guard guard(ledStatusesLock); | ||||
| 	ledStatuses[name] = false; | ||||
| 	updateStatusBarText(); | ||||
| 	QMetaObject::invokeMethod(this, "updateStatusBarText"); | ||||
| } | ||||
|  | ||||
| void MainWindow::set_led_status(const std::string &name, bool isLit) { | ||||
| 	std::lock_guard guard(ledStatusesLock); | ||||
| 	ledStatuses[name] = isLit; | ||||
| 	updateStatusBarText();	// Assumption here: Qt's attempt at automatic thread confinement will work here. | ||||
| 	QMetaObject::invokeMethod(this, "updateStatusBarText"); | ||||
| } | ||||
|  | ||||
| void MainWindow::updateStatusBarText() { | ||||
| 	QString fullText; | ||||
| 	bool isFirst = true; | ||||
| 	std::lock_guard guard(ledStatusesLock); | ||||
| 	for(const auto &pair: ledStatuses) { | ||||
| 		if(!isFirst) fullText += " | "; | ||||
| 		if(!fullText.isEmpty()) fullText += " | "; | ||||
| 		fullText += QString::fromStdString(pair.first); | ||||
| 		fullText += " "; | ||||
| 		fullText += pair.second ? "■" : "□"; | ||||
| 		isFirst = false; | ||||
| 	} | ||||
| 	statusBar()->showMessage(fullText); | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| #include <QMainWindow> | ||||
|  | ||||
| #include <memory> | ||||
| #include <mutex> | ||||
| #include <optional> | ||||
|  | ||||
| #include "audiobuffer.h" | ||||
| @@ -80,6 +81,7 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat | ||||
|  | ||||
| 	private slots: | ||||
| 		void startMachine(); | ||||
| 		void updateStatusBarText(); | ||||
|  | ||||
| 	private: | ||||
| 		void start_appleII(); | ||||
| @@ -100,7 +102,7 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat | ||||
| 		QAction *insertAction = nullptr; | ||||
| 		void insertFile(const QString &fileName); | ||||
|  | ||||
| 		void launchFile(const QString &fileName); | ||||
| 		bool launchFile(const QString &fileName); | ||||
| 		void launchTarget(std::unique_ptr<Analyser::Static::Target> &&); | ||||
|  | ||||
| 		void restoreSelections(); | ||||
| @@ -144,9 +146,11 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat | ||||
|  | ||||
| 		void register_led(const std::string &) override; | ||||
| 		void set_led_status(const std::string &, bool) override; | ||||
|  | ||||
| 		std::recursive_mutex ledStatusesLock; | ||||
| 		std::map<std::string, bool> ledStatuses; | ||||
|  | ||||
| 		void addActivityObserver(); | ||||
| 		void updateStatusBarText(); | ||||
| }; | ||||
|  | ||||
| #endif // MAINWINDOW_H | ||||
|   | ||||
| @@ -86,7 +86,7 @@ void ScanTargetWidget::vsync() { | ||||
| 	const auto time_now = Time::nanos_now(); | ||||
| 	requestedRedrawTime = vsyncPredictor.suggested_draw_time(); | ||||
| 	const auto delay_time = (requestedRedrawTime - time_now) / 1'000'000; | ||||
| 	if(delay_time > 0) { | ||||
| 	if(delay_time > 0 && delay_time < vsyncPredictor.frame_duration()) { | ||||
| 		QTimer::singleShot(delay_time, this, SLOT(repaint())); | ||||
| 	} else { | ||||
| 		requestedRedrawTime = 0; | ||||
|   | ||||
| @@ -1,19 +1,19 @@ | ||||
| import glob | ||||
| import sys | ||||
|  | ||||
| # establish UTF-8 encoding for Python 2 | ||||
| # Establish UTF-8 encoding for Python 2. | ||||
| if sys.version_info < (3, 0): | ||||
| 	reload(sys) | ||||
| 	sys.setdefaultencoding('utf-8') | ||||
|  | ||||
| # create build environment | ||||
| # Create build environment. | ||||
| env = Environment() | ||||
|  | ||||
| # determine compiler and linker flags for SDL | ||||
| # Determine compiler and linker flags for SDL. | ||||
| env.ParseConfig('sdl2-config --cflags') | ||||
| env.ParseConfig('sdl2-config --libs') | ||||
|  | ||||
| # gather a list of source files | ||||
| # Gather a list of source files. | ||||
| SOURCES = glob.glob('*.cpp') | ||||
|  | ||||
| SOURCES += glob.glob('../../Analyser/Dynamic/*.cpp') | ||||
| @@ -79,6 +79,7 @@ SOURCES += glob.glob('../../Machines/ZX8081/*.cpp') | ||||
|  | ||||
| SOURCES += glob.glob('../../Outputs/*.cpp') | ||||
| SOURCES += glob.glob('../../Outputs/CRT/*.cpp') | ||||
| SOURCES += glob.glob('../../Outputs/ScanTargets/*.cpp') | ||||
| SOURCES += glob.glob('../../Outputs/OpenGL/*.cpp') | ||||
| SOURCES += glob.glob('../../Outputs/OpenGL/Primitives/*.cpp') | ||||
|  | ||||
| @@ -117,11 +118,11 @@ SOURCES += glob.glob('../../Storage/Tape/*.cpp') | ||||
| SOURCES += glob.glob('../../Storage/Tape/Formats/*.cpp') | ||||
| SOURCES += glob.glob('../../Storage/Tape/Parsers/*.cpp') | ||||
|  | ||||
| # add additional compiler flags | ||||
| env.Append(CCFLAGS = ['--std=c++17', '-Wall', '-O2', '-DNDEBUG']) | ||||
| # Add additional compiler flags; c++1z is insurance in case c++17 isn't fully implemented. | ||||
| env.Append(CCFLAGS = ['--std=c++17', '--std=c++1z', '-Wall', '-O2', '-DNDEBUG']) | ||||
|  | ||||
| # add additional libraries to link against | ||||
| # Add additional libraries to link against. | ||||
| env.Append(LIBS = ['libz', 'pthread', 'GL']) | ||||
|  | ||||
| # build target | ||||
| # Build target. | ||||
| env.Program(target = 'clksignal', source = SOURCES) | ||||
|   | ||||
| @@ -27,7 +27,7 @@ void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Di | ||||
| 															//	7 microseconds for horizontal retrace and 500 to 750 microseconds for vertical retrace | ||||
| 															//  in NTSC and PAL TV." | ||||
|  | ||||
| 	time_multiplier_ = 65535 / cycles_per_line; | ||||
| 	time_multiplier_ = 63487 / cycles_per_line;	// 63475 = 65535 * 31/32, i.e. the same 1/32 error as below is permitted. | ||||
| 	phase_denominator_ = int64_t(cycles_per_line) * int64_t(colour_cycle_denominator) * int64_t(time_multiplier_); | ||||
| 	phase_numerator_ = 0; | ||||
| 	colour_cycle_numerator_ = int64_t(colour_cycle_numerator); | ||||
| @@ -194,8 +194,8 @@ Outputs::Display::ScanTarget::Scan::EndPoint CRT::end_point(uint16_t data_offset | ||||
| 	end_point.y = uint16_t(vertical_flywheel_->get_current_output_position() / vertical_flywheel_output_divider_); | ||||
| 	end_point.data_offset = data_offset; | ||||
|  | ||||
| 	// TODO: this is a workaround for the limited precision that can be posted onwards; | ||||
| 	// it'd be better to make time_multiplier_ an explicit modal and just not divide by it. | ||||
| 	// Ensure .composite_angle is sampled at the location indicated by .cycles_since_end_of_horizontal_retrace. | ||||
| 	// TODO: I could supply time_multiplier_ as a modal and just not round .cycles_since_end_of_horizontal_retrace. Would that be better? | ||||
| 	const auto lost_precision = cycles_since_horizontal_sync_ % time_multiplier_; | ||||
| 	end_point.composite_angle = int16_t(((phase_numerator_ - lost_precision * colour_cycle_numerator_) << 6) / phase_denominator_) * (is_alernate_line_ ? -1 : 1); | ||||
| 	end_point.cycles_since_end_of_horizontal_retrace = uint16_t(cycles_since_horizontal_sync_ / time_multiplier_); | ||||
| @@ -427,7 +427,8 @@ void CRT::set_immediate_default_phase(float phase) { | ||||
|  | ||||
| void CRT::output_data(int number_of_cycles, size_t number_of_samples) { | ||||
| #ifndef NDEBUG | ||||
| 	assert(number_of_samples > 0 && number_of_samples <= allocated_data_length_); | ||||
| 	assert(number_of_samples > 0); | ||||
| 	assert(number_of_samples <= allocated_data_length_); | ||||
| 	allocated_data_length_ = std::numeric_limits<size_t>::min(); | ||||
| #endif | ||||
| 	scan_target_->end_data(number_of_samples); | ||||
|   | ||||
| @@ -81,7 +81,7 @@ class CRT { | ||||
|  | ||||
| 		Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton; | ||||
| 		Outputs::Display::ScanTarget::Modals scan_target_modals_; | ||||
| 		static constexpr uint8_t DefaultAmplitude = 80; | ||||
| 		static constexpr uint8_t DefaultAmplitude = 41;	// Based upon a black level to maximum excursion and positive burst peak of: NTSC: 882 & 143; PAL: 933 & 150. | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 		size_t allocated_data_length_ = std::numeric_limits<size_t>::min(); | ||||
|   | ||||
| @@ -50,7 +50,7 @@ void Metrics::announce_did_resize() { | ||||
| 	frames_missed_ = frames_hit_ = 0; | ||||
| } | ||||
|  | ||||
| void Metrics::announce_draw_status(size_t, std::chrono::high_resolution_clock::duration, bool complete) { | ||||
| void Metrics::announce_draw_status(bool complete) { | ||||
| 	if(!complete) { | ||||
| 		++frames_missed_; | ||||
| 	} else { | ||||
| @@ -79,6 +79,10 @@ void Metrics::announce_draw_status(size_t, std::chrono::high_resolution_clock::d | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void Metrics::announce_draw_status(size_t, std::chrono::high_resolution_clock::duration, bool complete) { | ||||
| 	announce_draw_status(complete); | ||||
| } | ||||
|  | ||||
| bool Metrics::should_lower_resolution() const { | ||||
| 	// If less than 100 frames are on record, return no opinion; otherwise | ||||
| 	// suggest a lower resolution if more than 10 frames in the last 100-200 | ||||
|   | ||||
| @@ -12,6 +12,7 @@ | ||||
| #include "ScanTarget.hpp" | ||||
|  | ||||
| #include <array> | ||||
| #include <atomic> | ||||
| #include <chrono> | ||||
|  | ||||
| namespace Outputs { | ||||
| @@ -33,6 +34,9 @@ class Metrics { | ||||
| 		/// Provides Metrics with a new data point for output speed estimation. | ||||
| 		void announce_draw_status(size_t lines, std::chrono::high_resolution_clock::duration duration, bool complete); | ||||
|  | ||||
| 		/// Provides Metrics with a new data point for output speed estimation, albeit without line-specific information. | ||||
| 		void announce_draw_status(bool complete); | ||||
|  | ||||
| 		/// @returns @c true if Metrics thinks a lower output buffer resolution is desirable in the abstract; @c false otherwise. | ||||
| 		bool should_lower_resolution() const; | ||||
|  | ||||
| @@ -48,8 +52,8 @@ class Metrics { | ||||
| 		size_t line_total_history_pointer_ = 0; | ||||
| 		void add_line_total(int); | ||||
|  | ||||
| 		int frames_hit_ = 0; | ||||
| 		int frames_missed_ = 0; | ||||
| 		std::atomic<int> frames_hit_ = 0; | ||||
| 		std::atomic<int> frames_missed_ = 0; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -16,6 +16,8 @@ | ||||
| #ifdef __APPLE__ | ||||
| 	#if TARGET_OS_IPHONE | ||||
| 	#else | ||||
| 		// These remain so that I can, at least for now, build the kiosk version under macOS. | ||||
| 		// They can be eliminated if and when Apple fully withdraws OpenGL support. | ||||
| 		#include <OpenGL/OpenGL.h> | ||||
| 		#include <OpenGL/gl3.h> | ||||
| 		#include <OpenGL/gl3ext.h> | ||||
|   | ||||
| @@ -40,11 +40,6 @@ constexpr GLenum QAMChromaTextureUnit = GL_TEXTURE2; | ||||
| /// The texture unit that contains the current display. | ||||
| constexpr GLenum AccumulationTextureUnit = GL_TEXTURE3; | ||||
|  | ||||
| #define TextureAddress(x, y)	(((y) << 11) | (x)) | ||||
| #define TextureAddressGetY(v)	uint16_t((v) >> 11) | ||||
| #define TextureAddressGetX(v)	uint16_t((v) & 0x7ff) | ||||
| #define TextureSub(a, b)		(((a) - (b)) & 0x3fffff) | ||||
|  | ||||
| constexpr GLint internalFormatForDepth(std::size_t depth) { | ||||
| 	switch(depth) { | ||||
| 		default: return GL_FALSE; | ||||
| @@ -84,9 +79,8 @@ ScanTarget::ScanTarget(GLuint target_framebuffer, float output_gamma) : | ||||
| 	unprocessed_line_texture_(LineBufferWidth, LineBufferHeight, UnprocessedLineBufferTextureUnit, GL_NEAREST, false), | ||||
| 	full_display_rectangle_(-1.0f, -1.0f, 2.0f, 2.0f) { | ||||
|  | ||||
| 	// Ensure proper initialisation of the two atomic pointer sets. | ||||
| 	read_pointers_.store(write_pointers_); | ||||
| 	submit_pointers_.store(write_pointers_); | ||||
| 	set_scan_buffer(scan_buffer_.data(), scan_buffer_.size()); | ||||
| 	set_line_buffer(line_buffer_.data(), line_metadata_buffer_.data(), line_buffer_.size()); | ||||
|  | ||||
| 	// Allocate space for the scans and lines. | ||||
| 	allocate_buffer(scan_buffer_, scan_buffer_name_, scan_vertex_array_); | ||||
| @@ -101,265 +95,33 @@ ScanTarget::ScanTarget(GLuint target_framebuffer, float output_gamma) : | ||||
| 	test_gl(glBlendFunc, GL_SRC_ALPHA, GL_CONSTANT_COLOR); | ||||
| 	test_gl(glBlendColor, 0.4f, 0.4f, 0.4f, 1.0f); | ||||
|  | ||||
| 	// Establish initial state for the two atomic flags. | ||||
| 	is_updating_.clear(); | ||||
| 	// Establish initial state for is_drawing_to_accumulation_buffer_. | ||||
| 	is_drawing_to_accumulation_buffer_.clear(); | ||||
| } | ||||
|  | ||||
| ScanTarget::~ScanTarget() { | ||||
| 	while(is_updating_.test_and_set()); | ||||
| 	glDeleteBuffers(1, &scan_buffer_name_); | ||||
| 	glDeleteTextures(1, &write_area_texture_name_); | ||||
| 	glDeleteVertexArrays(1, &scan_vertex_array_); | ||||
| 	perform([=] { | ||||
| 		glDeleteBuffers(1, &scan_buffer_name_); | ||||
| 		glDeleteTextures(1, &write_area_texture_name_); | ||||
| 		glDeleteVertexArrays(1, &scan_vertex_array_); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void ScanTarget::set_target_framebuffer(GLuint target_framebuffer) { | ||||
| 	while(is_updating_.test_and_set()); | ||||
| 	target_framebuffer_ = target_framebuffer; | ||||
| 	is_updating_.clear(); | ||||
| } | ||||
|  | ||||
| void ScanTarget::set_modals(Modals modals) { | ||||
| 	// Don't change the modals while drawing is ongoing; a previous set might be | ||||
| 	// in the process of being established. | ||||
| 	while(is_updating_.test_and_set()); | ||||
| 	modals_ = modals; | ||||
| 	modals_are_dirty_ = true; | ||||
| 	is_updating_.clear(); | ||||
| } | ||||
|  | ||||
| Outputs::Display::ScanTarget::Scan *ScanTarget::begin_scan() { | ||||
| 	if(allocation_has_failed_) return nullptr; | ||||
|  | ||||
| 	std::lock_guard lock_guard(write_pointers_mutex_); | ||||
|  | ||||
| 	const auto result = &scan_buffer_[write_pointers_.scan_buffer]; | ||||
| 	const auto read_pointers = read_pointers_.load(); | ||||
|  | ||||
| 	// Advance the pointer. | ||||
| 	const auto next_write_pointer = decltype(write_pointers_.scan_buffer)((write_pointers_.scan_buffer + 1) % scan_buffer_.size()); | ||||
|  | ||||
| 	// Check whether that's too many. | ||||
| 	if(next_write_pointer == read_pointers.scan_buffer) { | ||||
| 		allocation_has_failed_ = true; | ||||
| 		return nullptr; | ||||
| 	} | ||||
| 	write_pointers_.scan_buffer = next_write_pointer; | ||||
| 	++provided_scans_; | ||||
|  | ||||
| 	// Fill in extra OpenGL-specific details. | ||||
| 	result->line = write_pointers_.line; | ||||
|  | ||||
| 	vended_scan_ = result; | ||||
| 	return &result->scan; | ||||
| } | ||||
|  | ||||
| void ScanTarget::end_scan() { | ||||
| 	if(vended_scan_) { | ||||
| 		std::lock_guard lock_guard(write_pointers_mutex_); | ||||
| 		vended_scan_->data_y = TextureAddressGetY(vended_write_area_pointer_); | ||||
| 		vended_scan_->line = write_pointers_.line; | ||||
| 		vended_scan_->scan.end_points[0].data_offset += TextureAddressGetX(vended_write_area_pointer_); | ||||
| 		vended_scan_->scan.end_points[1].data_offset += TextureAddressGetX(vended_write_area_pointer_); | ||||
|  | ||||
| #ifdef LOG_SCANS | ||||
| 		if(vended_scan_->scan.composite_amplitude) { | ||||
| 			std::cout << "S: "; | ||||
| 			std::cout << vended_scan_->scan.end_points[0].composite_angle << "/" << vended_scan_->scan.end_points[0].data_offset << "/" << vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace << " -> "; | ||||
| 			std::cout << vended_scan_->scan.end_points[1].composite_angle << "/" << vended_scan_->scan.end_points[1].data_offset << "/" << vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace << " => "; | ||||
| 			std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].data_offset - vended_scan_->scan.end_points[0].data_offset) * 64.0f) << "/"; | ||||
| 			std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace - vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f); | ||||
| 			std::cout << std::endl; | ||||
| 		} | ||||
| #endif | ||||
| 	} | ||||
| 	vended_scan_ = nullptr; | ||||
| } | ||||
|  | ||||
| uint8_t *ScanTarget::begin_data(size_t required_length, size_t required_alignment) { | ||||
| 	assert(required_alignment); | ||||
|  | ||||
| 	if(allocation_has_failed_) return nullptr; | ||||
|  | ||||
| 	std::lock_guard lock_guard(write_pointers_mutex_); | ||||
| 	if(write_area_texture_.empty()) { | ||||
| 		allocation_has_failed_ = true; | ||||
| 		return nullptr; | ||||
| 	} | ||||
|  | ||||
| 	// Determine where the proposed write area would start and end. | ||||
| 	uint16_t output_y = TextureAddressGetY(write_pointers_.write_area); | ||||
|  | ||||
| 	uint16_t aligned_start_x = TextureAddressGetX(write_pointers_.write_area & 0xffff) + 1; | ||||
| 	aligned_start_x += uint16_t((required_alignment - aligned_start_x%required_alignment)%required_alignment); | ||||
|  | ||||
| 	uint16_t end_x = aligned_start_x + uint16_t(1 + required_length); | ||||
|  | ||||
| 	if(end_x > WriteAreaWidth) { | ||||
| 		output_y = (output_y + 1) % WriteAreaHeight; | ||||
| 		aligned_start_x = uint16_t(required_alignment); | ||||
| 		end_x = aligned_start_x + uint16_t(1 + required_length); | ||||
| 	} | ||||
|  | ||||
| 	// Check whether that steps over the read pointer. | ||||
| 	const auto end_address = TextureAddress(end_x, output_y); | ||||
| 	const auto read_pointers = read_pointers_.load(); | ||||
|  | ||||
| 	const auto end_distance = TextureSub(end_address, read_pointers.write_area); | ||||
| 	const auto previous_distance = TextureSub(write_pointers_.write_area, read_pointers.write_area); | ||||
|  | ||||
| 	// If allocating this would somehow make the write pointer back away from the read pointer, | ||||
| 	// there must not be enough space left. | ||||
| 	if(end_distance < previous_distance) { | ||||
| 		allocation_has_failed_ = true; | ||||
| 		return nullptr; | ||||
| 	} | ||||
|  | ||||
| 	// Everything checks out, note expectation of a future end_data and return the pointer. | ||||
| 	data_is_allocated_ = true; | ||||
| 	vended_write_area_pointer_ = write_pointers_.write_area = TextureAddress(aligned_start_x, output_y); | ||||
|  | ||||
| 	assert(write_pointers_.write_area >= 1 && ((size_t(write_pointers_.write_area) + required_length + 1) * data_type_size_) <= write_area_texture_.size()); | ||||
| 	return &write_area_texture_[size_t(write_pointers_.write_area) * data_type_size_]; | ||||
|  | ||||
| 	// Note state at exit: | ||||
| 	//		write_pointers_.write_area points to the first pixel the client is expected to draw to. | ||||
| } | ||||
|  | ||||
| void ScanTarget::end_data(size_t actual_length) { | ||||
| 	if(allocation_has_failed_ || !data_is_allocated_) return; | ||||
|  | ||||
| 	std::lock_guard lock_guard(write_pointers_mutex_); | ||||
|  | ||||
| 	// Bookend the start of the new data, to safeguard for precision errors in sampling. | ||||
| 	memcpy( | ||||
| 		&write_area_texture_[size_t(write_pointers_.write_area - 1) * data_type_size_], | ||||
| 		&write_area_texture_[size_t(write_pointers_.write_area) * data_type_size_], | ||||
| 		data_type_size_); | ||||
|  | ||||
| 	// Advance to the end of the current run. | ||||
| 	write_pointers_.write_area += actual_length + 1; | ||||
|  | ||||
| 	// Also bookend the end. | ||||
| 	memcpy( | ||||
| 		&write_area_texture_[size_t(write_pointers_.write_area - 1) * data_type_size_], | ||||
| 		&write_area_texture_[size_t(write_pointers_.write_area - 2) * data_type_size_], | ||||
| 		data_type_size_); | ||||
|  | ||||
| 	// The write area was allocated in the knowledge that there's sufficient | ||||
| 	// distance left on the current line, but there's a risk of exactly filling | ||||
| 	// the final line, in which case this should wrap back to 0. | ||||
| 	write_pointers_.write_area %= (write_area_texture_.size() / data_type_size_); | ||||
|  | ||||
| 	// Record that no further end_data calls are expected. | ||||
| 	data_is_allocated_ = false; | ||||
| } | ||||
|  | ||||
| void ScanTarget::will_change_owner() { | ||||
| 	allocation_has_failed_ = true; | ||||
| 	vended_scan_ = nullptr; | ||||
| } | ||||
|  | ||||
| void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t composite_amplitude) { | ||||
| 	// Forward the event to the display metrics tracker. | ||||
| 	display_metrics_.announce_event(event); | ||||
|  | ||||
| 	if(event == ScanTarget::Event::EndVerticalRetrace) { | ||||
| 		// The previous-frame-is-complete flag is subject to a two-slot queue because | ||||
| 		// measurement for *this* frame needs to begin now, meaning that the previous | ||||
| 		// result needs to be put somewhere — it'll be attached to the first successful | ||||
| 		// line output. | ||||
| 		is_first_in_frame_ = true; | ||||
| 		previous_frame_was_complete_ = frame_is_complete_; | ||||
| 		frame_is_complete_ = true; | ||||
| 	} | ||||
|  | ||||
| 	if(output_is_visible_ == is_visible) return; | ||||
| 	if(is_visible) { | ||||
| 		const auto read_pointers = read_pointers_.load(); | ||||
| 		std::lock_guard lock_guard(write_pointers_mutex_); | ||||
|  | ||||
| 		// Commit the most recent line only if any scans fell on it. | ||||
| 		// Otherwise there's no point outputting it, it'll contribute nothing. | ||||
| 		if(provided_scans_) { | ||||
| 			// Store metadata if concluding a previous line. | ||||
| 			if(active_line_) { | ||||
| 				line_metadata_buffer_[size_t(write_pointers_.line)].is_first_in_frame = is_first_in_frame_; | ||||
| 				line_metadata_buffer_[size_t(write_pointers_.line)].previous_frame_was_complete = previous_frame_was_complete_; | ||||
| 				is_first_in_frame_ = false; | ||||
| 			} | ||||
|  | ||||
| 			// Attempt to allocate a new line; note allocation failure if necessary. | ||||
| 			const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight); | ||||
| 			if(next_line == read_pointers.line) { | ||||
| 				allocation_has_failed_ = true; | ||||
| 				active_line_ = nullptr; | ||||
| 			} else { | ||||
| 				write_pointers_.line = next_line; | ||||
| 				active_line_ = &line_buffer_[size_t(write_pointers_.line)]; | ||||
| 			} | ||||
| 			provided_scans_ = 0; | ||||
| 		} | ||||
|  | ||||
| 		if(active_line_) { | ||||
| 			active_line_->end_points[0].x = location.x; | ||||
| 			active_line_->end_points[0].y = location.y; | ||||
| 			active_line_->end_points[0].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace; | ||||
| 			active_line_->end_points[0].composite_angle = location.composite_angle; | ||||
| 			active_line_->line = write_pointers_.line; | ||||
| 			active_line_->composite_amplitude = composite_amplitude; | ||||
| 		} | ||||
| 	} else { | ||||
| 		if(active_line_) { | ||||
| 			// A successfully-allocated line is ending. | ||||
| 			active_line_->end_points[1].x = location.x; | ||||
| 			active_line_->end_points[1].y = location.y; | ||||
| 			active_line_->end_points[1].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace; | ||||
| 			active_line_->end_points[1].composite_angle = location.composite_angle; | ||||
|  | ||||
| #ifdef LOG_LINES | ||||
| 			if(active_line_->composite_amplitude) { | ||||
| 				std::cout << "L: "; | ||||
| 				std::cout << active_line_->end_points[0].composite_angle << "/" << active_line_->end_points[0].cycles_since_end_of_horizontal_retrace << " -> "; | ||||
| 				std::cout << active_line_->end_points[1].composite_angle << "/" << active_line_->end_points[1].cycles_since_end_of_horizontal_retrace << " => "; | ||||
| 				std::cout << (active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) << "/" << (active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) << " => "; | ||||
| 				std::cout << double(active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) / (double(active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f); | ||||
| 				std::cout << std::endl; | ||||
| 			} | ||||
| #endif | ||||
| 		} | ||||
|  | ||||
| 		// A line is complete; submit latest updates if nothing failed. | ||||
| 		if(allocation_has_failed_) { | ||||
| 			// Reset all pointers to where they were; this also means | ||||
| 			// the stencil won't be properly populated. | ||||
| 			write_pointers_ = submit_pointers_.load(); | ||||
| 			frame_is_complete_ = false; | ||||
| 		} else { | ||||
| 			// Advance submit pointer. | ||||
| 			submit_pointers_.store(write_pointers_); | ||||
| 		} | ||||
| 		allocation_has_failed_ = false; | ||||
| 	} | ||||
| 	output_is_visible_ = is_visible; | ||||
| 	perform([=] { | ||||
| 		target_framebuffer_ = target_framebuffer; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void ScanTarget::setup_pipeline() { | ||||
| 	const auto data_type_size = Outputs::Display::size_for_data_type(modals_.input_data_type); | ||||
| 	auto modals = BufferingScanTarget::modals(); | ||||
| 	const auto data_type_size = Outputs::Display::size_for_data_type(modals.input_data_type); | ||||
|  | ||||
| 	// Ensure the lock guard here has a restricted scope; this is the only time that a thread | ||||
| 	// other than the main owner of write_pointers_ may adjust it. | ||||
| 	{ | ||||
| 		std::lock_guard lock_guard(write_pointers_mutex_); | ||||
| 		if(data_type_size != data_type_size_) { | ||||
| 			// TODO: flush output. | ||||
|  | ||||
| 			data_type_size_ = data_type_size; | ||||
| 			write_area_texture_.resize(WriteAreaWidth*WriteAreaHeight*data_type_size_); | ||||
|  | ||||
| 			write_pointers_.scan_buffer = 0; | ||||
| 			write_pointers_.write_area = 0; | ||||
| 		} | ||||
| 	// Resize the texture only if required. | ||||
| 	const size_t required_size = WriteAreaWidth*WriteAreaHeight*data_type_size; | ||||
| 	if(required_size != write_area_data_size()) { | ||||
| 		write_area_texture_.resize(required_size); | ||||
| 		set_write_area(write_area_texture_.data()); | ||||
| 	} | ||||
|  | ||||
| 	// Prepare to bind line shaders. | ||||
| @@ -367,7 +129,7 @@ void ScanTarget::setup_pipeline() { | ||||
| 	test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_); | ||||
|  | ||||
| 	// Destroy or create a QAM buffer and shader, if appropriate. | ||||
| 	const bool needs_qam_buffer = (modals_.display_type == DisplayType::CompositeColour || modals_.display_type == DisplayType::SVideo); | ||||
| 	const bool needs_qam_buffer = (modals.display_type == DisplayType::CompositeColour || modals.display_type == DisplayType::SVideo); | ||||
| 	if(needs_qam_buffer) { | ||||
| 		if(!qam_chroma_texture_) { | ||||
| 			qam_chroma_texture_ = std::make_unique<TextureTarget>(LineBufferWidth, LineBufferHeight, QAMChromaTextureUnit, GL_NEAREST, false); | ||||
| @@ -386,8 +148,8 @@ void ScanTarget::setup_pipeline() { | ||||
| 	output_shader_ = conversion_shader(); | ||||
| 	enable_vertex_attributes(ShaderType::Conversion, *output_shader_); | ||||
| 	set_uniforms(ShaderType::Conversion, *output_shader_); | ||||
| 	output_shader_->set_uniform("origin", modals_.visible_area.origin.x, modals_.visible_area.origin.y); | ||||
| 	output_shader_->set_uniform("size", modals_.visible_area.size.width, modals_.visible_area.size.height); | ||||
| 	output_shader_->set_uniform("origin", modals.visible_area.origin.x, modals.visible_area.origin.y); | ||||
| 	output_shader_->set_uniform("size", modals.visible_area.size.width, modals.visible_area.size.height); | ||||
| 	output_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0)); | ||||
| 	output_shader_->set_uniform("qamTextureName", GLint(QAMChromaTextureUnit - GL_TEXTURE0)); | ||||
|  | ||||
| @@ -400,17 +162,14 @@ void ScanTarget::setup_pipeline() { | ||||
| 	input_shader_->set_uniform("textureName", GLint(SourceDataTextureUnit - GL_TEXTURE0)); | ||||
| } | ||||
|  | ||||
| Outputs::Display::Metrics &ScanTarget::display_metrics() { | ||||
| 	return display_metrics_; | ||||
| } | ||||
|  | ||||
| bool ScanTarget::is_soft_display_type() { | ||||
| 	return modals_.display_type == DisplayType::CompositeColour || modals_.display_type == DisplayType::CompositeMonochrome; | ||||
| 	const auto display_type = modals().display_type; | ||||
| 	return display_type == DisplayType::CompositeColour || display_type == DisplayType::CompositeMonochrome; | ||||
| } | ||||
|  | ||||
| void ScanTarget::update(int, int output_height) { | ||||
| 	// If the GPU is still busy, don't wait; we'll catch it next time. | ||||
| 	if(fence_ != nullptr) { | ||||
| 		// if the GPU is still busy, don't wait; we'll catch it next time | ||||
| 		if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, 0) == GL_TIMEOUT_EXPIRED) { | ||||
| 			display_metrics_.announce_draw_status( | ||||
| 				lines_submitted_, | ||||
| @@ -420,322 +179,314 @@ void ScanTarget::update(int, int output_height) { | ||||
| 		} | ||||
| 		fence_ = nullptr; | ||||
| 	} | ||||
|  | ||||
| 	// Update the display metrics. | ||||
| 	display_metrics_.announce_draw_status( | ||||
| 		lines_submitted_, | ||||
| 		std::chrono::high_resolution_clock::now() - line_submission_begin_time_, | ||||
| 		true); | ||||
|  | ||||
| 	// Spin until the is-drawing flag is reset; the wait sync above will deal | ||||
| 	// with instances where waiting is inappropriate. | ||||
| 	while(is_updating_.test_and_set()); | ||||
| 	// Grab the new output list. | ||||
| 	perform([=] { | ||||
| 		OutputArea area = get_output_area(); | ||||
|  | ||||
| 	// Establish the pipeline if necessary. | ||||
| 	const bool did_setup_pipeline = modals_are_dirty_; | ||||
| 	if(modals_are_dirty_) { | ||||
| 		setup_pipeline(); | ||||
| 		modals_are_dirty_ = false; | ||||
| 	} | ||||
|  | ||||
| 	// Determine the start time of this submission group. | ||||
| 	line_submission_begin_time_ = std::chrono::high_resolution_clock::now(); | ||||
|  | ||||
| 	// Grab the current read and submit pointers. | ||||
| 	const auto submit_pointers = submit_pointers_.load(); | ||||
| 	const auto read_pointers = read_pointers_.load(); | ||||
|  | ||||
| 	// Determine how many lines are about to be submitted. | ||||
| 	lines_submitted_ = (read_pointers.line + line_buffer_.size() - submit_pointers.line) % line_buffer_.size(); | ||||
|  | ||||
| 	// Submit scans; only the new ones need to be communicated. | ||||
| 	size_t new_scans = (submit_pointers.scan_buffer + scan_buffer_.size() - read_pointers.scan_buffer) % scan_buffer_.size(); | ||||
| 	if(new_scans) { | ||||
| 		test_gl(glBindBuffer, GL_ARRAY_BUFFER, scan_buffer_name_); | ||||
|  | ||||
| 		// Map only the required portion of the buffer. | ||||
| 		const size_t new_scans_size = new_scans * sizeof(Scan); | ||||
| 		uint8_t *const destination = static_cast<uint8_t *>( | ||||
| 			glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT) | ||||
| 		); | ||||
| 		test_gl_error(); | ||||
|  | ||||
| 		if(read_pointers.scan_buffer < submit_pointers.scan_buffer) { | ||||
| 			memcpy(destination, &scan_buffer_[read_pointers.scan_buffer], new_scans_size); | ||||
| 		} else { | ||||
| 			const size_t first_portion_length = (scan_buffer_.size() - read_pointers.scan_buffer) * sizeof(Scan); | ||||
| 			memcpy(destination, &scan_buffer_[read_pointers.scan_buffer], first_portion_length); | ||||
| 			memcpy(&destination[first_portion_length], &scan_buffer_[0], new_scans_size - first_portion_length); | ||||
| 		// Establish the pipeline if necessary. | ||||
| 		const auto new_modals = BufferingScanTarget::new_modals(); | ||||
| 		const bool did_setup_pipeline = bool(new_modals); | ||||
| 		if(did_setup_pipeline) { | ||||
| 			setup_pipeline(); | ||||
| 		} | ||||
|  | ||||
| 		// Flush and unmap the buffer. | ||||
| 		test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size)); | ||||
| 		test_gl(glUnmapBuffer, GL_ARRAY_BUFFER); | ||||
| 	} | ||||
| 		// Determine the start time of this submission group and the number of lines it will contain. | ||||
| 		line_submission_begin_time_ = std::chrono::high_resolution_clock::now(); | ||||
| 		lines_submitted_ = (area.end.line - area.start.line + line_buffer_.size()) % line_buffer_.size(); | ||||
|  | ||||
| 	// Submit texture. | ||||
| 	if(submit_pointers.write_area != read_pointers.write_area) { | ||||
| 		test_gl(glActiveTexture, SourceDataTextureUnit); | ||||
| 		test_gl(glBindTexture, GL_TEXTURE_2D, write_area_texture_name_); | ||||
| 		// Submit scans; only the new ones need to be communicated. | ||||
| 		size_t new_scans = (area.end.scan - area.start.scan + scan_buffer_.size()) % scan_buffer_.size(); | ||||
| 		if(new_scans) { | ||||
| 			test_gl(glBindBuffer, GL_ARRAY_BUFFER, scan_buffer_name_); | ||||
|  | ||||
| 		// Create storage for the texture if it doesn't yet exist; this was deferred until here | ||||
| 		// because the pixel format wasn't initially known. | ||||
| 		if(!texture_exists_) { | ||||
| 			test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | ||||
| 			test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | ||||
| 			test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); | ||||
| 			test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); | ||||
| 			test_gl(glTexImage2D, | ||||
| 				GL_TEXTURE_2D, | ||||
| 				0, | ||||
| 				internalFormatForDepth(data_type_size_), | ||||
| 				WriteAreaWidth, | ||||
| 				WriteAreaHeight, | ||||
| 				0, | ||||
| 				formatForDepth(data_type_size_), | ||||
| 				GL_UNSIGNED_BYTE, | ||||
| 				nullptr); | ||||
| 			texture_exists_ = true; | ||||
| 		} | ||||
| 			// Map only the required portion of the buffer. | ||||
| 			const size_t new_scans_size = new_scans * sizeof(Scan); | ||||
| 			uint8_t *const destination = static_cast<uint8_t *>( | ||||
| 				glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT) | ||||
| 			); | ||||
| 			test_gl_error(); | ||||
|  | ||||
| 		const auto start_y = TextureAddressGetY(read_pointers.write_area); | ||||
| 		const auto end_y = TextureAddressGetY(submit_pointers.write_area); | ||||
| 		if(end_y >= start_y) { | ||||
| 			// Submit the direct region from the submit pointer to the read pointer. | ||||
| 			test_gl(glTexSubImage2D, | ||||
| 				GL_TEXTURE_2D, 0, | ||||
| 				0, start_y, | ||||
| 				WriteAreaWidth, | ||||
| 				1 + end_y - start_y, | ||||
| 				formatForDepth(data_type_size_), | ||||
| 				GL_UNSIGNED_BYTE, | ||||
| 				&write_area_texture_[size_t(TextureAddress(0, start_y)) * data_type_size_]); | ||||
| 		} else { | ||||
| 			// The circular buffer wrapped around; submit the data from the read pointer to the end of | ||||
| 			// the buffer and from the start of the buffer to the submit pointer. | ||||
| 			test_gl(glTexSubImage2D, | ||||
| 				GL_TEXTURE_2D, 0, | ||||
| 				0, 0, | ||||
| 				WriteAreaWidth, | ||||
| 				1 + end_y, | ||||
| 				formatForDepth(data_type_size_), | ||||
| 				GL_UNSIGNED_BYTE, | ||||
| 				&write_area_texture_[0]); | ||||
| 			test_gl(glTexSubImage2D, | ||||
| 				GL_TEXTURE_2D, 0, | ||||
| 				0, start_y, | ||||
| 				WriteAreaWidth, | ||||
| 				WriteAreaHeight - start_y, | ||||
| 				formatForDepth(data_type_size_), | ||||
| 				GL_UNSIGNED_BYTE, | ||||
| 				&write_area_texture_[size_t(TextureAddress(0, start_y)) * data_type_size_]); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Push new input to the unprocessed line buffer. | ||||
| 	if(new_scans) { | ||||
| 		unprocessed_line_texture_.bind_framebuffer(); | ||||
|  | ||||
| 		// Clear newly-touched lines; that is everything from (read+1) to submit. | ||||
| 		const uint16_t first_line_to_clear = (read_pointers.line+1)%line_buffer_.size(); | ||||
| 		const uint16_t final_line_to_clear = submit_pointers.line; | ||||
| 		if(first_line_to_clear != final_line_to_clear) { | ||||
| 			test_gl(glEnable, GL_SCISSOR_TEST); | ||||
|  | ||||
| 			// Determine the proper clear colour — this needs to be anything that describes black | ||||
| 			// in the input colour encoding at use. | ||||
| 			if(modals_.input_data_type == InputDataType::Luminance8Phase8) { | ||||
| 				// Supply both a zero luminance and a colour-subcarrier-disengaging phase. | ||||
| 				test_gl(glClearColor, 0.0f, 1.0f, 0.0f, 0.0f); | ||||
| 			// Copy as a single chunk if possible; otherwise copy in two parts. | ||||
| 			if(area.start.scan < area.end.scan) { | ||||
| 				memcpy(destination, &scan_buffer_[size_t(area.start.scan)], new_scans_size); | ||||
| 			} else { | ||||
| 				test_gl(glClearColor, 0.0f, 0.0f, 0.0f, 0.0f); | ||||
| 				const size_t first_portion_length = (scan_buffer_.size() - area.start.scan) * sizeof(Scan); | ||||
| 				memcpy(destination, &scan_buffer_[area.start.scan], first_portion_length); | ||||
| 				memcpy(&destination[first_portion_length], &scan_buffer_[0], new_scans_size - first_portion_length); | ||||
| 			} | ||||
|  | ||||
| 			if(first_line_to_clear < final_line_to_clear) { | ||||
| 				test_gl(glScissor, 0, first_line_to_clear, unprocessed_line_texture_.get_width(), final_line_to_clear - first_line_to_clear); | ||||
| 				test_gl(glClear, GL_COLOR_BUFFER_BIT); | ||||
| 			// Flush and unmap the buffer. | ||||
| 			test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size)); | ||||
| 			test_gl(glUnmapBuffer, GL_ARRAY_BUFFER); | ||||
| 		} | ||||
|  | ||||
| 		// Submit texture. | ||||
| 		if(area.start.write_area_x != area.end.write_area_x || area.start.write_area_y != area.end.write_area_y) { | ||||
| 			test_gl(glActiveTexture, SourceDataTextureUnit); | ||||
| 			test_gl(glBindTexture, GL_TEXTURE_2D, write_area_texture_name_); | ||||
|  | ||||
| 			// Create storage for the texture if it doesn't yet exist; this was deferred until here | ||||
| 			// because the pixel format wasn't initially known. | ||||
| 			if(!texture_exists_) { | ||||
| 				test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | ||||
| 				test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | ||||
| 				test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); | ||||
| 				test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); | ||||
| 				test_gl(glTexImage2D, | ||||
| 					GL_TEXTURE_2D, | ||||
| 					0, | ||||
| 					internalFormatForDepth(write_area_data_size()), | ||||
| 					WriteAreaWidth, | ||||
| 					WriteAreaHeight, | ||||
| 					0, | ||||
| 					formatForDepth(write_area_data_size()), | ||||
| 					GL_UNSIGNED_BYTE, | ||||
| 					nullptr); | ||||
| 				texture_exists_ = true; | ||||
| 			} | ||||
|  | ||||
| 			if(area.end.write_area_y >= area.start.write_area_y) { | ||||
| 				// Submit the direct region from the submit pointer to the read pointer. | ||||
| 				test_gl(glTexSubImage2D, | ||||
| 					GL_TEXTURE_2D, 0, | ||||
| 					0, area.start.write_area_y, | ||||
| 					WriteAreaWidth, | ||||
| 					1 + area.end.write_area_y - area.start.write_area_y, | ||||
| 					formatForDepth(write_area_data_size()), | ||||
| 					GL_UNSIGNED_BYTE, | ||||
| 					&write_area_texture_[size_t(area.start.write_area_y * WriteAreaWidth) * write_area_data_size()]); | ||||
| 			} else { | ||||
| 				test_gl(glScissor, 0, 0, unprocessed_line_texture_.get_width(), final_line_to_clear); | ||||
| 				test_gl(glClear, GL_COLOR_BUFFER_BIT); | ||||
| 				test_gl(glScissor, 0, first_line_to_clear, unprocessed_line_texture_.get_width(), unprocessed_line_texture_.get_height() - first_line_to_clear); | ||||
| 				test_gl(glClear, GL_COLOR_BUFFER_BIT); | ||||
| 				// The circular buffer wrapped around; submit the data from the read pointer to the end of | ||||
| 				// the buffer and from the start of the buffer to the submit pointer. | ||||
| 				test_gl(glTexSubImage2D, | ||||
| 					GL_TEXTURE_2D, 0, | ||||
| 					0, area.start.write_area_y, | ||||
| 					WriteAreaWidth, | ||||
| 					WriteAreaHeight - area.start.write_area_y, | ||||
| 					formatForDepth(write_area_data_size()), | ||||
| 					GL_UNSIGNED_BYTE, | ||||
| 					&write_area_texture_[size_t(area.start.write_area_y * WriteAreaWidth) * write_area_data_size()]); | ||||
| 				test_gl(glTexSubImage2D, | ||||
| 					GL_TEXTURE_2D, 0, | ||||
| 					0, 0, | ||||
| 					WriteAreaWidth, | ||||
| 					1 + area.end.write_area_y, | ||||
| 					formatForDepth(write_area_data_size()), | ||||
| 					GL_UNSIGNED_BYTE, | ||||
| 					&write_area_texture_[0]); | ||||
| 			} | ||||
|  | ||||
| 			test_gl(glDisable, GL_SCISSOR_TEST); | ||||
| 		} | ||||
|  | ||||
| 		// Apply new spans. They definitely always go to the first buffer. | ||||
| 		test_gl(glBindVertexArray, scan_vertex_array_); | ||||
| 		input_shader_->bind(); | ||||
| 		test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_scans)); | ||||
| 	} | ||||
| 		// Push new input to the unprocessed line buffer. | ||||
| 		if(new_scans) { | ||||
| 			unprocessed_line_texture_.bind_framebuffer(); | ||||
|  | ||||
| 	// Logic for reducing resolution: start doing so if the metrics object reports that | ||||
| 	// it's a good idea. Go up to a quarter of the requested resolution, subject to | ||||
| 	// clamping at each stage. If the output resolution changes, or anything else about | ||||
| 	// the output pipeline, just start trying the highest size again. | ||||
| 	if(display_metrics_.should_lower_resolution() && is_soft_display_type()) { | ||||
| 		resolution_reduction_level_ = std::min(resolution_reduction_level_+1, 4); | ||||
| 	} | ||||
| 	if(output_height_ != output_height || did_setup_pipeline) { | ||||
| 		resolution_reduction_level_ = 1; | ||||
| 		output_height_ = output_height; | ||||
| 	} | ||||
| 			// Clear newly-touched lines; that is everything from (read+1) to submit. | ||||
| 			const auto first_line_to_clear = GLsizei((area.start.line+1)%line_buffer_.size()); | ||||
| 			const auto final_line_to_clear = GLsizei(area.end.line); | ||||
| 			if(first_line_to_clear != final_line_to_clear) { | ||||
| 				test_gl(glEnable, GL_SCISSOR_TEST); | ||||
|  | ||||
| 	// Ensure the accumulation buffer is properly sized, allowing for the metrics object's | ||||
| 	// feelings about whether too high a resolution is being used. | ||||
| 	const int framebuffer_height = std::max(output_height / resolution_reduction_level_, std::min(540, output_height)); | ||||
| 	const int proportional_width = (framebuffer_height * 4) / 3; | ||||
| 	const bool did_create_accumulation_texture = !accumulation_texture_ || ( (accumulation_texture_->get_width() != proportional_width || accumulation_texture_->get_height() != framebuffer_height)); | ||||
|  | ||||
| 	// Work with the accumulation_buffer_ potentially starts from here onwards; set its flag. | ||||
| 	while(is_drawing_to_accumulation_buffer_.test_and_set()); | ||||
| 	if(did_create_accumulation_texture) { | ||||
| 		LOG("Changed output resolution to " << proportional_width << " by " << framebuffer_height); | ||||
| 		display_metrics_.announce_did_resize(); | ||||
| 		std::unique_ptr<OpenGL::TextureTarget> new_framebuffer( | ||||
| 			new TextureTarget( | ||||
| 				GLsizei(proportional_width), | ||||
| 				GLsizei(framebuffer_height), | ||||
| 				AccumulationTextureUnit, | ||||
| 				GL_NEAREST, | ||||
| 				true)); | ||||
| 		if(accumulation_texture_) { | ||||
| 			new_framebuffer->bind_framebuffer(); | ||||
| 			test_gl(glClear, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | ||||
|  | ||||
| 			test_gl(glActiveTexture, AccumulationTextureUnit); | ||||
| 			accumulation_texture_->bind_texture(); | ||||
| 			accumulation_texture_->draw(4.0f / 3.0f); | ||||
|  | ||||
| 			test_gl(glClear, GL_STENCIL_BUFFER_BIT); | ||||
|  | ||||
| 			new_framebuffer->bind_texture(); | ||||
| 		} | ||||
| 		accumulation_texture_ = std::move(new_framebuffer); | ||||
|  | ||||
| 		// In the absence of a way to resize a stencil buffer, just mark | ||||
| 		// what's currently present as invalid to avoid an improper clear | ||||
| 		// for this frame. | ||||
| 		stencil_is_valid_ = false; | ||||
| 	} | ||||
|  | ||||
| 	if(did_setup_pipeline || did_create_accumulation_texture) { | ||||
| 		set_sampling_window(proportional_width, framebuffer_height, *output_shader_); | ||||
| 	} | ||||
|  | ||||
| 	// Figure out how many new lines are ready. | ||||
| 	uint16_t new_lines = (submit_pointers.line + LineBufferHeight - read_pointers.line) % LineBufferHeight; | ||||
| 	if(new_lines) { | ||||
| 		// Prepare to output lines. | ||||
| 		test_gl(glBindVertexArray, line_vertex_array_); | ||||
|  | ||||
| 		// Bind the accumulation framebuffer, unless there's going to be QAM work first. | ||||
| 		if(!qam_separation_shader_ || line_metadata_buffer_[read_pointers.line].is_first_in_frame) { | ||||
| 			accumulation_texture_->bind_framebuffer(); | ||||
| 			output_shader_->bind(); | ||||
|  | ||||
| 			// Enable blending and stenciling. | ||||
| 			test_gl(glEnable, GL_BLEND); | ||||
| 			test_gl(glEnable, GL_STENCIL_TEST); | ||||
| 		} | ||||
|  | ||||
| 		// Set the proper stencil function regardless. | ||||
| 		test_gl(glStencilFunc, GL_EQUAL, 0, GLuint(~0)); | ||||
| 		test_gl(glStencilOp, GL_KEEP, GL_KEEP, GL_INCR); | ||||
|  | ||||
| 		// Prepare to upload data that will consitute lines. | ||||
| 		test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_); | ||||
|  | ||||
| 		// Divide spans by which frame they're in. | ||||
| 		uint16_t start_line = read_pointers.line; | ||||
| 		while(new_lines) { | ||||
| 			uint16_t end_line = (start_line + 1) % LineBufferHeight; | ||||
|  | ||||
| 			// Find the limit of spans to draw in this cycle. | ||||
| 			size_t lines = 1; | ||||
| 			while(end_line != submit_pointers.line && !line_metadata_buffer_[end_line].is_first_in_frame) { | ||||
| 				end_line = (end_line + 1) % LineBufferHeight; | ||||
| 				++lines; | ||||
| 			} | ||||
|  | ||||
| 			// If this is start-of-frame, clear any untouched pixels and flush the stencil buffer | ||||
| 			if(line_metadata_buffer_[start_line].is_first_in_frame) { | ||||
| 				if(stencil_is_valid_ && line_metadata_buffer_[start_line].previous_frame_was_complete) { | ||||
| 					full_display_rectangle_.draw(0.0f, 0.0f, 0.0f); | ||||
| 				// Determine the proper clear colour — this needs to be anything that describes black | ||||
| 				// in the input colour encoding at use. | ||||
| 				if(modals().input_data_type == InputDataType::Luminance8Phase8) { | ||||
| 					// Supply both a zero luminance and a colour-subcarrier-disengaging phase. | ||||
| 					test_gl(glClearColor, 0.0f, 1.0f, 0.0f, 0.0f); | ||||
| 				} else { | ||||
| 					test_gl(glClearColor, 0.0f, 0.0f, 0.0f, 0.0f); | ||||
| 				} | ||||
| 				stencil_is_valid_ = true; | ||||
|  | ||||
| 				if(first_line_to_clear < final_line_to_clear) { | ||||
| 					test_gl(glScissor, GLint(0), GLint(first_line_to_clear), unprocessed_line_texture_.get_width(), final_line_to_clear - first_line_to_clear); | ||||
| 					test_gl(glClear, GL_COLOR_BUFFER_BIT); | ||||
| 				} else { | ||||
| 					test_gl(glScissor, GLint(0), GLint(0), unprocessed_line_texture_.get_width(), final_line_to_clear); | ||||
| 					test_gl(glClear, GL_COLOR_BUFFER_BIT); | ||||
| 					test_gl(glScissor, GLint(0), GLint(first_line_to_clear), unprocessed_line_texture_.get_width(), unprocessed_line_texture_.get_height() - first_line_to_clear); | ||||
| 					test_gl(glClear, GL_COLOR_BUFFER_BIT); | ||||
| 				} | ||||
|  | ||||
| 				test_gl(glDisable, GL_SCISSOR_TEST); | ||||
| 			} | ||||
|  | ||||
| 			// Apply new spans. They definitely always go to the first buffer. | ||||
| 			test_gl(glBindVertexArray, scan_vertex_array_); | ||||
| 			input_shader_->bind(); | ||||
| 			test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_scans)); | ||||
| 		} | ||||
|  | ||||
| 		// Logic for reducing resolution: start doing so if the metrics object reports that | ||||
| 		// it's a good idea. Go up to a quarter of the requested resolution, subject to | ||||
| 		// clamping at each stage. If the output resolution changes, or anything else about | ||||
| 		// the output pipeline, just start trying the highest size again. | ||||
| 		if(display_metrics_.should_lower_resolution() && is_soft_display_type()) { | ||||
| 			resolution_reduction_level_ = std::min(resolution_reduction_level_+1, 4); | ||||
| 		} | ||||
| 		if(output_height_ != output_height || did_setup_pipeline) { | ||||
| 			resolution_reduction_level_ = 1; | ||||
| 			output_height_ = output_height; | ||||
| 		} | ||||
|  | ||||
| 		// Ensure the accumulation buffer is properly sized, allowing for the metrics object's | ||||
| 		// feelings about whether too high a resolution is being used. | ||||
| 		const int framebuffer_height = std::max(output_height / resolution_reduction_level_, std::min(540, output_height)); | ||||
| 		const int proportional_width = (framebuffer_height * 4) / 3; | ||||
| 		const bool did_create_accumulation_texture = !accumulation_texture_ || ( (accumulation_texture_->get_width() != proportional_width || accumulation_texture_->get_height() != framebuffer_height)); | ||||
|  | ||||
| 		// Work with the accumulation_buffer_ potentially starts from here onwards; set its flag. | ||||
| 		while(is_drawing_to_accumulation_buffer_.test_and_set()); | ||||
| 		if(did_create_accumulation_texture) { | ||||
| 			LOG("Changed output resolution to " << proportional_width << " by " << framebuffer_height); | ||||
| 			display_metrics_.announce_did_resize(); | ||||
| 			std::unique_ptr<OpenGL::TextureTarget> new_framebuffer( | ||||
| 				new TextureTarget( | ||||
| 					GLsizei(proportional_width), | ||||
| 					GLsizei(framebuffer_height), | ||||
| 					AccumulationTextureUnit, | ||||
| 					GL_NEAREST, | ||||
| 					true)); | ||||
| 			if(accumulation_texture_) { | ||||
| 				new_framebuffer->bind_framebuffer(); | ||||
| 				test_gl(glClear, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | ||||
|  | ||||
| 				test_gl(glActiveTexture, AccumulationTextureUnit); | ||||
| 				accumulation_texture_->bind_texture(); | ||||
| 				accumulation_texture_->draw(4.0f / 3.0f); | ||||
|  | ||||
| 				test_gl(glClear, GL_STENCIL_BUFFER_BIT); | ||||
|  | ||||
| 				// Rebind the program for span output. | ||||
| 				test_gl(glBindVertexArray, line_vertex_array_); | ||||
| 				if(!qam_separation_shader_) { | ||||
| 					output_shader_->bind(); | ||||
| 				} | ||||
| 				new_framebuffer->bind_texture(); | ||||
| 			} | ||||
| 			accumulation_texture_ = std::move(new_framebuffer); | ||||
|  | ||||
| 			// Upload. | ||||
| 			const auto buffer_size = lines * sizeof(Line); | ||||
| 			if(!end_line || end_line > start_line) { | ||||
| 				test_gl(glBufferSubData, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), &line_buffer_[start_line]); | ||||
| 			} else { | ||||
| 				uint8_t *destination = static_cast<uint8_t *>( | ||||
| 					glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT) | ||||
| 				); | ||||
| 				assert(destination); | ||||
| 				test_gl_error(); | ||||
| 			// In the absence of a way to resize a stencil buffer, just mark | ||||
| 			// what's currently present as invalid to avoid an improper clear | ||||
| 			// for this frame. | ||||
| 			stencil_is_valid_ = false; | ||||
| 		} | ||||
|  | ||||
| 				const size_t buffer_length = line_buffer_.size() * sizeof(Line); | ||||
| 				const size_t start_position = start_line * sizeof(Line); | ||||
| 				memcpy(&destination[0], &line_buffer_[start_line], buffer_length - start_position); | ||||
| 				memcpy(&destination[buffer_length - start_position], &line_buffer_[0], end_line * sizeof(Line)); | ||||
| 		if(did_setup_pipeline || did_create_accumulation_texture) { | ||||
| 			set_sampling_window(proportional_width, framebuffer_height, *output_shader_); | ||||
| 		} | ||||
|  | ||||
| 				test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size)); | ||||
| 				test_gl(glUnmapBuffer, GL_ARRAY_BUFFER); | ||||
| 			} | ||||
|  | ||||
| 			// Produce colour information, if required. | ||||
| 			if(qam_separation_shader_) { | ||||
| 				qam_separation_shader_->bind(); | ||||
| 				qam_chroma_texture_->bind_framebuffer(); | ||||
| 				test_gl(glClear, GL_COLOR_BUFFER_BIT);	// TODO: this is here as a hint that the old framebuffer doesn't need reloading; | ||||
| 														// test whether that's a valid optimisation on desktop OpenGL. | ||||
|  | ||||
| 				test_gl(glDisable, GL_BLEND); | ||||
| 				test_gl(glDisable, GL_STENCIL_TEST); | ||||
| 				test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines)); | ||||
| 		// Figure out how many new lines are ready. | ||||
| 		auto new_lines = (area.end.line - area.start.line + LineBufferHeight) % LineBufferHeight; | ||||
| 		if(new_lines) { | ||||
| 			// Prepare to output lines. | ||||
| 			test_gl(glBindVertexArray, line_vertex_array_); | ||||
|  | ||||
| 			// Bind the accumulation framebuffer, unless there's going to be QAM work first. | ||||
| 			if(!qam_separation_shader_ || line_metadata_buffer_[area.start.line].is_first_in_frame) { | ||||
| 				accumulation_texture_->bind_framebuffer(); | ||||
| 				output_shader_->bind(); | ||||
|  | ||||
| 				// Enable blending and stenciling. | ||||
| 				test_gl(glEnable, GL_BLEND); | ||||
| 				test_gl(glEnable, GL_STENCIL_TEST); | ||||
| 			} | ||||
|  | ||||
| 			// Render to the output. | ||||
| 			test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines)); | ||||
| 			// Set the proper stencil function regardless. | ||||
| 			test_gl(glStencilFunc, GL_EQUAL, 0, GLuint(~0)); | ||||
| 			test_gl(glStencilOp, GL_KEEP, GL_KEEP, GL_INCR); | ||||
|  | ||||
| 			start_line = end_line; | ||||
| 			new_lines -= lines; | ||||
| 			// Prepare to upload data that will consitute lines. | ||||
| 			test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_); | ||||
|  | ||||
| 			// Divide spans by which frame they're in. | ||||
| 			auto start_line = area.start.line; | ||||
| 			while(new_lines) { | ||||
| 				uint16_t end_line = (start_line + 1) % LineBufferHeight; | ||||
|  | ||||
| 				// Find the limit of spans to draw in this cycle. | ||||
| 				size_t lines = 1; | ||||
| 				while(end_line != area.end.line && !line_metadata_buffer_[end_line].is_first_in_frame) { | ||||
| 					end_line = (end_line + 1) % LineBufferHeight; | ||||
| 					++lines; | ||||
| 				} | ||||
|  | ||||
| 				// If this is start-of-frame, clear any untouched pixels and flush the stencil buffer | ||||
| 				if(line_metadata_buffer_[start_line].is_first_in_frame) { | ||||
| 					if(stencil_is_valid_ && line_metadata_buffer_[start_line].previous_frame_was_complete) { | ||||
| 						full_display_rectangle_.draw(0.0f, 0.0f, 0.0f); | ||||
| 					} | ||||
| 					stencil_is_valid_ = true; | ||||
| 					test_gl(glClear, GL_STENCIL_BUFFER_BIT); | ||||
|  | ||||
| 					// Rebind the program for span output. | ||||
| 					test_gl(glBindVertexArray, line_vertex_array_); | ||||
| 					if(!qam_separation_shader_) { | ||||
| 						output_shader_->bind(); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				// Upload. | ||||
| 				const auto buffer_size = lines * sizeof(Line); | ||||
| 				if(!end_line || end_line > start_line) { | ||||
| 					test_gl(glBufferSubData, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), &line_buffer_[start_line]); | ||||
| 				} else { | ||||
| 					uint8_t *destination = static_cast<uint8_t *>( | ||||
| 						glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT) | ||||
| 					); | ||||
| 					assert(destination); | ||||
| 					test_gl_error(); | ||||
|  | ||||
| 					const size_t buffer_length = line_buffer_.size() * sizeof(Line); | ||||
| 					const size_t start_position = start_line * sizeof(Line); | ||||
| 					memcpy(&destination[0], &line_buffer_[start_line], buffer_length - start_position); | ||||
| 					memcpy(&destination[buffer_length - start_position], &line_buffer_[0], end_line * sizeof(Line)); | ||||
|  | ||||
| 					test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size)); | ||||
| 					test_gl(glUnmapBuffer, GL_ARRAY_BUFFER); | ||||
| 				} | ||||
|  | ||||
| 				// Produce colour information, if required. | ||||
| 				if(qam_separation_shader_) { | ||||
| 					qam_separation_shader_->bind(); | ||||
| 					qam_chroma_texture_->bind_framebuffer(); | ||||
| 					test_gl(glClear, GL_COLOR_BUFFER_BIT);	// TODO: this is here as a hint that the old framebuffer doesn't need reloading; | ||||
| 															// test whether that's a valid optimisation on desktop OpenGL. | ||||
|  | ||||
| 					test_gl(glDisable, GL_BLEND); | ||||
| 					test_gl(glDisable, GL_STENCIL_TEST); | ||||
| 					test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines)); | ||||
|  | ||||
| 					accumulation_texture_->bind_framebuffer(); | ||||
| 					output_shader_->bind(); | ||||
| 					test_gl(glEnable, GL_BLEND); | ||||
| 					test_gl(glEnable, GL_STENCIL_TEST); | ||||
| 				} | ||||
|  | ||||
| 				// Render to the output. | ||||
| 				test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines)); | ||||
|  | ||||
| 				start_line = end_line; | ||||
| 				new_lines -= lines; | ||||
| 			} | ||||
|  | ||||
| 			// Disable blending and the stencil test again. | ||||
| 			test_gl(glDisable, GL_STENCIL_TEST); | ||||
| 			test_gl(glDisable, GL_BLEND); | ||||
| 		} | ||||
|  | ||||
| 		// Disable blending and the stencil test again. | ||||
| 		test_gl(glDisable, GL_STENCIL_TEST); | ||||
| 		test_gl(glDisable, GL_BLEND); | ||||
| 	} | ||||
| 		// That's it for operations affecting the accumulation buffer. | ||||
| 		is_drawing_to_accumulation_buffer_.clear(); | ||||
|  | ||||
| 	// That's it for operations affecting the accumulation buffer. | ||||
| 	is_drawing_to_accumulation_buffer_.clear(); | ||||
|  | ||||
| 	// All data now having been spooled to the GPU, update the read pointers to | ||||
| 	// the submit pointer location. | ||||
| 	read_pointers_.store(submit_pointers); | ||||
|  | ||||
| 	// Grab a fence sync object to avoid busy waiting upon the next extry into this | ||||
| 	// function, and reset the is_updating_ flag. | ||||
| 	fence_ = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); | ||||
| 	is_updating_.clear(); | ||||
| 		// Grab a fence sync object to avoid busy waiting upon the next extry into this | ||||
| 		// function, and reset the is_updating_ flag. | ||||
| 		fence_ = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); | ||||
| 		complete_output_area(area); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void ScanTarget::draw(int output_width, int output_height) { | ||||
| 	while(is_drawing_to_accumulation_buffer_.test_and_set()); | ||||
| 	while(is_drawing_to_accumulation_buffer_.test_and_set(std::memory_order_acquire)); | ||||
|  | ||||
| 	if(accumulation_texture_) { | ||||
| 		// Copy the accumulation texture to the target. | ||||
| @@ -748,5 +499,5 @@ void ScanTarget::draw(int output_width, int output_height) { | ||||
| 		accumulation_texture_->draw(float(output_width) / float(output_height), 4.0f / 255.0f); | ||||
| 	} | ||||
|  | ||||
| 	is_drawing_to_accumulation_buffer_.clear(); | ||||
| 	is_drawing_to_accumulation_buffer_.clear(std::memory_order_release); | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|  | ||||
| #include "../Log.hpp" | ||||
| #include "../DisplayMetrics.hpp" | ||||
| #include "../ScanTarget.hpp" | ||||
| #include "../ScanTargets/BufferingScanTarget.hpp" | ||||
|  | ||||
| #include "OpenGL.hpp" | ||||
| #include "Primitives/TextureTarget.hpp" | ||||
| @@ -32,12 +32,13 @@ namespace Outputs { | ||||
| namespace Display { | ||||
| namespace OpenGL { | ||||
|  | ||||
|  | ||||
| /*! | ||||
| 	Provides a ScanTarget that uses OpenGL to render its output; | ||||
| 	this uses various internal buffers so that the only geometry | ||||
| 	drawn to the target framebuffer is a quad. | ||||
| */ | ||||
| class ScanTarget: public Outputs::Display::ScanTarget { | ||||
| class ScanTarget: public Outputs::Display::BufferingScanTarget {	// TODO: use private inheritance and expose only display_metrics() and a custom cast? | ||||
| 	public: | ||||
| 		ScanTarget(GLuint target_framebuffer = 0, float output_gamma = 2.2f); | ||||
| 		~ScanTarget(); | ||||
| @@ -49,10 +50,10 @@ class ScanTarget: public Outputs::Display::ScanTarget { | ||||
| 		/*! Processes all the latest input, at a resolution suitable for later output to a framebuffer of the specified size. */ | ||||
| 		void update(int output_width, int output_height); | ||||
|  | ||||
| 		/*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */ | ||||
| 		Metrics &display_metrics(); | ||||
|  | ||||
| 	private: | ||||
| 		static constexpr int LineBufferWidth = 2048; | ||||
| 		static constexpr int LineBufferHeight = 2048; | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 		struct OpenGLVersionDumper { | ||||
| 			OpenGLVersionDumper() { | ||||
| @@ -62,93 +63,15 @@ class ScanTarget: public Outputs::Display::ScanTarget { | ||||
| 		} dumper_; | ||||
| #endif | ||||
|  | ||||
| 		static constexpr int WriteAreaWidth = 2048; | ||||
| 		static constexpr int WriteAreaHeight = 2048; | ||||
|  | ||||
| 		static constexpr int LineBufferWidth = 2048; | ||||
| 		static constexpr int LineBufferHeight = 2048; | ||||
|  | ||||
| 		GLuint target_framebuffer_; | ||||
| 		const float output_gamma_; | ||||
|  | ||||
| 		// Outputs::Display::ScanTarget finals. | ||||
| 		void set_modals(Modals) final; | ||||
| 		Scan *begin_scan() final; | ||||
| 		void end_scan() final; | ||||
| 		uint8_t *begin_data(size_t required_length, size_t required_alignment) final; | ||||
| 		void end_data(size_t actual_length) final; | ||||
| 		void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final; | ||||
| 		void will_change_owner() final; | ||||
|  | ||||
| 		bool output_is_visible_ = false; | ||||
|  | ||||
| 		Metrics display_metrics_; | ||||
| 		int resolution_reduction_level_ = 1; | ||||
| 		int output_height_ = 0; | ||||
|  | ||||
| 		size_t lines_submitted_ = 0; | ||||
| 		std::chrono::high_resolution_clock::time_point line_submission_begin_time_; | ||||
|  | ||||
| 		// Extends the definition of a Scan to include two extra fields, | ||||
| 		// relevant to the way that this scan target processes video. | ||||
| 		struct Scan { | ||||
| 			Outputs::Display::ScanTarget::Scan scan; | ||||
|  | ||||
| 			/// Stores the y coordinate that this scan's data is at, within the write area texture. | ||||
| 			uint16_t data_y; | ||||
| 			/// Stores the y coordinate of this scan within the line buffer. | ||||
| 			uint16_t line; | ||||
| 		}; | ||||
|  | ||||
| 		struct PointerSet { | ||||
| 			// This constructor is here to appease GCC's interpretation of | ||||
| 			// an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377 | ||||
| 			PointerSet() noexcept {} | ||||
|  | ||||
| 			// The sizes below might be less hassle as something more natural like ints, | ||||
| 			// but squeezing this struct into 64 bits makes the std::atomics more likely | ||||
| 			// to be lock free; they are under LLVM x86-64. | ||||
| 			int write_area = 1;	// By convention this points to the vended area. Which is preceded by a guard pixel. So a sensible default construction is write_area = 1. | ||||
| 			uint16_t scan_buffer = 0; | ||||
| 			uint16_t line = 0; | ||||
| 		}; | ||||
|  | ||||
| 		/// A pointer to the next thing that should be provided to the caller for data. | ||||
| 		PointerSet write_pointers_; | ||||
|  | ||||
| 		/// A mutex for gettng access to write_pointers_; access to write_pointers_, | ||||
| 		/// data_type_size_ or write_area_texture_ is almost never contended, so this | ||||
| 		/// is cheap for the main use case. | ||||
| 		std::mutex write_pointers_mutex_; | ||||
|  | ||||
| 		/// A pointer to the final thing currently cleared for submission. | ||||
| 		std::atomic<PointerSet> submit_pointers_; | ||||
|  | ||||
| 		/// A pointer to the first thing not yet submitted for display. | ||||
| 		std::atomic<PointerSet> read_pointers_; | ||||
|  | ||||
| 		/// Maintains a buffer of the most recent scans. | ||||
| 		std::array<Scan, 16384> scan_buffer_; | ||||
|  | ||||
| 		// Maintains a list of composite scan buffer coordinates; the Line struct | ||||
| 		// is transported to the GPU in its entirety; the LineMetadatas live in CPU | ||||
| 		// space only. | ||||
| 		struct Line { | ||||
| 			struct EndPoint { | ||||
| 				uint16_t x, y; | ||||
| 				uint16_t cycles_since_end_of_horizontal_retrace; | ||||
| 				int16_t composite_angle; | ||||
| 			} end_points[2]; | ||||
| 			uint16_t line; | ||||
| 			uint8_t composite_amplitude; | ||||
| 		}; | ||||
| 		struct LineMetadata { | ||||
| 			bool is_first_in_frame; | ||||
| 			bool previous_frame_was_complete; | ||||
| 		}; | ||||
| 		std::array<Line, LineBufferHeight> line_buffer_; | ||||
| 		std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_; | ||||
|  | ||||
| 		// Contains the first composition of scans into lines; | ||||
| 		// they're accumulated prior to output to allow for continuous | ||||
| 		// application of any necessary conversions — e.g. composite processing. | ||||
| @@ -164,13 +87,6 @@ class ScanTarget: public Outputs::Display::ScanTarget { | ||||
| 		Rectangle full_display_rectangle_; | ||||
| 		bool stencil_is_valid_ = false; | ||||
|  | ||||
| 		// Ephemeral state that helps in line composition. | ||||
| 		Line *active_line_ = nullptr; | ||||
| 		int provided_scans_ = 0; | ||||
| 		bool is_first_in_frame_ = true; | ||||
| 		bool frame_is_complete_ = true; | ||||
| 		bool previous_frame_was_complete_ = true; | ||||
|  | ||||
| 		// OpenGL storage handles for buffer data. | ||||
| 		GLuint scan_buffer_name_ = 0, scan_vertex_array_ = 0; | ||||
| 		GLuint line_buffer_name_ = 0, line_vertex_array_ = 0; | ||||
| @@ -178,24 +94,10 @@ class ScanTarget: public Outputs::Display::ScanTarget { | ||||
| 		template <typename T> void allocate_buffer(const T &array, GLuint &buffer_name, GLuint &vertex_array_name); | ||||
| 		template <typename T> void patch_buffer(const T &array, GLuint target, uint16_t submit_pointer, uint16_t read_pointer); | ||||
|  | ||||
| 		// Uses a texture to vend write areas. | ||||
| 		std::vector<uint8_t> write_area_texture_; | ||||
| 		size_t data_type_size_ = 0; | ||||
|  | ||||
| 		GLuint write_area_texture_name_ = 0; | ||||
| 		bool texture_exists_ = false; | ||||
|  | ||||
| 		// Ephemeral information for the begin/end functions. | ||||
| 		Scan *vended_scan_ = nullptr; | ||||
| 		int vended_write_area_pointer_ = 0; | ||||
|  | ||||
| 		// Track allocation failures. | ||||
| 		bool data_is_allocated_ = false; | ||||
| 		bool allocation_has_failed_ = false; | ||||
|  | ||||
| 		// Receives scan target modals. | ||||
| 		Modals modals_; | ||||
| 		bool modals_are_dirty_ = false; | ||||
| 		void setup_pipeline(); | ||||
|  | ||||
| 		enum class ShaderType { | ||||
| @@ -213,14 +115,12 @@ class ScanTarget: public Outputs::Display::ScanTarget { | ||||
| 		std::vector<std::string> bindings(ShaderType type) const; | ||||
|  | ||||
| 		GLsync fence_ = nullptr; | ||||
| 		std::atomic_flag is_updating_; | ||||
| 		std::atomic_flag is_drawing_to_accumulation_buffer_; | ||||
|  | ||||
| 		std::unique_ptr<Shader> input_shader_; | ||||
| 		std::unique_ptr<Shader> output_shader_; | ||||
| 		std::unique_ptr<Shader> qam_separation_shader_; | ||||
|  | ||||
|  | ||||
| 		/*! | ||||
| 			Produces a shader that composes fragment of the input stream to a single buffer, | ||||
| 			normalising the data into one of four forms: RGB, 8-bit luminance, | ||||
| @@ -248,6 +148,12 @@ class ScanTarget: public Outputs::Display::ScanTarget { | ||||
| 			contrast tends to be low, such as a composite colour display. | ||||
| 		*/ | ||||
| 		bool is_soft_display_type(); | ||||
|  | ||||
| 		// Storage for the various buffers. | ||||
| 		std::vector<uint8_t> write_area_texture_; | ||||
| 		std::array<Scan, LineBufferHeight*5> scan_buffer_; | ||||
| 		std::array<Line, LineBufferHeight> line_buffer_; | ||||
| 		std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -23,14 +23,15 @@ void ScanTarget::set_uniforms(ShaderType type, Shader &target) const { | ||||
| 	// converge even allowing for the fact that they may not be spaced by exactly | ||||
| 	// the expected distance. Cf. the stencil-powered logic for making sure all | ||||
| 	// pixels are painted only exactly once per field. | ||||
| 	const auto modals = BufferingScanTarget::modals(); | ||||
| 	switch(type) { | ||||
| 		case ShaderType::Composition: break; | ||||
| 		default: | ||||
| 			target.set_uniform("rowHeight", GLfloat(1.05f / modals_.expected_vertical_lines)); | ||||
| 			target.set_uniform("scale", GLfloat(modals_.output_scale.x), GLfloat(modals_.output_scale.y) * modals_.aspect_ratio * (3.0f / 4.0f)); | ||||
| 			target.set_uniform("phaseOffset", GLfloat(modals_.input_data_tweaks.phase_linked_luminance_offset)); | ||||
| 			target.set_uniform("rowHeight", GLfloat(1.05f / modals.expected_vertical_lines)); | ||||
| 			target.set_uniform("scale", GLfloat(modals.output_scale.x), GLfloat(modals.output_scale.y) * modals.aspect_ratio * (3.0f / 4.0f)); | ||||
| 			target.set_uniform("phaseOffset", GLfloat(modals.input_data_tweaks.phase_linked_luminance_offset)); | ||||
|  | ||||
| 			const float clocks_per_angle = float(modals_.cycles_per_line) * float(modals_.colour_cycle_denominator) / float(modals_.colour_cycle_numerator); | ||||
| 			const float clocks_per_angle = float(modals.cycles_per_line) * float(modals.colour_cycle_denominator) / float(modals.colour_cycle_numerator); | ||||
| 			GLfloat texture_offsets[4]; | ||||
| 			GLfloat angles[4]; | ||||
| 			for(int c = 0; c < 4; ++c) { | ||||
| @@ -41,7 +42,7 @@ void ScanTarget::set_uniforms(ShaderType type, Shader &target) const { | ||||
| 			target.set_uniform("textureCoordinateOffsets", 1, 4, texture_offsets); | ||||
| 			target.set_uniform("compositeAngleOffsets", 4, 1, angles); | ||||
|  | ||||
| 			switch(modals_.composite_colour_space) { | ||||
| 			switch(modals.composite_colour_space) { | ||||
| 				case ColourSpace::YIQ: { | ||||
| 					const GLfloat rgbToYIQ[] = {0.299f, 0.596f, 0.211f, 0.587f, -0.274f, -0.523f, 0.114f, -0.322f, 0.312f}; | ||||
| 					const GLfloat yiqToRGB[] = {1.0f, 1.0f, 1.0f, 0.956f, -0.272f, -1.106f, 0.621f, -0.647f, 1.703f}; | ||||
| @@ -61,9 +62,10 @@ void ScanTarget::set_uniforms(ShaderType type, Shader &target) const { | ||||
| } | ||||
|  | ||||
| void ScanTarget::set_sampling_window(int output_width, int, Shader &target) { | ||||
| 	if(modals_.display_type != DisplayType::CompositeColour) { | ||||
| 		const float one_pixel_width = float(modals_.cycles_per_line) * modals_.visible_area.size.width / float(output_width); | ||||
| 		const float clocks_per_angle = float(modals_.cycles_per_line) * float(modals_.colour_cycle_denominator) / float(modals_.colour_cycle_numerator); | ||||
| 	const auto modals = BufferingScanTarget::modals(); | ||||
| 	if(modals.display_type != DisplayType::CompositeColour) { | ||||
| 		const float one_pixel_width = float(modals.cycles_per_line) * modals.visible_area.size.width / float(output_width); | ||||
| 		const float clocks_per_angle = float(modals.cycles_per_line) * float(modals.colour_cycle_denominator) / float(modals.colour_cycle_numerator); | ||||
| 		GLfloat texture_offsets[4]; | ||||
| 		GLfloat angles[4]; | ||||
| 		for(int c = 0; c < 4; ++c) { | ||||
| @@ -191,8 +193,9 @@ std::vector<std::string> ScanTarget::bindings(ShaderType type) const { | ||||
|  | ||||
| std::string ScanTarget::sampling_function() const { | ||||
| 	std::string fragment_shader; | ||||
| 	const auto modals = BufferingScanTarget::modals(); | ||||
|  | ||||
| 	if(modals_.display_type == DisplayType::SVideo) { | ||||
| 	if(modals.display_type == DisplayType::SVideo) { | ||||
| 		fragment_shader += | ||||
| 			"vec2 svideo_sample(vec2 coordinate, float angle) {"; | ||||
| 	} else { | ||||
| @@ -200,8 +203,8 @@ std::string ScanTarget::sampling_function() const { | ||||
| 			"float composite_sample(vec2 coordinate, float angle) {"; | ||||
| 	} | ||||
|  | ||||
| 	const bool is_svideo = modals_.display_type == DisplayType::SVideo; | ||||
| 	switch(modals_.input_data_type) { | ||||
| 	const bool is_svideo = modals.display_type == DisplayType::SVideo; | ||||
| 	switch(modals.input_data_type) { | ||||
| 		case InputDataType::Luminance1: | ||||
| 		case InputDataType::Luminance8: | ||||
| 			// Easy, just copy across. | ||||
| @@ -255,6 +258,8 @@ std::string ScanTarget::sampling_function() const { | ||||
| } | ||||
|  | ||||
| std::unique_ptr<Shader> ScanTarget::conversion_shader() const { | ||||
| 	const auto modals = BufferingScanTarget::modals(); | ||||
|  | ||||
| 	// Compose a vertex shader. If the display type is RGB, generate just the proper | ||||
| 	// geometry position, plus a solitary textureCoordinate. | ||||
| 	// | ||||
| @@ -301,7 +306,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const { | ||||
|  | ||||
| 		"out vec4 fragColour;"; | ||||
|  | ||||
| 	if(modals_.display_type != DisplayType::RGB) { | ||||
| 	if(modals.display_type != DisplayType::RGB) { | ||||
| 		vertex_shader += | ||||
| 			"out float compositeAngle;" | ||||
| 			"out float compositeAmplitude;" | ||||
| @@ -316,7 +321,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const { | ||||
| 			"uniform vec4 compositeAngleOffsets;"; | ||||
| 	} | ||||
|  | ||||
| 	if(modals_.display_type == DisplayType::SVideo || modals_.display_type == DisplayType::CompositeColour) { | ||||
| 	if(modals.display_type == DisplayType::SVideo || modals.display_type == DisplayType::CompositeColour) { | ||||
| 		vertex_shader += "out vec2 qamTextureCoordinates[4];"; | ||||
| 		fragment_shader += "in vec2 qamTextureCoordinates[4];"; | ||||
| 	} | ||||
| @@ -332,7 +337,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const { | ||||
| 			"gl_Position = vec4(eyePosition, 0.0, 1.0);"; | ||||
|  | ||||
| 	// For everything other than RGB, calculate the two composite outputs. | ||||
| 	if(modals_.display_type != DisplayType::RGB) { | ||||
| 	if(modals.display_type != DisplayType::RGB) { | ||||
| 		vertex_shader += | ||||
| 			"compositeAngle = (mix(startCompositeAngle, endCompositeAngle, lateral) / 32.0) * 3.141592654;" | ||||
| 			"compositeAmplitude = lineCompositeAmplitude / 255.0;" | ||||
| @@ -346,7 +351,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const { | ||||
| 		"textureCoordinates[2] = vec2(centreClock + textureCoordinateOffsets[2], lineY + 0.5) / textureSize(textureName, 0);" | ||||
| 		"textureCoordinates[3] = vec2(centreClock + textureCoordinateOffsets[3], lineY + 0.5) / textureSize(textureName, 0);"; | ||||
|  | ||||
| 	if((modals_.display_type == DisplayType::SVideo) || (modals_.display_type == DisplayType::CompositeColour)) { | ||||
| 	if((modals.display_type == DisplayType::SVideo) || (modals.display_type == DisplayType::CompositeColour)) { | ||||
| 		vertex_shader += | ||||
| 			"float centreCompositeAngle = abs(mix(startCompositeAngle, endCompositeAngle, lateral)) * 4.0 / 64.0;" | ||||
| 			"centreCompositeAngle = floor(centreCompositeAngle);" | ||||
| @@ -360,7 +365,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const { | ||||
|  | ||||
| 	// Compose a fragment shader. | ||||
|  | ||||
| 	if(modals_.display_type != DisplayType::RGB) { | ||||
| 	if(modals.display_type != DisplayType::RGB) { | ||||
| 		fragment_shader += | ||||
| 			"uniform mat3 lumaChromaToRGB;" | ||||
| 			"uniform mat3 rgbToLumaChroma;"; | ||||
| @@ -372,7 +377,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const { | ||||
| 		"void main(void) {" | ||||
| 			"vec3 fragColour3;"; | ||||
|  | ||||
| 	switch(modals_.display_type) { | ||||
| 	switch(modals.display_type) { | ||||
| 		case DisplayType::CompositeColour: | ||||
| 			fragment_shader += | ||||
| 				"vec4 angles = compositeAngle + compositeAngleOffsets;" | ||||
| @@ -460,13 +465,13 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const { | ||||
| 	} | ||||
|  | ||||
| 	// Apply a brightness adjustment if requested. | ||||
| 	if(fabs(modals_.brightness - 1.0f) > 0.05f) { | ||||
| 		fragment_shader += "fragColour3 = fragColour3 * " + std::to_string(modals_.brightness) + ";"; | ||||
| 	if(fabs(modals.brightness - 1.0f) > 0.05f) { | ||||
| 		fragment_shader += "fragColour3 = fragColour3 * " + std::to_string(modals.brightness) + ";"; | ||||
| 	} | ||||
|  | ||||
| 	// Apply a gamma correction if required. | ||||
| 	if(fabs(output_gamma_ - modals_.intended_gamma) > 0.05f) { | ||||
| 		const float gamma_ratio = output_gamma_ / modals_.intended_gamma; | ||||
| 	if(fabs(output_gamma_ - modals.intended_gamma) > 0.05f) { | ||||
| 		const float gamma_ratio = output_gamma_ / modals.intended_gamma; | ||||
| 		fragment_shader += "fragColour3 = pow(fragColour3, vec3(" + std::to_string(gamma_ratio) + "));"; | ||||
| 	} | ||||
|  | ||||
| @@ -482,41 +487,44 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const { | ||||
| } | ||||
|  | ||||
| std::unique_ptr<Shader> ScanTarget::composition_shader() const { | ||||
| 	const auto modals = BufferingScanTarget::modals(); | ||||
| 	const std::string vertex_shader = | ||||
| 		"#version 150\n" | ||||
| 	R"x(#version 150 | ||||
|  | ||||
| 		"in float startDataX;" | ||||
| 		"in float startClock;" | ||||
| 		in float startDataX; | ||||
| 		in float startClock; | ||||
|  | ||||
| 		"in float endDataX;" | ||||
| 		"in float endClock;" | ||||
| 		in float endDataX; | ||||
| 		in float endClock; | ||||
|  | ||||
| 		"in float dataY;" | ||||
| 		"in float lineY;" | ||||
| 		in float dataY; | ||||
| 		in float lineY; | ||||
|  | ||||
| 		"out vec2 textureCoordinate;" | ||||
| 		"uniform usampler2D textureName;" | ||||
| 		out vec2 textureCoordinate; | ||||
| 		uniform usampler2D textureName; | ||||
|  | ||||
| 		"void main(void) {" | ||||
| 			"float lateral = float(gl_VertexID & 1);" | ||||
| 			"float longitudinal = float((gl_VertexID & 2) >> 1);" | ||||
| 		void main(void) { | ||||
| 			float lateral = float(gl_VertexID & 1); | ||||
| 			float longitudinal = float((gl_VertexID & 2) >> 1); | ||||
|  | ||||
| 			"textureCoordinate = vec2(mix(startDataX, endDataX, lateral), dataY + 0.5) / textureSize(textureName, 0);" | ||||
| 			"vec2 eyePosition = vec2(mix(startClock, endClock, lateral), lineY + longitudinal) / vec2(2048.0, 2048.0);" | ||||
| 			"gl_Position = vec4(eyePosition*2.0 - vec2(1.0), 0.0, 1.0);" | ||||
| 		"}"; | ||||
| 			textureCoordinate = vec2(mix(startDataX, endDataX, lateral), dataY + 0.5) / textureSize(textureName, 0); | ||||
| 			vec2 eyePosition = vec2(mix(startClock, endClock, lateral), lineY + longitudinal) / vec2(2048.0, 2048.0); | ||||
| 			gl_Position = vec4(eyePosition*2.0 - vec2(1.0), 0.0, 1.0); | ||||
| 		} | ||||
| 	)x"; | ||||
|  | ||||
| 	std::string fragment_shader = | ||||
| 		"#version 150\n" | ||||
| 	R"x(#version 150 | ||||
|  | ||||
| 		"out vec4 fragColour;" | ||||
| 		"in vec2 textureCoordinate;" | ||||
| 		out vec4 fragColour; | ||||
| 		in vec2 textureCoordinate; | ||||
|  | ||||
| 		"uniform usampler2D textureName;" | ||||
| 		uniform usampler2D textureName; | ||||
|  | ||||
| 		"void main(void) {"; | ||||
| 		void main(void) { | ||||
| 	)x"; | ||||
|  | ||||
| 	switch(modals_.input_data_type) { | ||||
| 	switch(modals.input_data_type) { | ||||
| 		case InputDataType::Luminance1: | ||||
| 			fragment_shader += "fragColour = textureLod(textureName, textureCoordinate, 0).rrrr;"; | ||||
| 		break; | ||||
| @@ -556,7 +564,8 @@ std::unique_ptr<Shader> ScanTarget::composition_shader() const { | ||||
| } | ||||
|  | ||||
| std::unique_ptr<Shader> ScanTarget::qam_separation_shader() const { | ||||
| 	const bool is_svideo = modals_.display_type == DisplayType::SVideo; | ||||
| 	const auto modals = BufferingScanTarget::modals(); | ||||
| 	const bool is_svideo = modals.display_type == DisplayType::SVideo; | ||||
|  | ||||
| 	// Sets up texture coordinates to run between startClock and endClock, mapping to | ||||
| 	// coordinates that correlate with four times the absolute value of the composite angle. | ||||
| @@ -632,7 +641,7 @@ std::unique_ptr<Shader> ScanTarget::qam_separation_shader() const { | ||||
| 		sampling_function() + | ||||
| 		"void main(void) {"; | ||||
|  | ||||
| 	if(modals_.display_type == DisplayType::SVideo) { | ||||
| 	if(modals.display_type == DisplayType::SVideo) { | ||||
| 		fragment_shader += | ||||
| 			"fragColour = vec4(svideo_sample(textureCoordinate, compositeAngle).rgg * vec3(1.0, cos(compositeAngle), sin(compositeAngle)), 1.0);"; | ||||
| 	} else { | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
| #ifndef Outputs_Display_ScanTarget_h | ||||
| #define Outputs_Display_ScanTarget_h | ||||
|  | ||||
| #include <array> | ||||
| #include <cstddef> | ||||
| #include <cstdint> | ||||
| #include "../ClockReceiver/TimeTypes.hpp" | ||||
| @@ -53,6 +54,9 @@ enum class DisplayType { | ||||
|  | ||||
| /*! | ||||
| 	Enumerates the potential formats of input data. | ||||
|  | ||||
| 	All types are designed to be 1, 2 or 4 bytes per pixel; this hopefully creates appropriate alignment | ||||
| 	on all formats. | ||||
| */ | ||||
| enum class InputDataType { | ||||
|  | ||||
| @@ -72,8 +76,10 @@ enum class InputDataType { | ||||
| 	// of a colour subcarrier. So they can be used to generate a luminance signal, | ||||
| 	// or an s-video pipeline. | ||||
|  | ||||
| 	Luminance8Phase8,		// 2 bytes/pixel; first is luminance, second is phase. | ||||
| 							// Phase is encoded on a 192-unit circle; anything | ||||
| 	Luminance8Phase8,		// 2 bytes/pixel; first is luminance, second is phase | ||||
| 							// of a cosine wave. | ||||
| 							// | ||||
| 							// Phase is encoded on a 128-unit circle; anything | ||||
| 							// greater than 192 implies that the colour part of | ||||
| 							// the signal should be omitted. | ||||
|  | ||||
| @@ -86,7 +92,8 @@ enum class InputDataType { | ||||
| 	Red8Green8Blue8,		// 4 bytes/pixel; first is red, second is green, third is blue, fourth is vacant. | ||||
| }; | ||||
|  | ||||
| inline size_t size_for_data_type(InputDataType data_type) { | ||||
| /// @returns the number of bytes per sample for data of type @c data_type. | ||||
| constexpr inline size_t size_for_data_type(InputDataType data_type) { | ||||
| 	switch(data_type) { | ||||
| 		case InputDataType::Luminance1: | ||||
| 		case InputDataType::Luminance8: | ||||
| @@ -107,7 +114,28 @@ inline size_t size_for_data_type(InputDataType data_type) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| inline DisplayType natural_display_type_for_data_type(InputDataType data_type) { | ||||
| /// @returns @c true if this data type presents normalised data, i.e. each byte holds a | ||||
| /// value in the range [0, 255] representing a real number in the range [0.0, 1.0]; @c false otherwise. | ||||
| constexpr inline size_t data_type_is_normalised(InputDataType data_type) { | ||||
| 	switch(data_type) { | ||||
| 		case InputDataType::Luminance8: | ||||
| 		case InputDataType::Luminance8Phase8: | ||||
| 		case InputDataType::Red8Green8Blue8: | ||||
| 		case InputDataType::PhaseLinkedLuminance8: | ||||
| 			return true; | ||||
|  | ||||
| 		default: | ||||
| 		case InputDataType::Luminance1: | ||||
| 		case InputDataType::Red1Green1Blue1: | ||||
| 		case InputDataType::Red2Green2Blue2: | ||||
| 		case InputDataType::Red4Green4Blue4: | ||||
| 			return false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /// @returns The 'natural' display type for data of type @c data_type. The natural display is whichever would | ||||
| /// display it with the least number of conversions. Caveat: a colour display is assumed for pure-composite data types. | ||||
| constexpr inline DisplayType natural_display_type_for_data_type(InputDataType data_type) { | ||||
| 	switch(data_type) { | ||||
| 		default: | ||||
| 		case InputDataType::Luminance1: | ||||
| @@ -126,6 +154,34 @@ inline DisplayType natural_display_type_for_data_type(InputDataType data_type) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /// @returns A 3x3 matrix in row-major order to convert from @c colour_space to RGB. | ||||
| inline std::array<float, 9> to_rgb_matrix(ColourSpace colour_space) { | ||||
| 	const std::array<float, 9> yiq_to_rgb = {1.0f, 1.0f, 1.0f, 0.956f, -0.272f, -1.106f, 0.621f, -0.647f, 1.703f}; | ||||
| 	const std::array<float, 9> yuv_to_rgb = {1.0f, 1.0f, 1.0f, 0.0f, -0.39465f, 2.03211f, 1.13983f, -0.58060f, 0.0f}; | ||||
|  | ||||
| 	switch(colour_space) { | ||||
| 		case ColourSpace::YIQ:	return yiq_to_rgb; | ||||
| 		case ColourSpace::YUV:	return yuv_to_rgb; | ||||
| 	} | ||||
|  | ||||
| 	// Should be unreachable. | ||||
| 	return std::array<float, 9>{}; | ||||
| } | ||||
|  | ||||
| /// @returns A 3x3 matrix in row-major order to convert to @c colour_space to RGB. | ||||
| inline std::array<float, 9> from_rgb_matrix(ColourSpace colour_space) { | ||||
| 	const std::array<float, 9> rgb_to_yiq = {0.299f, 0.596f, 0.211f, 0.587f, -0.274f, -0.523f, 0.114f, -0.322f, 0.312f}; | ||||
| 	const std::array<float, 9> rgb_to_yuv = {0.299f, -0.14713f, 0.615f, 0.587f, -0.28886f, -0.51499f, 0.114f, 0.436f, -0.10001f}; | ||||
|  | ||||
| 	switch(colour_space) { | ||||
| 		case ColourSpace::YIQ:	return rgb_to_yiq; | ||||
| 		case ColourSpace::YUV:	return rgb_to_yuv; | ||||
| 	} | ||||
|  | ||||
| 	// Should be unreachable. | ||||
| 	return std::array<float, 9>{}; | ||||
| } | ||||
|  | ||||
| /*! | ||||
| 	Provides an abstract target for 'scans' i.e. continuous sweeps of output data, | ||||
| 	which are identified by 2d start and end coordinates, and the PCM-sampled data | ||||
| @@ -325,22 +381,22 @@ struct ScanTarget { | ||||
|  | ||||
| struct ScanStatus { | ||||
| 	/// The current (prediced) length of a field (including retrace). | ||||
| 	Time::Seconds field_duration; | ||||
| 	Time::Seconds field_duration = 0.0; | ||||
| 	/// The difference applied to the field_duration estimate during the last field. | ||||
| 	Time::Seconds field_duration_gradient; | ||||
| 	Time::Seconds field_duration_gradient = 0.0; | ||||
| 	/// The amount of time this device spends in retrace. | ||||
| 	Time::Seconds retrace_duration; | ||||
| 	Time::Seconds retrace_duration = 0.0; | ||||
| 	/// The distance into the current field, from a small negative amount (in retrace) through | ||||
| 	/// 0 (start of visible area field) to 1 (end of field). | ||||
| 	/// | ||||
| 	/// This will increase monotonically, being a measure | ||||
| 	/// of the current vertical position — i.e. if current_position = 0.8 then a caller can | ||||
| 	/// conclude that the top 80% of the visible part of the display has been painted. | ||||
| 	float current_position; | ||||
| 	float current_position = 0.0f; | ||||
| 	/// The total number of hsyncs so far encountered; | ||||
| 	int hsync_count; | ||||
| 	int hsync_count = 0; | ||||
| 	/// @c true if retrace is currently going on; @c false otherwise. | ||||
| 	bool is_in_retrace; | ||||
| 	bool is_in_retrace = false; | ||||
|  | ||||
| 	/*! | ||||
| 		@returns this ScanStatus, with time-relative fields scaled by dividing them by @c dividend. | ||||
|   | ||||
							
								
								
									
										384
									
								
								Outputs/ScanTargets/BufferingScanTarget.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										384
									
								
								Outputs/ScanTargets/BufferingScanTarget.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,384 @@ | ||||
| // | ||||
| //  BufferingScanTarget.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 22/07/2020. | ||||
| //  Copyright © 2020 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "BufferingScanTarget.hpp" | ||||
|  | ||||
| #include <cassert> | ||||
| #include <cstring> | ||||
|  | ||||
| #define TextureAddressGetY(v)	uint16_t((v) >> 11) | ||||
| #define TextureAddressGetX(v)	uint16_t((v) & 0x7ff) | ||||
| #define TextureSub(a, b)		(((a) - (b)) & 0x3fffff) | ||||
| #define TextureAddress(x, y)	(((y) << 11) | (x)) | ||||
|  | ||||
| using namespace Outputs::Display; | ||||
|  | ||||
| BufferingScanTarget::BufferingScanTarget() { | ||||
| 	// Ensure proper initialisation of the two atomic pointer sets. | ||||
| 	read_pointers_.store(write_pointers_, std::memory_order::memory_order_relaxed); | ||||
| 	submit_pointers_.store(write_pointers_, std::memory_order::memory_order_relaxed); | ||||
|  | ||||
| 	// Establish initial state for is_updating_. | ||||
| 	is_updating_.clear(std::memory_order::memory_order_relaxed); | ||||
| } | ||||
|  | ||||
| // MARK: - Producer; pixel data. | ||||
|  | ||||
| uint8_t *BufferingScanTarget::begin_data(size_t required_length, size_t required_alignment) { | ||||
| 	assert(required_alignment); | ||||
|  | ||||
| 	// Acquire the standard producer lock, nominally over write_pointers_. | ||||
| 	std::lock_guard lock_guard(producer_mutex_); | ||||
|  | ||||
| 	// If allocation has already failed on this line, continue the trend. | ||||
| 	if(allocation_has_failed_) return nullptr; | ||||
|  | ||||
| 	// If there isn't yet a write area then mark allocation as failed and finish. | ||||
| 	if(!write_area_) { | ||||
| 		allocation_has_failed_ = true; | ||||
| 		return nullptr; | ||||
| 	} | ||||
|  | ||||
| 	// Determine where the proposed write area would start and end. | ||||
| 	uint16_t output_y = TextureAddressGetY(write_pointers_.write_area); | ||||
|  | ||||
| 	uint16_t aligned_start_x = TextureAddressGetX(write_pointers_.write_area & 0xffff) + 1; | ||||
| 	aligned_start_x += uint16_t((required_alignment - aligned_start_x%required_alignment)%required_alignment); | ||||
|  | ||||
| 	uint16_t end_x = aligned_start_x + uint16_t(1 + required_length); | ||||
|  | ||||
| 	if(end_x > WriteAreaWidth) { | ||||
| 		output_y = (output_y + 1) % WriteAreaHeight; | ||||
| 		aligned_start_x = uint16_t(required_alignment); | ||||
| 		end_x = aligned_start_x + uint16_t(1 + required_length); | ||||
| 	} | ||||
|  | ||||
| 	// Check whether that steps over the read pointer; if so then the final address will be closer | ||||
| 	// to the write pointer than the old. | ||||
| 	const auto end_address = TextureAddress(end_x, output_y); | ||||
| 	const auto read_pointers = read_pointers_.load(std::memory_order::memory_order_relaxed); | ||||
|  | ||||
| 	const auto end_distance = TextureSub(end_address, read_pointers.write_area); | ||||
| 	const auto previous_distance = TextureSub(write_pointers_.write_area, read_pointers.write_area); | ||||
|  | ||||
| 	// Perform a quick sanity check. | ||||
| 	assert(end_distance >= 0); | ||||
| 	assert(previous_distance >= 0); | ||||
|  | ||||
| 	// If allocating this would somehow make the write pointer back away from the read pointer, | ||||
| 	// there must not be enough space left. | ||||
| 	if(end_distance < previous_distance) { | ||||
| 		allocation_has_failed_ = true; | ||||
| 		return nullptr; | ||||
| 	} | ||||
|  | ||||
| 	// Everything checks out, note expectation of a future end_data and return the pointer. | ||||
| 	assert(!data_is_allocated_); | ||||
| 	data_is_allocated_ = true; | ||||
| 	vended_write_area_pointer_ = write_pointers_.write_area = TextureAddress(aligned_start_x, output_y); | ||||
|  | ||||
| 	assert(write_pointers_.write_area >= 1 && ((size_t(write_pointers_.write_area) + required_length + 1) * data_type_size_) <= WriteAreaWidth*WriteAreaHeight*data_type_size_); | ||||
| 	return &write_area_[size_t(write_pointers_.write_area) * data_type_size_]; | ||||
|  | ||||
| 	// Note state at exit: | ||||
| 	//		write_pointers_.write_area points to the first pixel the client is expected to draw to. | ||||
| } | ||||
|  | ||||
| void BufferingScanTarget::end_data(size_t actual_length) { | ||||
| 	// Acquire the producer lock. | ||||
| 	std::lock_guard lock_guard(producer_mutex_); | ||||
|  | ||||
| 	// Do nothing if no data write is actually ongoing. | ||||
| 	if(allocation_has_failed_ || !data_is_allocated_) return; | ||||
|  | ||||
| 	// Bookend the start of the new data, to safeguard for precision errors in sampling. | ||||
| 	memcpy( | ||||
| 		&write_area_[size_t(write_pointers_.write_area - 1) * data_type_size_], | ||||
| 		&write_area_[size_t(write_pointers_.write_area) * data_type_size_], | ||||
| 		data_type_size_); | ||||
|  | ||||
| 	// Advance to the end of the current run. | ||||
| 	write_pointers_.write_area += actual_length + 1; | ||||
|  | ||||
| 	// Also bookend the end. | ||||
| 	memcpy( | ||||
| 		&write_area_[size_t(write_pointers_.write_area - 1) * data_type_size_], | ||||
| 		&write_area_[size_t(write_pointers_.write_area - 2) * data_type_size_], | ||||
| 		data_type_size_); | ||||
|  | ||||
| 	// The write area was allocated in the knowledge that there's sufficient | ||||
| 	// distance left on the current line, but there's a risk of exactly filling | ||||
| 	// the final line, in which case this should wrap back to 0. | ||||
| 	write_pointers_.write_area %= WriteAreaWidth*WriteAreaHeight; | ||||
|  | ||||
| 	// Record that no further end_data calls are expected. | ||||
| 	data_is_allocated_ = false; | ||||
| } | ||||
|  | ||||
| // MARK: - Producer; scans. | ||||
|  | ||||
| Outputs::Display::ScanTarget::Scan *BufferingScanTarget::begin_scan() { | ||||
| 	std::lock_guard lock_guard(producer_mutex_); | ||||
|  | ||||
| 	// If there's already an allocation failure on this line, do no work. | ||||
| 	if(allocation_has_failed_) { | ||||
| 		vended_scan_ = nullptr; | ||||
| 		return nullptr; | ||||
| 	} | ||||
|  | ||||
| 	const auto result = &scan_buffer_[write_pointers_.scan]; | ||||
| 	const auto read_pointers = read_pointers_.load(std::memory_order::memory_order_relaxed); | ||||
|  | ||||
| 	// Advance the pointer. | ||||
| 	const auto next_write_pointer = decltype(write_pointers_.scan)((write_pointers_.scan + 1) % scan_buffer_size_); | ||||
|  | ||||
| 	// Check whether that's too many. | ||||
| 	if(next_write_pointer == read_pointers.scan) { | ||||
| 		allocation_has_failed_ = true; | ||||
| 		vended_scan_ = nullptr; | ||||
| 		return nullptr; | ||||
| 	} | ||||
| 	write_pointers_.scan = next_write_pointer; | ||||
| 	++provided_scans_; | ||||
|  | ||||
| 	// Fill in extra OpenGL-specific details. | ||||
| 	result->line = write_pointers_.line; | ||||
|  | ||||
| 	vended_scan_ = result; | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 	assert(!scan_is_ongoing_); | ||||
| 	scan_is_ongoing_ = true; | ||||
| #endif | ||||
|  | ||||
| 	return &result->scan; | ||||
| } | ||||
|  | ||||
| void BufferingScanTarget::end_scan() { | ||||
| 	std::lock_guard lock_guard(producer_mutex_); | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 	assert(scan_is_ongoing_); | ||||
| 	scan_is_ongoing_ = false; | ||||
| #endif | ||||
|  | ||||
| 	// Complete the scan only if one is afoot. | ||||
| 	if(vended_scan_) { | ||||
| 		vended_scan_->data_y = TextureAddressGetY(vended_write_area_pointer_); | ||||
| 		vended_scan_->line = write_pointers_.line; | ||||
| 		vended_scan_->scan.end_points[0].data_offset += TextureAddressGetX(vended_write_area_pointer_); | ||||
| 		vended_scan_->scan.end_points[1].data_offset += TextureAddressGetX(vended_write_area_pointer_); | ||||
| 		vended_scan_ = nullptr; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - Producer; lines. | ||||
|  | ||||
| void BufferingScanTarget::announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t composite_amplitude) { | ||||
| 	std::lock_guard lock_guard(producer_mutex_); | ||||
|  | ||||
| 	// Forward the event to the display metrics tracker. | ||||
| 	display_metrics_.announce_event(event); | ||||
|  | ||||
| 	if(event == ScanTarget::Event::EndVerticalRetrace) { | ||||
| 		// The previous-frame-is-complete flag is subject to a two-slot queue because | ||||
| 		// measurement for *this* frame needs to begin now, meaning that the previous | ||||
| 		// result needs to be put somewhere — it'll be attached to the first successful | ||||
| 		// line output, whenever that comes. | ||||
| 		is_first_in_frame_ = true; | ||||
| 		previous_frame_was_complete_ = frame_is_complete_; | ||||
| 		frame_is_complete_ = true; | ||||
| 	} | ||||
|  | ||||
| 	// Proceed from here only if a change in visibility has occurred. | ||||
| 	if(output_is_visible_ == is_visible) return; | ||||
| 	output_is_visible_ = is_visible; | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 	assert(!scan_is_ongoing_); | ||||
| #endif | ||||
|  | ||||
| 	if(is_visible) { | ||||
| 		const auto read_pointers = read_pointers_.load(std::memory_order::memory_order_relaxed); | ||||
|  | ||||
| 		// Attempt to allocate a new line, noting allocation success or failure. | ||||
| 		const auto next_line = uint16_t((write_pointers_.line + 1) % line_buffer_size_); | ||||
| 		allocation_has_failed_ = next_line == read_pointers.line; | ||||
| 		if(!allocation_has_failed_) { | ||||
| 			// If there was space for a new line, establish its start and reset the count of provided scans. | ||||
| 			Line &active_line = line_buffer_[size_t(write_pointers_.line)]; | ||||
| 			active_line.end_points[0].x = location.x; | ||||
| 			active_line.end_points[0].y = location.y; | ||||
| 			active_line.end_points[0].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace; | ||||
| 			active_line.end_points[0].composite_angle = location.composite_angle; | ||||
| 			active_line.line = write_pointers_.line; | ||||
| 			active_line.composite_amplitude = composite_amplitude; | ||||
|  | ||||
| 			provided_scans_ = 0; | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Commit the most recent line only if any scans fell on it and all allocation was successful. | ||||
| 		if(!allocation_has_failed_ && provided_scans_) { | ||||
| 			const auto submit_pointers = submit_pointers_.load(std::memory_order::memory_order_relaxed); | ||||
|  | ||||
| 			// Store metadata. | ||||
| 			LineMetadata &metadata = line_metadata_buffer_[size_t(write_pointers_.line)]; | ||||
| 			metadata.is_first_in_frame = is_first_in_frame_; | ||||
| 			metadata.previous_frame_was_complete = previous_frame_was_complete_; | ||||
| 			metadata.first_scan = submit_pointers.scan; | ||||
| 			is_first_in_frame_ = false; | ||||
|  | ||||
| 			// Sanity check. | ||||
| 			assert(((metadata.first_scan + size_t(provided_scans_)) % scan_buffer_size_) == write_pointers_.scan); | ||||
|  | ||||
| 			// Store actual line data. | ||||
| 			Line &active_line = line_buffer_[size_t(write_pointers_.line)]; | ||||
| 			active_line.end_points[1].x = location.x; | ||||
| 			active_line.end_points[1].y = location.y; | ||||
| 			active_line.end_points[1].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace; | ||||
| 			active_line.end_points[1].composite_angle = location.composite_angle; | ||||
|  | ||||
| 			// Advance the line pointer. | ||||
| 			write_pointers_.line = uint16_t((write_pointers_.line + 1) % line_buffer_size_); | ||||
|  | ||||
| 			// Update the submit pointers with all lines, scans and data written during this line. | ||||
| 			std::atomic_thread_fence(std::memory_order::memory_order_release); | ||||
| 			submit_pointers_.store(write_pointers_, std::memory_order::memory_order_release); | ||||
| 		} else { | ||||
| 			// Something failed, or there was nothing on the line anyway, so reset all pointers to where they | ||||
| 			// were before this line. Mark frame as incomplete if this was an allocation failure. | ||||
| 			write_pointers_ = submit_pointers_.load(std::memory_order::memory_order_relaxed); | ||||
| 			frame_is_complete_ &= !allocation_has_failed_; | ||||
| 		} | ||||
|  | ||||
| 		// Don't permit anything to be allocated on invisible areas. | ||||
| 		allocation_has_failed_ = true; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - Producer; other state. | ||||
|  | ||||
| void BufferingScanTarget::will_change_owner() { | ||||
| 	std::lock_guard lock_guard(producer_mutex_); | ||||
| 	allocation_has_failed_ = true; | ||||
| 	vended_scan_ = nullptr; | ||||
| #ifdef DEBUG | ||||
| 	data_is_allocated_ = false; | ||||
| #endif | ||||
| } | ||||
|  | ||||
| const Outputs::Display::Metrics &BufferingScanTarget::display_metrics() { | ||||
| 	return display_metrics_; | ||||
| } | ||||
|  | ||||
| void BufferingScanTarget::set_write_area(uint8_t *base) { | ||||
| 	std::lock_guard lock_guard(producer_mutex_); | ||||
| 	write_area_ = base; | ||||
| 	write_pointers_ = submit_pointers_ = read_pointers_ = PointerSet(); | ||||
| 	allocation_has_failed_ = true; | ||||
| 	vended_scan_ = nullptr; | ||||
| } | ||||
|  | ||||
| size_t BufferingScanTarget::write_area_data_size() const { | ||||
| 	// TODO: can I guarantee this is safe without requiring that set_write_area | ||||
| 	// be within an @c perform block? | ||||
| 	return data_type_size_; | ||||
| } | ||||
|  | ||||
| void BufferingScanTarget::set_modals(Modals modals) { | ||||
| 	perform([=] { | ||||
| 		modals_ = modals; | ||||
| 		modals_are_dirty_ = true; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| // MARK: - Consumer. | ||||
|  | ||||
| BufferingScanTarget::OutputArea BufferingScanTarget::get_output_area() { | ||||
| 	// The area to draw is that between the read pointers, representing wherever reading | ||||
| 	// last stopped, and the submit pointers, representing all the new data that has been | ||||
| 	// cleared for submission. | ||||
| 	const auto submit_pointers = submit_pointers_.load(std::memory_order::memory_order_acquire); | ||||
| 	const auto read_ahead_pointers = read_ahead_pointers_.load(std::memory_order::memory_order_relaxed); | ||||
| 	std::atomic_thread_fence(std::memory_order::memory_order_acquire); | ||||
|  | ||||
| 	OutputArea area; | ||||
|  | ||||
| 	area.start.line = read_ahead_pointers.line; | ||||
| 	area.end.line = submit_pointers.line; | ||||
|  | ||||
| 	area.start.scan = read_ahead_pointers.scan; | ||||
| 	area.end.scan = submit_pointers.scan; | ||||
|  | ||||
| 	area.start.write_area_x = TextureAddressGetX(read_ahead_pointers.write_area); | ||||
| 	area.start.write_area_y = TextureAddressGetY(read_ahead_pointers.write_area); | ||||
| 	area.end.write_area_x = TextureAddressGetX(submit_pointers.write_area); | ||||
| 	area.end.write_area_y = TextureAddressGetY(submit_pointers.write_area); | ||||
|  | ||||
| 	// Update the read-ahead pointers. | ||||
| 	read_ahead_pointers_.store(submit_pointers, std::memory_order::memory_order_relaxed); | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 	area.counter = output_area_counter_; | ||||
| 	++output_area_counter_; | ||||
| #endif | ||||
|  | ||||
| 	return area; | ||||
| } | ||||
|  | ||||
| void BufferingScanTarget::complete_output_area(const OutputArea &area) { | ||||
| 	// TODO: check that this is the expected next area if in DEBUG mode. | ||||
|  | ||||
| 	PointerSet new_read_pointers; | ||||
| 	new_read_pointers.line = uint16_t(area.end.line); | ||||
| 	new_read_pointers.scan = uint16_t(area.end.scan); | ||||
| 	new_read_pointers.write_area = TextureAddress(area.end.write_area_x, area.end.write_area_y); | ||||
| 	read_pointers_.store(new_read_pointers, std::memory_order::memory_order_relaxed); | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 	// This will fire if the caller is announcing completed output areas out of order. | ||||
| 	assert(area.counter == output_area_next_returned_); | ||||
| 	++output_area_next_returned_; | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void BufferingScanTarget::perform(const std::function<void(void)> &function) { | ||||
| 	while(is_updating_.test_and_set(std::memory_order_acquire)); | ||||
| 	function(); | ||||
| 	is_updating_.clear(std::memory_order_release); | ||||
| } | ||||
|  | ||||
| void BufferingScanTarget::set_scan_buffer(Scan *buffer, size_t size) { | ||||
| 	scan_buffer_ = buffer; | ||||
| 	scan_buffer_size_ = size; | ||||
| } | ||||
|  | ||||
| void BufferingScanTarget::set_line_buffer(Line *line_buffer, LineMetadata *metadata_buffer, size_t size) { | ||||
| 	line_buffer_ = line_buffer; | ||||
| 	line_metadata_buffer_ = metadata_buffer; | ||||
| 	line_buffer_size_ = size; | ||||
| } | ||||
|  | ||||
| const Outputs::Display::ScanTarget::Modals *BufferingScanTarget::new_modals() { | ||||
| 	if(!modals_are_dirty_) { | ||||
| 		return nullptr; | ||||
| 	} | ||||
| 	modals_are_dirty_ = false; | ||||
|  | ||||
| 	// MAJOR SHARP EDGE HERE: assume that because the new_modals have been fetched then the caller will | ||||
| 	// now ensure their texture buffer is appropriate. They might provide a new pointer and might now. | ||||
| 	// But either way it's now appropriate to start treating the data size as implied by the data type. | ||||
| 	std::lock_guard lock_guard(producer_mutex_); | ||||
| 	data_type_size_ = Outputs::Display::size_for_data_type(modals_.input_data_type); | ||||
|  | ||||
| 	return &modals_; | ||||
| } | ||||
|  | ||||
| const Outputs::Display::ScanTarget::Modals &BufferingScanTarget::modals() const { | ||||
| 	return modals_; | ||||
| } | ||||
							
								
								
									
										270
									
								
								Outputs/ScanTargets/BufferingScanTarget.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								Outputs/ScanTargets/BufferingScanTarget.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,270 @@ | ||||
| // | ||||
| //  BufferingScanTarget.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 22/07/2020. | ||||
| //  Copyright © 2020 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef BufferingScanTarget_hpp | ||||
| #define BufferingScanTarget_hpp | ||||
|  | ||||
| #include "../ScanTarget.hpp" | ||||
| #include "../DisplayMetrics.hpp" | ||||
|  | ||||
| #include <array> | ||||
| #include <atomic> | ||||
| #include <functional> | ||||
| #include <mutex> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Outputs { | ||||
| namespace Display { | ||||
|  | ||||
| /*! | ||||
| 	Provides basic thread-safe (hopefully) circular queues for any scan target that: | ||||
|  | ||||
| 		*	will store incoming Scans into a linear circular buffer and pack regions of | ||||
| 			incoming pixel data into a 2048x2048 2d texture; | ||||
| 		*	will compose whole lines of content by partioning the Scans based on sync | ||||
| 			placement and then pasting together their content; | ||||
| 		*	will process those lines as necessary to map from input format to whatever | ||||
| 			suits the display; and | ||||
| 		*	will then output the lines. | ||||
|  | ||||
| 	This buffer rejects new data when full. | ||||
| */ | ||||
| class BufferingScanTarget: public Outputs::Display::ScanTarget { | ||||
| 	public: | ||||
| 		/*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */ | ||||
| 		const Metrics &display_metrics(); | ||||
|  | ||||
| 		static constexpr int WriteAreaWidth = 2048; | ||||
| 		static constexpr int WriteAreaHeight = 2048; | ||||
|  | ||||
| 		BufferingScanTarget(); | ||||
|  | ||||
| 		// This is included because it's assumed that scan targets will want to expose one. | ||||
| 		// It is the subclass's responsibility to post timings. | ||||
| 		Metrics display_metrics_; | ||||
|  | ||||
| 		/// Extends the definition of a Scan to include two extra fields, | ||||
| 		/// completing this scan's source data and destination locations. | ||||
| 		struct Scan { | ||||
| 			Outputs::Display::ScanTarget::Scan scan; | ||||
|  | ||||
| 			/// Stores the y coordinate for this scan's data within the write area texture. | ||||
| 			/// Use this plus the scan's endpoints' data_offsets to locate this data in 2d. | ||||
| 			/// Note that the data_offsets will have been adjusted to be relative to the line | ||||
| 			/// they fall within, not the data allocation. | ||||
| 			uint16_t data_y; | ||||
| 			/// Stores the y coordinate assigned to this scan within the intermediate buffers. | ||||
| 			/// Use this plus this scan's endpoints' x locations to determine where to composite | ||||
| 			/// this data for intermediate processing. | ||||
| 			uint16_t line; | ||||
| 		}; | ||||
|  | ||||
| 		/// Defines the boundaries of a complete line of video — a 2d start and end location, | ||||
| 		/// composite phase and amplitude (if relevant), the source line in the intermediate buffer | ||||
| 		/// plus the start and end offsets of the area that is visible from the intermediate buffer. | ||||
| 		struct Line { | ||||
| 			struct EndPoint { | ||||
| 				uint16_t x, y; | ||||
| 				int16_t composite_angle; | ||||
| 				uint16_t cycles_since_end_of_horizontal_retrace; | ||||
| 			} end_points[2]; | ||||
|  | ||||
| 			uint8_t composite_amplitude; | ||||
| 			uint16_t line; | ||||
| 		}; | ||||
|  | ||||
| 		/// Provides additional metadata about lines; this is separate because it's unlikely to be of | ||||
| 		/// interest to the GPU, unlike the fields in Line. | ||||
| 		struct LineMetadata { | ||||
| 			/// @c true if this line was the first drawn after vertical sync; @c false otherwise. | ||||
| 			bool is_first_in_frame; | ||||
| 			/// @c true if this line is the first in the frame and if every single piece of output | ||||
| 			/// from the previous frame was recorded; @c false otherwise. Data can be dropped | ||||
| 			/// from a frame if performance problems mean that the emulated machine is running | ||||
| 			/// more quickly than complete frames can be generated. | ||||
| 			bool previous_frame_was_complete; | ||||
| 			/// The index of the first scan that will appear on this line. | ||||
| 			size_t first_scan; | ||||
| 		}; | ||||
|  | ||||
| 		/// Sets the area of memory to use as a scan buffer. | ||||
| 		void set_scan_buffer(Scan *buffer, size_t size); | ||||
|  | ||||
| 		/// Sets the area of memory to use as line and line metadata buffers. | ||||
| 		void set_line_buffer(Line *line_buffer, LineMetadata *metadata_buffer, size_t size); | ||||
|  | ||||
| 		/// Sets a new base address for the texture. | ||||
| 		/// When called this will flush all existing data and load up the | ||||
| 		/// new data size. | ||||
| 		void set_write_area(uint8_t *base); | ||||
|  | ||||
| 		/// @returns The number of bytes per input sample, as per the latest modals. | ||||
| 		size_t write_area_data_size() const; | ||||
|  | ||||
| 		/// Defines a segment of data now ready for output, consisting of start and endpoints for: | ||||
| 		/// | ||||
| 		///	(i) the region of the write area that has been modified; if the caller is using shared memory | ||||
| 		/// for the write area then it can ignore this information; | ||||
| 		/// | ||||
| 		/// (ii) the number of scans that have been completed; and | ||||
| 		/// | ||||
| 		/// (iii) the number of lines that have been completed. | ||||
| 		/// | ||||
| 		/// New write areas and scans are exposed only upon completion of the corresponding lines. | ||||
| 		/// The values indicated by the start point are the first that should be drawn. Those indicated | ||||
| 		/// by the end point are one after the final that should be drawn. | ||||
| 		/// | ||||
| 		/// So e.g. start.scan = 23, end.scan = 24 means draw a single scan, index 23. | ||||
| 		struct OutputArea { | ||||
| 			struct Endpoint { | ||||
| 				int write_area_x, write_area_y; | ||||
| 				size_t scan; | ||||
| 				size_t line; | ||||
| 			}; | ||||
|  | ||||
| 			Endpoint start, end; | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 			size_t counter; | ||||
| #endif | ||||
| 		}; | ||||
|  | ||||
| 		/// Gets the current range of content that has been posted but not yet returned by | ||||
| 		/// a previous call to get_output_area(). | ||||
| 		/// | ||||
| 		/// Does not require the caller to be within a @c perform block. | ||||
| 		OutputArea get_output_area(); | ||||
|  | ||||
| 		/// Announces that the output area has now completed output, freeing up its memory for | ||||
| 		/// further modification. | ||||
| 		/// | ||||
| 		/// It is the caller's responsibility to ensure that the areas passed to complete_output_area | ||||
| 		/// are those from get_output_area and are marked as completed in the same order that | ||||
| 		/// they were originally provided. | ||||
| 		/// | ||||
| 		/// Does not require the caller to be within a @c perform block. | ||||
| 		void complete_output_area(const OutputArea &); | ||||
|  | ||||
| 		/// Performs @c action ensuring that no other @c perform actions, or any | ||||
| 		/// change to modals, occurs simultaneously. | ||||
| 		void perform(const std::function<void(void)> &action); | ||||
|  | ||||
| 		/// @returns new Modals if any have been set since the last call to get_new_modals(). | ||||
| 		///		The caller must be within a @c perform block. | ||||
| 		const Modals *new_modals(); | ||||
|  | ||||
| 		/// @returns the current @c Modals. | ||||
| 		const Modals &modals() const; | ||||
|  | ||||
| 	private: | ||||
| 		// ScanTarget overrides. | ||||
| 		void set_modals(Modals) final; | ||||
| 		Outputs::Display::ScanTarget::Scan *begin_scan() final; | ||||
| 		void end_scan() final; | ||||
| 		uint8_t *begin_data(size_t required_length, size_t required_alignment) final; | ||||
| 		void end_data(size_t actual_length) final; | ||||
| 		void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final; | ||||
| 		void will_change_owner() final; | ||||
|  | ||||
| 		// Uses a texture to vend write areas. | ||||
| 		uint8_t *write_area_ = nullptr; | ||||
| 		size_t data_type_size_ = 0; | ||||
|  | ||||
| 		// Tracks changes in raster visibility in order to populate | ||||
| 		// Lines and LineMetadatas. | ||||
| 		bool output_is_visible_ = false; | ||||
|  | ||||
| 		// Track allocation failures. | ||||
| 		bool data_is_allocated_ = false; | ||||
| 		bool allocation_has_failed_ = false; | ||||
|  | ||||
| 		// Ephemeral information for the begin/end functions. | ||||
| 		Scan *vended_scan_ = nullptr; | ||||
| 		int vended_write_area_pointer_ = 0; | ||||
|  | ||||
| 		// Ephemeral state that helps in line composition. | ||||
| 		int provided_scans_ = 0; | ||||
| 		bool is_first_in_frame_ = true; | ||||
| 		bool frame_is_complete_ = true; | ||||
| 		bool previous_frame_was_complete_ = true; | ||||
|  | ||||
| 		// By convention everything in the PointerSet points to the next instance | ||||
| 		// of whatever it is that will be used. So a client should start with whatever | ||||
| 		// is pointed to by the read pointers and carry until it gets to a value that | ||||
| 		// is equal to whatever is in the submit pointers. | ||||
| 		struct PointerSet { | ||||
| 			// This constructor is here to appease GCC's interpretation of | ||||
| 			// an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377 | ||||
| 			PointerSet() noexcept {} | ||||
|  | ||||
| 			// Squeezing this struct into 64 bits makes the std::atomics more likely | ||||
| 			// to be lock free; they are under LLVM x86-64. | ||||
|  | ||||
| 			// Points to the vended area in the write area texture. | ||||
| 			// The vended area is always preceded by a guard pixel, so a | ||||
| 			// sensible default construction is write_area = 1. | ||||
| 			int32_t write_area = 1; | ||||
|  | ||||
| 			// Points into the scan buffer. | ||||
| 			uint16_t scan = 0; | ||||
|  | ||||
| 			// Points into the line buffer. | ||||
| 			uint16_t line = 0; | ||||
| 		}; | ||||
|  | ||||
| 		/// A pointer to the final thing currently cleared for submission. | ||||
| 		std::atomic<PointerSet> submit_pointers_; | ||||
|  | ||||
| 		/// A pointer to the first thing not yet submitted for display; this is | ||||
| 		/// atomic since it also acts as the buffer into which the write_pointers_ | ||||
| 		/// may run and is therefore used by both producer and consumer. | ||||
| 		std::atomic<PointerSet> read_pointers_; | ||||
|  | ||||
| 		std::atomic<PointerSet> read_ahead_pointers_; | ||||
|  | ||||
| 		/// This is used as a spinlock to guard `perform` calls. | ||||
| 		std::atomic_flag is_updating_; | ||||
|  | ||||
| 		/// A mutex for gettng access to anything the producer modifies — i.e. the write_pointers_, | ||||
| 		/// data_type_size_ and write_area_texture_, and all other state to do with capturing | ||||
| 		/// data, scans and lines. | ||||
| 		/// | ||||
| 		/// This is almost never contended. The main collision is a user-prompted change of modals while the | ||||
| 		/// emulation thread is running. | ||||
| 		std::mutex producer_mutex_; | ||||
|  | ||||
| 		/// A pointer to the next thing that should be provided to the caller for data. | ||||
| 		PointerSet write_pointers_; | ||||
|  | ||||
| 		// The owner-supplied scan buffer and size. | ||||
| 		Scan *scan_buffer_ = nullptr; | ||||
| 		size_t scan_buffer_size_ = 0; | ||||
|  | ||||
| 		// The owner-supplied line buffer and size. | ||||
| 		Line *line_buffer_ = nullptr; | ||||
| 		LineMetadata *line_metadata_buffer_ = nullptr; | ||||
| 		size_t line_buffer_size_ = 0; | ||||
|  | ||||
| 		// Current modals and whether they've yet been returned | ||||
| 		// from a call to @c get_new_modals. | ||||
| 		Modals modals_; | ||||
| 		bool modals_are_dirty_ = false; | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| 		// Debug features; these amount to API validation. | ||||
| 		bool scan_is_ongoing_ = false; | ||||
| 		size_t output_area_counter_ = 0; | ||||
| 		size_t output_area_next_returned_ = 0; | ||||
| #endif | ||||
| }; | ||||
|  | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* BufferingScanTarget_hpp */ | ||||
| @@ -10,7 +10,6 @@ | ||||
| #define FilteringSpeaker_h | ||||
|  | ||||
| #include "../Speaker.hpp" | ||||
| #include "../../../SignalProcessing/Stepper.hpp" | ||||
| #include "../../../SignalProcessing/FIRFilter.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../Concurrency/AsyncTaskQueue.hpp" | ||||
| @@ -131,7 +130,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker { | ||||
| 			at construction, filtering it and passing it on to the speaker's delegate if there is one. | ||||
| 		*/ | ||||
| 		void run_for(const Cycles cycles) { | ||||
| 			const auto delegate = delegate_.load(); | ||||
| 			const auto delegate = delegate_.load(std::memory_order::memory_order_relaxed); | ||||
| 			if(!delegate) return; | ||||
|  | ||||
| 			const int scale = get_scale(); | ||||
| @@ -198,7 +197,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker { | ||||
| 		std::vector<int16_t> input_buffer_; | ||||
| 		std::vector<int16_t> output_buffer_; | ||||
|  | ||||
| 		std::unique_ptr<SignalProcessing::Stepper> stepper_; | ||||
| 		float step_rate_ = 0.0f; | ||||
| 		float position_error_ = 0.0f; | ||||
| 		std::unique_ptr<SignalProcessing::FIRFilter> filter_; | ||||
|  | ||||
| 		std::mutex filter_parameters_mutex_; | ||||
| @@ -223,9 +223,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker { | ||||
| 			); | ||||
| 			number_of_taps = (number_of_taps * 2) | 1; | ||||
|  | ||||
| 			stepper_ = std::make_unique<SignalProcessing::Stepper>( | ||||
| 				uint64_t(filter_parameters.input_cycles_per_second), | ||||
| 				uint64_t(filter_parameters.output_cycles_per_second)); | ||||
| 			step_rate_ = filter_parameters.input_cycles_per_second / filter_parameters.output_cycles_per_second; | ||||
| 			position_error_ = 0.0f; | ||||
|  | ||||
| 			filter_ = std::make_unique<SignalProcessing::FIRFilter>( | ||||
| 				unsigned(number_of_taps), | ||||
| @@ -304,7 +303,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker { | ||||
| 			// If the next loop around is going to reuse some of the samples just collected, use a memmove to | ||||
| 			// preserve them in the correct locations (TODO: use a longer buffer to fix that?) and don't skip | ||||
| 			// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse. | ||||
| 			const auto steps = stepper_->step() * (SampleSource::get_is_stereo() ? 2 : 1); | ||||
| 			const size_t steps = size_t(step_rate_ + position_error_) * (SampleSource::get_is_stereo() ? 2 : 1); | ||||
| 			position_error_ = fmodf(step_rate_ + position_error_, 1.0f); | ||||
| 			if(steps < input_buffer_.size()) { | ||||
| 				auto *const input_buffer = input_buffer_.data(); | ||||
| 				std::memmove(	input_buffer, | ||||
|   | ||||
| @@ -45,6 +45,16 @@ class Speaker { | ||||
| 			compute_output_rate(); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Takes a copy of the most recent output rate provided to @c rhs. | ||||
| 		*/ | ||||
| 		void copy_output_rate(const Speaker &rhs) { | ||||
| 			output_cycles_per_second_ = rhs.output_cycles_per_second_; | ||||
| 			output_buffer_size_ = rhs.output_buffer_size_; | ||||
| 			stereo_output_.store(rhs.stereo_output_.load(std::memory_order::memory_order_relaxed), std::memory_order::memory_order_relaxed); | ||||
| 			compute_output_rate(); | ||||
| 		} | ||||
|  | ||||
| 		/// Sets the output volume, in the range [0, 1]. | ||||
| 		virtual void set_output_volume(float) = 0; | ||||
|  | ||||
| @@ -79,7 +89,7 @@ class Speaker { | ||||
| 			virtual void speaker_did_change_input_clock([[maybe_unused]] Speaker *speaker) {} | ||||
| 		}; | ||||
| 		virtual void set_delegate(Delegate *delegate) { | ||||
| 			delegate_ = delegate; | ||||
| 			delegate_.store(delegate, std::memory_order::memory_order_relaxed); | ||||
| 		} | ||||
|  | ||||
|  | ||||
| @@ -89,7 +99,7 @@ class Speaker { | ||||
| 	protected: | ||||
| 		void did_complete_samples(Speaker *, const std::vector<int16_t> &buffer, bool is_stereo) { | ||||
| 			// Test the delegate for existence again, as it may have changed. | ||||
| 			const auto delegate = delegate_.load(); | ||||
| 			const auto delegate = delegate_.load(std::memory_order::memory_order_relaxed); | ||||
| 			if(!delegate) return; | ||||
|  | ||||
| 			++completed_sample_sets_; | ||||
|   | ||||
| @@ -2,9 +2,9 @@ | ||||
| # Clock Signal | ||||
| Clock Signal ('CLK') is an emulator for tourists that seeks to be invisible. Users directly launch classic software with no emulator or per-emulated-machine learning curve. | ||||
|  | ||||
| [Releases](https://github.com/TomHarte/CLK/releases) are hosted on GitHub. | ||||
| macOS and source releases are [hosted on GitHub](https://github.com/TomHarte/CLK/releases). For desktop Linux it is also available as a [Snap](https://snapcraft.io/clock-signal). | ||||
|  | ||||
| On the Mac it is a native Cocoa application; under Linux, BSD and other UNIXes and UNIX-alikes it can be built either with Qt or with SDL; the Qt build should be considered preliminary and is currently closely bound to X11 as Qt doesn't abstract game-like keyboard handling. | ||||
| On the Mac it is a native Cocoa and Metal application; under Linux, BSD and other UNIXes and UNIX-alikes it uses OpenGL and can be built either with Qt or with SDL. | ||||
|  | ||||
| So its aims are: | ||||
| * single-click load of any piece of source media for any supported platform; | ||||
| @@ -17,6 +17,7 @@ It currently contains emulations of the: | ||||
| * Amstrad CPC; | ||||
| * Apple II/II+ and IIe; | ||||
| * Atari 2600; | ||||
| * Atari ST; | ||||
| * ColecoVision; | ||||
| * Commodore Vic-20 (and Commodore 1540/1); | ||||
| * Macintosh 512ke and Plus; | ||||
| @@ -25,8 +26,6 @@ It currently contains emulations of the: | ||||
| * Sega Master System; and | ||||
| * Sinclair ZX80/81. | ||||
|  | ||||
| In addition, emulation of the Atari ST is experimental. | ||||
|  | ||||
| ## Single-click Loading | ||||
|  | ||||
| Through the combination of static analysis and runtime analysis, CLK seeks to be able automatically to select and configure the appropriate machine to run any provided disk, tape or ROM; to issue any commands necessary to run the software contained on the disk, tape or ROM; and to provide accelerated loading where feasible. | ||||
|   | ||||
| @@ -90,7 +90,7 @@ void Controller::set_drive(int index_mask) { | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	ClockingHint::Preference former_prefernece = preferred_clocking(); | ||||
| 	const ClockingHint::Preference former_preference = preferred_clocking(); | ||||
|  | ||||
| 	// Stop receiving events from the current drive. | ||||
| 	get_drive().set_event_delegate(nullptr); | ||||
| @@ -114,7 +114,7 @@ void Controller::set_drive(int index_mask) { | ||||
|  | ||||
| 	get_drive().set_event_delegate(this); | ||||
|  | ||||
| 	if(preferred_clocking() != former_prefernece) { | ||||
| 	if(preferred_clocking() != former_preference) { | ||||
| 		update_clocking_observer(); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -166,6 +166,7 @@ std::shared_ptr<Track> WOZ::get_track_at_position(Track::Address address) { | ||||
| 				number_of_bits = std::min(file_.get16le(), uint16_t(6646*8)); | ||||
| 			break; | ||||
|  | ||||
| 			default: | ||||
| 			case Type::WOZ2: { | ||||
| 				// In WOZ 2 an extra level of indirection allows for variable track sizes. | ||||
| 				const uint16_t starting_block = file_.get16le(); | ||||
|   | ||||
| @@ -247,10 +247,10 @@ void TZX::get_data_block(const DataBlock &data_block) { | ||||
|  | ||||
| void TZX::get_data(const Data &data) { | ||||
| 	// Output data. | ||||
| 	for(unsigned int c = 0; c < data.data_length; c++) { | ||||
| 	for(decltype(data.data_length) c = 0; c < data.data_length; c++) { | ||||
| 		uint8_t next_byte = file_.get8(); | ||||
|  | ||||
| 		unsigned int bits = (c != data.data_length-1) ? 8 : data.number_of_bits_in_final_byte; | ||||
| 		auto bits = (c != data.data_length-1) ? 8 : data.number_of_bits_in_final_byte; | ||||
| 		while(bits--) { | ||||
| 			unsigned int pulse_length = (next_byte & 0x80) ? data.length_of_one_bit_pulse : data.length_of_zero_bit_pulse; | ||||
| 			next_byte <<= 1; | ||||
|   | ||||
| @@ -77,7 +77,7 @@ class TZX: public PulseQueuedTape { | ||||
| 			unsigned int length_of_one_bit_pulse; | ||||
| 			unsigned int number_of_bits_in_final_byte; | ||||
| 			unsigned int pause_after_block; | ||||
| 			long data_length; | ||||
| 			uint32_t data_length; | ||||
| 		}; | ||||
|  | ||||
| 		struct DataBlock { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user