mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
201 Commits
2020-07-19
...
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 | ||
|
|
e8cd5a0511 | ||
|
|
5ebbab6f35 | ||
|
|
84dd194afd | ||
|
|
e1ad1a4cb6 | ||
|
|
47f121ee4c | ||
|
|
d8b699c869 | ||
|
|
a7855e8c98 | ||
|
|
8dcb48254a | ||
|
|
f6b7467d75 | ||
|
|
9d1d162cc8 | ||
|
|
4ee29b3266 | ||
|
|
cbb0594e6b | ||
|
|
8aeebdbc99 |
@@ -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) {
|
||||
|
||||
@@ -191,7 +191,6 @@ void DiskII::set_state_machine(const std::vector<uint8_t> &state_machine) {
|
||||
((source_address&0x02) ? 0x02 : 0x00);
|
||||
uint8_t source_value = state_machine[source_address];
|
||||
|
||||
// Remap into Beneath Apple Pro-DOS value form.
|
||||
source_value =
|
||||
((source_value & 0x80) ? 0x10 : 0x0) |
|
||||
((source_value & 0x40) ? 0x20 : 0x0) |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -20,7 +20,9 @@ DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sec
|
||||
} else {
|
||||
roms = rom_fetcher({
|
||||
{"DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ff},
|
||||
{"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 }
|
||||
{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } }
|
||||
// {"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 }
|
||||
/* TODO: once the DiskII knows how to decode common images of the 13-sector state machine, use that instead of the 16-sector. */
|
||||
});
|
||||
}
|
||||
if(!roms[0] || !roms[1]) {
|
||||
|
||||
@@ -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, pending a sustainable workaround for its 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,12 @@ class Disk {
|
||||
@returns whether the disk image is read only. Defaults to @c true if not overridden.
|
||||
*/
|
||||
virtual bool get_is_read_only() = 0;
|
||||
|
||||
/*!
|
||||
@returns @c true if the tracks at the two addresses are different. @c false if they are the same track.
|
||||
This can avoid some degree of work when disk images offer sub-head-position precision.
|
||||
*/
|
||||
virtual bool tracks_differ(Track::Address, Track::Address) = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -67,6 +67,12 @@ class DiskImage {
|
||||
@returns whether the disk image is read only. Defaults to @c true if not overridden.
|
||||
*/
|
||||
virtual bool get_is_read_only() { return true; }
|
||||
|
||||
/*!
|
||||
@returns @c true if the tracks at the two addresses are different. @c false if they are the same track.
|
||||
This can avoid some degree of work when disk images offer sub-head-position precision.
|
||||
*/
|
||||
virtual bool tracks_differ(Track::Address lhs, Track::Address rhs) { return lhs != rhs; }
|
||||
};
|
||||
|
||||
class DiskImageHolderBase: public Disk {
|
||||
@@ -93,6 +99,7 @@ template <typename T> class DiskImageHolder: public DiskImageHolderBase {
|
||||
void set_track_at_position(Track::Address address, const std::shared_ptr<Track> &track);
|
||||
void flush_tracks();
|
||||
bool get_is_read_only();
|
||||
bool tracks_differ(Track::Address lhs, Track::Address rhs);
|
||||
|
||||
private:
|
||||
T disk_image_;
|
||||
|
||||
@@ -58,3 +58,7 @@ template <typename T> std::shared_ptr<Track> DiskImageHolder<T>::get_track_at_po
|
||||
template <typename T> DiskImageHolder<T>::~DiskImageHolder() {
|
||||
if(update_queue_) update_queue_->flush();
|
||||
}
|
||||
|
||||
template <typename T> bool DiskImageHolder<T>::tracks_differ(Track::Address lhs, Track::Address rhs) {
|
||||
return disk_image_.tracks_differ(lhs, rhs);
|
||||
}
|
||||
|
||||
@@ -112,10 +112,22 @@ int WOZ::get_head_count() {
|
||||
}
|
||||
|
||||
long WOZ::file_offset(Track::Address address) {
|
||||
// Calculate table position; if this track is defined to be unformatted, return no track.
|
||||
const int table_position = address.head * (is_3_5_disk_ ? 80 : 160) +
|
||||
(is_3_5_disk_ ? address.position.as_int() : address.position.as_quarter());
|
||||
if(track_map_[table_position] == 0xff) return NoSuchTrack;
|
||||
// Calculate table position.
|
||||
int table_position;
|
||||
if(!is_3_5_disk_) {
|
||||
table_position = address.head * 160 + address.position.as_quarter();
|
||||
} else {
|
||||
if(type_ == Type::WOZ1) {
|
||||
table_position = address.head * 80 + address.position.as_int();
|
||||
} else {
|
||||
table_position = address.head + (address.position.as_int() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that this track actually exists.
|
||||
if(track_map_[table_position] == 0xff) {
|
||||
return NoSuchTrack;
|
||||
}
|
||||
|
||||
// Seek to the real track.
|
||||
switch(type_) {
|
||||
@@ -125,9 +137,17 @@ long WOZ::file_offset(Track::Address address) {
|
||||
}
|
||||
}
|
||||
|
||||
bool WOZ::tracks_differ(Track::Address lhs, Track::Address rhs) {
|
||||
const long offset1 = file_offset(lhs);
|
||||
const long offset2 = file_offset(rhs);
|
||||
return offset1 != offset2;
|
||||
}
|
||||
|
||||
std::shared_ptr<Track> WOZ::get_track_at_position(Track::Address address) {
|
||||
const long offset = file_offset(address);
|
||||
if(offset == NoSuchTrack) return nullptr;
|
||||
if(offset == NoSuchTrack) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Seek to the real track.
|
||||
std::vector<uint8_t> track_contents;
|
||||
@@ -146,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();
|
||||
@@ -194,5 +215,12 @@ void WOZ::set_tracks(const std::map<Track::Address, std::shared_ptr<Track>> &tra
|
||||
}
|
||||
|
||||
bool WOZ::get_is_read_only() {
|
||||
return file_.get_is_known_read_only() || is_read_only_ || type_ == Type::WOZ2; // WOZ 2 disks are currently read only.
|
||||
/*
|
||||
There is an unintended issue with the disk code that sites above here: it doesn't understand the idea
|
||||
of multiple addresses mapping to the same track, yet it maintains a cache of track contents. Therefore
|
||||
if a WOZ is written to, what's written will magically be exactly 1/4 track wide, not affecting its
|
||||
neighbours. I've made WOZs readonly until I can correct that issue.
|
||||
*/
|
||||
return true;
|
||||
// return file_.get_is_known_read_only() || is_read_only_ || type_ == Type::WOZ2; // WOZ 2 disks are currently read only.
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class WOZ: public DiskImage {
|
||||
std::shared_ptr<Track> get_track_at_position(Track::Address address) final;
|
||||
void set_tracks(const std::map<Track::Address, std::shared_ptr<Track>> &tracks) final;
|
||||
bool get_is_read_only() final;
|
||||
bool tracks_differ(Track::Address, Track::Address) final;
|
||||
|
||||
private:
|
||||
Storage::FileHolder file_;
|
||||
|
||||
@@ -80,6 +80,10 @@ bool Drive::get_is_track_zero() const {
|
||||
}
|
||||
|
||||
void Drive::step(HeadPosition offset) {
|
||||
if(offset == HeadPosition(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(ready_type_ == ReadyType::IBMRDY) {
|
||||
is_ready_ = true;
|
||||
}
|
||||
@@ -94,7 +98,7 @@ void Drive::step(HeadPosition offset) {
|
||||
}
|
||||
|
||||
// If the head moved, flush the old track.
|
||||
if(head_position_ != old_head_position) {
|
||||
if(disk_ && disk_->tracks_differ(Track::Address(head_, head_position_), Track::Address(head_, old_head_position))) {
|
||||
track_ = nullptr;
|
||||
}
|
||||
|
||||
@@ -300,11 +304,6 @@ void Drive::get_next_event(float duration_already_passed) {
|
||||
current_event_.type = Track::Event::IndexHole;
|
||||
}
|
||||
|
||||
// Begin a 2ms period of holding the index line pulse active if this is an index pulse event.
|
||||
if(current_event_.type == Track::Event::IndexHole) {
|
||||
index_pulse_remaining_ = Cycles((get_input_clock_rate() * 2) / 1000);
|
||||
}
|
||||
|
||||
// divide interval, which is in terms of a single rotation of the disk, by rotation speed to
|
||||
// convert it into revolutions per second; this is achieved by multiplying by rotational_multiplier_
|
||||
float interval = std::max((current_event_.length - duration_already_passed) * rotational_multiplier_, 0.0f);
|
||||
@@ -327,6 +326,9 @@ void Drive::process_next_event() {
|
||||
is_ready_ = true;
|
||||
}
|
||||
cycles_since_index_hole_ = 0;
|
||||
|
||||
// Begin a 2ms period of holding the index line pulse active.
|
||||
index_pulse_remaining_ = Cycles((get_input_clock_rate() * 2) / 1000);
|
||||
}
|
||||
if(
|
||||
event_delegate_ &&
|
||||
@@ -355,8 +357,8 @@ void Drive::setup_track() {
|
||||
}
|
||||
|
||||
float offset = 0.0f;
|
||||
const auto track_time_now = get_time_into_track();
|
||||
const auto time_found = track_->seek_to(Time(track_time_now)).get<float>();
|
||||
const float track_time_now = get_time_into_track();
|
||||
const float time_found = track_->seek_to(track_time_now);
|
||||
|
||||
// `time_found` can be greater than `track_time_now` if limited precision caused rounding.
|
||||
if(time_found <= track_time_now) {
|
||||
|
||||
@@ -21,9 +21,9 @@ struct Sector {
|
||||
Describes the location of a sector, implementing < to allow for use as a set key.
|
||||
*/
|
||||
struct Address {
|
||||
struct {
|
||||
union {
|
||||
/// For Apple II-type sectors, provides the volume number.
|
||||
uint_fast8_t volume = 0;
|
||||
uint_fast8_t volume;
|
||||
/// For Macintosh-type sectors, provides the format from the sector header.
|
||||
uint_fast8_t format = 0;
|
||||
};
|
||||
|
||||
@@ -57,14 +57,14 @@ uint8_t unmap_five_and_three(uint8_t source) {
|
||||
return five_and_three_unmapping[source - 0xab];
|
||||
}
|
||||
|
||||
std::unique_ptr<Sector> decode_macintosh_sector(const std::array<uint_fast8_t, 8> &header, const std::unique_ptr<Sector> &original) {
|
||||
// There must be at least 704 bytes to decode from.
|
||||
if(original->data.size() < 704) return nullptr;
|
||||
std::unique_ptr<Sector> decode_macintosh_sector(const std::array<uint_fast8_t, 8> *header, const std::unique_ptr<Sector> &original) {
|
||||
// There must be a header and at least 704 bytes to decode from.
|
||||
if(!header || original->data.size() < 704) return nullptr;
|
||||
|
||||
// Attempt a six-and-two unmapping of the header.
|
||||
std::array<uint_fast8_t, 5> decoded_header;
|
||||
for(size_t c = 0; c < decoded_header.size(); ++c) {
|
||||
decoded_header[c] = unmap_six_and_two(header[c]);
|
||||
decoded_header[c] = unmap_six_and_two((*header)[c]);
|
||||
if(decoded_header[c] == 0xff) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -140,29 +140,32 @@ std::unique_ptr<Sector> decode_macintosh_sector(const std::array<uint_fast8_t, 8
|
||||
return sector;
|
||||
}
|
||||
|
||||
std::unique_ptr<Sector> decode_appleii_sector(const std::array<uint_fast8_t, 8> &header, const std::unique_ptr<Sector> &original, bool is_five_and_three) {
|
||||
std::unique_ptr<Sector> decode_appleii_sector(const std::array<uint_fast8_t, 8> *header, const std::unique_ptr<Sector> &original, bool is_five_and_three) {
|
||||
// There must be at least 411 bytes to decode a five-and-three sector from;
|
||||
// there must be only 343 if this is a six-and-two sector.
|
||||
const size_t data_size = is_five_and_three ? 411 : 343;
|
||||
if(original->data.size() < data_size) return nullptr;
|
||||
|
||||
// Check for apparent four and four encoding.
|
||||
uint_fast8_t header_mask = 0xff;
|
||||
for(auto c : header) header_mask &= c;
|
||||
header_mask &= 0xaa;
|
||||
if(header_mask != 0xaa) return nullptr;
|
||||
|
||||
// Allocate a sector and fill the header fields.
|
||||
// Allocate a sector.
|
||||
auto sector = std::make_unique<Sector>();
|
||||
sector->data.resize(data_size);
|
||||
|
||||
sector->address.volume = ((header[0] << 1) | 1) & header[1];
|
||||
sector->address.track = ((header[2] << 1) | 1) & header[3];
|
||||
sector->address.sector = ((header[4] << 1) | 1) & header[5];
|
||||
// If there is a header, check for apparent four and four encoding.
|
||||
if(header) {
|
||||
uint_fast8_t header_mask = 0xff;
|
||||
for(auto c : *header) header_mask &= c;
|
||||
header_mask &= 0xaa;
|
||||
if(header_mask != 0xaa) return nullptr;
|
||||
|
||||
// Check the header checksum.
|
||||
const uint_fast8_t checksum = ((header[6] << 1) | 1) & header[7];
|
||||
if(checksum != (sector->address.volume^sector->address.track^sector->address.sector)) return nullptr;
|
||||
// Fill the header fields.
|
||||
sector->address.volume = (((*header)[0] << 1) | 1) & (*header)[1];
|
||||
sector->address.track = (((*header)[2] << 1) | 1) & (*header)[3];
|
||||
sector->address.sector = (((*header)[4] << 1) | 1) & (*header)[5];
|
||||
|
||||
// Check the header checksum.
|
||||
const uint_fast8_t checksum = (((*header)[6] << 1) | 1) & (*header)[7];
|
||||
if(checksum != (sector->address.volume^sector->address.track^sector->address.sector)) return nullptr;
|
||||
}
|
||||
|
||||
// Unmap the sector contents.
|
||||
for(size_t index = 0; index < data_size; ++index) {
|
||||
@@ -178,9 +181,6 @@ std::unique_ptr<Sector> decode_appleii_sector(const std::array<uint_fast8_t, 8>
|
||||
}
|
||||
if(sector->data.back()) return nullptr;
|
||||
|
||||
// Having checked the checksum, remove it.
|
||||
sector->data.resize(sector->data.size() - 1);
|
||||
|
||||
if(is_five_and_three) {
|
||||
// TODO: the below is almost certainly incorrect; Beneath Apple DOS partly documents
|
||||
// the process, enough to give the basic outline below of how five source bytes are
|
||||
@@ -250,6 +250,7 @@ std::map<std::size_t, Sector> Storage::Encodings::AppleGCR::sectors_from_segment
|
||||
size_t bit = 0;
|
||||
int header_delay = 0;
|
||||
bool is_five_and_three = false;
|
||||
bool has_header = false;
|
||||
while(bit < segment.data.size() || pointer != scanning_sentinel || header_delay) {
|
||||
shift_register = uint_fast8_t((shift_register << 1) | (segment.data[bit % segment.data.size()] ? 1 : 0));
|
||||
++bit;
|
||||
@@ -290,6 +291,7 @@ std::map<std::size_t, Sector> Storage::Encodings::AppleGCR::sectors_from_segment
|
||||
sector_location = size_t(bit % segment.data.size());
|
||||
header_delay = 200; // Allow up to 200 bytes to find the body, if the
|
||||
// track split comes in between.
|
||||
has_header = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -308,18 +310,19 @@ std::map<std::size_t, Sector> Storage::Encodings::AppleGCR::sectors_from_segment
|
||||
pointer = scanning_sentinel;
|
||||
|
||||
// Potentially this is a Macintosh sector.
|
||||
auto macintosh_sector = decode_macintosh_sector(header, sector);
|
||||
auto macintosh_sector = decode_macintosh_sector(has_header ? &header : nullptr, sector);
|
||||
if(macintosh_sector) {
|
||||
result.insert(std::make_pair(sector_location, std::move(*macintosh_sector)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apple II then?
|
||||
auto appleii_sector = decode_appleii_sector(header, sector, is_five_and_three);
|
||||
auto appleii_sector = decode_appleii_sector(has_header ? &header : nullptr, sector, is_five_and_three);
|
||||
if(appleii_sector) {
|
||||
result.insert(std::make_pair(sector_location, std::move(*appleii_sector)));
|
||||
}
|
||||
|
||||
has_header = false;
|
||||
} else {
|
||||
new_sector->data.push_back(value);
|
||||
}
|
||||
|
||||
@@ -109,9 +109,9 @@ Storage::Time PCMSegmentEventSource::get_length() {
|
||||
return segment_->length_of_a_bit * unsigned(segment_->data.size());
|
||||
}
|
||||
|
||||
Storage::Time PCMSegmentEventSource::seek_to(const Time &time_from_start) {
|
||||
float PCMSegmentEventSource::seek_to(float time_from_start) {
|
||||
// test for requested time being beyond the end
|
||||
const Time length = get_length();
|
||||
const float length = get_length().get<float>();
|
||||
if(time_from_start >= length) {
|
||||
next_event_.type = Track::Event::IndexHole;
|
||||
bit_pointer_ = segment_->data.size()+1;
|
||||
@@ -122,21 +122,21 @@ Storage::Time PCMSegmentEventSource::seek_to(const Time &time_from_start) {
|
||||
next_event_.type = Track::Event::FluxTransition;
|
||||
|
||||
// test for requested time being before the first bit
|
||||
Time half_bit_length = segment_->length_of_a_bit;
|
||||
half_bit_length.length >>= 1;
|
||||
const float bit_length = segment_->length_of_a_bit.get<float>();
|
||||
const float half_bit_length = bit_length / 2.0f;
|
||||
if(time_from_start < half_bit_length) {
|
||||
bit_pointer_ = 0;
|
||||
return Storage::Time(0);
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// adjust for time to get to bit zero and determine number of bits in;
|
||||
// bit_pointer_ always records _the next bit_ that might trigger an event,
|
||||
// so should be one beyond the one reached by a seek.
|
||||
const Time relative_time = time_from_start - half_bit_length;
|
||||
bit_pointer_ = 1 + (relative_time / segment_->length_of_a_bit).get<unsigned int>();
|
||||
const float relative_time = time_from_start + half_bit_length; // the period [0, 0.5) should map to window 0, ending with bit 0; [0.5, 1.5) should map to window 1; etc.
|
||||
bit_pointer_ = size_t(relative_time / bit_length);
|
||||
|
||||
// map up to the correct amount of time
|
||||
return half_bit_length + segment_->length_of_a_bit * unsigned(bit_pointer_ - 1);
|
||||
// Map up to the correct amount of time; this should be the start of the window that ends upon the bit at bit_pointer_.
|
||||
return bit_length * float(bit_pointer_) - half_bit_length;
|
||||
}
|
||||
|
||||
const PCMSegment &PCMSegmentEventSource::segment() const {
|
||||
|
||||
@@ -183,7 +183,7 @@ class PCMSegmentEventSource {
|
||||
|
||||
@returns the time the source is now at.
|
||||
*/
|
||||
Time seek_to(const Time &time_from_start);
|
||||
float seek_to(float time_from_start);
|
||||
|
||||
/*!
|
||||
@returns the total length of the stream of data that the source will provide.
|
||||
|
||||
@@ -121,17 +121,17 @@ Track::Event PCMTrack::get_next_event() {
|
||||
return event;
|
||||
}
|
||||
|
||||
Storage::Time PCMTrack::seek_to(const Time &time_since_index_hole) {
|
||||
float PCMTrack::seek_to(float time_since_index_hole) {
|
||||
// initial condition: no time yet accumulated, the whole thing requested yet to navigate
|
||||
Storage::Time accumulated_time;
|
||||
Storage::Time time_left_to_seek = time_since_index_hole;
|
||||
float accumulated_time = 0.0f;
|
||||
float time_left_to_seek = time_since_index_hole;
|
||||
|
||||
// search from the first segment
|
||||
segment_pointer_ = 0;
|
||||
do {
|
||||
// if this segment extends beyond the amount of time left to seek, trust it to complete
|
||||
// the seek
|
||||
Storage::Time segment_time = segment_event_sources_[segment_pointer_].get_length();
|
||||
const float segment_time = segment_event_sources_[segment_pointer_].get_length().get<float>();
|
||||
if(segment_time > time_left_to_seek) {
|
||||
return accumulated_time + segment_event_sources_[segment_pointer_].seek_to(time_left_to_seek);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class PCMTrack: public Track {
|
||||
|
||||
// as per @c Track
|
||||
Event get_next_event() final;
|
||||
Time seek_to(const Time &time_since_index_hole) final;
|
||||
float seek_to(float time_since_index_hole) final;
|
||||
Track *clone() const final;
|
||||
|
||||
// Obtains a copy of this track, flattened to a single PCMSegment, which
|
||||
|
||||
@@ -84,6 +84,13 @@ class Track {
|
||||
int rhs_largest_position = rhs.position.as_largest();
|
||||
return std::tie(head, largest_position) < std::tie(rhs.head, rhs_largest_position);
|
||||
}
|
||||
constexpr bool operator == (const Address &rhs) const {
|
||||
return head == rhs.head && position == rhs.position;
|
||||
}
|
||||
constexpr bool operator != (const Address &rhs) const {
|
||||
return head != rhs.head || position != rhs.position;
|
||||
}
|
||||
|
||||
constexpr Address(int head, HeadPosition position) : head(head), position(position) {}
|
||||
};
|
||||
|
||||
@@ -107,11 +114,11 @@ class Track {
|
||||
virtual Event get_next_event() = 0;
|
||||
|
||||
/*!
|
||||
Jumps to the event latest offset that is less than or equal to the input time.
|
||||
Jumps to the start of the fist event that will occur after @c time_since_index_hole.
|
||||
|
||||
@returns the time jumped to.
|
||||
*/
|
||||
virtual Time seek_to(const Time &time_since_index_hole) = 0;
|
||||
virtual float seek_to(float time_since_index_hole) = 0;
|
||||
|
||||
/*!
|
||||
The virtual copy constructor pattern; returns a copy of the Track.
|
||||
|
||||
@@ -35,7 +35,7 @@ Storage::Disk::PCMSegment Storage::Disk::track_serialisation(const Track &track,
|
||||
length_multiplier.simplify();
|
||||
|
||||
// start at the index hole
|
||||
track_copy->seek_to(Time(0));
|
||||
track_copy->seek_to(0.0f);
|
||||
|
||||
// grab events until the next index hole
|
||||
Time time_error = Time(0);
|
||||
@@ -54,7 +54,7 @@ Storage::Disk::PCMSegment Storage::Disk::track_serialisation(const Track &track,
|
||||
if(history_size) {
|
||||
history_size--;
|
||||
if(!history_size) {
|
||||
track_copy->seek_to(Time(0));
|
||||
track_copy->seek_to(0.0f);
|
||||
time_error.set_zero();
|
||||
result_accumulator.is_recording = true;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ Track::Event UnformattedTrack::get_next_event() {
|
||||
return event;
|
||||
}
|
||||
|
||||
Storage::Time UnformattedTrack::seek_to(const Time &) {
|
||||
return Time(0);
|
||||
float UnformattedTrack::seek_to(float) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
Track *UnformattedTrack::clone() const {
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Disk {
|
||||
class UnformattedTrack: public Track {
|
||||
public:
|
||||
Event get_next_event() final;
|
||||
Time seek_to(const Time &time_since_index_hole) final;
|
||||
float seek_to(float time_since_index_hole) final;
|
||||
Track *clone() const final;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -69,16 +69,16 @@ void TimedEventLoop::set_next_event_time_interval(Time interval) {
|
||||
|
||||
void TimedEventLoop::set_next_event_time_interval(float interval) {
|
||||
// Calculate [interval]*[input clock rate] + [subcycles until this event]
|
||||
float float_interval = interval * float(input_clock_rate_) + subcycles_until_event_;
|
||||
const float float_interval = interval * float(input_clock_rate_) + subcycles_until_event_;
|
||||
|
||||
// So this event will fire in the integral number of cycles from now, putting us at the remainder
|
||||
// number of subcycles
|
||||
// This event will fire in the integral number of cycles from now, putting us at the remainder
|
||||
// number of subcycles.
|
||||
const Cycles::IntType addition = Cycles::IntType(float_interval);
|
||||
cycles_until_event_ += addition;
|
||||
subcycles_until_event_ = fmodf(float_interval, 1.0);
|
||||
subcycles_until_event_ = fmodf(float_interval, 1.0f);
|
||||
|
||||
assert(cycles_until_event_ >= 0);
|
||||
assert(subcycles_until_event_ >= 0.0);
|
||||
assert(subcycles_until_event_ >= 0.0f);
|
||||
}
|
||||
|
||||
Time TimedEventLoop::get_time_into_next_event() {
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace Storage {
|
||||
private:
|
||||
Cycles::IntType input_clock_rate_ = 0;
|
||||
Cycles::IntType cycles_until_event_ = 0;
|
||||
float subcycles_until_event_ = 0.0;
|
||||
float subcycles_until_event_ = 0.0f;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user