mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
313 Commits
2020-01-06
...
2020-03-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9668ec789a | ||
|
|
ead32fb6b2 | ||
|
|
2ee24d29e5 | ||
|
|
a560601338 | ||
|
|
a51fe70498 | ||
|
|
e47aa7653b | ||
|
|
58b8dfb929 | ||
|
|
462a76dd96 | ||
|
|
3758ec79ac | ||
|
|
a0311858f9 | ||
|
|
f08d500fd6 | ||
|
|
df76d57c47 | ||
|
|
0ef953a1ea | ||
|
|
05cbed6b6c | ||
|
|
9225c4ef70 | ||
|
|
32136b75cd | ||
|
|
1f41d9c5f5 | ||
|
|
dc47a2b7d7 | ||
|
|
1a539521f2 | ||
|
|
2db30a91c6 | ||
|
|
b2c07b3110 | ||
|
|
90e6bef6d7 | ||
|
|
535634daca | ||
|
|
575d0da4d1 | ||
|
|
ed18092088 | ||
|
|
611182910a | ||
|
|
9273e9b6ed | ||
|
|
77c0cc8b5f | ||
|
|
0705a99ea0 | ||
|
|
560394fead | ||
|
|
86a09b5e7d | ||
|
|
32b2026734 | ||
|
|
b33f568fdd | ||
|
|
6e4bd4f505 | ||
|
|
b971e2a42c | ||
|
|
3c103506c9 | ||
|
|
41d2062342 | ||
|
|
672c59f970 | ||
|
|
99229df017 | ||
|
|
346d80e30b | ||
|
|
54b3e511e9 | ||
|
|
f25683ebec | ||
|
|
d5e781e8e1 | ||
|
|
4572c86f0f | ||
|
|
8a5c4e384a | ||
|
|
4594a3c02b | ||
|
|
bd45c1c963 | ||
|
|
5f8bb92f36 | ||
|
|
3f64cdaff8 | ||
|
|
7ac0ea8529 | ||
|
|
a3569d7201 | ||
|
|
01faffd5bf | ||
|
|
26de5be07c | ||
|
|
87474d5916 | ||
|
|
a366077509 | ||
|
|
06163165d9 | ||
|
|
ec82c075be | ||
|
|
3b0df172a7 | ||
|
|
7058dbc3cc | ||
|
|
b64de89d2d | ||
|
|
8878396339 | ||
|
|
da6d5e2e24 | ||
|
|
18bb90329a | ||
|
|
604bb50adf | ||
|
|
e4887c0c56 | ||
|
|
3097c4ccae | ||
|
|
7959d243f6 | ||
|
|
79dd402bc8 | ||
|
|
3f3229851b | ||
|
|
989628a024 | ||
|
|
e0475343f5 | ||
|
|
da0a9113d4 | ||
|
|
cf7ab97451 | ||
|
|
2370575eb5 | ||
|
|
825b68e5c4 | ||
|
|
851cba0b25 | ||
|
|
f0ec168ac7 | ||
|
|
fa933952f7 | ||
|
|
ba6e23784c | ||
|
|
614032198e | ||
|
|
3715e6b48a | ||
|
|
91e7400bbb | ||
|
|
a8d082c7d2 | ||
|
|
95756f9716 | ||
|
|
a5e1765ce4 | ||
|
|
f43c31da1f | ||
|
|
95d0adf10e | ||
|
|
2e1b245cd8 | ||
|
|
5400c47f07 | ||
|
|
4153442703 | ||
|
|
5e4b721e97 | ||
|
|
aca41ac089 | ||
|
|
01a883e669 | ||
|
|
1e4356f83a | ||
|
|
545a6177bb | ||
|
|
50d356be2f | ||
|
|
9835e800ec | ||
|
|
5242362f31 | ||
|
|
808e4e8537 | ||
|
|
43740a4b2f | ||
|
|
f99d672237 | ||
|
|
337cb4fb86 | ||
|
|
90856a0e7a | ||
|
|
ea1c8a3b81 | ||
|
|
d55d077a95 | ||
|
|
f760a68173 | ||
|
|
e66a3523b6 | ||
|
|
89d6b85b83 | ||
|
|
e02d109864 | ||
|
|
743981e9ad | ||
|
|
49b8e771b5 | ||
|
|
dde672701f | ||
|
|
9ca2d8f9f2 | ||
|
|
fd786412aa | ||
|
|
eb88c7cfba | ||
|
|
e1892ff370 | ||
|
|
763159a6f6 | ||
|
|
6810a6ee58 | ||
|
|
65e6c3a9fe | ||
|
|
dcbbf988c1 | ||
|
|
199cafebcf | ||
|
|
555d807d76 | ||
|
|
003c6ad11b | ||
|
|
dc77d87427 | ||
|
|
cfc44cf778 | ||
|
|
3df99788ff | ||
|
|
3600d2d193 | ||
|
|
5f661adb7f | ||
|
|
109d072cb6 | ||
|
|
0c1c5a0ab8 | ||
|
|
e01c66fd65 | ||
|
|
9f32fa7f5b | ||
|
|
91a3d42919 | ||
|
|
3cb6bbf771 | ||
|
|
452e281009 | ||
|
|
3da948db52 | ||
|
|
0c2f77305f | ||
|
|
05bcd73f82 | ||
|
|
654f5b0478 | ||
|
|
886d923e30 | ||
|
|
6624cb7a78 | ||
|
|
6147134423 | ||
|
|
bf6bc7c684 | ||
|
|
0b0a7e241b | ||
|
|
705d14259c | ||
|
|
f1cd35fa16 | ||
|
|
6bda4034c6 | ||
|
|
b04daca98e | ||
|
|
85dcdbfe9e | ||
|
|
24340d1d4f | ||
|
|
6ae42d07a7 | ||
|
|
2ea1e059a8 | ||
|
|
b5d6126a2d | ||
|
|
dac217c98c | ||
|
|
c26c8992ae | ||
|
|
b76a5870b3 | ||
|
|
7c0f3bb237 | ||
|
|
f615d096ca | ||
|
|
09132306e4 | ||
|
|
f95b07efea | ||
|
|
14d976eecb | ||
|
|
e1cbad0b6d | ||
|
|
e7410b8ed8 | ||
|
|
5caf74b930 | ||
|
|
b41920990f | ||
|
|
709c229cd7 | ||
|
|
01fd1b1a2e | ||
|
|
96769c52f6 | ||
|
|
cf9729c74f | ||
|
|
0f2783075f | ||
|
|
256f4a6679 | ||
|
|
0310f94f0c | ||
|
|
085529ed72 | ||
|
|
8aabf1b374 | ||
|
|
ff39f71ca0 | ||
|
|
019474300d | ||
|
|
af976b8b3d | ||
|
|
f3db1a0c60 | ||
|
|
ce28213a5e | ||
|
|
f9ce50d2bb | ||
|
|
ee16095863 | ||
|
|
f0a6e0f3d5 | ||
|
|
8c4fb0f688 | ||
|
|
baa51853c4 | ||
|
|
0e29c6b0ab | ||
|
|
1b27eedf6b | ||
|
|
8b1f183198 | ||
|
|
4766ec55fe | ||
|
|
c5edc879b6 | ||
|
|
65309e60c4 | ||
|
|
5c4623e9f7 | ||
|
|
2c0cab9e4d | ||
|
|
d0117556d1 | ||
|
|
b1ff031b54 | ||
|
|
7e8405e68a | ||
|
|
c8fd00217d | ||
|
|
9d340599a6 | ||
|
|
8e094598ca | ||
|
|
189122ab84 | ||
|
|
4b53f6a9f0 | ||
|
|
561e149058 | ||
|
|
5975fc8e63 | ||
|
|
7316a3aa88 | ||
|
|
50be991415 | ||
|
|
52e49439a6 | ||
|
|
6bcdd3177d | ||
|
|
83dbd257e1 | ||
|
|
b514756272 | ||
|
|
7e4c13c43e | ||
|
|
79bb0f8222 | ||
|
|
43bf6aca67 | ||
|
|
03d23aad41 | ||
|
|
c398aa60c1 | ||
|
|
9666193c67 | ||
|
|
3f57020b00 | ||
|
|
294e09f275 | ||
|
|
ba516387ba | ||
|
|
2103e1b470 | ||
|
|
7bac439e95 | ||
|
|
9136917f00 | ||
|
|
6802318784 | ||
|
|
428d141bc9 | ||
|
|
a86fb33789 | ||
|
|
beefb70f75 | ||
|
|
3c6a00dc3c | ||
|
|
8404409c0d | ||
|
|
a5f285b4ce | ||
|
|
9d97a294a7 | ||
|
|
56448373ae | ||
|
|
a71c5946f0 | ||
|
|
e7fff6e123 | ||
|
|
82e5def7c4 | ||
|
|
d97a073d1b | ||
|
|
e74f37d6ed | ||
|
|
3aa2c297a2 | ||
|
|
290db67f09 | ||
|
|
4de121142b | ||
|
|
3c760e585a | ||
|
|
8adb2283b5 | ||
|
|
cb61e84868 | ||
|
|
8349005c4b | ||
|
|
a2847f4f8e | ||
|
|
add3ebcb44 | ||
|
|
98daad45c7 | ||
|
|
1b4b6b0aee | ||
|
|
8f94da9daf | ||
|
|
357137918d | ||
|
|
b0f7b762af | ||
|
|
da3ee381f4 | ||
|
|
d27d14d2b0 | ||
|
|
b0326530d6 | ||
|
|
c2bd5be51a | ||
|
|
84f5feab70 | ||
|
|
4b2c68c3d3 | ||
|
|
5391a699a4 | ||
|
|
f3f8345e5e | ||
|
|
c755411636 | ||
|
|
f02759b76b | ||
|
|
f34ddce28f | ||
|
|
50348c9fe7 | ||
|
|
3bfeebf2a1 | ||
|
|
dca79ea10e | ||
|
|
b7fd4de32f | ||
|
|
78d08278ed | ||
|
|
d4be052e76 | ||
|
|
d674fd0e67 | ||
|
|
229b7b36ed | ||
|
|
8a8b8db5d1 | ||
|
|
d30f83871d | ||
|
|
1422f8a93a | ||
|
|
f0da75f8e9 | ||
|
|
cb8a7a4137 | ||
|
|
efd684dc56 | ||
|
|
aeac6b5888 | ||
|
|
9bb294a023 | ||
|
|
1972ca00a4 | ||
|
|
6a185a574a | ||
|
|
c606931c93 | ||
|
|
93cecf0882 | ||
|
|
aac3d27c10 | ||
|
|
99122efbbc | ||
|
|
30e856b9e4 | ||
|
|
91fae86e73 | ||
|
|
f5c194386c | ||
|
|
98f7662185 | ||
|
|
62c3720c97 | ||
|
|
6b08239199 | ||
|
|
f258fc2971 | ||
|
|
6b84ae3095 | ||
|
|
5dd8c677f1 | ||
|
|
1cbcd5355f | ||
|
|
9799250f2c | ||
|
|
ecb5807ec0 | ||
|
|
942986aadc | ||
|
|
1f539822ee | ||
|
|
fab35b360a | ||
|
|
80fcf5b5c0 | ||
|
|
b3b2e18c4b | ||
|
|
2d233b6358 | ||
|
|
83ed36eb08 | ||
|
|
89f4032ffc | ||
|
|
8c90ec4636 | ||
|
|
514141f8c5 | ||
|
|
8e3a618619 | ||
|
|
6df6af09de | ||
|
|
f42655a0fc | ||
|
|
f81a7f0faf | ||
|
|
2b4c924399 | ||
|
|
64517a02b7 | ||
|
|
b4befd57a9 | ||
|
|
2c742a051e | ||
|
|
6595f8f527 | ||
|
|
985b36da73 |
@@ -22,7 +22,7 @@ namespace Dynamic {
|
||||
class ConfidenceCounter: public ConfidenceSource {
|
||||
public:
|
||||
/*! @returns The computed probability, based on the history of events. */
|
||||
float get_confidence() override;
|
||||
float get_confidence() final;
|
||||
|
||||
/*! Records an event that implies this is the appropriate class: pushes probability up towards 1.0. */
|
||||
void add_hit();
|
||||
|
||||
@@ -32,7 +32,7 @@ class ConfidenceSummary: public ConfidenceSource {
|
||||
const std::vector<float> &weights);
|
||||
|
||||
/*! @returns The weighted sum of all sources. */
|
||||
float get_confidence() override;
|
||||
float get_confidence() final;
|
||||
|
||||
private:
|
||||
std::vector<ConfidenceSource *> sources_;
|
||||
|
||||
@@ -60,12 +60,19 @@ void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target)
|
||||
if(crt_machine) crt_machine->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus MultiCRTMachine::get_scan_status() const {
|
||||
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
|
||||
if(crt_machine) crt_machine->get_scan_status();
|
||||
|
||||
return Outputs::Display::ScanStatus();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() {
|
||||
return speaker_;
|
||||
}
|
||||
|
||||
void MultiCRTMachine::run_for(Time::Seconds duration) {
|
||||
perform_parallel([=](::CRTMachine::Machine *machine) {
|
||||
perform_parallel([duration](::CRTMachine::Machine *machine) {
|
||||
if(machine->get_confidence() >= 0.01f) machine->run_for(duration);
|
||||
});
|
||||
|
||||
@@ -75,7 +82,7 @@ void MultiCRTMachine::run_for(Time::Seconds duration) {
|
||||
void MultiCRTMachine::did_change_machine_order() {
|
||||
if(scan_target_) scan_target_->will_change_owner();
|
||||
|
||||
perform_serial([=](::CRTMachine::Machine *machine) {
|
||||
perform_serial([](::CRTMachine::Machine *machine) {
|
||||
machine->set_scan_target(nullptr);
|
||||
});
|
||||
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
|
||||
|
||||
@@ -53,12 +53,13 @@ class MultiCRTMachine: public CRTMachine::Machine {
|
||||
}
|
||||
|
||||
// Below is the standard CRTMachine::Machine interface; see there for documentation.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override;
|
||||
Outputs::Speaker::Speaker *get_speaker() override;
|
||||
void run_for(Time::Seconds duration) override;
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final;
|
||||
Outputs::Display::ScanStatus get_scan_status() const final;
|
||||
Outputs::Speaker::Speaker *get_speaker() final;
|
||||
void run_for(Time::Seconds duration) final;
|
||||
|
||||
private:
|
||||
void run_for(const Cycles cycles) override {}
|
||||
void run_for(const Cycles cycles) final {}
|
||||
const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_;
|
||||
std::recursive_mutex &machines_mutex_;
|
||||
std::vector<Concurrency::AsyncTaskQueue> queues_;
|
||||
|
||||
@@ -28,10 +28,10 @@ class MultiConfigurable: public Configurable::Device {
|
||||
MultiConfigurable(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
// Below is the standard Configurable::Device interface; see there for documentation.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override;
|
||||
void set_selections(const Configurable::SelectionSet &selection_by_option) override;
|
||||
Configurable::SelectionSet get_accurate_selections() override;
|
||||
Configurable::SelectionSet get_user_friendly_selections() override;
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final;
|
||||
void set_selections(const Configurable::SelectionSet &selection_by_option) final;
|
||||
Configurable::SelectionSet get_accurate_selections() final;
|
||||
Configurable::SelectionSet get_user_friendly_selections() final;
|
||||
|
||||
private:
|
||||
std::vector<Configurable::Device *> devices_;
|
||||
|
||||
@@ -25,7 +25,7 @@ class MultiJoystick: public Inputs::Joystick {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Input> &get_inputs() override {
|
||||
std::vector<Input> &get_inputs() final {
|
||||
if(inputs.empty()) {
|
||||
for(const auto &joystick: joysticks_) {
|
||||
std::vector<Input> joystick_inputs = joystick->get_inputs();
|
||||
@@ -40,19 +40,19 @@ class MultiJoystick: public Inputs::Joystick {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
void set_input(const Input &digital_input, bool is_active) override {
|
||||
void set_input(const Input &digital_input, bool is_active) final {
|
||||
for(const auto &joystick: joysticks_) {
|
||||
joystick->set_input(digital_input, is_active);
|
||||
}
|
||||
}
|
||||
|
||||
void set_input(const Input &digital_input, float value) override {
|
||||
void set_input(const Input &digital_input, float value) final {
|
||||
for(const auto &joystick: joysticks_) {
|
||||
joystick->set_input(digital_input, value);
|
||||
}
|
||||
}
|
||||
|
||||
void reset_all_inputs() override {
|
||||
void reset_all_inputs() final {
|
||||
for(const auto &joystick: joysticks_) {
|
||||
joystick->reset_all_inputs();
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class MultiJoystickMachine: public JoystickMachine::Machine {
|
||||
MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
// Below is the standard JoystickMachine::Machine interface; see there for documentation.
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override;
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
|
||||
@@ -36,6 +36,14 @@ void MultiKeyboardMachine::type_string(const std::string &string) {
|
||||
}
|
||||
}
|
||||
|
||||
bool MultiKeyboardMachine::can_type(char c) {
|
||||
bool can_type = true;
|
||||
for(const auto &machine: machines_) {
|
||||
can_type &= machine->can_type(c);
|
||||
}
|
||||
return can_type;
|
||||
}
|
||||
|
||||
Inputs::Keyboard &MultiKeyboardMachine::get_keyboard() {
|
||||
return keyboard_;
|
||||
}
|
||||
@@ -48,10 +56,12 @@ MultiKeyboardMachine::MultiKeyboard::MultiKeyboard(const std::vector<::KeyboardM
|
||||
}
|
||||
}
|
||||
|
||||
void MultiKeyboardMachine::MultiKeyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
||||
bool MultiKeyboardMachine::MultiKeyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
||||
bool was_consumed = false;
|
||||
for(const auto &machine: machines_) {
|
||||
machine->get_keyboard().set_key_pressed(key, value, is_pressed);
|
||||
was_consumed |= machine->get_keyboard().set_key_pressed(key, value, is_pressed);
|
||||
}
|
||||
return was_consumed;
|
||||
}
|
||||
|
||||
void MultiKeyboardMachine::MultiKeyboard::reset_all_keys() {
|
||||
|
||||
@@ -32,10 +32,10 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine {
|
||||
public:
|
||||
MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines);
|
||||
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) override;
|
||||
void reset_all_keys() override;
|
||||
const std::set<Key> &observed_keys() override;
|
||||
bool is_exclusive() override;
|
||||
bool set_key_pressed(Key key, char value, bool is_pressed) final;
|
||||
void reset_all_keys() final;
|
||||
const std::set<Key> &observed_keys() final;
|
||||
bool is_exclusive() final;
|
||||
|
||||
private:
|
||||
const std::vector<::KeyboardMachine::Machine *> &machines_;
|
||||
@@ -48,10 +48,11 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine {
|
||||
MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
// Below is the standard KeyboardMachine::Machine interface; see there for documentation.
|
||||
void clear_all_keys() override;
|
||||
void set_key_state(uint16_t key, bool is_pressed) override;
|
||||
void type_string(const std::string &) override;
|
||||
Inputs::Keyboard &get_keyboard() override;
|
||||
void clear_all_keys() final;
|
||||
void set_key_state(uint16_t key, bool is_pressed) final;
|
||||
void type_string(const std::string &) final;
|
||||
bool can_type(char c) final;
|
||||
Inputs::Keyboard &get_keyboard() final;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct MultiMediaTarget: public MediaTarget::Machine {
|
||||
MultiMediaTarget(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
// Below is the standard MediaTarget::Machine interface; see there for documentation.
|
||||
bool insert_media(const Analyser::Static::Media &media) override;
|
||||
bool insert_media(const Analyser::Static::Media &media) final;
|
||||
|
||||
private:
|
||||
std::vector<MediaTarget::Machine *> targets_;
|
||||
|
||||
@@ -37,12 +37,23 @@ float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum)
|
||||
return ideal / static_cast<float>(speakers_.size());
|
||||
}
|
||||
|
||||
void MultiSpeaker::set_output_rate(float cycles_per_second, int buffer_size) {
|
||||
void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) {
|
||||
stereo_output_ = stereo;
|
||||
for(const auto &speaker: speakers_) {
|
||||
speaker->set_output_rate(cycles_per_second, buffer_size);
|
||||
speaker->set_computed_output_rate(cycles_per_second, buffer_size, stereo);
|
||||
}
|
||||
}
|
||||
|
||||
bool MultiSpeaker::get_is_stereo() {
|
||||
// Return as stereo if any subspeaker is stereo.
|
||||
for(const auto &speaker: speakers_) {
|
||||
if(speaker->get_is_stereo()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MultiSpeaker::set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
@@ -53,7 +64,7 @@ void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vec
|
||||
std::lock_guard<std::mutex> lock_guard(front_speaker_mutex_);
|
||||
if(speaker != front_speaker_) return;
|
||||
}
|
||||
delegate_->speaker_did_complete_samples(this, buffer);
|
||||
did_complete_samples(this, buffer, stereo_output_);
|
||||
}
|
||||
|
||||
void MultiSpeaker::speaker_did_change_input_clock(Speaker *speaker) {
|
||||
|
||||
@@ -39,18 +39,21 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker:
|
||||
|
||||
// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation.
|
||||
float get_ideal_clock_rate_in_range(float minimum, float maximum) override;
|
||||
void set_output_rate(float cycles_per_second, int buffer_size) override;
|
||||
void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) override;
|
||||
void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override;
|
||||
bool get_is_stereo() override;
|
||||
|
||||
private:
|
||||
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) override;
|
||||
void speaker_did_change_input_clock(Speaker *speaker) override;
|
||||
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final;
|
||||
void speaker_did_change_input_clock(Speaker *speaker) final;
|
||||
MultiSpeaker(const std::vector<Outputs::Speaker::Speaker *> &speakers);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -50,17 +50,17 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De
|
||||
static bool would_collapse(const std::vector<std::unique_ptr<DynamicMachine>> &machines);
|
||||
MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines);
|
||||
|
||||
Activity::Source *activity_source() override;
|
||||
Configurable::Device *configurable_device() override;
|
||||
CRTMachine::Machine *crt_machine() override;
|
||||
JoystickMachine::Machine *joystick_machine() override;
|
||||
MouseMachine::Machine *mouse_machine() override;
|
||||
KeyboardMachine::Machine *keyboard_machine() override;
|
||||
MediaTarget::Machine *media_target() override;
|
||||
void *raw_pointer() override;
|
||||
Activity::Source *activity_source() final;
|
||||
Configurable::Device *configurable_device() final;
|
||||
CRTMachine::Machine *crt_machine() final;
|
||||
JoystickMachine::Machine *joystick_machine() final;
|
||||
MouseMachine::Machine *mouse_machine() final;
|
||||
KeyboardMachine::Machine *keyboard_machine() final;
|
||||
MediaTarget::Machine *media_target() final;
|
||||
void *raw_pointer() final;
|
||||
|
||||
private:
|
||||
void multi_crt_did_run_machines() override;
|
||||
void multi_crt_did_run_machines() final;
|
||||
|
||||
std::vector<std::unique_ptr<DynamicMachine>> machines_;
|
||||
std::recursive_mutex machines_mutex_;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
#include "../../../Storage/Disk/Controller/DiskController.hpp"
|
||||
#include "../../../Storage/Disk/Encodings/MFM/Parser.hpp"
|
||||
#include "../../../NumberTheory/CRC.hpp"
|
||||
#include "../../../Numeric/CRC.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
#include <deque>
|
||||
|
||||
#include "../../../NumberTheory/CRC.hpp"
|
||||
#include "../../../Numeric/CRC.hpp"
|
||||
#include "../../../Storage/Tape/Parsers/Acorn.hpp"
|
||||
|
||||
using namespace Analyser::Static::Acorn;
|
||||
|
||||
@@ -19,12 +19,10 @@ using namespace Analyser::Static::Commodore;
|
||||
|
||||
class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||
public:
|
||||
std::shared_ptr<Storage::Disk::Drive> drive;
|
||||
|
||||
CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) {
|
||||
drive = std::make_shared<Storage::Disk::Drive>(4000000, 300, 2);
|
||||
set_drive(drive);
|
||||
drive->set_motor_on(true);
|
||||
emplace_drive(4000000, 300, 2);
|
||||
set_drive(1);
|
||||
get_drive().set_motor_on(true);
|
||||
}
|
||||
|
||||
struct Sector {
|
||||
@@ -61,6 +59,10 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||
return get_sector(sector);
|
||||
}
|
||||
|
||||
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
get_drive().set_disk(disk);
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned int shift_register_;
|
||||
int index_count_;
|
||||
@@ -170,7 +172,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||
std::vector<File> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
std::vector<File> files;
|
||||
CommodoreGCRParser parser;
|
||||
parser.drive->set_disk(disk);
|
||||
parser.set_disk(disk);
|
||||
|
||||
// find any sector whatsoever to establish the current track
|
||||
std::shared_ptr<CommodoreGCRParser::Sector> sector;
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
|
||||
using namespace Analyser::Static::Oric;
|
||||
|
||||
static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) {
|
||||
namespace {
|
||||
|
||||
int score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) {
|
||||
int score = 0;
|
||||
|
||||
for(const auto address : disassembly.outward_calls) score += (rom_functions.find(address) != rom_functions.end()) ? 1 : -1;
|
||||
@@ -30,7 +32,7 @@ static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, cons
|
||||
return score;
|
||||
}
|
||||
|
||||
static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||
int basic10_score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||
const std::set<uint16_t> rom_functions = {
|
||||
0x0228, 0x022b,
|
||||
0xc3ca, 0xc3f8, 0xc448, 0xc47c, 0xc4b5, 0xc4e3, 0xc4e0, 0xc524, 0xc56f, 0xc5a2, 0xc5f8, 0xc60a, 0xc6a5, 0xc6de, 0xc719, 0xc738,
|
||||
@@ -51,10 +53,10 @@ static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembl
|
||||
0x0228, 0x0229, 0x022a, 0x022b, 0x022c, 0x022d, 0x0230
|
||||
};
|
||||
|
||||
return Score(disassembly, rom_functions, variable_locations);
|
||||
return score(disassembly, rom_functions, variable_locations);
|
||||
}
|
||||
|
||||
static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||
int basic11_score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||
const std::set<uint16_t> rom_functions = {
|
||||
0x0238, 0x023b, 0x023e, 0x0241, 0x0244, 0x0247,
|
||||
0xc3c6, 0xc3f4, 0xc444, 0xc47c, 0xc4a8, 0xc4d3, 0xc4e0, 0xc524, 0xc55f, 0xc592, 0xc5e8, 0xc5fa, 0xc692, 0xc6b3, 0xc6ee, 0xc70d,
|
||||
@@ -76,10 +78,10 @@ static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembl
|
||||
0x0244, 0x0245, 0x0246, 0x0247, 0x0248, 0x0249, 0x024a, 0x024b, 0x024c
|
||||
};
|
||||
|
||||
return Score(disassembly, rom_functions, variable_locations);
|
||||
return score(disassembly, rom_functions, variable_locations);
|
||||
}
|
||||
|
||||
static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) {
|
||||
bool is_microdisc(Storage::Encodings::MFM::Parser &parser) {
|
||||
/*
|
||||
The Microdisc boot sector is sector 2 of track 0 and contains a 23-byte signature.
|
||||
*/
|
||||
@@ -100,17 +102,22 @@ static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) {
|
||||
return !std::memcmp(signature, first_sample.data(), sizeof(signature));
|
||||
}
|
||||
|
||||
static bool IsJasmin(Storage::Encodings::MFM::Parser &parser) {
|
||||
bool is_400_loader(Storage::Encodings::MFM::Parser &parser, uint16_t range_start, uint16_t range_end) {
|
||||
/*
|
||||
The Jasmin boot sector is sector 1 of track 0 and is loaded at $400;
|
||||
disassemble it to test it for validity.
|
||||
Both the Jasmin and BD-DOS boot sectors are sector 1 of track 0 and are loaded at $400;
|
||||
use disassembly to test for likely matches.
|
||||
*/
|
||||
|
||||
Storage::Encodings::MFM::Sector *sector = parser.get_sector(0, 0, 1);
|
||||
if(!sector) return false;
|
||||
if(sector->samples.empty()) return false;
|
||||
|
||||
const std::vector<uint8_t> &first_sample = sector->samples[0];
|
||||
if(first_sample.size() != 256) return false;
|
||||
// Take a copy of the first sampling, and keep only the final 256 bytes (assuming at least that many were found).
|
||||
std::vector<uint8_t> first_sample = sector->samples[0];
|
||||
if(first_sample.size() < 256) return false;
|
||||
if(first_sample.size() > 256) {
|
||||
first_sample.erase(first_sample.end() - 256, first_sample.end());
|
||||
}
|
||||
|
||||
// Grab a disassembly.
|
||||
const auto disassembly =
|
||||
@@ -120,14 +127,24 @@ static bool IsJasmin(Storage::Encodings::MFM::Parser &parser) {
|
||||
int register_hits = 0;
|
||||
for(auto list : {disassembly.external_stores, disassembly.external_loads, disassembly.external_modifies}) {
|
||||
for(auto address : list) {
|
||||
register_hits += (address >= 0x3f4 && address <= 0x3ff);
|
||||
register_hits += (address >= range_start && address <= range_end);
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary, sure, but as long as at least two accesses to Jasmin registers are found, accept this.
|
||||
// Arbitrary, sure, but as long as at least two accesses to the requested register range are found, accept this.
|
||||
return register_hits >= 2;
|
||||
}
|
||||
|
||||
bool is_jasmin(Storage::Encodings::MFM::Parser &parser) {
|
||||
return is_400_loader(parser, 0x3f4, 0x3ff);
|
||||
}
|
||||
|
||||
bool is_bd500(Storage::Encodings::MFM::Parser &parser) {
|
||||
return is_400_loader(parser, 0x310, 0x323);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::Oric;
|
||||
@@ -146,9 +163,7 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
|
||||
const Analyser::Static::MOS6502::Disassembly disassembly =
|
||||
Analyser::Static::MOS6502::Disassemble(file.data, Analyser::Static::Disassembler::OffsetMapper(file.starting_address), entry_points);
|
||||
|
||||
int basic10_score = Basic10Score(disassembly);
|
||||
int basic11_score = Basic11Score(disassembly);
|
||||
if(basic10_score > basic11_score) basic10_votes++; else basic11_votes++;
|
||||
if(basic10_score(disassembly) > basic11_score(disassembly)) ++basic10_votes; else ++basic11_votes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,17 +173,22 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
|
||||
}
|
||||
|
||||
if(!media.disks.empty()) {
|
||||
// 8-DOS is recognised by a dedicated Disk II analyser, so check only for Microdisc and
|
||||
// Jasmin formats here.
|
||||
// 8-DOS is recognised by a dedicated Disk II analyser, so check only for Microdisc,
|
||||
// Jasmin and BD-DOS formats here.
|
||||
for(auto &disk: media.disks) {
|
||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||
if(IsMicrodisc(parser)) {
|
||||
|
||||
if(is_microdisc(parser)) {
|
||||
target->disk_interface = Target::DiskInterface::Microdisc;
|
||||
target->media.disks.push_back(disk);
|
||||
} else if(IsJasmin(parser)) {
|
||||
} else if(is_jasmin(parser)) {
|
||||
target->disk_interface = Target::DiskInterface::Jasmin;
|
||||
target->should_start_jasmin = true;
|
||||
target->media.disks.push_back(disk);
|
||||
} else if(is_bd500(parser)) {
|
||||
target->disk_interface = Target::DiskInterface::BD500;
|
||||
target->media.disks.push_back(disk);
|
||||
target->rom = Target::ROM::BASIC10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ struct Target: public ::Analyser::Static::Target {
|
||||
Microdisc,
|
||||
Pravetz,
|
||||
Jasmin,
|
||||
BD500,
|
||||
None
|
||||
};
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/SSD.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/ST.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/STX.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp"
|
||||
|
||||
// Mass Storage Devices (i.e. usually, hard disks)
|
||||
@@ -147,6 +148,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS
|
||||
Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // SSD
|
||||
Format("st", result.disks, Disk::DiskImageHolder<Storage::Disk::ST>, TargetPlatform::AtariST) // ST
|
||||
Format("stx", result.disks, Disk::DiskImageHolder<Storage::Disk::STX>, TargetPlatform::AtariST) // STX
|
||||
Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore)
|
||||
Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric)
|
||||
Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX
|
||||
|
||||
@@ -13,62 +13,68 @@
|
||||
#include <vector>
|
||||
|
||||
/*!
|
||||
A DeferredQueue maintains a list of ordered actions and the times at which
|
||||
they should happen, and divides a total execution period up into the portions
|
||||
that occur between those actions, triggering each action when it is reached.
|
||||
Provides the logic to insert into and traverse a list of future scheduled items.
|
||||
*/
|
||||
template <typename TimeUnit> class DeferredQueue {
|
||||
public:
|
||||
/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
|
||||
DeferredQueue(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
|
||||
|
||||
/*!
|
||||
Schedules @c action to occur in @c delay units of time.
|
||||
|
||||
Actions must be scheduled in the order they will occur. It is undefined behaviour
|
||||
to schedule them out of order.
|
||||
*/
|
||||
void defer(TimeUnit delay, const std::function<void(void)> &action) {
|
||||
pending_actions_.emplace_back(delay, action);
|
||||
}
|
||||
|
||||
/*!
|
||||
Runs for @c length units of time.
|
||||
|
||||
The constructor-supplied target will be called with one or more periods that add up to @c length;
|
||||
any scheduled actions will be called between periods.
|
||||
*/
|
||||
void run_for(TimeUnit length) {
|
||||
// If there are no pending actions, just run for the entire length.
|
||||
// This should be the normal branch.
|
||||
if(pending_actions_.empty()) {
|
||||
target_(length);
|
||||
// Apply immediately if there's no delay (or a negative delay).
|
||||
if(delay <= TimeUnit(0)) {
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
// Divide the time to run according to the pending actions.
|
||||
while(length > TimeUnit(0)) {
|
||||
TimeUnit next_period = pending_actions_.empty() ? length : std::min(length, pending_actions_[0].delay);
|
||||
target_(next_period);
|
||||
length -= next_period;
|
||||
if(!pending_actions_.empty()) {
|
||||
// Otherwise enqueue, having subtracted the delay for any preceding events,
|
||||
// and subtracting from the subsequent, if any.
|
||||
auto insertion_point = pending_actions_.begin();
|
||||
while(insertion_point != pending_actions_.end() && insertion_point->delay < delay) {
|
||||
delay -= insertion_point->delay;
|
||||
++insertion_point;
|
||||
}
|
||||
if(insertion_point != pending_actions_.end()) {
|
||||
insertion_point->delay -= delay;
|
||||
}
|
||||
|
||||
off_t performances = 0;
|
||||
for(auto &action: pending_actions_) {
|
||||
action.delay -= next_period;
|
||||
if(!action.delay) {
|
||||
action.action();
|
||||
++performances;
|
||||
}
|
||||
}
|
||||
if(performances) {
|
||||
pending_actions_.erase(pending_actions_.begin(), pending_actions_.begin() + performances);
|
||||
pending_actions_.emplace(insertion_point, delay, action);
|
||||
} else {
|
||||
pending_actions_.emplace_back(delay, action);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The amount of time until the next enqueued action will occur,
|
||||
or TimeUnit(-1) if the queue is empty.
|
||||
*/
|
||||
TimeUnit time_until_next_action() {
|
||||
if(pending_actions_.empty()) return TimeUnit(-1);
|
||||
return pending_actions_.front().delay;
|
||||
}
|
||||
|
||||
/*!
|
||||
Advances the queue the specified amount of time, performing any actions it reaches.
|
||||
*/
|
||||
void advance(TimeUnit time) {
|
||||
auto erase_iterator = pending_actions_.begin();
|
||||
while(erase_iterator != pending_actions_.end()) {
|
||||
erase_iterator->delay -= time;
|
||||
if(erase_iterator->delay <= TimeUnit(0)) {
|
||||
time = -erase_iterator->delay;
|
||||
erase_iterator->action();
|
||||
++erase_iterator;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(erase_iterator != pending_actions_.begin()) {
|
||||
pending_actions_.erase(pending_actions_.begin(), erase_iterator);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void(TimeUnit)> target_;
|
||||
|
||||
// The list of deferred actions.
|
||||
struct DeferredAction {
|
||||
TimeUnit delay;
|
||||
@@ -79,4 +85,40 @@ template <typename TimeUnit> class DeferredQueue {
|
||||
std::vector<DeferredAction> pending_actions_;
|
||||
};
|
||||
|
||||
/*!
|
||||
A DeferredQueue maintains a list of ordered actions and the times at which
|
||||
they should happen, and divides a total execution period up into the portions
|
||||
that occur between those actions, triggering each action when it is reached.
|
||||
|
||||
This list is efficient only for short queues.
|
||||
*/
|
||||
template <typename TimeUnit> class DeferredQueuePerformer: public DeferredQueue<TimeUnit> {
|
||||
public:
|
||||
/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
|
||||
DeferredQueuePerformer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
|
||||
|
||||
/*!
|
||||
Runs for @c length units of time.
|
||||
|
||||
The constructor-supplied target will be called with one or more periods that add up to @c length;
|
||||
any scheduled actions will be called between periods.
|
||||
*/
|
||||
void run_for(TimeUnit length) {
|
||||
auto time_to_next = DeferredQueue<TimeUnit>::time_until_next_action();
|
||||
while(time_to_next != TimeUnit(-1) && time_to_next <= length) {
|
||||
target_(time_to_next);
|
||||
length -= time_to_next;
|
||||
DeferredQueue<TimeUnit>::advance(time_to_next);
|
||||
}
|
||||
|
||||
DeferredQueue<TimeUnit>::advance(length);
|
||||
target_(length);
|
||||
|
||||
// TODO: optimise this to avoid the multiple std::vector deletes. Find a neat way to expose that solution, maybe?
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void(TimeUnit)> target_;
|
||||
};
|
||||
|
||||
#endif /* DeferredQueue_h */
|
||||
|
||||
@@ -43,6 +43,13 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Acts exactly as per the standard ->, but preserves constness.
|
||||
forceinline const T *operator->() const {
|
||||
auto non_const_this = const_cast<JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *>(this);
|
||||
non_const_this->flush();
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Returns a pointer to the included object without flushing time.
|
||||
forceinline T *last_valid() {
|
||||
return &object_;
|
||||
@@ -53,7 +60,8 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
if(!is_flushed_) {
|
||||
is_flushed_ = true;
|
||||
if constexpr (divider == 1) {
|
||||
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
||||
const auto duration = time_since_update_.template flush<TargetTimeScale>();
|
||||
object_.run_for(duration);
|
||||
} else {
|
||||
const auto duration = time_since_update_.template divide<TargetTimeScale>(LocalTimeScale(divider));
|
||||
if(duration > TargetTimeScale(0))
|
||||
@@ -68,6 +76,48 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
bool is_flushed_ = true;
|
||||
};
|
||||
|
||||
/*!
|
||||
A RealTimeActor presents the same interface as a JustInTimeActor but doesn't defer work.
|
||||
Time added will be performed immediately.
|
||||
|
||||
Its primary purpose is to allow consumers to remain flexible in their scheduling.
|
||||
*/
|
||||
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class RealTimeActor {
|
||||
public:
|
||||
template<typename... Args> RealTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
|
||||
forceinline void operator += (const LocalTimeScale &rhs) {
|
||||
if constexpr (multiplier == 1 && divider == 1) {
|
||||
object_.run_for(TargetTimeScale(rhs));
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (multiplier == 1) {
|
||||
accumulated_time_ += rhs;
|
||||
} else {
|
||||
accumulated_time_ += rhs * multiplier;
|
||||
}
|
||||
|
||||
if constexpr (divider == 1) {
|
||||
const auto duration = accumulated_time_.template flush<TargetTimeScale>();
|
||||
object_.run_for(duration);
|
||||
} else {
|
||||
const auto duration = accumulated_time_.template divide<TargetTimeScale>(LocalTimeScale(divider));
|
||||
if(duration > TargetTimeScale(0))
|
||||
object_.run_for(duration);
|
||||
}
|
||||
}
|
||||
|
||||
forceinline T *operator->() { return &object_; }
|
||||
forceinline const T *operator->() const { return &object_; }
|
||||
forceinline T *last_valid() { return &object_; }
|
||||
forceinline void flush() {}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale accumulated_time_;
|
||||
};
|
||||
|
||||
/*!
|
||||
A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
|
||||
Any time the amount of accumulated time crosses a threshold provided at construction time,
|
||||
|
||||
88
ClockReceiver/ScanSynchroniser.hpp
Normal file
88
ClockReceiver/ScanSynchroniser.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// ScanSynchroniser.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 09/02/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef ScanSynchroniser_h
|
||||
#define ScanSynchroniser_h
|
||||
|
||||
#include "../Outputs/ScanTarget.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Time {
|
||||
|
||||
/*!
|
||||
Where an emulated machine is sufficiently close to a host machine's frame rate that a small nudge in
|
||||
its speed multiplier will bring it into frame synchronisation, the ScanSynchroniser provides a sequence of
|
||||
speed multipliers designed both to adjust the machine to the proper speed and, in a reasonable amount
|
||||
of time, to bring it into phase.
|
||||
*/
|
||||
class ScanSynchroniser {
|
||||
public:
|
||||
/*!
|
||||
@returns @c true if the emulated machine can be synchronised with the host frame output based on its
|
||||
current @c [scan]status and the host machine's @c frame_duration; @c false otherwise.
|
||||
*/
|
||||
bool can_synchronise(const Outputs::Display::ScanStatus &scan_status, double frame_duration) {
|
||||
ratio_ = 1.0;
|
||||
if(scan_status.field_duration_gradient < 0.00001) {
|
||||
// Check out the machine's current frame time.
|
||||
// If it's within 3% of a non-zero integer multiple of the
|
||||
// display rate, mark this time window to be split over the sync.
|
||||
ratio_ = (frame_duration * base_multiplier_) / scan_status.field_duration;
|
||||
const double integer_ratio = round(ratio_);
|
||||
if(integer_ratio > 0.0) {
|
||||
ratio_ /= integer_ratio;
|
||||
return ratio_ <= maximum_rate_adjustment && ratio_ >= 1.0 / maximum_rate_adjustment;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The appropriate speed multiplier for the next frame based on the inputs previously supplied to @c can_synchronise.
|
||||
Results are undefined if @c can_synchroise returned @c false.
|
||||
*/
|
||||
double next_speed_multiplier(const Outputs::Display::ScanStatus &scan_status) {
|
||||
// The host versus emulated ratio is calculated based on the current perceived frame duration of the machine.
|
||||
// Either that number is exactly correct or it's already the result of some sort of low-pass filter. So there's
|
||||
// no benefit to second guessing it here — just take it to be correct.
|
||||
//
|
||||
// ... with one slight caveat, which is that it is desireable to adjust phase here, to align vertical sync points.
|
||||
// So the set speed multiplier may be adjusted slightly to aim for that.
|
||||
double speed_multiplier = 1.0 / (ratio_ / base_multiplier_);
|
||||
if(scan_status.current_position > 0.0) {
|
||||
if(scan_status.current_position < 0.5) speed_multiplier /= phase_adjustment_ratio;
|
||||
else speed_multiplier *= phase_adjustment_ratio;
|
||||
}
|
||||
speed_multiplier_ = (speed_multiplier_ * 0.95) + (speed_multiplier * 0.05);
|
||||
return speed_multiplier_ * base_multiplier_;
|
||||
}
|
||||
|
||||
void set_base_speed_multiplier(double multiplier) {
|
||||
base_multiplier_ = multiplier;
|
||||
}
|
||||
|
||||
double get_base_speed_multiplier() {
|
||||
return base_multiplier_;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr double maximum_rate_adjustment = 1.03;
|
||||
static constexpr double phase_adjustment_ratio = 1.005;
|
||||
|
||||
// Managed local state.
|
||||
double speed_multiplier_ = 1.0;
|
||||
double base_multiplier_ = 1.0;
|
||||
|
||||
// Temporary storage to bridge the can_synchronise -> next_speed_multiplier gap.
|
||||
double ratio_ = 1.0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* ScanSynchroniser_h */
|
||||
@@ -9,9 +9,16 @@
|
||||
#ifndef TimeTypes_h
|
||||
#define TimeTypes_h
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace Time {
|
||||
|
||||
typedef double Seconds;
|
||||
typedef int64_t Nanos;
|
||||
|
||||
inline Nanos nanos_now() {
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -336,6 +336,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
READ_ID();
|
||||
|
||||
if(index_hole_count_ == 6) {
|
||||
LOG("Nothing found to verify");
|
||||
update_status([] (Status &status) {
|
||||
status.seek_error = true;
|
||||
});
|
||||
@@ -480,7 +481,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
status.data_request = true;
|
||||
});
|
||||
distance_into_section_++;
|
||||
if(distance_into_section_ == 128 << header_[3]) {
|
||||
if(distance_into_section_ == 128 << (header_[3]&3)) {
|
||||
distance_into_section_ = 0;
|
||||
goto type2_check_crc;
|
||||
}
|
||||
@@ -563,7 +564,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
*/
|
||||
write_byte(data_);
|
||||
distance_into_section_++;
|
||||
if(distance_into_section_ == 128 << header_[3]) {
|
||||
if(distance_into_section_ == 128 << (header_[3]&3)) {
|
||||
goto type2_write_crc;
|
||||
}
|
||||
|
||||
@@ -823,6 +824,10 @@ void WD1770::set_head_loaded(bool head_loaded) {
|
||||
if(head_loaded) posit_event(int(Event1770::HeadLoad));
|
||||
}
|
||||
|
||||
bool WD1770::get_head_loaded() {
|
||||
return head_is_loaded_;
|
||||
}
|
||||
|
||||
ClockingHint::Preference WD1770::preferred_clocking() {
|
||||
if(status_.busy) return ClockingHint::Preference::RealTime;
|
||||
return Storage::Disk::MFMController::preferred_clocking();
|
||||
|
||||
@@ -45,20 +45,20 @@ class WD1770: public Storage::Disk::MFMController {
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
enum Flag: uint8_t {
|
||||
NotReady = 0x80,
|
||||
NotReady = 0x80, // 0x80
|
||||
MotorOn = 0x80,
|
||||
WriteProtect = 0x40,
|
||||
RecordType = 0x20,
|
||||
WriteProtect = 0x40, // 0x40
|
||||
RecordType = 0x20, // 0x20
|
||||
SpinUp = 0x20,
|
||||
HeadLoaded = 0x20,
|
||||
RecordNotFound = 0x10,
|
||||
RecordNotFound = 0x10, // 0x10
|
||||
SeekError = 0x10,
|
||||
CRCError = 0x08,
|
||||
LostData = 0x04,
|
||||
CRCError = 0x08, // 0x08
|
||||
LostData = 0x04, // 0x04
|
||||
TrackZero = 0x04,
|
||||
DataRequest = 0x02,
|
||||
DataRequest = 0x02, // 0x02
|
||||
Index = 0x02,
|
||||
Busy = 0x01
|
||||
Busy = 0x01 // 0x01
|
||||
};
|
||||
|
||||
/// @returns The current value of the IRQ line output.
|
||||
@@ -80,6 +80,9 @@ class WD1770: public Storage::Disk::MFMController {
|
||||
virtual void set_motor_on(bool motor_on);
|
||||
void set_head_loaded(bool head_loaded);
|
||||
|
||||
/// @returns The last value posted to @c set_head_loaded.
|
||||
bool get_head_loaded();
|
||||
|
||||
private:
|
||||
Personality personality_;
|
||||
inline bool has_motor_on_line() { return (personality_ != P1793 ) && (personality_ != P1773); }
|
||||
|
||||
@@ -17,13 +17,13 @@ AudioGenerator::AudioGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue
|
||||
|
||||
|
||||
void AudioGenerator::set_volume(uint8_t volume) {
|
||||
audio_queue_.defer([=]() {
|
||||
volume_ = static_cast<int16_t>(volume) * range_multiplier_;
|
||||
audio_queue_.defer([this, volume]() {
|
||||
volume_ = int16_t(volume) * range_multiplier_;
|
||||
});
|
||||
}
|
||||
|
||||
void AudioGenerator::set_control(int channel, uint8_t value) {
|
||||
audio_queue_.defer([=]() {
|
||||
audio_queue_.defer([this, channel, value]() {
|
||||
control_registers_[channel] = value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
void skip_samples(std::size_t number_of_samples);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
@@ -83,8 +84,9 @@ template <class BusHandler> class MOS6560 {
|
||||
speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0));
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const { return crt_.get_scaled_scan_status() / 4.0f; }
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
|
||||
Outputs::Speaker::Speaker *get_speaker() { return &speaker_; }
|
||||
|
||||
void set_high_frequency_cutoff(float cutoff) {
|
||||
|
||||
@@ -67,7 +67,7 @@ class i8272 : public Storage::Disk::MFMController {
|
||||
ResultEmpty = (1 << 5),
|
||||
NoLongerReady = (1 << 6)
|
||||
};
|
||||
void posit_event(int type) override;
|
||||
void posit_event(int type) final;
|
||||
int interesting_event_mask_ = static_cast<int>(Event8272::CommandByte);
|
||||
int resume_point_ = 0;
|
||||
bool is_access_command_ = false;
|
||||
|
||||
@@ -117,6 +117,14 @@ void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus TMS9918::get_scaled_scan_status() const {
|
||||
// The input was scaled by 3/4 to convert half cycles to internal ticks,
|
||||
// so undo that and also allow for: (i) the multiply by 4 that it takes
|
||||
// to reach the CRT; and (ii) the fact that the half-cycles value was scaled,
|
||||
// and this should really reply in whole cycles.
|
||||
return crt_.get_scaled_scan_status() * (4.0f / (3.0f * 8.0f));
|
||||
}
|
||||
|
||||
void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class TMS9918: public Base {
|
||||
/*! Sets the scan target this TMS will post content to. */
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*! Sets the type of display the CRT will request. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
// Copyright 2016 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "AY38910.hpp"
|
||||
|
||||
#include <cmath>
|
||||
//namespace GI {
|
||||
//namespace AY38910 {
|
||||
|
||||
using namespace GI::AY38910;
|
||||
|
||||
AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
|
||||
template <bool is_stereo>
|
||||
AY38910<is_stereo>::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
|
||||
// Don't use the low bit of the envelope position if this is an AY.
|
||||
envelope_position_mask_ |= personality == Personality::AY38910;
|
||||
|
||||
@@ -70,18 +74,34 @@ AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &
|
||||
set_sample_volume_range(0);
|
||||
}
|
||||
|
||||
void AY38910::set_sample_volume_range(std::int16_t range) {
|
||||
// set up volume lookup table
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::int16_t range) {
|
||||
// Set up volume lookup table; the function below is based on a combination of the graph
|
||||
// from the YM's datasheet, showing a clear power curve, and fitting that to observed
|
||||
// values reported elsewhere.
|
||||
const float max_volume = float(range) / 3.0f; // As there are three channels.
|
||||
constexpr float root_two = 1.414213562373095f;
|
||||
for(int v = 0; v < 32; v++) {
|
||||
volumes_[v] = int(max_volume / powf(root_two, float(v ^ 0x1f) / 2.0f));
|
||||
volumes_[v] = int(max_volume / powf(root_two, float(v ^ 0x1f) / 3.18f));
|
||||
}
|
||||
volumes_[0] = 0; // Tie level 0 to silence.
|
||||
|
||||
// Tie level 0 to silence.
|
||||
for(int v = 31; v >= 0; --v) {
|
||||
volumes_[v] -= volumes_[0];
|
||||
}
|
||||
|
||||
evaluate_output_volume();
|
||||
}
|
||||
|
||||
void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
|
||||
a_left_ = uint8_t(a_left * 255.0f);
|
||||
b_left_ = uint8_t(b_left * 255.0f);
|
||||
c_left_ = uint8_t(c_left * 255.0f);
|
||||
a_right_ = uint8_t(a_right * 255.0f);
|
||||
b_right_ = uint8_t(b_right * 255.0f);
|
||||
c_right_ = uint8_t(c_right * 255.0f);
|
||||
}
|
||||
|
||||
template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
// Note on structure below: the real AY has a built-in divider of 8
|
||||
// prior to applying its tone and noise dividers. But the YM fills the
|
||||
// same total periods for noise and tone with double-precision envelopes.
|
||||
@@ -93,7 +113,11 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
|
||||
std::size_t c = 0;
|
||||
while((master_divider_&3) && c < number_of_samples) {
|
||||
target[c] = output_volume_;
|
||||
if constexpr (is_stereo) {
|
||||
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||
} else {
|
||||
target[c] = int16_t(output_volume_);
|
||||
}
|
||||
master_divider_++;
|
||||
c++;
|
||||
}
|
||||
@@ -135,7 +159,11 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
evaluate_output_volume();
|
||||
|
||||
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
|
||||
target[c] = output_volume_;
|
||||
if constexpr (is_stereo) {
|
||||
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||
} else {
|
||||
target[c] = int16_t(output_volume_);
|
||||
}
|
||||
c++;
|
||||
master_divider_++;
|
||||
}
|
||||
@@ -144,7 +172,7 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
master_divider_ &= 3;
|
||||
}
|
||||
|
||||
void AY38910::evaluate_output_volume() {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
|
||||
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
|
||||
|
||||
// The output level for a channel is:
|
||||
@@ -184,34 +212,47 @@ void AY38910::evaluate_output_volume() {
|
||||
};
|
||||
#undef channel_volume
|
||||
|
||||
// Mix additively.
|
||||
output_volume_ = static_cast<int16_t>(
|
||||
volumes_[volumes[0]] * channel_levels[0] +
|
||||
volumes_[volumes[1]] * channel_levels[1] +
|
||||
volumes_[volumes[2]] * channel_levels[2]
|
||||
);
|
||||
// Mix additively, weighting if in stereo.
|
||||
if constexpr (is_stereo) {
|
||||
int16_t *const output_volumes = reinterpret_cast<int16_t *>(&output_volume_);
|
||||
output_volumes[0] = int16_t((
|
||||
volumes_[volumes[0]] * channel_levels[0] * a_left_ +
|
||||
volumes_[volumes[1]] * channel_levels[1] * b_left_ +
|
||||
volumes_[volumes[2]] * channel_levels[2] * c_left_
|
||||
) >> 8);
|
||||
output_volumes[1] = int16_t((
|
||||
volumes_[volumes[0]] * channel_levels[0] * a_right_ +
|
||||
volumes_[volumes[1]] * channel_levels[1] * b_right_ +
|
||||
volumes_[volumes[2]] * channel_levels[2] * c_right_
|
||||
) >> 8);
|
||||
} else {
|
||||
output_volume_ = uint32_t(
|
||||
volumes_[volumes[0]] * channel_levels[0] +
|
||||
volumes_[volumes[1]] * channel_levels[1] +
|
||||
volumes_[volumes[2]] * channel_levels[2]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool AY38910::is_zero_level() {
|
||||
template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() {
|
||||
// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero.
|
||||
return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0;
|
||||
}
|
||||
|
||||
// MARK: - Register manipulation
|
||||
|
||||
void AY38910::select_register(uint8_t r) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::select_register(uint8_t r) {
|
||||
selected_register_ = r;
|
||||
}
|
||||
|
||||
void AY38910::set_register_value(uint8_t value) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t value) {
|
||||
// There are only 16 registers.
|
||||
if(selected_register_ > 15) return;
|
||||
|
||||
// If this is a register that affects audio output, enqueue a mutation onto the
|
||||
// audio generation thread.
|
||||
if(selected_register_ < 14) {
|
||||
const int selected_register = selected_register_;
|
||||
task_queue_.defer([=] () {
|
||||
task_queue_.defer([this, selected_register = selected_register_, value] () {
|
||||
// Perform any register-specific mutation to output generation.
|
||||
uint8_t masked_value = value;
|
||||
switch(selected_register) {
|
||||
@@ -220,7 +261,7 @@ void AY38910::set_register_value(uint8_t value) {
|
||||
int channel = selected_register >> 1;
|
||||
|
||||
if(selected_register & 1)
|
||||
tone_periods_[channel] = (tone_periods_[channel] & 0xff) | static_cast<uint16_t>((value&0xf) << 8);
|
||||
tone_periods_[channel] = (tone_periods_[channel] & 0xff) | uint16_t((value&0xf) << 8);
|
||||
else
|
||||
tone_periods_[channel] = (tone_periods_[channel] & ~0xff) | value;
|
||||
}
|
||||
@@ -235,7 +276,7 @@ void AY38910::set_register_value(uint8_t value) {
|
||||
break;
|
||||
|
||||
case 12:
|
||||
envelope_period_ = (envelope_period_ & 0xff) | static_cast<int>(value << 8);
|
||||
envelope_period_ = (envelope_period_ & 0xff) | int(value << 8);
|
||||
break;
|
||||
|
||||
case 13:
|
||||
@@ -273,7 +314,7 @@ void AY38910::set_register_value(uint8_t value) {
|
||||
if(update_port_a) set_port_output(false);
|
||||
}
|
||||
|
||||
uint8_t AY38910::get_register_value() {
|
||||
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() {
|
||||
// This table ensures that bits that aren't defined within the AY are returned as 0s
|
||||
// when read, conforming to CPC-sourced unit tests.
|
||||
const uint8_t register_masks[16] = {
|
||||
@@ -287,24 +328,24 @@ uint8_t AY38910::get_register_value() {
|
||||
|
||||
// MARK: - Port querying
|
||||
|
||||
uint8_t AY38910::get_port_output(bool port_b) {
|
||||
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_port_output(bool port_b) {
|
||||
return registers_[port_b ? 15 : 14];
|
||||
}
|
||||
|
||||
// MARK: - Bus handling
|
||||
|
||||
void AY38910::set_port_handler(PortHandler *handler) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_port_handler(PortHandler *handler) {
|
||||
port_handler_ = handler;
|
||||
set_port_output(true);
|
||||
set_port_output(false);
|
||||
}
|
||||
|
||||
void AY38910::set_data_input(uint8_t r) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_data_input(uint8_t r) {
|
||||
data_input_ = r;
|
||||
update_bus();
|
||||
}
|
||||
|
||||
void AY38910::set_port_output(bool port_b) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b) {
|
||||
// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor,
|
||||
// so that when in the "input" mode, all pins will read normally high". Therefore,
|
||||
// report programmer selection of input mode as creating an output of 0xff.
|
||||
@@ -314,7 +355,7 @@ void AY38910::set_port_output(bool port_b) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t AY38910::get_data_output() {
|
||||
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() {
|
||||
if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) {
|
||||
// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the
|
||||
// value returned to the CPU when reading it is the and of the output value and any input.
|
||||
@@ -330,22 +371,22 @@ uint8_t AY38910::get_data_output() {
|
||||
return data_output_;
|
||||
}
|
||||
|
||||
void AY38910::set_control_lines(ControlLines control_lines) {
|
||||
switch(static_cast<int>(control_lines)) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) {
|
||||
switch(int(control_lines)) {
|
||||
default: control_state_ = Inactive; break;
|
||||
|
||||
case static_cast<int>(BDIR | BC2 | BC1):
|
||||
case int(BDIR | BC2 | BC1):
|
||||
case BDIR:
|
||||
case BC1: control_state_ = LatchAddress; break;
|
||||
|
||||
case static_cast<int>(BC2 | BC1): control_state_ = Read; break;
|
||||
case static_cast<int>(BDIR | BC2): control_state_ = Write; break;
|
||||
case int(BC2 | BC1): control_state_ = Read; break;
|
||||
case int(BDIR | BC2): control_state_ = Write; break;
|
||||
}
|
||||
|
||||
update_bus();
|
||||
}
|
||||
|
||||
void AY38910::update_bus() {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::update_bus() {
|
||||
// Assume no output, unless this turns out to be a read.
|
||||
data_output_ = 0xff;
|
||||
switch(control_state_) {
|
||||
@@ -355,3 +396,7 @@ void AY38910::update_bus() {
|
||||
case Read: data_output_ = get_register_value(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure both mono and stereo versions of the AY are built.
|
||||
template class GI::AY38910::AY38910<true>;
|
||||
template class GI::AY38910::AY38910<false>;
|
||||
|
||||
@@ -63,8 +63,10 @@ enum class Personality {
|
||||
Provides emulation of an AY-3-8910 / YM2149, which is a three-channel sound chip with a
|
||||
noise generator and a volume envelope generator, which also provides two bidirectional
|
||||
interface ports.
|
||||
|
||||
This AY has an attached mono or stereo mixer.
|
||||
*/
|
||||
class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
/// Creates a new AY38910.
|
||||
AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &);
|
||||
@@ -91,10 +93,23 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
*/
|
||||
void set_port_handler(PortHandler *);
|
||||
|
||||
/*!
|
||||
Enables or disables stereo output; if stereo output is enabled then also sets the weight of each of the AY's
|
||||
channels in each of the output channels.
|
||||
|
||||
If a_left_ = b_left = c_left = a_right = b_right = c_right = 1.0 then you'll get output that's effectively mono.
|
||||
|
||||
a_left = 0.0, a_right = 1.0 will make A full volume on the right output, and silent on the left.
|
||||
|
||||
a_left = 0.5, a_right = 0.5 will make A half volume on both outputs.
|
||||
*/
|
||||
void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0);
|
||||
|
||||
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
bool is_zero_level();
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return is_stereo; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
@@ -135,14 +150,21 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
|
||||
uint8_t data_input_, data_output_;
|
||||
|
||||
int16_t output_volume_;
|
||||
void evaluate_output_volume();
|
||||
uint32_t output_volume_;
|
||||
|
||||
void update_bus();
|
||||
PortHandler *port_handler_ = nullptr;
|
||||
void set_port_output(bool port_b);
|
||||
|
||||
void evaluate_output_volume();
|
||||
|
||||
// Output mixing control.
|
||||
uint8_t a_left_ = 255, a_right_ = 255;
|
||||
uint8_t b_left_ = 255, b_right_ = 255;
|
||||
uint8_t c_left_ = 255, c_right_ = 255;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ void Toggle::skip_samples(const std::size_t number_of_samples) {}
|
||||
void Toggle::set_output(bool enabled) {
|
||||
if(is_enabled_ == enabled) return;
|
||||
is_enabled_ = enabled;
|
||||
audio_queue_.defer([=] {
|
||||
audio_queue_.defer([this, enabled] {
|
||||
level_ = enabled ? volume_ : 0;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ class Toggle: public Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
void skip_samples(const std::size_t number_of_samples);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
void set_output(bool enabled);
|
||||
bool get_output();
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Apple {
|
||||
/*!
|
||||
Provides an emulation of the Apple Disk II.
|
||||
*/
|
||||
class DiskII final:
|
||||
class DiskII :
|
||||
public Storage::Disk::Drive::EventDelegate,
|
||||
public ClockingHint::Source,
|
||||
public ClockingHint::Observer {
|
||||
@@ -98,8 +98,8 @@ class DiskII final:
|
||||
void select_drive(int drive);
|
||||
|
||||
uint8_t trigger_address(int address, uint8_t value);
|
||||
void process_event(const Storage::Disk::Drive::Event &event) override;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) override;
|
||||
void process_event(const Storage::Disk::Drive::Event &event) final;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) final;
|
||||
|
||||
const Cycles::IntType clock_rate_ = 0;
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class IWM:
|
||||
|
||||
private:
|
||||
// Storage::Disk::Drive::EventDelegate.
|
||||
void process_event(const Storage::Disk::Drive::Event &event) override;
|
||||
void process_event(const Storage::Disk::Drive::Event &event) final;
|
||||
|
||||
const int clock_rate_;
|
||||
|
||||
@@ -91,7 +91,7 @@ class IWM:
|
||||
IWMDrive *drives_[2] = {nullptr, nullptr};
|
||||
bool drive_is_rotating_[2] = {false, false};
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final;
|
||||
|
||||
Cycles cycles_until_disable_;
|
||||
uint8_t write_handshake_ = 0x80;
|
||||
|
||||
@@ -32,14 +32,14 @@ class DoubleDensityDrive: public IWMDrive {
|
||||
*/
|
||||
void set_rotation_speed(float revolutions_per_minute);
|
||||
|
||||
void set_enabled(bool) override;
|
||||
void set_control_lines(int) override;
|
||||
bool read() override;
|
||||
|
||||
private:
|
||||
void set_enabled(bool) final;
|
||||
void set_control_lines(int) final;
|
||||
bool read() final;
|
||||
|
||||
// To receive the proper notifications from Storage::Disk::Drive.
|
||||
void did_step(Storage::Disk::HeadPosition to_position) override;
|
||||
void did_set_disk() override;
|
||||
void did_step(Storage::Disk::HeadPosition to_position) final;
|
||||
void did_set_disk() final;
|
||||
|
||||
const bool is_800k_;
|
||||
bool has_new_disk_ = false;
|
||||
|
||||
@@ -55,7 +55,7 @@ void SCC::write(uint16_t address, uint8_t value) {
|
||||
address &= 0xff;
|
||||
if(address < 0x80) ram_[address] = value;
|
||||
|
||||
task_queue_.defer([=] {
|
||||
task_queue_.defer([this, address, value] {
|
||||
// Check for a write into waveform memory.
|
||||
if(address < 0x80) {
|
||||
waves_[address >> 5].samples[address & 0x1f] = value;
|
||||
|
||||
@@ -32,6 +32,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
|
||||
/// As per ::SampleSource; provides audio output.
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
/// Writes to the SCC.
|
||||
void write(uint16_t address, uint8_t value);
|
||||
|
||||
@@ -32,6 +32,7 @@ class SN76489: public Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
bool is_zero_level();
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
int master_divider_ = 0;
|
||||
|
||||
@@ -12,61 +12,89 @@
|
||||
|
||||
using namespace Concurrency;
|
||||
|
||||
BestEffortUpdater::BestEffortUpdater() {
|
||||
// ATOMIC_FLAG_INIT isn't necessarily safe to use, so establish default state by other means.
|
||||
update_is_ongoing_.clear();
|
||||
}
|
||||
BestEffortUpdater::BestEffortUpdater() :
|
||||
update_thread_([this]() {
|
||||
this->update_loop();
|
||||
}) {}
|
||||
|
||||
BestEffortUpdater::~BestEffortUpdater() {
|
||||
// Don't allow further deconstruction until the task queue is stopped.
|
||||
// Sever the delegate now, as soon as possible, then wait for any
|
||||
// pending tasks to finish.
|
||||
set_delegate(nullptr);
|
||||
flush();
|
||||
|
||||
// Wind up the update thread.
|
||||
should_quit_ = true;
|
||||
update();
|
||||
update_thread_.join();
|
||||
}
|
||||
|
||||
void BestEffortUpdater::update() {
|
||||
// Perform an update only if one is not currently ongoing.
|
||||
if(!update_is_ongoing_.test_and_set()) {
|
||||
async_task_queue_.enqueue([this]() {
|
||||
// Get time now using the highest-resolution clock provided by the implementation, and determine
|
||||
// the duration since the last time this section was entered.
|
||||
const std::chrono::time_point<std::chrono::high_resolution_clock> now = std::chrono::high_resolution_clock::now();
|
||||
const auto elapsed = now - previous_time_point_;
|
||||
previous_time_point_ = now;
|
||||
void BestEffortUpdater::update(int flags) {
|
||||
// Bump the requested target time and set the update requested flag.
|
||||
{
|
||||
std::lock_guard<decltype(update_mutex_)> lock(update_mutex_);
|
||||
has_skipped_ = update_requested_;
|
||||
update_requested_ = true;
|
||||
flags_ |= flags;
|
||||
target_time_ = std::chrono::high_resolution_clock::now().time_since_epoch().count();
|
||||
}
|
||||
update_condition_.notify_one();
|
||||
}
|
||||
|
||||
if(has_previous_time_point_) {
|
||||
// If the duration is valid, convert it to integer cycles, maintaining a rolling error and call the delegate
|
||||
// if there is one. Proceed only if the number of cycles is positive, and cap it to the per-second maximum as
|
||||
// it's possible this is an adjustable clock so be ready to swallow unexpected adjustments.
|
||||
const int64_t integer_duration = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
|
||||
if(integer_duration > 0) {
|
||||
if(delegate_) {
|
||||
// Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
|
||||
// brief system interruption.
|
||||
const double duration = std::min(static_cast<double>(integer_duration) / 1e9, 0.2);
|
||||
delegate_->update(this, duration, has_skipped_);
|
||||
}
|
||||
has_skipped_ = false;
|
||||
}
|
||||
} else {
|
||||
has_previous_time_point_ = true;
|
||||
}
|
||||
void BestEffortUpdater::update_loop() {
|
||||
while(true) {
|
||||
std::unique_lock<decltype(update_mutex_)> lock(update_mutex_);
|
||||
is_updating_ = false;
|
||||
|
||||
// Allow furthers updates to occur.
|
||||
update_is_ongoing_.clear();
|
||||
});
|
||||
} else {
|
||||
async_task_queue_.enqueue([this]() {
|
||||
has_skipped_ = true;
|
||||
// Wait to be signalled.
|
||||
update_condition_.wait(lock, [this]() -> bool {
|
||||
return update_requested_;
|
||||
});
|
||||
|
||||
// Possibly this signalling really means 'quit'.
|
||||
if(should_quit_) return;
|
||||
|
||||
// Note update started, crib the target time.
|
||||
auto target_time = target_time_;
|
||||
update_requested_ = false;
|
||||
|
||||
// If this was actually the first update request, silently swallow it.
|
||||
if(!has_previous_time_point_) {
|
||||
has_previous_time_point_ = true;
|
||||
previous_time_point_ = target_time;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Release the lock on requesting new updates.
|
||||
is_updating_ = true;
|
||||
const int flags = flags_;
|
||||
flags_ = 0;
|
||||
lock.unlock();
|
||||
|
||||
// Invoke the delegate, if supplied, in order to run.
|
||||
const int64_t integer_duration = std::max(target_time - previous_time_point_, int64_t(0));
|
||||
const auto delegate = delegate_.load();
|
||||
if(delegate) {
|
||||
// Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
|
||||
// brief system interruption.
|
||||
const double duration = std::min(double(integer_duration) / 1e9, 0.2);
|
||||
const double elapsed_duration = delegate->update(this, duration, has_skipped_, flags);
|
||||
|
||||
previous_time_point_ += int64_t(elapsed_duration * 1e9);
|
||||
has_skipped_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BestEffortUpdater::flush() {
|
||||
async_task_queue_.flush();
|
||||
// Spin lock; this is allowed to be slow.
|
||||
while(true) {
|
||||
std::lock_guard<decltype(update_mutex_)> lock(update_mutex_);
|
||||
if(!is_updating_) return;
|
||||
}
|
||||
}
|
||||
|
||||
void BestEffortUpdater::set_delegate(Delegate *const delegate) {
|
||||
async_task_queue_.enqueue([this, delegate]() {
|
||||
delegate_ = delegate;
|
||||
});
|
||||
delegate_.store(delegate);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "AsyncTaskQueue.hpp"
|
||||
#include "../ClockReceiver/TimeTypes.hpp"
|
||||
|
||||
namespace Concurrency {
|
||||
@@ -31,7 +33,13 @@ class BestEffortUpdater {
|
||||
|
||||
/// A delegate receives timing cues.
|
||||
struct Delegate {
|
||||
virtual void update(BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update) = 0;
|
||||
/*!
|
||||
Instructs the delegate to run for at least @c duration, providing hints as to whether multiple updates were requested before the previous had completed
|
||||
(as @c did_skip_previous_update) and providing the union of any flags supplied to @c update.
|
||||
|
||||
@returns The amount of time actually run for.
|
||||
*/
|
||||
virtual Time::Seconds update(BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update, int flags) = 0;
|
||||
};
|
||||
|
||||
/// Sets the current delegate.
|
||||
@@ -41,20 +49,32 @@ class BestEffortUpdater {
|
||||
If the delegate is not currently in the process of an `update` call, calls it now to catch up to the current time.
|
||||
The call is asynchronous; this method will return immediately.
|
||||
*/
|
||||
void update();
|
||||
void update(int flags = 0);
|
||||
|
||||
/// Blocks until any ongoing update is complete.
|
||||
/// Blocks until any ongoing update is complete; may spin.
|
||||
void flush();
|
||||
|
||||
private:
|
||||
std::atomic_flag update_is_ongoing_;
|
||||
AsyncTaskQueue async_task_queue_;
|
||||
std::atomic<bool> should_quit_;
|
||||
std::atomic<bool> is_updating_;
|
||||
|
||||
std::chrono::time_point<std::chrono::high_resolution_clock> previous_time_point_;
|
||||
int64_t target_time_;
|
||||
int flags_ = 0;
|
||||
bool update_requested_;
|
||||
std::mutex update_mutex_;
|
||||
std::condition_variable update_condition_;
|
||||
|
||||
decltype(target_time_) previous_time_point_;
|
||||
bool has_previous_time_point_ = false;
|
||||
bool has_skipped_ = false;
|
||||
std::atomic<bool> has_skipped_ = false;
|
||||
|
||||
Delegate *delegate_ = nullptr;
|
||||
std::atomic<Delegate *>delegate_ = nullptr;
|
||||
|
||||
void update_loop();
|
||||
|
||||
// This is deliberately at the bottom, to ensure it constructs after the various
|
||||
// mutexs, conditions, etc, that it'll depend upon.
|
||||
std::thread update_thread_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -170,11 +170,11 @@ class ConcreteJoystick: public Joystick {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Input> &get_inputs() override final {
|
||||
std::vector<Input> &get_inputs() final {
|
||||
return inputs_;
|
||||
}
|
||||
|
||||
void set_input(const Input &input, bool is_active) override final {
|
||||
void set_input(const Input &input, bool is_active) final {
|
||||
// If this is a digital setting to a digital property, just pass it along.
|
||||
if(input.is_button() || stick_types_[input.info.control.index] == StickType::Digital) {
|
||||
did_set_input(input, is_active);
|
||||
@@ -193,7 +193,7 @@ class ConcreteJoystick: public Joystick {
|
||||
}
|
||||
}
|
||||
|
||||
void set_input(const Input &input, float value) override final {
|
||||
void set_input(const Input &input, float value) final {
|
||||
// If this is an analogue setting to an analogue property, just pass it along.
|
||||
if(!input.is_button() && stick_types_[input.info.control.index] == StickType::Analogue) {
|
||||
did_set_input(input, value);
|
||||
|
||||
@@ -19,14 +19,15 @@ Keyboard::Keyboard(const std::set<Key> &essential_modifiers) : essential_modifie
|
||||
Keyboard::Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers) :
|
||||
observed_keys_(observed_keys), essential_modifiers_(essential_modifiers), is_exclusive_(false) {}
|
||||
|
||||
void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
||||
bool Keyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
||||
std::size_t key_offset = static_cast<std::size_t>(key);
|
||||
if(key_offset >= key_states_.size()) {
|
||||
key_states_.resize(key_offset+1, false);
|
||||
}
|
||||
key_states_[key_offset] = is_pressed;
|
||||
|
||||
if(delegate_) delegate_->keyboard_did_change_key(this, key, is_pressed);
|
||||
if(delegate_) return delegate_->keyboard_did_change_key(this, key, is_pressed);
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::set<Inputs::Keyboard::Key> &Keyboard::get_essential_modifiers() {
|
||||
|
||||
@@ -35,7 +35,9 @@ class Keyboard {
|
||||
Keypad4, Keypad5, Keypad6, KeypadMinus,
|
||||
Keypad1, Keypad2, Keypad3, KeypadEnter,
|
||||
Keypad0, KeypadDecimalPoint, KeypadEquals,
|
||||
Help
|
||||
Help,
|
||||
|
||||
Max = Help
|
||||
};
|
||||
|
||||
/// Constructs a Keyboard that declares itself to observe all keys.
|
||||
@@ -45,7 +47,9 @@ class Keyboard {
|
||||
Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers);
|
||||
|
||||
// Host interface.
|
||||
virtual void set_key_pressed(Key key, char value, bool is_pressed);
|
||||
|
||||
/// @returns @c true if the key press affects the machine; @c false otherwise.
|
||||
virtual bool set_key_pressed(Key key, char value, bool is_pressed);
|
||||
virtual void reset_all_keys();
|
||||
|
||||
/// @returns a set of all Keys that this keyboard responds to.
|
||||
@@ -68,7 +72,7 @@ class Keyboard {
|
||||
|
||||
// Delegate interface.
|
||||
struct Delegate {
|
||||
virtual void keyboard_did_change_key(Keyboard *keyboard, Key key, bool is_pressed) = 0;
|
||||
virtual bool keyboard_did_change_key(Keyboard *keyboard, Key key, bool is_pressed) = 0;
|
||||
virtual void reset_all_keys(Keyboard *keyboard) = 0;
|
||||
};
|
||||
void set_delegate(Delegate *delegate);
|
||||
|
||||
@@ -34,24 +34,24 @@ class QuadratureMouse: public Mouse {
|
||||
/*
|
||||
Inputs, to satisfy the Mouse interface.
|
||||
*/
|
||||
void move(int x, int y) override {
|
||||
void move(int x, int y) final {
|
||||
// Accumulate all provided motion.
|
||||
axes_[0] += x;
|
||||
axes_[1] += y;
|
||||
}
|
||||
|
||||
int get_number_of_buttons() override {
|
||||
int get_number_of_buttons() final {
|
||||
return number_of_buttons_;
|
||||
}
|
||||
|
||||
void set_button_pressed(int index, bool is_pressed) override {
|
||||
void set_button_pressed(int index, bool is_pressed) final {
|
||||
if(is_pressed)
|
||||
button_flags_ |= (1 << index);
|
||||
else
|
||||
button_flags_ &= ~(1 << index);
|
||||
}
|
||||
|
||||
void reset_all_buttons() override {
|
||||
void reset_all_buttons() final {
|
||||
button_flags_ = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "../../Analyser/Static/AmstradCPC/Target.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -62,7 +63,7 @@ class InterruptTimer {
|
||||
inline void signal_hsync() {
|
||||
// Increment the timer and if it has hit 52 then reset it and
|
||||
// set the interrupt request line to true.
|
||||
timer_++;
|
||||
++timer_;
|
||||
if(timer_ == 52) {
|
||||
timer_ = 0;
|
||||
interrupt_request_ = true;
|
||||
@@ -72,7 +73,7 @@ class InterruptTimer {
|
||||
// further horizontal syncs the timer should either (i) set the interrupt
|
||||
// line, if bit 4 is clear; or (ii) reset the timer.
|
||||
if(reset_counter_) {
|
||||
reset_counter_--;
|
||||
--reset_counter_;
|
||||
if(!reset_counter_) {
|
||||
if(timer_ & 32) {
|
||||
interrupt_request_ = true;
|
||||
@@ -126,6 +127,9 @@ class AYDeferrer {
|
||||
/// Constructs a new AY instance and sets its clock rate.
|
||||
AYDeferrer() : ay_(GI::AY38910::Personality::AY38910, audio_queue_), speaker_(ay_) {
|
||||
speaker_.set_input_rate(1000000);
|
||||
// Per the CPC Wiki:
|
||||
// "A is output to the right, channel C is output left, and channel B is output to both left and right".
|
||||
ay_.set_output_mixing(0.0, 0.5, 1.0, 1.0, 0.5, 0.0);
|
||||
}
|
||||
|
||||
~AYDeferrer() {
|
||||
@@ -153,14 +157,14 @@ class AYDeferrer {
|
||||
}
|
||||
|
||||
/// @returns the AY itself.
|
||||
GI::AY38910::AY38910 &ay() {
|
||||
GI::AY38910::AY38910<true> &ay() {
|
||||
return ay_;
|
||||
}
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910 ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
|
||||
GI::AY38910::AY38910<true> ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910<true>> speaker_;
|
||||
HalfCycles cycles_since_update_;
|
||||
};
|
||||
|
||||
@@ -196,12 +200,12 @@ class CRTCBusHandler {
|
||||
cycles_into_hsync_ = 0;
|
||||
}
|
||||
|
||||
bool is_hsync = (cycles_into_hsync_ >= 2 && cycles_into_hsync_ < 6);
|
||||
bool is_colour_burst = (cycles_into_hsync_ >= 7 && cycles_into_hsync_ < 11);
|
||||
const bool is_hsync = (cycles_into_hsync_ >= 2 && cycles_into_hsync_ < 6);
|
||||
const bool is_colour_burst = (cycles_into_hsync_ >= 7 && cycles_into_hsync_ < 11);
|
||||
|
||||
// Sync is taken to override pixels, and is combined as a simple OR.
|
||||
bool is_sync = is_hsync || state.vsync;
|
||||
bool is_blank = !is_sync && state.hsync;
|
||||
const bool is_sync = is_hsync || state.vsync;
|
||||
const bool is_blank = !is_sync && state.hsync;
|
||||
|
||||
OutputMode output_mode;
|
||||
if(is_sync) {
|
||||
@@ -303,9 +307,16 @@ class CRTCBusHandler {
|
||||
visible early. The CPC uses changes in sync to clock the interrupt timer.
|
||||
*/
|
||||
void perform_bus_cycle_phase2(const Motorola::CRTC::BusState &state) {
|
||||
// check for a trailing CRTC hsync; if one occurred then that's the trigger potentially to change
|
||||
// modes, and should also be sent on to the interrupt timer
|
||||
// Notify a leading hsync edge to the interrupt timer.
|
||||
// Per Interrupts in the CPC: "to be confirmed: does gate array count positive or negative edge transitions of HSYNC signal?";
|
||||
// if you take it as given that display mode is latched as a result of hsync then Pipe Mania seems to imply that the count
|
||||
// occurs on a leading edge and the mode lock on a trailing.
|
||||
if(was_hsync_ && !state.hsync) {
|
||||
interrupt_timer_.signal_hsync();
|
||||
}
|
||||
|
||||
// Check for a trailing CRTC hsync; if one occurred then that's the trigger potentially to change modes.
|
||||
if(!was_hsync_ && state.hsync) {
|
||||
if(mode_ != next_mode_) {
|
||||
mode_ = next_mode_;
|
||||
switch(mode_) {
|
||||
@@ -316,8 +327,6 @@ class CRTCBusHandler {
|
||||
}
|
||||
build_mode_table();
|
||||
}
|
||||
|
||||
interrupt_timer_.signal_hsync();
|
||||
}
|
||||
|
||||
// check for a leading vsync; that also needs to be communicated to the interrupt timer
|
||||
@@ -335,6 +344,11 @@ class CRTCBusHandler {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// @returns The current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 4.0f;
|
||||
}
|
||||
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
@@ -365,7 +379,7 @@ class CRTCBusHandler {
|
||||
border_ = mapped_palette_value(colour);
|
||||
} else {
|
||||
palette_[pen_] = mapped_palette_value(colour);
|
||||
patch_mode_table(pen_);
|
||||
patch_mode_table(size_t(pen_));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,27 +398,38 @@ class CRTCBusHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#define Mode0Colour0(c) ((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2)
|
||||
#define Mode0Colour1(c) ((c & 0x40) >> 6) | ((c & 0x10) >> 2) | ((c & 0x04) >> 1) | ((c & 0x01) << 3)
|
||||
#define Mode0Colour0(c) (((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2))
|
||||
#define Mode0Colour1(c) (((c & 0x40) >> 6) | ((c & 0x10) >> 2) | ((c & 0x04) >> 1) | ((c & 0x01) << 3))
|
||||
|
||||
#define Mode1Colour0(c) ((c & 0x80) >> 7) | ((c & 0x08) >> 2)
|
||||
#define Mode1Colour1(c) ((c & 0x40) >> 6) | ((c & 0x04) >> 1)
|
||||
#define Mode1Colour2(c) ((c & 0x20) >> 5) | ((c & 0x02) >> 0)
|
||||
#define Mode1Colour3(c) ((c & 0x10) >> 4) | ((c & 0x01) << 1)
|
||||
#define Mode1Colour0(c) (((c & 0x80) >> 7) | ((c & 0x08) >> 2))
|
||||
#define Mode1Colour1(c) (((c & 0x40) >> 6) | ((c & 0x04) >> 1))
|
||||
#define Mode1Colour2(c) (((c & 0x20) >> 5) | ((c & 0x02) >> 0))
|
||||
#define Mode1Colour3(c) (((c & 0x10) >> 4) | ((c & 0x01) << 1))
|
||||
|
||||
#define Mode3Colour0(c) ((c & 0x80) >> 7) | ((c & 0x08) >> 2)
|
||||
#define Mode3Colour1(c) ((c & 0x40) >> 6) | ((c & 0x04) >> 1)
|
||||
#define Mode3Colour0(c) (((c & 0x80) >> 7) | ((c & 0x08) >> 2))
|
||||
#define Mode3Colour1(c) (((c & 0x40) >> 6) | ((c & 0x04) >> 1))
|
||||
|
||||
/*!
|
||||
Creates a lookup table from palette entry to list of affected entries in the value -> pixels lookup tables.
|
||||
*/
|
||||
void establish_palette_hits() {
|
||||
for(int c = 0; c < 256; c++) {
|
||||
for(size_t c = 0; c < 256; c++) {
|
||||
assert(Mode0Colour0(c) < mode0_palette_hits_.size());
|
||||
assert(Mode0Colour1(c) < mode0_palette_hits_.size());
|
||||
mode0_palette_hits_[Mode0Colour0(c)].push_back(uint8_t(c));
|
||||
mode0_palette_hits_[Mode0Colour1(c)].push_back(uint8_t(c));
|
||||
|
||||
assert(Mode1Colour0(c) < mode1_palette_hits_.size());
|
||||
assert(Mode1Colour1(c) < mode1_palette_hits_.size());
|
||||
assert(Mode1Colour2(c) < mode1_palette_hits_.size());
|
||||
assert(Mode1Colour3(c) < mode1_palette_hits_.size());
|
||||
mode1_palette_hits_[Mode1Colour0(c)].push_back(uint8_t(c));
|
||||
mode1_palette_hits_[Mode1Colour1(c)].push_back(uint8_t(c));
|
||||
mode1_palette_hits_[Mode1Colour2(c)].push_back(uint8_t(c));
|
||||
mode1_palette_hits_[Mode1Colour3(c)].push_back(uint8_t(c));
|
||||
|
||||
assert(Mode3Colour0(c) < mode3_palette_hits_.size());
|
||||
assert(Mode3Colour1(c) < mode3_palette_hits_.size());
|
||||
mode3_palette_hits_[Mode3Colour0(c)].push_back(uint8_t(c));
|
||||
mode3_palette_hits_[Mode3Colour1(c)].push_back(uint8_t(c));
|
||||
}
|
||||
@@ -414,8 +439,8 @@ class CRTCBusHandler {
|
||||
switch(mode_) {
|
||||
case 0:
|
||||
// Mode 0: abcdefgh -> [gcea] [hdfb]
|
||||
for(int c = 0; c < 256; c++) {
|
||||
// prepare mode 0
|
||||
for(size_t c = 0; c < 256; c++) {
|
||||
// Prepare mode 0.
|
||||
uint8_t *const mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]);
|
||||
mode0_pixels[0] = palette_[Mode0Colour0(c)];
|
||||
mode0_pixels[1] = palette_[Mode0Colour1(c)];
|
||||
@@ -423,8 +448,8 @@ class CRTCBusHandler {
|
||||
break;
|
||||
|
||||
case 1:
|
||||
for(int c = 0; c < 256; c++) {
|
||||
// prepare mode 1
|
||||
for(size_t c = 0; c < 256; c++) {
|
||||
// Prepare mode 1.
|
||||
uint8_t *const mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]);
|
||||
mode1_pixels[0] = palette_[Mode1Colour0(c)];
|
||||
mode1_pixels[1] = palette_[Mode1Colour1(c)];
|
||||
@@ -434,8 +459,8 @@ class CRTCBusHandler {
|
||||
break;
|
||||
|
||||
case 2:
|
||||
for(int c = 0; c < 256; c++) {
|
||||
// prepare mode 2
|
||||
for(size_t c = 0; c < 256; c++) {
|
||||
// Prepare mode 2.
|
||||
uint8_t *const mode2_pixels = reinterpret_cast<uint8_t *>(&mode2_output_[c]);
|
||||
mode2_pixels[0] = palette_[((c & 0x80) >> 7)];
|
||||
mode2_pixels[1] = palette_[((c & 0x40) >> 6)];
|
||||
@@ -449,8 +474,8 @@ class CRTCBusHandler {
|
||||
break;
|
||||
|
||||
case 3:
|
||||
for(int c = 0; c < 256; c++) {
|
||||
// prepare mode 3
|
||||
for(size_t c = 0; c < 256; c++) {
|
||||
// Prepare mode 3.
|
||||
uint8_t *const mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]);
|
||||
mode3_pixels[0] = palette_[Mode3Colour0(c)];
|
||||
mode3_pixels[1] = palette_[Mode3Colour1(c)];
|
||||
@@ -459,18 +484,20 @@ class CRTCBusHandler {
|
||||
}
|
||||
}
|
||||
|
||||
void patch_mode_table(int pen) {
|
||||
void patch_mode_table(size_t pen) {
|
||||
switch(mode_) {
|
||||
case 0: {
|
||||
for(uint8_t c : mode0_palette_hits_[pen]) {
|
||||
assert(c < mode0_output_.size());
|
||||
uint8_t *const mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]);
|
||||
mode0_pixels[0] = palette_[Mode0Colour0(c)];
|
||||
mode0_pixels[1] = palette_[Mode0Colour1(c)];
|
||||
}
|
||||
} break;
|
||||
case 1:
|
||||
if(pen > 3) return;
|
||||
if(pen >= mode1_palette_hits_.size()) return;
|
||||
for(uint8_t c : mode1_palette_hits_[pen]) {
|
||||
assert(c < mode1_output_.size());
|
||||
uint8_t *const mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]);
|
||||
mode1_pixels[0] = palette_[Mode1Colour0(c)];
|
||||
mode1_pixels[1] = palette_[Mode1Colour1(c)];
|
||||
@@ -485,9 +512,10 @@ class CRTCBusHandler {
|
||||
build_mode_table();
|
||||
break;
|
||||
case 3:
|
||||
if(pen > 3) return;
|
||||
if(pen >= mode3_palette_hits_.size()) return;
|
||||
// Same argument applies here as to case 1, as the unused bits aren't masked out.
|
||||
for(uint8_t c : mode3_palette_hits_[pen]) {
|
||||
assert(c < mode3_output_.size());
|
||||
uint8_t *const mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]);
|
||||
mode3_pixels[0] = palette_[Mode3Colour0(c)];
|
||||
mode3_pixels[1] = palette_[Mode3Colour1(c)];
|
||||
@@ -543,14 +571,14 @@ class CRTCBusHandler {
|
||||
int next_mode_ = 2, mode_ = 2;
|
||||
|
||||
int pixel_divider_ = 1;
|
||||
uint16_t mode0_output_[256];
|
||||
uint32_t mode1_output_[256];
|
||||
uint64_t mode2_output_[256];
|
||||
uint16_t mode3_output_[256];
|
||||
std::array<uint16_t, 256> mode0_output_;
|
||||
std::array<uint32_t, 256> mode1_output_;
|
||||
std::array<uint64_t, 256> mode2_output_;
|
||||
std::array<uint16_t, 256> mode3_output_;
|
||||
|
||||
std::vector<uint8_t> mode0_palette_hits_[16];
|
||||
std::vector<uint8_t> mode1_palette_hits_[4];
|
||||
std::vector<uint8_t> mode3_palette_hits_[4];
|
||||
std::array<std::vector<uint8_t>, 16> mode0_palette_hits_;
|
||||
std::array<std::vector<uint8_t>, 4> mode1_palette_hits_;
|
||||
std::array<std::vector<uint8_t>, 4> mode3_palette_hits_;
|
||||
|
||||
int pen_ = 0;
|
||||
uint8_t palette_[16];
|
||||
@@ -627,7 +655,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
||||
}),
|
||||
state_(state) {}
|
||||
|
||||
void did_set_input(const Input &input, bool is_active) override {
|
||||
void did_set_input(const Input &input, bool is_active) final {
|
||||
uint8_t mask = 0;
|
||||
switch(input.type) {
|
||||
default: return;
|
||||
@@ -656,17 +684,15 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
||||
class FDC: public Intel::i8272::i8272 {
|
||||
private:
|
||||
Intel::i8272::BusHandler bus_handler_;
|
||||
std::shared_ptr<Storage::Disk::Drive> drive_;
|
||||
|
||||
public:
|
||||
FDC() :
|
||||
i8272(bus_handler_, Cycles(8000000)),
|
||||
drive_(new Storage::Disk::Drive(8000000, 300, 1)) {
|
||||
set_drive(drive_);
|
||||
FDC() : i8272(bus_handler_, Cycles(8000000)) {
|
||||
emplace_drive(8000000, 300, 1);
|
||||
set_drive(1);
|
||||
}
|
||||
|
||||
void set_motor_on(bool on) {
|
||||
drive_->set_motor_on(on);
|
||||
get_drive().set_motor_on(on);
|
||||
}
|
||||
|
||||
void select_drive(int c) {
|
||||
@@ -674,11 +700,11 @@ class FDC: public Intel::i8272::i8272 {
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
|
||||
drive_->set_disk(disk);
|
||||
get_drive().set_disk(disk);
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) {
|
||||
drive_->set_activity_observer(observer, "Drive 1", true);
|
||||
get_drive().set_activity_observer(observer, "Drive 1", true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -760,7 +786,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public Utility::TypeRecipient,
|
||||
public Utility::TypeRecipient<CharacterMapper>,
|
||||
public CPU::Z80::BusHandler,
|
||||
public ClockingHint::Observer,
|
||||
public Configurable::Device,
|
||||
@@ -826,18 +852,18 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
for(std::size_t index = 0; index < roms.size(); ++index) {
|
||||
auto &data = roms[index];
|
||||
if(!data) throw ROMMachine::Error::MissingROMs;
|
||||
roms_[int(index)] = std::move(*data);
|
||||
roms_[int(index)].resize(16384);
|
||||
roms_[index] = std::move(*data);
|
||||
roms_[index].resize(16384);
|
||||
}
|
||||
|
||||
// Establish default memory map
|
||||
upper_rom_is_paged_ = true;
|
||||
upper_rom_ = ROMType::BASIC;
|
||||
|
||||
write_pointers_[0] = &ram_[0];
|
||||
write_pointers_[1] = &ram_[16384];
|
||||
write_pointers_[2] = &ram_[32768];
|
||||
write_pointers_[3] = &ram_[49152];
|
||||
write_pointers_[0] = &ram_[0x0000];
|
||||
write_pointers_[1] = &ram_[0x4000];
|
||||
write_pointers_[2] = &ram_[0x8000];
|
||||
write_pointers_[3] = &ram_[0xc000];
|
||||
|
||||
read_pointers_[0] = roms_[ROMType::OS].data();
|
||||
read_pointers_[1] = write_pointers_[1];
|
||||
@@ -863,7 +889,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
// will do as it's safe to conclude that nobody else has touched video RAM
|
||||
// during that whole window
|
||||
crtc_counter_ += cycle.length;
|
||||
Cycles crtc_cycles = crtc_counter_.divide_cycles(Cycles(4));
|
||||
const Cycles crtc_cycles = crtc_counter_.divide_cycles(Cycles(4));
|
||||
if(crtc_cycles > Cycles(0)) crtc_.run_for(crtc_cycles);
|
||||
|
||||
// Check whether that prompted a change in the interrupt line. If so then date
|
||||
@@ -986,6 +1012,9 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Check whether the interrupt signal has changed the other way.
|
||||
if(interrupt_timer_.request_has_changed()) z80_.set_interrupt_line(interrupt_timer_.get_request());
|
||||
|
||||
// This implementation doesn't use time-stuffing; once in-phase waits won't be longer
|
||||
// than a single cycle so there's no real performance benefit to trying to find the
|
||||
// next non-wait when a wait cycle comes in, and there'd be no benefit to reproducing
|
||||
@@ -1002,26 +1031,31 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
}
|
||||
|
||||
/// A CRTMachine function; sets the destination for video.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
crtc_bus_handler_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// A CRTMachine function; returns the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return crtc_bus_handler_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
/// A CRTMachine function; sets the output display type.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override final {
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
crtc_bus_handler_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/// @returns the speaker in use.
|
||||
Outputs::Speaker::Speaker *get_speaker() override final {
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return ay_.get_speaker();
|
||||
}
|
||||
|
||||
/// Wires virtual-dispatched CRTMachine run_for requests to the static Z80 method.
|
||||
void run_for(const Cycles cycles) override final {
|
||||
void run_for(const Cycles cycles) final {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) override final {
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
// If there are any tapes supplied, use the first of them.
|
||||
if(!media.tapes.empty()) {
|
||||
tape_player_.set_tape(media.tapes.front());
|
||||
@@ -1038,70 +1072,73 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
return !media.tapes.empty() || (!media.disks.empty() && has_fdc);
|
||||
}
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override final {
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
|
||||
fdc_is_sleeping_ = fdc_.preferred_clocking() == ClockingHint::Preference::None;
|
||||
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
|
||||
}
|
||||
|
||||
// MARK: - Keyboard
|
||||
void type_string(const std::string &string) override final {
|
||||
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper());
|
||||
Utility::TypeRecipient::add_typer(string, std::move(mapper));
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
|
||||
}
|
||||
|
||||
HalfCycles get_typer_delay() override final {
|
||||
return Cycles(4000000); // Wait 1 second before typing.
|
||||
bool can_type(char c) final {
|
||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
||||
}
|
||||
|
||||
HalfCycles get_typer_frequency() override final {
|
||||
return Cycles(160000); // Type one character per frame.
|
||||
HalfCycles get_typer_delay() final {
|
||||
return z80_.get_is_resetting() ? Cycles(3'400'000) : Cycles(0);
|
||||
}
|
||||
|
||||
HalfCycles get_typer_frequency() final {
|
||||
return Cycles(80'000); // Perform one key transition per frame.
|
||||
}
|
||||
|
||||
// See header; sets a key as either pressed or released.
|
||||
void set_key_state(uint16_t key, bool isPressed) override final {
|
||||
void set_key_state(uint16_t key, bool isPressed) final {
|
||||
key_state_.set_is_pressed(isPressed, key >> 4, key & 7);
|
||||
}
|
||||
|
||||
// See header; sets all keys to released.
|
||||
void clear_all_keys() override final {
|
||||
void clear_all_keys() final {
|
||||
key_state_.clear_all_keys();
|
||||
}
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
if constexpr (has_fdc) fdc_.set_activity_observer(observer);
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return AmstradCPC::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
Configurable::Display display;
|
||||
if(Configurable::get_display(selections_by_option, display)) {
|
||||
set_video_signal_configurable(display);
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
// MARK: - Joysticks
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return key_state_.get_joysticks();
|
||||
}
|
||||
|
||||
@@ -1126,8 +1163,8 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
case 3:
|
||||
// Perform RAM paging, if 128kb is permitted.
|
||||
if(has_128k_) {
|
||||
bool adjust_low_read_pointer = read_pointers_[0] == write_pointers_[0];
|
||||
bool adjust_high_read_pointer = read_pointers_[3] == write_pointers_[3];
|
||||
const bool adjust_low_read_pointer = read_pointers_[0] == write_pointers_[0];
|
||||
const bool adjust_high_read_pointer = read_pointers_[3] == write_pointers_[3];
|
||||
#define RAM_BANK(x) &ram_[x * 16384]
|
||||
#define RAM_CONFIG(a, b, c, d) write_pointers_[0] = RAM_BANK(a); write_pointers_[1] = RAM_BANK(b); write_pointers_[2] = RAM_BANK(c); write_pointers_[3] = RAM_BANK(d);
|
||||
switch(value & 7) {
|
||||
@@ -1179,8 +1216,6 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
HalfCycles crtc_counter_;
|
||||
HalfCycles half_cycles_since_ay_update_;
|
||||
|
||||
uint8_t ram_[128 * 1024];
|
||||
|
||||
bool fdc_is_sleeping_;
|
||||
bool tape_player_is_sleeping_;
|
||||
bool has_128k_;
|
||||
@@ -1198,6 +1233,9 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
|
||||
KeyboardState key_state_;
|
||||
AmstradCPC::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
bool has_run_ = false;
|
||||
uint8_t ram_[128 * 1024];
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ using namespace AmstradCPC;
|
||||
uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
#define BIND(source, dest) case Inputs::Keyboard::Key::source: return dest
|
||||
switch(key) {
|
||||
default: return KeyCopy;
|
||||
default: break;
|
||||
|
||||
BIND(BackTick, KeyCopy);
|
||||
|
||||
BIND(k0, Key0); BIND(k1, Key1); BIND(k2, Key2); BIND(k3, Key3); BIND(k4, Key4);
|
||||
BIND(k5, Key5); BIND(k6, Key6); BIND(k7, Key7); BIND(k8, Key8); BIND(k9, Key9);
|
||||
@@ -72,6 +74,7 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
BIND(KeypadDelete, KeyDelete);
|
||||
}
|
||||
#undef BIND
|
||||
return KeyboardMachine::MappedMachine::KeyNotMapped;
|
||||
}
|
||||
|
||||
uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
@@ -85,7 +88,7 @@ uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
/* ACK */ X, /* BEL */ X,
|
||||
/* BS */ KEYS(KeyDelete), /* HT */ X,
|
||||
/* LF */ KEYS(KeyReturn), /* VT */ X,
|
||||
/* FF */ X, /* CR */ X,
|
||||
/* FF */ X, /* CR */ KEYS(KeyReturn),
|
||||
/* SO */ X, /* SI */ X,
|
||||
/* DLE */ X, /* DC1 */ X,
|
||||
/* DC2 */ X, /* DC3 */ X,
|
||||
@@ -140,8 +143,8 @@ uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
/* t */ KEYS(KeyT), /* u */ KEYS(KeyU),
|
||||
/* v */ KEYS(KeyV), /* w */ KEYS(KeyW),
|
||||
/* x */ KEYS(KeyX), /* y */ KEYS(KeyY),
|
||||
/* z */ KEYS(KeyZ), /* { */ X,
|
||||
/* | */ SHIFT(KeyAt), /* } */ X,
|
||||
/* z */ KEYS(KeyZ), /* { */ SHIFT(KeyLeftSquareBracket),
|
||||
/* | */ SHIFT(KeyAt), /* } */ SHIFT(KeyRightSquareBracket),
|
||||
/* ~ */ X
|
||||
};
|
||||
#undef KEYS
|
||||
@@ -150,3 +153,7 @@ uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
|
||||
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
|
||||
}
|
||||
|
||||
bool CharacterMapper::needs_pause_after_key(uint16_t key) {
|
||||
return key != KeyControl && key != KeyShift;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,10 @@ struct KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
|
||||
};
|
||||
|
||||
struct CharacterMapper: public ::Utility::CharacterMapper {
|
||||
uint16_t *sequence_for_character(char character);
|
||||
uint16_t *sequence_for_character(char character) override;
|
||||
|
||||
bool needs_pause_after_reset_all_keys() override { return false; }
|
||||
bool needs_pause_after_key(uint16_t key) override;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
@@ -147,7 +147,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
card_became_just_in_time_ |= !is_every_cycle;
|
||||
}
|
||||
|
||||
void card_did_change_select_constraints(Apple::II::Card *card) override {
|
||||
void card_did_change_select_constraints(Apple::II::Card *card) final {
|
||||
pick_card_messaging_group(card);
|
||||
}
|
||||
|
||||
@@ -283,12 +283,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
Input(Input::Fire, 2),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, float value) override {
|
||||
void did_set_input(const Input &input, float value) final {
|
||||
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
|
||||
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
|
||||
}
|
||||
|
||||
void did_set_input(const Input &input, bool value) override {
|
||||
void did_set_input(const Input &input, bool value) final {
|
||||
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
|
||||
buttons[input.info.control.index] = value;
|
||||
}
|
||||
@@ -417,16 +417,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
@@ -799,26 +803,26 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
void run_for(const Cycles cycles) final {
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void reset_all_keys() override {
|
||||
void reset_all_keys() final {
|
||||
open_apple_is_pressed_ = closed_apple_is_pressed_ = key_is_down_ = false;
|
||||
}
|
||||
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) override {
|
||||
bool set_key_pressed(Key key, char value, bool is_pressed) final {
|
||||
switch(key) {
|
||||
default: break;
|
||||
case Key::F12:
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
return;
|
||||
return true;
|
||||
case Key::LeftOption:
|
||||
open_apple_is_pressed_ = is_pressed;
|
||||
return;
|
||||
return true;
|
||||
case Key::RightOption:
|
||||
closed_apple_is_pressed_ = is_pressed;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
@@ -829,7 +833,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::Backspace: value = 0x7f; break;
|
||||
default: return;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,40 +848,47 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
key_is_down_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Inputs::Keyboard &get_keyboard() override {
|
||||
Inputs::Keyboard &get_keyboard() final {
|
||||
return *this;
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) override {
|
||||
void type_string(const std::string &string) final {
|
||||
string_serialiser_ = std::make_unique<Utility::StringSerialiser>(string, true);
|
||||
}
|
||||
|
||||
bool can_type(char c) final {
|
||||
// Make an effort to type the entire printable ASCII range.
|
||||
return c >= 32 && c < 127;
|
||||
}
|
||||
|
||||
// MARK:: Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return Apple::II::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
Configurable::Display display;
|
||||
if(Configurable::get_display(selections_by_option, display)) {
|
||||
set_video_signal_configurable(display);
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
return get_accurate_selections();
|
||||
}
|
||||
|
||||
// MARK: MediaTarget
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if(!media.disks.empty()) {
|
||||
auto diskii = diskii_card();
|
||||
if(diskii) diskii->set_disk(media.disks[0], 0);
|
||||
@@ -886,14 +897,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: Activity::Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
for(const auto &card: cards_) {
|
||||
if(card) card->set_activity_observer(observer);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: JoystickMachine
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,16 +27,16 @@ class DiskIICard: public Card, public ClockingHint::Observer {
|
||||
public:
|
||||
DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector);
|
||||
|
||||
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) override;
|
||||
void run_for(Cycles cycles, int stretches) override;
|
||||
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final;
|
||||
void run_for(Cycles cycles, int stretches) final;
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) override;
|
||||
void set_activity_observer(Activity::Observer *observer) final;
|
||||
|
||||
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
|
||||
Storage::Disk::Drive &get_drive(int drive);
|
||||
|
||||
private:
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final;
|
||||
std::vector<uint8_t> boot_;
|
||||
Apple::DiskII diskii_;
|
||||
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
|
||||
@@ -47,6 +47,10 @@ void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus VideoBase::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 14.0f;
|
||||
}
|
||||
|
||||
void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
@@ -56,7 +60,7 @@ void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
*/
|
||||
void VideoBase::set_alternative_character_set(bool alternative_character_set) {
|
||||
set_alternative_character_set_ = alternative_character_set;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
deferrer_.defer(Cycles(2), [this, alternative_character_set] {
|
||||
alternative_character_set_ = alternative_character_set;
|
||||
if(alternative_character_set) {
|
||||
character_zones[1].address_mask = 0xff;
|
||||
@@ -74,7 +78,7 @@ bool VideoBase::get_alternative_character_set() {
|
||||
|
||||
void VideoBase::set_80_columns(bool columns_80) {
|
||||
set_columns_80_ = columns_80;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
deferrer_.defer(Cycles(2), [this, columns_80] {
|
||||
columns_80_ = columns_80;
|
||||
});
|
||||
}
|
||||
@@ -101,7 +105,7 @@ bool VideoBase::get_page2() {
|
||||
|
||||
void VideoBase::set_text(bool text) {
|
||||
set_text_ = text;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
deferrer_.defer(Cycles(2), [this, text] {
|
||||
text_ = text;
|
||||
});
|
||||
}
|
||||
@@ -112,7 +116,7 @@ bool VideoBase::get_text() {
|
||||
|
||||
void VideoBase::set_mixed(bool mixed) {
|
||||
set_mixed_ = mixed;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
deferrer_.defer(Cycles(2), [this, mixed] {
|
||||
mixed_ = mixed;
|
||||
});
|
||||
}
|
||||
@@ -123,7 +127,7 @@ bool VideoBase::get_mixed() {
|
||||
|
||||
void VideoBase::set_high_resolution(bool high_resolution) {
|
||||
set_high_resolution_ = high_resolution;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
deferrer_.defer(Cycles(2), [this, high_resolution] {
|
||||
high_resolution_ = high_resolution;
|
||||
});
|
||||
}
|
||||
@@ -134,7 +138,7 @@ bool VideoBase::get_high_resolution() {
|
||||
|
||||
void VideoBase::set_annunciator_3(bool annunciator_3) {
|
||||
set_annunciator_3_ = annunciator_3;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
deferrer_.defer(Cycles(2), [this, annunciator_3] {
|
||||
annunciator_3_ = annunciator_3;
|
||||
high_resolution_mask_ = annunciator_3_ ? 0x7f : 0xff;
|
||||
});
|
||||
|
||||
@@ -40,6 +40,9 @@ class VideoBase {
|
||||
/// Sets the scan target.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
@@ -252,14 +255,14 @@ class VideoBase {
|
||||
void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
|
||||
|
||||
// Maintain a DeferredQueue for delayed mode switches.
|
||||
DeferredQueue<Cycles> deferrer_;
|
||||
DeferredQueuePerformer<Cycles> deferrer_;
|
||||
};
|
||||
|
||||
template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
public:
|
||||
/// Constructs an instance of the video feed; a CRT is also created.
|
||||
Video(BusHandler &bus_handler) :
|
||||
VideoBase(is_iie, [=] (Cycles cycles) { advance(cycles); }),
|
||||
VideoBase(is_iie, [this] (Cycles cycles) { advance(cycles); }),
|
||||
bus_handler_(bus_handler) {}
|
||||
|
||||
/*!
|
||||
|
||||
@@ -35,7 +35,7 @@ void Audio::set_volume(int volume) {
|
||||
posted_volume_ = volume;
|
||||
|
||||
// Post the volume change as a deferred event.
|
||||
task_queue_.defer([=] () {
|
||||
task_queue_.defer([this, volume] () {
|
||||
volume_ = volume;
|
||||
set_volume_multiplier();
|
||||
});
|
||||
@@ -47,7 +47,7 @@ void Audio::set_enabled(bool on) {
|
||||
posted_enable_mask_ = int(on);
|
||||
|
||||
// Post the enabled mask change as a deferred event.
|
||||
task_queue_.defer([=] () {
|
||||
task_queue_.defer([this, on] () {
|
||||
enabled_mask_ = int(on);
|
||||
set_volume_multiplier();
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ class Audio: public ::Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
bool is_zero_level();
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
constexpr static bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
|
||||
@@ -123,10 +123,10 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s);
|
||||
} break;
|
||||
}
|
||||
ram_mask_ = (ram_size >> 1) - 1;
|
||||
rom_mask_ = (rom_size >> 1) - 1;
|
||||
ram_.resize(ram_size >> 1);
|
||||
video_.set_ram(ram_.data(), ram_mask_);
|
||||
ram_mask_ = ram_size - 1;
|
||||
rom_mask_ = rom_size - 1;
|
||||
ram_.resize(ram_size);
|
||||
video_.set_ram(reinterpret_cast<uint16_t *>(ram_.data()), ram_mask_ >> 1);
|
||||
|
||||
// Grab a copy of the ROM and convert it into big-endian data.
|
||||
const auto roms = rom_fetcher(rom_descriptions);
|
||||
@@ -171,15 +171,19 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
audio_.queue.flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &audio_.speaker;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
void run_for(const Cycles cycles) final {
|
||||
mc68000_.run_for(cycles);
|
||||
}
|
||||
|
||||
@@ -192,11 +196,11 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
// A null cycle leaves nothing else to do.
|
||||
if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return HalfCycles(0);
|
||||
|
||||
// Grab the value on the address bus, at word precision.
|
||||
uint32_t word_address = cycle.active_operation_word_address();
|
||||
// Grab the address.
|
||||
auto address = cycle.host_endian_byte_address();
|
||||
|
||||
// Everything above E0 0000 is signalled as being on the peripheral bus.
|
||||
mc68000_.set_is_peripheral_address(word_address >= 0x700000);
|
||||
mc68000_.set_is_peripheral_address(address >= 0xe0'0000);
|
||||
|
||||
// All code below deals only with reads and writes — cycles in which a
|
||||
// data select is active. So quit now if this is not the active part of
|
||||
@@ -209,9 +213,9 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
if(!cycle.data_select_active() || (cycle.operation & Microcycle::InterruptAcknowledge)) return HalfCycles(0);
|
||||
|
||||
// Grab the word-precision address being accessed.
|
||||
uint16_t *memory_base = nullptr;
|
||||
uint8_t *memory_base = nullptr;
|
||||
HalfCycles delay;
|
||||
switch(memory_map_[word_address >> 16]) {
|
||||
switch(memory_map_[address >> 17]) {
|
||||
default: assert(false);
|
||||
|
||||
case BusDevice::Unassigned:
|
||||
@@ -222,7 +226,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
if(*cycle.address & 1) {
|
||||
fill_unmapped(cycle);
|
||||
} else {
|
||||
const int register_address = word_address >> 8;
|
||||
const int register_address = address >> 9;
|
||||
|
||||
// VIA accesses are via address 0xefe1fe + register*512,
|
||||
// which at word precision is 0x77f0ff + register*256.
|
||||
@@ -246,7 +250,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
case BusDevice::IWM: {
|
||||
if(*cycle.address & 1) {
|
||||
const int register_address = word_address >> 8;
|
||||
const int register_address = address >> 9;
|
||||
|
||||
// The IWM; this is a purely polled device, so can be run on demand.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
@@ -262,8 +266,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
} return delay;
|
||||
|
||||
case BusDevice::SCSI: {
|
||||
const int register_address = word_address >> 3;
|
||||
const bool dma_acknowledge = word_address & 0x100;
|
||||
const int register_address = address >> 4;
|
||||
const bool dma_acknowledge = address & 0x200;
|
||||
|
||||
// Even accesses = read; odd = write.
|
||||
if(*cycle.address & 1) {
|
||||
@@ -304,7 +308,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
cycle.value->halves.low = 0xff;
|
||||
}
|
||||
} else {
|
||||
const auto read = scc_.read(int(word_address));
|
||||
const auto read = scc_.read(int(address >> 1));
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = read;
|
||||
}
|
||||
@@ -319,10 +323,10 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
} else {
|
||||
if(*cycle.address & 1) {
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
scc_.write(int(word_address), 0xff);
|
||||
scc_.write(int(address >> 1), 0xff);
|
||||
cycle.value->halves.low = 0xff;
|
||||
} else {
|
||||
scc_.write(int(word_address), cycle.value->halves.low);
|
||||
scc_.write(int(address >> 1), cycle.value->halves.low);
|
||||
}
|
||||
} else {
|
||||
fill_unmapped(cycle);
|
||||
@@ -334,13 +338,13 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
// This is coupled with the Macintosh implementation of video; the magic
|
||||
// constant should probably be factored into the Video class.
|
||||
// It embodies knowledge of the fact that video (and audio) will always
|
||||
// be fetched from the final $d900 bytes (i.e. $6c80 words) of memory.
|
||||
// be fetched from the final $d900 bytes of memory.
|
||||
// (And that ram_mask_ = ram size - 1).
|
||||
if(word_address > ram_mask_ - 0x6c80)
|
||||
if(address > ram_mask_ - 0xd900)
|
||||
update_video();
|
||||
|
||||
memory_base = ram_.data();
|
||||
word_address &= ram_mask_;
|
||||
address &= ram_mask_;
|
||||
|
||||
// Apply a delay due to video contention if applicable; scheme applied:
|
||||
// only every other access slot is available during the period of video
|
||||
@@ -355,7 +359,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
case BusDevice::ROM: {
|
||||
if(!(cycle.operation & Microcycle::Read)) return delay;
|
||||
memory_base = rom_;
|
||||
word_address &= rom_mask_;
|
||||
address &= rom_mask_;
|
||||
} break;
|
||||
}
|
||||
|
||||
@@ -365,19 +369,16 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
break;
|
||||
|
||||
case Microcycle::SelectWord | Microcycle::Read:
|
||||
cycle.value->full = memory_base[word_address];
|
||||
cycle.value->full = *reinterpret_cast<uint16_t *>(&memory_base[address]);
|
||||
break;
|
||||
case Microcycle::SelectByte | Microcycle::Read:
|
||||
cycle.value->halves.low = uint8_t(memory_base[word_address] >> cycle.byte_shift());
|
||||
cycle.value->halves.low = memory_base[address];
|
||||
break;
|
||||
case Microcycle::SelectWord:
|
||||
memory_base[word_address] = cycle.value->full;
|
||||
*reinterpret_cast<uint16_t *>(&memory_base[address]) = cycle.value->full;
|
||||
break;
|
||||
case Microcycle::SelectByte:
|
||||
memory_base[word_address] = uint16_t(
|
||||
(cycle.value->halves.low << cycle.byte_shift()) |
|
||||
(memory_base[word_address] & cycle.untouched_byte_mask())
|
||||
);
|
||||
memory_base[address] = cycle.value->halves.low;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -395,7 +396,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
via_.flush();
|
||||
audio_.queue.perform();
|
||||
|
||||
// Experimental?
|
||||
// This avoids deferring IWM costs indefinitely, until
|
||||
// they become artbitrarily large.
|
||||
iwm_.flush();
|
||||
}
|
||||
|
||||
@@ -454,7 +456,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
video_.set_use_alternate_buffers(use_alternate_screen_buffer, use_alternate_audio_buffer);
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if(media.disks.empty() && media.mass_storage_devices.empty())
|
||||
return false;
|
||||
|
||||
@@ -468,7 +470,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
drives_[0].set_disk(media.disks[0]);
|
||||
}
|
||||
|
||||
// TODO: allow this only at machine startup.
|
||||
// TODO: allow this only at machine startup?
|
||||
if(!media.mass_storage_devices.empty()) {
|
||||
const auto volume = dynamic_cast<Storage::MassStorage::Encodings::Macintosh::Volume *>(media.mass_storage_devices.front().get());
|
||||
if(volume) {
|
||||
@@ -482,11 +484,11 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
// MARK: Keyboard input.
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool is_pressed) override {
|
||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||||
keyboard_.enqueue_key_state(key, is_pressed);
|
||||
}
|
||||
|
||||
@@ -494,7 +496,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
// MARK: Interrupt updates.
|
||||
|
||||
void did_change_interrupt_status(Zilog::SCC::z8530 *sender, bool new_status) override {
|
||||
void did_change_interrupt_status(Zilog::SCC::z8530 *sender, bool new_status) final {
|
||||
update_interrupt_input();
|
||||
}
|
||||
|
||||
@@ -511,7 +513,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
}
|
||||
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
iwm_->set_activity_observer(observer);
|
||||
|
||||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
@@ -520,40 +522,42 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return Apple::Macintosh::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
bool quick_boot;
|
||||
if(Configurable::get_quick_boot(selections_by_option, quick_boot)) {
|
||||
if(quick_boot) {
|
||||
// Cf. Big Mess o' Wires' disassembly of the Mac Plus ROM, and the
|
||||
// test at $E00. TODO: adapt as(/if?) necessary for other Macs.
|
||||
ram_[0x02ae >> 1] = 0x40;
|
||||
ram_[0x02b0 >> 1] = 0x00;
|
||||
ram_[0x02ae] = 0x40;
|
||||
ram_[0x02af] = 0x00;
|
||||
ram_[0x02b0] = 0x00;
|
||||
ram_[0x02b1] = 0x00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_boot_selection(selection_set, false);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_boot_selection(selection_set, true);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
private:
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override {
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
|
||||
scsi_bus_is_clocked_ = scsi_bus_.preferred_clocking() != ClockingHint::Preference::None;
|
||||
}
|
||||
|
||||
void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) override {
|
||||
void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) final {
|
||||
iwm_.flush();
|
||||
drives_[0].set_rotation_speed(speed);
|
||||
drives_[1].set_rotation_speed(speed);
|
||||
@@ -565,11 +569,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
forceinline void fill_unmapped(const Microcycle &cycle) {
|
||||
if(!(cycle.operation & Microcycle::Read)) return;
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
cycle.value->full = 0xffff;
|
||||
} else {
|
||||
cycle.value->halves.low = 0xff;
|
||||
}
|
||||
cycle.set_value16(0xffff);
|
||||
}
|
||||
|
||||
/// Advances all non-CPU components by @c duration half cycles.
|
||||
@@ -652,7 +652,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
time_until_video_event_ = video_.get_next_sequence_point();
|
||||
}
|
||||
|
||||
Inputs::Mouse &get_mouse() override {
|
||||
Inputs::Mouse &get_mouse() final {
|
||||
return mouse_;
|
||||
}
|
||||
|
||||
@@ -852,8 +852,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
uint32_t ram_mask_ = 0;
|
||||
uint32_t rom_mask_ = 0;
|
||||
uint16_t rom_[64*1024]; // i.e. up to 128kb in size.
|
||||
std::vector<uint16_t> ram_;
|
||||
uint8_t rom_[128*1024];
|
||||
std::vector<uint8_t> ram_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ using namespace Apple::Macintosh;
|
||||
Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) :
|
||||
audio_(audio),
|
||||
drive_speed_accumulator_(drive_speed_accumulator),
|
||||
crt_(704, 1, 370, Outputs::Display::ColourSpace::YIQ, 1, 1, 6, false, Outputs::Display::InputDataType::Luminance1) {
|
||||
crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) {
|
||||
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
|
||||
@@ -37,6 +37,10 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 2.0f;
|
||||
}
|
||||
|
||||
void Video::run_for(HalfCycles duration) {
|
||||
// Determine the current video and audio bases. These values don't appear to be latched, they apply immediately.
|
||||
const size_t video_base = (use_alternate_screen_buffer_ ? (0xffff2700 >> 1) : (0xffffa700 >> 1)) & ram_mask_;
|
||||
|
||||
@@ -42,6 +42,9 @@ class Video {
|
||||
*/
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*!
|
||||
Produces the next @c duration period of pixels.
|
||||
*/
|
||||
|
||||
@@ -48,7 +48,7 @@ class Joystick: public Inputs::ConcreteJoystick {
|
||||
}),
|
||||
bus_(bus), shift_(shift), fire_tia_input_(fire_tia_input) {}
|
||||
|
||||
void did_set_input(const Input &digital_input, bool is_active) override {
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
switch(digital_input.type) {
|
||||
case Input::Up: bus_->mos6532_.update_port_input(0, 0x10 >> shift_, is_active); break;
|
||||
case Input::Down: bus_->mos6532_.update_port_input(0, 0x20 >> shift_, is_active); break;
|
||||
@@ -77,12 +77,9 @@ using Target = Analyser::Static::Atari2600::Target;
|
||||
class ConcreteMachine:
|
||||
public Machine,
|
||||
public CRTMachine::Machine,
|
||||
public JoystickMachine::Machine,
|
||||
public Outputs::CRT::Delegate {
|
||||
public JoystickMachine::Machine {
|
||||
public:
|
||||
ConcreteMachine(const Target &target) {
|
||||
set_clock_rate(NTSC_clock_rate);
|
||||
|
||||
ConcreteMachine(const Target &target) : frequency_mismatch_warner_(*this) {
|
||||
const std::vector<uint8_t> &rom = target.media.cartridges.front()->get_segments().front().data;
|
||||
|
||||
using PagingModel = Target::PagingModel;
|
||||
@@ -122,13 +119,15 @@ class ConcreteMachine:
|
||||
|
||||
joysticks_.emplace_back(new Joystick(bus_.get(), 0, 0));
|
||||
joysticks_.emplace_back(new Joystick(bus_.get(), 4, 1));
|
||||
|
||||
set_is_ntsc(is_ntsc_);
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
void set_switch_is_enabled(Atari2600Switch input, bool state) override {
|
||||
void set_switch_is_enabled(Atari2600Switch input, bool state) final {
|
||||
switch(input) {
|
||||
case Atari2600SwitchReset: bus_->mos6532_.update_port_input(1, 0x01, state); break;
|
||||
case Atari2600SwitchSelect: bus_->mos6532_.update_port_input(1, 0x02, state); break;
|
||||
@@ -138,7 +137,7 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
bool get_switch_is_enabled(Atari2600Switch input) override {
|
||||
bool get_switch_is_enabled(Atari2600Switch input) final {
|
||||
uint8_t port_input = bus_->mos6532_.get_port_input(1);
|
||||
switch(input) {
|
||||
case Atari2600SwitchReset: return !!(port_input & 0x01);
|
||||
@@ -150,83 +149,62 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
void set_reset_switch(bool state) override {
|
||||
void set_reset_switch(bool state) final {
|
||||
bus_->set_reset_line(state);
|
||||
}
|
||||
|
||||
// to satisfy CRTMachine::Machine
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
bus_->speaker_.set_input_rate(static_cast<float>(get_clock_rate() / static_cast<double>(CPUTicksPerAudioTick)));
|
||||
bus_->tia_.set_crt_delegate(this);
|
||||
bus_->tia_.set_crt_delegate(&frequency_mismatch_warner_);
|
||||
bus_->tia_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return bus_->tia_.get_scaled_scan_status() / 3.0f;
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &bus_->speaker_;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
void run_for(const Cycles cycles) final {
|
||||
bus_->run_for(cycles);
|
||||
bus_->apply_confidence(confidence_counter_);
|
||||
}
|
||||
|
||||
// to satisfy Outputs::CRT::Delegate
|
||||
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, int number_of_frames, int number_of_unexpected_vertical_syncs) override {
|
||||
const std::size_t number_of_frame_records = sizeof(frame_records_) / sizeof(frame_records_[0]);
|
||||
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_frames = number_of_frames;
|
||||
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
|
||||
frame_record_pointer_ ++;
|
||||
|
||||
if(frame_record_pointer_ >= 6) {
|
||||
int total_number_of_frames = 0;
|
||||
int total_number_of_unexpected_vertical_syncs = 0;
|
||||
for(std::size_t c = 0; c < number_of_frame_records; c++) {
|
||||
total_number_of_frames += frame_records_[c].number_of_frames;
|
||||
total_number_of_unexpected_vertical_syncs += frame_records_[c].number_of_unexpected_vertical_syncs;
|
||||
}
|
||||
|
||||
if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) {
|
||||
for(std::size_t c = 0; c < number_of_frame_records; c++) {
|
||||
frame_records_[c].number_of_frames = 0;
|
||||
frame_records_[c].number_of_unexpected_vertical_syncs = 0;
|
||||
}
|
||||
is_ntsc_ ^= true;
|
||||
|
||||
double clock_rate;
|
||||
if(is_ntsc_) {
|
||||
clock_rate = NTSC_clock_rate;
|
||||
bus_->tia_.set_output_mode(TIA::OutputMode::NTSC);
|
||||
} else {
|
||||
clock_rate = PAL_clock_rate;
|
||||
bus_->tia_.set_output_mode(TIA::OutputMode::PAL);
|
||||
}
|
||||
|
||||
bus_->speaker_.set_input_rate(static_cast<float>(clock_rate / static_cast<double>(CPUTicksPerAudioTick)));
|
||||
bus_->speaker_.set_high_frequency_cutoff(static_cast<float>(clock_rate / (static_cast<double>(CPUTicksPerAudioTick) * 2.0)));
|
||||
set_clock_rate(clock_rate);
|
||||
}
|
||||
}
|
||||
void flush() {
|
||||
bus_->flush();
|
||||
}
|
||||
|
||||
float get_confidence() override {
|
||||
void register_crt_frequency_mismatch() {
|
||||
is_ntsc_ ^= true;
|
||||
set_is_ntsc(is_ntsc_);
|
||||
}
|
||||
|
||||
float get_confidence() final {
|
||||
return confidence_counter_.get_confidence();
|
||||
}
|
||||
|
||||
private:
|
||||
// the bus
|
||||
// The bus.
|
||||
std::unique_ptr<Bus> bus_;
|
||||
|
||||
// output frame rate tracker
|
||||
struct FrameRecord {
|
||||
int number_of_frames = 0;
|
||||
int number_of_unexpected_vertical_syncs = 0;
|
||||
} frame_records_[4];
|
||||
unsigned int frame_record_pointer_ = 0;
|
||||
// Output frame rate tracker.
|
||||
Outputs::CRT::CRTFrequencyMismatchWarner<ConcreteMachine> frequency_mismatch_warner_;
|
||||
bool is_ntsc_ = true;
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
|
||||
// a confidence counter
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
|
||||
void set_is_ntsc(bool is_ntsc) {
|
||||
bus_->tia_.set_output_mode(is_ntsc ? TIA::OutputMode::NTSC : TIA::OutputMode::PAL);
|
||||
const double clock_rate = is_ntsc ? NTSC_clock_rate : PAL_clock_rate;
|
||||
bus_->speaker_.set_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick));
|
||||
bus_->speaker_.set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2));
|
||||
set_clock_rate(clock_rate);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class Bus {
|
||||
virtual void run_for(const Cycles cycles) = 0;
|
||||
virtual void apply_confidence(Analyser::Dynamic::ConfidenceCounter &confidence_counter) = 0;
|
||||
virtual void set_reset_line(bool state) = 0;
|
||||
virtual void flush() = 0;
|
||||
|
||||
// the RIOT, TIA and speaker
|
||||
PIA mos6532_;
|
||||
|
||||
@@ -39,7 +39,7 @@ template<class T> class Cartridge:
|
||||
// consider doing something less fragile.
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) {
|
||||
void run_for(const Cycles cycles) override {
|
||||
// Horizontal counter resets are used as a proxy for whether this really is an Atari 2600
|
||||
// title. Random memory accesses are likely to trigger random counter resets.
|
||||
horizontal_counter_resets_ = 0;
|
||||
@@ -50,13 +50,13 @@ template<class T> class Cartridge:
|
||||
/*!
|
||||
Adjusts @c confidence_counter according to the results of the most recent run_for.
|
||||
*/
|
||||
void apply_confidence(Analyser::Dynamic::ConfidenceCounter &confidence_counter) {
|
||||
void apply_confidence(Analyser::Dynamic::ConfidenceCounter &confidence_counter) override {
|
||||
if(cycle_count_.as_integral() < 200) return;
|
||||
if(horizontal_counter_resets_ > 10)
|
||||
confidence_counter.add_miss();
|
||||
}
|
||||
|
||||
void set_reset_line(bool state) { m6502_.set_reset_line(state); }
|
||||
void set_reset_line(bool state) override { m6502_.set_reset_line(state); }
|
||||
|
||||
// to satisfy CPU::MOS6502::Processor
|
||||
Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
|
||||
@@ -197,7 +197,7 @@ template<class T> class Cartridge:
|
||||
return Cycles(cycles_run_for / 3);
|
||||
}
|
||||
|
||||
void flush() {
|
||||
void flush() override {
|
||||
update_audio();
|
||||
update_video();
|
||||
audio_queue_.perform();
|
||||
|
||||
@@ -142,6 +142,10 @@ void TIA::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus TIA::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 2.0f;
|
||||
}
|
||||
|
||||
void TIA::run_for(const Cycles cycles) {
|
||||
int number_of_cycles = int(cycles.as_integral());
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ class TIA {
|
||||
|
||||
void set_crt_delegate(Outputs::CRT::Delegate *);
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
private:
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
@@ -18,21 +18,21 @@ Atari2600::TIASound::TIASound(Concurrency::DeferringAsyncTaskQueue &audio_queue)
|
||||
{}
|
||||
|
||||
void Atari2600::TIASound::set_volume(int channel, uint8_t volume) {
|
||||
audio_queue_.defer([=]() {
|
||||
volume_[channel] = volume & 0xf;
|
||||
audio_queue_.defer([target = &volume_[channel], volume]() {
|
||||
*target = volume & 0xf;
|
||||
});
|
||||
}
|
||||
|
||||
void Atari2600::TIASound::set_divider(int channel, uint8_t divider) {
|
||||
audio_queue_.defer([=]() {
|
||||
audio_queue_.defer([this, channel, divider]() {
|
||||
divider_[channel] = divider & 0x1f;
|
||||
divider_counter_[channel] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
void Atari2600::TIASound::set_control(int channel, uint8_t control) {
|
||||
audio_queue_.defer([=]() {
|
||||
control_[channel] = control & 0xf;
|
||||
audio_queue_.defer([target = &control_[channel], control]() {
|
||||
*target = control & 0xf;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class TIASound: public Outputs::Speaker::SampleSource {
|
||||
// To satisfy ::SampleSource.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace ST {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour | Configurable::QuickLoadTape)
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,8 +101,10 @@ class ConcreteMachine:
|
||||
|
||||
const bool is_early_tos = true;
|
||||
if(is_early_tos) {
|
||||
rom_start_ = 0xfc0000;
|
||||
for(c = 0xfc; c < 0xff; ++c) memory_map_[c] = BusDevice::ROM;
|
||||
} else {
|
||||
rom_start_ = 0xe00000;
|
||||
for(c = 0xe0; c < 0xe4; ++c) memory_map_[c] = BusDevice::ROM;
|
||||
}
|
||||
|
||||
@@ -139,6 +141,10 @@ class ConcreteMachine:
|
||||
video_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_->set_display_type(display_type);
|
||||
}
|
||||
@@ -241,7 +247,7 @@ class ConcreteMachine:
|
||||
|
||||
case BusDevice::ROM:
|
||||
memory = rom_.data();
|
||||
address %= rom_.size();
|
||||
address -= rom_start_;
|
||||
break;
|
||||
|
||||
case BusDevice::Floating:
|
||||
@@ -435,7 +441,8 @@ class ConcreteMachine:
|
||||
// Advance the relevant counters.
|
||||
cycles_since_audio_update_ += length;
|
||||
mfp_ += length;
|
||||
dma_ += length;
|
||||
if(dma_clocking_preference_ != ClockingHint::Preference::None)
|
||||
dma_ += length;
|
||||
keyboard_acia_ += length;
|
||||
midi_acia_ += length;
|
||||
bus_phase_ += length;
|
||||
@@ -456,7 +463,7 @@ class ConcreteMachine:
|
||||
mfp_.flush();
|
||||
}
|
||||
|
||||
if(dma_is_realtime_) {
|
||||
if(dma_clocking_preference_ == ClockingHint::Preference::RealTime) {
|
||||
dma_.flush();
|
||||
}
|
||||
|
||||
@@ -465,6 +472,7 @@ class ConcreteMachine:
|
||||
length -= cycles_until_video_event_;
|
||||
video_ += cycles_until_video_event_;
|
||||
cycles_until_video_event_ = video_->get_next_sequence_point();
|
||||
assert(cycles_until_video_event_ > HalfCycles(0));
|
||||
|
||||
mfp_->set_timer_event_input(1, video_->display_enabled());
|
||||
update_interrupt_input();
|
||||
@@ -489,8 +497,8 @@ class ConcreteMachine:
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, 16> midi_acia_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910 ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910<false>> speaker_;
|
||||
HalfCycles cycles_since_audio_update_;
|
||||
|
||||
JustInTimeActor<DMAController> dma_;
|
||||
@@ -500,6 +508,7 @@ class ConcreteMachine:
|
||||
|
||||
std::vector<uint8_t> ram_;
|
||||
std::vector<uint8_t> rom_;
|
||||
uint32_t rom_start_ = 0;
|
||||
|
||||
enum class BusDevice {
|
||||
/// A mostly RAM page is one that returns ROM for the first 8 bytes, RAM elsewhere.
|
||||
@@ -523,7 +532,7 @@ class ConcreteMachine:
|
||||
bool may_defer_acias_ = true;
|
||||
bool keyboard_needs_clock_ = false;
|
||||
bool mfp_is_realtime_ = false;
|
||||
bool dma_is_realtime_ = false;
|
||||
ClockingHint::Preference dma_clocking_preference_ = ClockingHint::Preference::None;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
|
||||
// This is being called by one of the components; avoid any time flushing here as that's
|
||||
// already dealt with (and, just to be absolutely sure, to avoid recursive mania).
|
||||
@@ -532,7 +541,7 @@ class ConcreteMachine:
|
||||
(midi_acia_.last_valid()->preferred_clocking() != ClockingHint::Preference::RealTime);
|
||||
keyboard_needs_clock_ = ikbd_.preferred_clocking() != ClockingHint::Preference::None;
|
||||
mfp_is_realtime_ = mfp_.last_valid()->preferred_clocking() == ClockingHint::Preference::RealTime;
|
||||
dma_is_realtime_ = dma_.last_valid()->preferred_clocking() == ClockingHint::Preference::RealTime;
|
||||
dma_clocking_preference_ = dma_.last_valid()->preferred_clocking();
|
||||
}
|
||||
|
||||
// MARK: - GPIP input.
|
||||
@@ -563,7 +572,7 @@ class ConcreteMachine:
|
||||
GPIP 0: centronics busy
|
||||
*/
|
||||
mfp_->set_port_input(
|
||||
0x80 | // b7: Monochrome monitor detect (1 = is monochrome).
|
||||
0x80 | // b7: Monochrome monitor detect (0 = is monochrome).
|
||||
0x40 | // b6: RS-232 ring indicator.
|
||||
(dma_->get_interrupt_line() ? 0x00 : 0x20) | // b5: FD/HS interrupt (0 = interrupt requested).
|
||||
((keyboard_acia_->get_interrupt_line() || midi_acia_->get_interrupt_line()) ? 0x00 : 0x10) | // b4: Keyboard/MIDI interrupt (0 = interrupt requested).
|
||||
@@ -658,7 +667,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
dma_->set_activity_observer(observer);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ void DMAController::set_floppy_drive_selection(bool drive1, bool drive2, bool si
|
||||
}
|
||||
|
||||
void DMAController::set_floppy_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
fdc_.drives_[drive]->set_disk(disk);
|
||||
fdc_.set_disk(disk, drive);
|
||||
}
|
||||
|
||||
void DMAController::run_for(HalfCycles duration) {
|
||||
@@ -256,6 +256,5 @@ ClockingHint::Preference DMAController::preferred_clocking() {
|
||||
}
|
||||
|
||||
void DMAController::set_activity_observer(Activity::Observer *observer) {
|
||||
fdc_.drives_[0]->set_activity_observer(observer, "Internal", true);
|
||||
fdc_.drives_[1]->set_activity_observer(observer, "External", true);
|
||||
fdc_.set_activity_observer(observer);
|
||||
}
|
||||
|
||||
@@ -56,30 +56,36 @@ class DMAController: public WD::WD1770::Delegate, public ClockingHint::Source, p
|
||||
HalfCycles running_time_;
|
||||
struct WD1772: public WD::WD1770 {
|
||||
WD1772(): WD::WD1770(WD::WD1770::P1772) {
|
||||
drives_.emplace_back(new Storage::Disk::Drive(8000000, 300, 2));
|
||||
drives_.emplace_back(new Storage::Disk::Drive(8000000, 300, 2));
|
||||
set_drive(drives_[0]);
|
||||
emplace_drives(2, 8000000, 300, 2);
|
||||
set_is_double_density(true); // TODO: is this selectable on the ST?
|
||||
}
|
||||
|
||||
void set_motor_on(bool motor_on) final {
|
||||
drives_[0]->set_motor_on(motor_on);
|
||||
drives_[1]->set_motor_on(motor_on);
|
||||
for_all_drives([motor_on] (Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_motor_on(motor_on);
|
||||
});
|
||||
}
|
||||
|
||||
void set_floppy_drive_selection(bool drive1, bool drive2, bool side2) {
|
||||
// TODO: handle no drives and/or both drives selected.
|
||||
if(drive1) {
|
||||
set_drive(drives_[0]);
|
||||
} else {
|
||||
set_drive(drives_[1]);
|
||||
}
|
||||
set_drive(
|
||||
(drive1 ? 1 : 0) |
|
||||
(drive2 ? 2 : 0)
|
||||
);
|
||||
|
||||
drives_[0]->set_head(side2);
|
||||
drives_[1]->set_head(side2);
|
||||
for_all_drives([side2] (Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_head(side2);
|
||||
});
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) {
|
||||
get_drive(0).set_activity_observer(observer, "Internal", true);
|
||||
get_drive(1).set_activity_observer(observer, "External", true);
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
get_drive(drive).set_disk(disk);
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<Storage::Disk::Drive>> drives_;
|
||||
} fdc_;
|
||||
|
||||
void wd1770_did_change_output(WD::WD1770 *) final;
|
||||
|
||||
@@ -162,7 +162,7 @@ class IntelligentKeyboard:
|
||||
Input(Input::Fire, 0),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, bool is_active) override {
|
||||
void did_set_input(const Input &input, bool is_active) final {
|
||||
uint8_t mask = 0;
|
||||
switch(input.type) {
|
||||
default: return;
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#define CYCLE(x) ((x) * 2)
|
||||
|
||||
using namespace Atari::ST;
|
||||
|
||||
namespace {
|
||||
@@ -29,7 +31,7 @@ const struct VerticalParams {
|
||||
} vertical_params[3] = {
|
||||
{63, 263, 313}, // 47 rather than 63 on early machines.
|
||||
{34, 234, 263},
|
||||
{1, 401, 500} // 72 Hz mode: who knows?
|
||||
{34, 434, 500} // Guesswork: (i) nobody ever recommends 72Hz mode for opening the top border, so it's likely to be the same as another mode; (ii) being the same as PAL feels too late.
|
||||
};
|
||||
|
||||
/// @returns The correct @c VerticalParams for output at @c frequency.
|
||||
@@ -37,8 +39,6 @@ const VerticalParams &vertical_parameters(Video::FieldFrequency frequency) {
|
||||
return vertical_params[int(frequency)];
|
||||
}
|
||||
|
||||
|
||||
#define CYCLE(x) ((x) * 2)
|
||||
/*!
|
||||
Defines the horizontal counts at which mode-specific events will occur:
|
||||
horizontal enable being set and being reset, blank being set and reset, and the
|
||||
@@ -57,13 +57,23 @@ const struct HorizontalParams {
|
||||
const int set_blank;
|
||||
const int reset_blank;
|
||||
|
||||
const int length;
|
||||
const int vertical_decision;
|
||||
|
||||
LineLength length;
|
||||
} horizontal_params[3] = {
|
||||
{CYCLE(56), CYCLE(376), CYCLE(450), CYCLE(28), CYCLE(512)},
|
||||
{CYCLE(52), CYCLE(372), CYCLE(450), CYCLE(24), CYCLE(508)},
|
||||
{CYCLE(4), CYCLE(164), CYCLE(999), CYCLE(999), CYCLE(224)} // 72Hz mode doesn't set or reset blank.
|
||||
{CYCLE(56), CYCLE(376), CYCLE(450), CYCLE(28), CYCLE(502), { CYCLE(512), CYCLE(464), CYCLE(504) }},
|
||||
{CYCLE(52), CYCLE(372), CYCLE(450), CYCLE(24), CYCLE(502), { CYCLE(508), CYCLE(460), CYCLE(500) }},
|
||||
{CYCLE(4), CYCLE(164), CYCLE(999), CYCLE(999), CYCLE(214), { CYCLE(224), CYCLE(194), CYCLE(212) }}
|
||||
// 72Hz mode doesn't set or reset blank.
|
||||
};
|
||||
|
||||
// Re: 'vertical_decision':
|
||||
// This is cycle 502 if in 50 or 60 Hz mode; in 70 Hz mode I've put it on cycle 214
|
||||
// in order to be analogous to 50 and 60 Hz mode. I have no idea where it should
|
||||
// actually go.
|
||||
//
|
||||
// Ditto the horizontal sync timings for 72Hz are plucked out of thin air.
|
||||
|
||||
const HorizontalParams &horizontal_parameters(Video::FieldFrequency frequency) {
|
||||
return horizontal_params[int(frequency)];
|
||||
}
|
||||
@@ -79,10 +89,10 @@ struct Checker {
|
||||
assert(horizontal.reset_blank < horizontal.set_enable);
|
||||
assert(horizontal.set_enable < horizontal.reset_enable);
|
||||
assert(horizontal.reset_enable < horizontal.set_blank);
|
||||
assert(horizontal.set_blank+50 < horizontal.length);
|
||||
assert(horizontal.set_blank+50 < horizontal.length.length);
|
||||
} else {
|
||||
assert(horizontal.set_enable < horizontal.reset_enable);
|
||||
assert(horizontal.set_enable+50 <horizontal.length);
|
||||
assert(horizontal.set_enable+50 <horizontal.length.length);
|
||||
}
|
||||
|
||||
// Expected vertical order of events: reset blank, enable display, disable display, enable blank (at least 50 before end of line), end of line
|
||||
@@ -97,12 +107,13 @@ struct Checker {
|
||||
const int de_delay_period = CYCLE(28); // Amount of time after DE that observed DE changes. NB: HACK HERE. This currently incorporates the MFP recognition delay. MUST FIX.
|
||||
const int vsync_x_position = CYCLE(56); // Horizontal cycle on which vertical sync changes happen.
|
||||
|
||||
const int hsync_start = CYCLE(48); // Cycles before end of line when hsync starts.
|
||||
const int hsync_end = CYCLE(8); // Cycles before end of line when hsync ends.
|
||||
const int line_length_latch_position = CYCLE(54);
|
||||
|
||||
const int hsync_delay_period = hsync_end; // Signal hsync at the end of the line.
|
||||
const int hsync_delay_period = CYCLE(8); // Signal hsync at the end of the line.
|
||||
const int vsync_delay_period = hsync_delay_period; // Signal vsync with the same delay as hsync.
|
||||
|
||||
const int load_delay_period = CYCLE(4); // Amount of time after DE that observed DE changes. NB: HACK HERE. This currently incorporates the MFP recognition delay. MUST FIX.
|
||||
|
||||
// "VSYNC starts 104 cycles after the start of the previous line's HSYNC, so that's 4 cycles before DE would be activated. ";
|
||||
// that's an inconsistent statement since it would imply VSYNC at +54, which is 2 cycles before DE in 60Hz mode and 6 before
|
||||
// in 50Hz mode. I've gone with 56, to be four cycles ahead of DE in 50Hz mode.
|
||||
@@ -110,13 +121,13 @@ const int vsync_delay_period = hsync_delay_period; // Signal vsync with the same
|
||||
}
|
||||
|
||||
Video::Video() :
|
||||
deferrer_([=] (HalfCycles duration) { advance(duration); }),
|
||||
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
|
||||
crt_(2048, 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
|
||||
// crt_(896, 1, 500, 5, Outputs::Display::InputDataType::Red4Green4Blue4),
|
||||
video_stream_(crt_, palette_) {
|
||||
|
||||
// Show a total of 260 lines; a little short for PAL but a compromise between that and the ST's
|
||||
// usual output height of 200 lines.
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(33, 260, 220, 850, 4.0f / 3.0f));
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(33, 260, 440, 1700, 4.0f / 3.0f));
|
||||
}
|
||||
|
||||
void Video::set_ram(uint16_t *ram, size_t size) {
|
||||
@@ -127,52 +138,41 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 4.0f;
|
||||
}
|
||||
|
||||
void Video::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
void Video::run_for(HalfCycles duration) {
|
||||
deferrer_.run_for(duration);
|
||||
}
|
||||
|
||||
void Video::advance(HalfCycles duration) {
|
||||
const auto horizontal_timings = horizontal_parameters(field_frequency_);
|
||||
const auto vertical_timings = vertical_parameters(field_frequency_);
|
||||
int integer_duration = int(duration.as_integral());
|
||||
|
||||
// Effect any changes in visible state out here; they're not relevant in the inner loop.
|
||||
if(!pending_events_.empty()) {
|
||||
auto erase_iterator = pending_events_.begin();
|
||||
int duration_remaining = integer_duration;
|
||||
while(erase_iterator != pending_events_.end()) {
|
||||
erase_iterator->delay -= duration_remaining;
|
||||
if(erase_iterator->delay <= 0) {
|
||||
duration_remaining = -erase_iterator->delay;
|
||||
erase_iterator->apply(public_state_);
|
||||
++erase_iterator;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(erase_iterator != pending_events_.begin()) {
|
||||
pending_events_.erase(pending_events_.begin(), erase_iterator);
|
||||
}
|
||||
}
|
||||
assert(integer_duration >= 0);
|
||||
|
||||
while(integer_duration) {
|
||||
const auto horizontal_timings = horizontal_parameters(field_frequency_);
|
||||
const auto vertical_timings = vertical_parameters(field_frequency_);
|
||||
|
||||
// Determine time to next event; this'll either be one of the ones informally scheduled in here,
|
||||
// or something from the deferral queue.
|
||||
|
||||
// Seed next event to end of line.
|
||||
int next_event = line_length_;
|
||||
int next_event = line_length_.length;
|
||||
|
||||
const int next_deferred_event = deferrer_.time_until_next_action().as<int>();
|
||||
if(next_deferred_event >= 0)
|
||||
next_event = std::min(next_event, next_deferred_event + x_);
|
||||
|
||||
// Check the explicitly-placed events.
|
||||
if(horizontal_timings.reset_blank > x_) next_event = std::min(next_event, horizontal_timings.reset_blank);
|
||||
if(horizontal_timings.set_blank > x_) next_event = std::min(next_event, horizontal_timings.set_blank);
|
||||
if(horizontal_timings.reset_enable > x_) next_event = std::min(next_event, horizontal_timings.reset_enable);
|
||||
if(horizontal_timings.set_enable > x_) next_event = std::min(next_event, horizontal_timings.set_enable);
|
||||
if(next_load_toggle_ > x_) next_event = std::min(next_event, next_load_toggle_);
|
||||
|
||||
// Check for events that are relative to existing latched state.
|
||||
if(line_length_ - hsync_start > x_) next_event = std::min(next_event, line_length_ - hsync_start);
|
||||
if(line_length_ - hsync_end > x_) next_event = std::min(next_event, line_length_ - hsync_end);
|
||||
if(line_length_.hsync_start > x_) next_event = std::min(next_event, line_length_.hsync_start);
|
||||
if(line_length_.hsync_end > x_) next_event = std::min(next_event, line_length_.hsync_end);
|
||||
|
||||
// Also, a vertical sync event might intercede.
|
||||
if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && x_ < vsync_x_position && next_event >= vsync_x_position) {
|
||||
@@ -185,6 +185,8 @@ void Video::advance(HalfCycles duration) {
|
||||
const bool hsync = horizontal_.sync;
|
||||
const bool vsync = vertical_.sync;
|
||||
|
||||
assert(run_length > 0);
|
||||
|
||||
// Ensure proper fetching irrespective of the output.
|
||||
if(load_) {
|
||||
const int since_load = x_ - load_base_;
|
||||
@@ -192,8 +194,8 @@ void Video::advance(HalfCycles duration) {
|
||||
// There will be pixels this line, subject to the shifter pipeline.
|
||||
// Divide into 8-[half-]cycle windows; at the start of each window fetch a word,
|
||||
// and during the rest of the window, shift out.
|
||||
int start_column = since_load >> 3;
|
||||
const int end_column = (since_load + run_length) >> 3;
|
||||
int start_column = (since_load - 1) >> 3;
|
||||
const int end_column = (since_load + run_length - 1) >> 3;
|
||||
|
||||
while(start_column != end_column) {
|
||||
data_latch_[data_latch_position_] = ram_[current_address_ & 262143];
|
||||
@@ -210,13 +212,16 @@ void Video::advance(HalfCycles duration) {
|
||||
} else if(!load_) {
|
||||
video_stream_.output(run_length, VideoStream::OutputMode::Pixels);
|
||||
} else {
|
||||
const int since_load = x_ - load_base_;
|
||||
const int start = x_ - load_base_;
|
||||
const int end = start + run_length;
|
||||
|
||||
// There will be pixels this line, subject to the shifter pipeline.
|
||||
// Divide into 8-[half-]cycle windows; at the start of each window fetch a word,
|
||||
// and during the rest of the window, shift out.
|
||||
int start_column = since_load >> 3;
|
||||
const int end_column = (since_load + run_length) >> 3;
|
||||
int start_column = start >> 3;
|
||||
const int end_column = end >> 3;
|
||||
const int start_offset = start & 7;
|
||||
const int end_offset = end & 7;
|
||||
|
||||
// Rules obeyed below:
|
||||
//
|
||||
@@ -225,35 +230,40 @@ void Video::advance(HalfCycles duration) {
|
||||
// was reloaded by the fetch depends on the FIFO.
|
||||
|
||||
if(start_column == end_column) {
|
||||
if(!start_offset) {
|
||||
push_latched_data();
|
||||
}
|
||||
video_stream_.output(run_length, VideoStream::OutputMode::Pixels);
|
||||
} else {
|
||||
// Continue the current column if partway across.
|
||||
if(since_load&7) {
|
||||
if(start_offset) {
|
||||
// If at least one column boundary is crossed, complete this column.
|
||||
video_stream_.output(8 - (since_load & 7), VideoStream::OutputMode::Pixels);
|
||||
video_stream_.output(8 - start_offset, VideoStream::OutputMode::Pixels);
|
||||
++start_column; // This starts a new column, so latch a new word.
|
||||
push_latched_data();
|
||||
}
|
||||
|
||||
// Run for all columns that have their starts in this time period.
|
||||
int complete_columns = end_column - start_column;
|
||||
while(complete_columns--) {
|
||||
video_stream_.output(8, VideoStream::OutputMode::Pixels);
|
||||
push_latched_data();
|
||||
video_stream_.output(8, VideoStream::OutputMode::Pixels);
|
||||
}
|
||||
|
||||
// Output the start of the next column, if necessary.
|
||||
if((since_load + run_length) & 7) {
|
||||
video_stream_.output((since_load + run_length) & 7, VideoStream::OutputMode::Pixels);
|
||||
if(end_offset) {
|
||||
push_latched_data();
|
||||
video_stream_.output(end_offset, VideoStream::OutputMode::Pixels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for whether line length should have been latched during this run.
|
||||
if(x_ <= CYCLE(54) && (x_ + run_length) > CYCLE(54)) line_length_ = horizontal_timings.length;
|
||||
if(x_ < line_length_latch_position && (x_ + run_length) >= line_length_latch_position) {
|
||||
line_length_ = horizontal_timings.length;
|
||||
}
|
||||
|
||||
// Make a decision about vertical state on cycle 502.
|
||||
if(x_ <= CYCLE(502) && (x_ + run_length) > CYCLE(502)) {
|
||||
// Make a decision about vertical state on the appropriate cycle.
|
||||
if(x_ < horizontal_timings.vertical_decision && (x_ + run_length) >= horizontal_timings.vertical_decision) {
|
||||
next_y_ = y_ + 1;
|
||||
next_vertical_ = vertical_;
|
||||
next_vertical_.sync_schedule = VerticalState::SyncSchedule::None;
|
||||
@@ -278,23 +288,17 @@ void Video::advance(HalfCycles duration) {
|
||||
|
||||
// Apply the next event.
|
||||
x_ += run_length;
|
||||
assert(integer_duration >= run_length);
|
||||
integer_duration -= run_length;
|
||||
deferrer_.advance(HalfCycles(run_length));
|
||||
|
||||
// Check horizontal events; the first six are guaranteed to occur separately.
|
||||
if(horizontal_timings.reset_blank == x_) horizontal_.blank = false;
|
||||
else if(horizontal_timings.set_blank == x_) horizontal_.blank = true;
|
||||
else if(horizontal_timings.reset_enable == x_) horizontal_.enable = false;
|
||||
else if(horizontal_timings.set_enable == x_) horizontal_.enable = true;
|
||||
else if(line_length_ - hsync_start == x_) { horizontal_.sync = true; horizontal_.enable = false; }
|
||||
else if(line_length_ - hsync_end == x_) horizontal_.sync = false;
|
||||
|
||||
// next_load_toggle_ is less predictable; test separately because it may coincide
|
||||
// with one of the above tests.
|
||||
if(next_load_toggle_ == x_) {
|
||||
next_load_toggle_ = -1;
|
||||
load_ ^= true;
|
||||
load_base_ = x_;
|
||||
}
|
||||
else if(line_length_.hsync_start == x_) { horizontal_.sync = true; horizontal_.enable = false; }
|
||||
else if(line_length_.hsync_end == x_) horizontal_.sync = false;
|
||||
|
||||
// Check vertical events.
|
||||
if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && x_ == vsync_x_position) {
|
||||
@@ -306,7 +310,7 @@ void Video::advance(HalfCycles duration) {
|
||||
|
||||
// Check whether the terminating event was end-of-line; if so then advance
|
||||
// the vertical bits of state.
|
||||
if(x_ == line_length_) {
|
||||
if(x_ == line_length_.length) {
|
||||
x_ = 0;
|
||||
vertical_ = next_vertical_;
|
||||
y_ = next_y_;
|
||||
@@ -329,21 +333,30 @@ void Video::advance(HalfCycles duration) {
|
||||
// Chuck any deferred output changes into the queue.
|
||||
const bool next_display_enable = vertical_.enable && horizontal_.enable;
|
||||
if(display_enable != next_display_enable) {
|
||||
// Schedule change in outwardly-visible DE line.
|
||||
add_event(de_delay_period - integer_duration, next_display_enable ? Event::Type::SetDisplayEnable : Event::Type::ResetDisplayEnable);
|
||||
// Schedule change in load line.
|
||||
deferrer_.defer(load_delay_period, [this, next_display_enable] {
|
||||
this->load_ = next_display_enable;
|
||||
this->load_base_ = this->x_;
|
||||
});
|
||||
|
||||
// Schedule change in inwardly-visible effect.
|
||||
next_load_toggle_ = x_ + 8; // 4 cycles = 8 half-cycles
|
||||
// Schedule change in outwardly-visible DE line.
|
||||
deferrer_.defer(de_delay_period, [this, next_display_enable] {
|
||||
this->public_state_.display_enable = next_display_enable;
|
||||
});
|
||||
}
|
||||
|
||||
if(horizontal_.sync != hsync) {
|
||||
// Schedule change in outwardly-visible hsync line.
|
||||
add_event(hsync_delay_period - integer_duration, horizontal_.sync ? Event::Type::SetHsync : Event::Type::ResetHsync);
|
||||
deferrer_.defer(hsync_delay_period, [this, next_horizontal_sync = horizontal_.sync] {
|
||||
this->public_state_.hsync = next_horizontal_sync;
|
||||
});
|
||||
}
|
||||
|
||||
if(vertical_.sync != vsync) {
|
||||
// Schedule change in outwardly-visible hsync line.
|
||||
add_event(vsync_delay_period - integer_duration, vertical_.sync ? Event::Type::SetVsync : Event::Type::ResetVsync);
|
||||
deferrer_.defer(vsync_delay_period, [this, next_vertical_sync = vertical_.sync] {
|
||||
this->public_state_.vsync = next_vertical_sync;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,11 +409,12 @@ HalfCycles Video::get_next_sequence_point() {
|
||||
|
||||
const auto horizontal_timings = horizontal_parameters(field_frequency_);
|
||||
|
||||
int event_time = line_length_; // Worst case: report end of line.
|
||||
int event_time = line_length_.length; // Worst case: report end of line.
|
||||
|
||||
// If any events are pending, give the first of those the chance to be next.
|
||||
if(!pending_events_.empty()) {
|
||||
event_time = std::min(event_time, x_ + pending_events_.front().delay);
|
||||
const auto next_deferred_item = deferrer_.time_until_next_action();
|
||||
if(next_deferred_item != HalfCycles(-1)) {
|
||||
event_time = std::min(event_time, x_ + next_deferred_item.as<int>());
|
||||
}
|
||||
|
||||
// If this is a vertically-enabled line, check for the display enable boundaries, + the standard delay.
|
||||
@@ -419,11 +433,17 @@ HalfCycles Video::get_next_sequence_point() {
|
||||
}
|
||||
|
||||
// Test for beginning and end of horizontal sync.
|
||||
if(x_ < line_length_ - hsync_start + hsync_delay_period) {
|
||||
event_time = std::min(line_length_ - hsync_start + hsync_delay_period, event_time);
|
||||
if(x_ < line_length_.hsync_start + hsync_delay_period) {
|
||||
event_time = std::min(line_length_.hsync_start + hsync_delay_period, event_time);
|
||||
}
|
||||
if(x_ < line_length_.hsync_end + hsync_delay_period) {
|
||||
event_time = std::min(line_length_.hsync_end + hsync_delay_period, event_time);
|
||||
}
|
||||
|
||||
// Also factor in the line length latching time.
|
||||
if(x_ < line_length_latch_position) {
|
||||
event_time = std::min(line_length_latch_position, event_time);
|
||||
}
|
||||
/* Hereby assumed: hsync end will be communicated at end of line: */
|
||||
static_assert(hsync_end == hsync_delay_period);
|
||||
|
||||
// It wasn't any of those, just supply end of line. That's when the static_assert above assumes a visible hsync transition.
|
||||
return HalfCycles(event_time - x_);
|
||||
@@ -465,7 +485,7 @@ void Video::write(int address, uint16_t value) {
|
||||
// Sync mode and pixel mode.
|
||||
case 0x05:
|
||||
// Writes to sync mode have a one-cycle delay in effect.
|
||||
deferrer_.defer(HalfCycles(2), [=] {
|
||||
deferrer_.defer(HalfCycles(2), [this, value] {
|
||||
sync_mode_ = value;
|
||||
update_output_mode();
|
||||
});
|
||||
@@ -555,12 +575,12 @@ void Video::VideoStream::generate(int duration, OutputMode mode, bool is_termina
|
||||
if(mode != OutputMode::Pixels) {
|
||||
switch(mode) {
|
||||
default:
|
||||
case OutputMode::Sync: crt_.output_sync(duration_); break;
|
||||
case OutputMode::Blank: crt_.output_blank(duration_); break;
|
||||
case OutputMode::ColourBurst: crt_.output_default_colour_burst(duration_); break;
|
||||
case OutputMode::Sync: crt_.output_sync(duration_*2); break;
|
||||
case OutputMode::Blank: crt_.output_blank(duration_*2); break;
|
||||
case OutputMode::ColourBurst: crt_.output_default_colour_burst(duration_*2); break;
|
||||
}
|
||||
|
||||
// Reseed duration
|
||||
// Reseed duration.
|
||||
duration_ = duration;
|
||||
|
||||
// The shifter should keep running, so throw away the proper amount of content.
|
||||
@@ -610,7 +630,7 @@ void Video::VideoStream::flush_border() {
|
||||
// Output colour 0 for the entirety of duration_ (or black, if this is 1bpp mode).
|
||||
uint16_t *const colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = (bpp_ != OutputBpp::One) ? palette_[0] : 0;
|
||||
crt_.output_level(duration_);
|
||||
crt_.output_level(duration_*2);
|
||||
|
||||
duration_ = 0;
|
||||
}
|
||||
@@ -711,7 +731,7 @@ void Video::VideoStream::output_pixels(int duration) {
|
||||
}
|
||||
|
||||
// Check whether the limit has been reached.
|
||||
if(pixel_pointer_ == allocation_size) {
|
||||
if(pixel_pointer_ >= allocation_size - 32) {
|
||||
flush_pixels();
|
||||
}
|
||||
}
|
||||
@@ -721,12 +741,12 @@ void Video::VideoStream::output_pixels(int duration) {
|
||||
if(pixels) {
|
||||
int leftover_duration = pixels;
|
||||
switch(bpp_) {
|
||||
case OutputBpp::One: leftover_duration >>= 1; break;
|
||||
default: break;
|
||||
case OutputBpp::Four: leftover_duration <<= 1; break;
|
||||
default: leftover_duration >>= 1; break;
|
||||
case OutputBpp::Two: break;
|
||||
case OutputBpp::Four: leftover_duration <<= 1; break;
|
||||
}
|
||||
shift(leftover_duration);
|
||||
crt_.output_data(leftover_duration);
|
||||
crt_.output_data(leftover_duration*2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,9 +754,9 @@ void Video::VideoStream::flush_pixels() {
|
||||
// Flush only if there's something to flush.
|
||||
if(pixel_pointer_) {
|
||||
switch(bpp_) {
|
||||
case OutputBpp::One: crt_.output_data(pixel_pointer_ >> 1, size_t(pixel_pointer_)); break;
|
||||
default: crt_.output_data(pixel_pointer_); break;
|
||||
case OutputBpp::Four: crt_.output_data(pixel_pointer_ << 1, size_t(pixel_pointer_)); break;
|
||||
case OutputBpp::One: crt_.output_data(pixel_pointer_); break;
|
||||
default: crt_.output_data(pixel_pointer_ << 1, size_t(pixel_pointer_)); break;
|
||||
case OutputBpp::Four: crt_.output_data(pixel_pointer_ << 2, size_t(pixel_pointer_)); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,7 +777,13 @@ void Video::VideoStream::set_bpp(OutputBpp bpp) {
|
||||
}
|
||||
|
||||
void Video::VideoStream::load(uint64_t value) {
|
||||
output_shifter_ = value;
|
||||
// In 1bpp mode, a 0 bit is white and a 1 bit is black.
|
||||
// Invert the input so that the 'just output the border colour
|
||||
// when the shifter is empty' optimisation works.
|
||||
if(bpp_ == OutputBpp::One)
|
||||
output_shifter_ = ~value;
|
||||
else
|
||||
output_shifter_ = value;
|
||||
}
|
||||
|
||||
// MARK: - Range observer.
|
||||
|
||||
@@ -21,6 +21,12 @@ class VideoTester;
|
||||
namespace Atari {
|
||||
namespace ST {
|
||||
|
||||
struct LineLength {
|
||||
int length = 1024;
|
||||
int hsync_start = 1024;
|
||||
int hsync_end = 1024;
|
||||
};
|
||||
|
||||
/*!
|
||||
Models a combination of the parts of the GLUE, MMU and Shifter that in net
|
||||
form the video subsystem of the Atari ST. So not accurate to a real chip, but
|
||||
@@ -40,6 +46,9 @@ class Video {
|
||||
*/
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*!
|
||||
Sets the type of output.
|
||||
*/
|
||||
@@ -112,7 +121,6 @@ class Video {
|
||||
Range get_memory_access_range();
|
||||
|
||||
private:
|
||||
void advance(HalfCycles duration);
|
||||
DeferredQueue<HalfCycles> deferrer_;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
@@ -125,10 +133,8 @@ class Video {
|
||||
int current_address_ = 0;
|
||||
|
||||
uint16_t *ram_ = nullptr;
|
||||
uint16_t line_buffer_[256];
|
||||
|
||||
int x_ = 0, y_ = 0, next_y_ = 0;
|
||||
int next_load_toggle_ = -1;
|
||||
bool load_ = false;
|
||||
int load_base_ = 0;
|
||||
|
||||
@@ -160,7 +166,7 @@ class Video {
|
||||
} sync_schedule = SyncSchedule::None;
|
||||
bool sync = false;
|
||||
} vertical_, next_vertical_;
|
||||
int line_length_ = 1024;
|
||||
LineLength line_length_;
|
||||
|
||||
int data_latch_position_ = 0;
|
||||
int data_latch_read_position_ = 0;
|
||||
@@ -234,59 +240,6 @@ class Video {
|
||||
bool vsync = false;
|
||||
} public_state_;
|
||||
|
||||
struct Event {
|
||||
int delay;
|
||||
enum class Type {
|
||||
SetDisplayEnable, ResetDisplayEnable,
|
||||
SetHsync, ResetHsync,
|
||||
SetVsync, ResetVsync,
|
||||
} type;
|
||||
|
||||
Event(Type type, int delay) : delay(delay), type(type) {}
|
||||
|
||||
void apply(PublicState &state) {
|
||||
apply(type, state);
|
||||
}
|
||||
|
||||
static void apply(Type type, PublicState &state) {
|
||||
switch(type) {
|
||||
default:
|
||||
case Type::SetDisplayEnable: state.display_enable = true; break;
|
||||
case Type::ResetDisplayEnable: state.display_enable = false; break;
|
||||
case Type::SetHsync: state.hsync = true; break;
|
||||
case Type::ResetHsync: state.hsync = false; break;
|
||||
case Type::SetVsync: state.vsync = true; break;
|
||||
case Type::ResetVsync: state.vsync = false; break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<Event> pending_events_;
|
||||
void add_event(int delay, Event::Type type) {
|
||||
// Apply immediately if there's no delay (or a negative delay).
|
||||
if(delay <= 0) {
|
||||
Event::apply(type, public_state_);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!pending_events_.empty()) {
|
||||
// Otherwise enqueue, having subtracted the delay for any preceding events,
|
||||
// and subtracting from the subsequent, if any.
|
||||
auto insertion_point = pending_events_.begin();
|
||||
while(insertion_point != pending_events_.end() && insertion_point->delay < delay) {
|
||||
delay -= insertion_point->delay;
|
||||
++insertion_point;
|
||||
}
|
||||
if(insertion_point != pending_events_.end()) {
|
||||
insertion_point->delay -= delay;
|
||||
}
|
||||
|
||||
pending_events_.emplace(insertion_point, type, delay);
|
||||
} else {
|
||||
pending_events_.emplace_back(type, delay);
|
||||
}
|
||||
}
|
||||
|
||||
friend class ::VideoTester;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include "../Configurable/StandardOptions.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
// TODO: rename.
|
||||
@@ -37,6 +38,13 @@ class Machine {
|
||||
*/
|
||||
virtual void set_scan_target(Outputs::Display::ScanTarget *scan_target) = 0;
|
||||
|
||||
/*!
|
||||
@returns The current scan status.
|
||||
*/
|
||||
virtual Outputs::Display::ScanStatus get_scan_status() const {
|
||||
return get_scaled_scan_status() / float(clock_rate_);
|
||||
}
|
||||
|
||||
/// @returns The speaker that receives this machine's output, or @c nullptr if this machine is mute.
|
||||
virtual Outputs::Speaker::Speaker *get_speaker() = 0;
|
||||
|
||||
@@ -46,11 +54,89 @@ class Machine {
|
||||
|
||||
/// Runs the machine for @c duration seconds.
|
||||
virtual void run_for(Time::Seconds duration) {
|
||||
const double cycles = (duration * clock_rate_) + clock_conversion_error_;
|
||||
const double cycles = (duration * clock_rate_ * speed_multiplier_) + clock_conversion_error_;
|
||||
clock_conversion_error_ = std::fmod(cycles, 1.0);
|
||||
run_for(Cycles(static_cast<int>(cycles)));
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets a speed multiplier to apply to this machine; e.g. a multiplier of 1.5 will cause the
|
||||
emulated machine to run 50% faster than a real machine. This speed-up is an emulation
|
||||
fiction: it will apply across the system, including to the CRT.
|
||||
*/
|
||||
virtual void set_speed_multiplier(double multiplier) {
|
||||
speed_multiplier_ = multiplier;
|
||||
auto speaker = get_speaker();
|
||||
if(speaker) {
|
||||
speaker->set_input_rate_multiplier(float(multiplier));
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The current speed multiplier.
|
||||
*/
|
||||
virtual double get_speed_multiplier() {
|
||||
return speed_multiplier_;
|
||||
}
|
||||
|
||||
/*!
|
||||
Runs for the machine for at least @c duration seconds, and then until @c condition is true.
|
||||
|
||||
@returns The amount of time run for.
|
||||
*/
|
||||
Time::Seconds run_until(Time::Seconds minimum_duration, std::function<bool()> condition) {
|
||||
Time::Seconds total_runtime = minimum_duration;
|
||||
run_for(minimum_duration);
|
||||
while(!condition()) {
|
||||
// Advance in increments of one 500th of a second until the condition
|
||||
// is true; that's 1/10th of a 50Hz frame, but more like 1/8.33 of a
|
||||
// 60Hz frame. Though most machines aren't exactly 50Hz or 60Hz, and some
|
||||
// are arbitrary other refresh rates. So those observations are merely
|
||||
// for scale.
|
||||
run_for(0.002);
|
||||
total_runtime += 0.002;
|
||||
}
|
||||
return total_runtime;
|
||||
}
|
||||
|
||||
enum MachineEvent: int {
|
||||
/// At least one new packet of audio has been delivered to the spaker's delegate.
|
||||
NewSpeakerSamplesGenerated = 1 << 0,
|
||||
|
||||
/// The next vertical retrace has begun.
|
||||
VerticalSync = 1 << 1,
|
||||
};
|
||||
|
||||
/*!
|
||||
Runs for at least @c duration seconds, and then every one of the @c events has occurred at least once since this
|
||||
call to @c run_until_event.
|
||||
|
||||
@param events A bitmask comprised of @c MachineEvent flags.
|
||||
@returns The amount of time run for.
|
||||
*/
|
||||
Time::Seconds run_until(Time::Seconds minimum_duration, int events) {
|
||||
// Tie up a wait-for-samples, if requested.
|
||||
const Outputs::Speaker::Speaker *speaker = nullptr;
|
||||
int sample_sets = 0;
|
||||
if(events & MachineEvent::NewSpeakerSamplesGenerated) {
|
||||
speaker = get_speaker();
|
||||
if(!speaker) events &= ~MachineEvent::NewSpeakerSamplesGenerated;
|
||||
sample_sets = speaker->completed_sample_sets();
|
||||
}
|
||||
|
||||
int retraces = 0;
|
||||
if(events & MachineEvent::VerticalSync) {
|
||||
retraces = get_scan_status().hsync_count;
|
||||
}
|
||||
|
||||
// Run until all requested events are satisfied.
|
||||
return run_until(minimum_duration, [=]() {
|
||||
return
|
||||
(!(events & MachineEvent::NewSpeakerSamplesGenerated) || (sample_sets != speaker->completed_sample_sets())) &&
|
||||
(!(events & MachineEvent::VerticalSync) || (retraces != get_scan_status().hsync_count));
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Runs the machine for @c cycles.
|
||||
virtual void run_for(const Cycles cycles) = 0;
|
||||
@@ -61,6 +147,15 @@ class Machine {
|
||||
return clock_rate_;
|
||||
}
|
||||
|
||||
virtual Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||
// This deliberately sets up an infinite loop if the user hasn't
|
||||
// overridden at least one of this or get_scan_status.
|
||||
//
|
||||
// Most likely you want to override this, and let the base class
|
||||
// throw in a divide-by-clock-rate at the end for you.
|
||||
return get_scan_status();
|
||||
}
|
||||
|
||||
/*!
|
||||
Maps from Configurable::Display to Outputs::Display::VideoSignal and calls
|
||||
@c set_display_type with the result.
|
||||
@@ -90,9 +185,11 @@ class Machine {
|
||||
*/
|
||||
virtual void set_display_type(Outputs::Display::DisplayType display_type) {}
|
||||
|
||||
|
||||
private:
|
||||
double clock_rate_ = 1.0;
|
||||
double clock_conversion_error_ = 0.0;
|
||||
double speed_multiplier_ = 1.0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class Joystick: public Inputs::ConcreteJoystick {
|
||||
Input('9'), Input('*'), Input('#'),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &digital_input, bool is_active) override {
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
switch(digital_input.type) {
|
||||
default: return;
|
||||
|
||||
@@ -177,23 +177,27 @@ class ConcreteMachine:
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return vdp_->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
void run_for(const Cycles cycles) final {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
|
||||
@@ -358,30 +362,30 @@ class ConcreteMachine:
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
float get_confidence() override {
|
||||
float get_confidence() final {
|
||||
if(pc_zero_accesses_ > 1) return 0.0f;
|
||||
return confidence_counter_.get_confidence();
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return Coleco::Vision::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
Configurable::Display display;
|
||||
if(Configurable::get_display(selections_by_option, display)) {
|
||||
set_video_signal_configurable(display);
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::SVideo);
|
||||
return selection_set;
|
||||
@@ -401,9 +405,9 @@ class ConcreteMachine:
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
GI::AY38910::AY38910 ay_;
|
||||
Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910> mixer_;
|
||||
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910>> speaker_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910<false>> mixer_;
|
||||
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910<false>>> speaker_;
|
||||
|
||||
std::vector<uint8_t> bios_;
|
||||
std::vector<uint8_t> cartridge_;
|
||||
|
||||
@@ -19,7 +19,6 @@ using namespace Commodore::C1540;
|
||||
MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
Storage::Disk::Controller(1000000),
|
||||
m6502_(*this),
|
||||
drive_(new Storage::Disk::Drive(1000000, 300, 2)),
|
||||
serial_port_VIA_port_handler_(new SerialPortVIA(serial_port_VIA_)),
|
||||
serial_port_(new SerialPort),
|
||||
drive_VIA_(drive_VIA_port_handler_),
|
||||
@@ -37,7 +36,8 @@ MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher &
|
||||
set_expected_bit_length(Storage::Encodings::CommodoreGCR::length_of_a_bit_in_time_zone(3));
|
||||
|
||||
// attach the only drive there is
|
||||
set_drive(drive_);
|
||||
emplace_drive(1000000, 300, 2);
|
||||
set_drive(1);
|
||||
|
||||
std::string device_name;
|
||||
uint32_t crc = 0;
|
||||
@@ -103,21 +103,21 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
|
||||
}
|
||||
|
||||
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk) {
|
||||
drive_->set_disk(disk);
|
||||
get_drive().set_disk(disk);
|
||||
}
|
||||
|
||||
void Machine::run_for(const Cycles cycles) {
|
||||
m6502_.run_for(cycles);
|
||||
|
||||
bool drive_motor = drive_VIA_port_handler_.get_motor_enabled();
|
||||
drive_->set_motor_on(drive_motor);
|
||||
const bool drive_motor = drive_VIA_port_handler_.get_motor_enabled();
|
||||
get_drive().set_motor_on(drive_motor);
|
||||
if(drive_motor)
|
||||
Storage::Disk::Controller::run_for(cycles);
|
||||
}
|
||||
|
||||
void MachineBase::set_activity_observer(Activity::Observer *observer) {
|
||||
drive_VIA_.bus_handler().set_activity_observer(observer);
|
||||
drive_->set_activity_observer(observer, "Drive", false);
|
||||
get_drive().set_activity_observer(observer, "Drive", false);
|
||||
}
|
||||
|
||||
// MARK: - 6522 delegate
|
||||
@@ -154,7 +154,7 @@ void MachineBase::process_index_hole() {}
|
||||
// MARK: - Drive VIA delegate
|
||||
|
||||
void MachineBase::drive_via_did_step_head(void *driveVIA, int direction) {
|
||||
drive_->step(Storage::Disk::HeadPosition(direction, 2));
|
||||
get_drive().step(Storage::Disk::HeadPosition(direction, 2));
|
||||
}
|
||||
|
||||
void MachineBase::drive_via_did_set_data_density(void *driveVIA, int density) {
|
||||
|
||||
@@ -144,7 +144,6 @@ class MachineBase:
|
||||
|
||||
protected:
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_;
|
||||
std::shared_ptr<Storage::Disk::Drive> drive_;
|
||||
|
||||
uint8_t ram_[0x800];
|
||||
uint8_t rom_[0x4000];
|
||||
|
||||
@@ -24,7 +24,7 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
BIND(Z, KeyZ); BIND(X, KeyX); BIND(C, KeyC); BIND(V, KeyV);
|
||||
BIND(B, KeyB); BIND(N, KeyN); BIND(M, KeyM);
|
||||
|
||||
BIND(BackTick, KeyLeft);
|
||||
BIND(BackTick, KeyLeftArrow);
|
||||
BIND(Hyphen, KeyPlus);
|
||||
BIND(Equals, KeyDash);
|
||||
BIND(F11, KeyGBP);
|
||||
@@ -35,8 +35,8 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
BIND(CloseSquareBracket, KeyAsterisk);
|
||||
|
||||
BIND(Backslash, KeyRestore);
|
||||
BIND(Hash, KeyUp);
|
||||
BIND(F10, KeyUp);
|
||||
BIND(Hash, KeyUpArrow);
|
||||
BIND(F10, KeyUpArrow);
|
||||
|
||||
BIND(Semicolon, KeyColon);
|
||||
BIND(Quote, KeySemicolon);
|
||||
@@ -66,6 +66,14 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
BIND(F3, KeyF3);
|
||||
BIND(F5, KeyF5);
|
||||
BIND(F7, KeyF7);
|
||||
|
||||
// Mappings to virtual keys.
|
||||
BIND(Left, KeyLeft);
|
||||
BIND(Up, KeyUp);
|
||||
BIND(F2, KeyF2);
|
||||
BIND(F4, KeyF4);
|
||||
BIND(F6, KeyF6);
|
||||
BIND(F8, KeyF8);
|
||||
}
|
||||
#undef BIND
|
||||
return KeyboardMachine::MappedMachine::KeyNotMapped;
|
||||
|
||||
@@ -20,7 +20,7 @@ enum Key: uint16_t {
|
||||
Key2 = key(7, 0x01), Key4 = key(7, 0x02), Key6 = key(7, 0x04), Key8 = key(7, 0x08),
|
||||
Key0 = key(7, 0x10), KeyDash = key(7, 0x20), KeyHome = key(7, 0x40), KeyF7 = key(7, 0x80),
|
||||
KeyQ = key(6, 0x01), KeyE = key(6, 0x02), KeyT = key(6, 0x04), KeyU = key(6, 0x08),
|
||||
KeyO = key(6, 0x10), KeyAt = key(6, 0x20), KeyUp = key(6, 0x40), KeyF5 = key(6, 0x80),
|
||||
KeyO = key(6, 0x10), KeyAt = key(6, 0x20), KeyUpArrow = key(6, 0x40), KeyF5 = key(6, 0x80),
|
||||
KeyCBM = key(5, 0x01), KeyS = key(5, 0x02), KeyF = key(5, 0x04), KeyH = key(5, 0x08),
|
||||
KeyK = key(5, 0x10), KeyColon = key(5, 0x20), KeyEquals = key(5, 0x40), KeyF3 = key(5, 0x80),
|
||||
KeySpace = key(4, 0x01), KeyZ = key(4, 0x02), KeyC = key(4, 0x04), KeyB = key(4, 0x08),
|
||||
@@ -29,12 +29,21 @@ enum Key: uint16_t {
|
||||
KeyN = key(3, 0x10), KeyComma = key(3, 0x20), KeySlash = key(3, 0x40), KeyDown = key(3, 0x80),
|
||||
KeyControl = key(2, 0x01), KeyA = key(2, 0x02), KeyD = key(2, 0x04), KeyG = key(2, 0x08),
|
||||
KeyJ = key(2, 0x10), KeyL = key(2, 0x20), KeySemicolon = key(2, 0x40), KeyRight = key(2, 0x80),
|
||||
KeyLeft = key(1, 0x01), KeyW = key(1, 0x02), KeyR = key(1, 0x04), KeyY = key(1, 0x08),
|
||||
KeyLeftArrow= key(1, 0x01), KeyW = key(1, 0x02), KeyR = key(1, 0x04), KeyY = key(1, 0x08),
|
||||
KeyI = key(1, 0x10), KeyP = key(1, 0x20), KeyAsterisk = key(1, 0x40), KeyReturn = key(1, 0x80),
|
||||
Key1 = key(0, 0x01), Key3 = key(0, 0x02), Key5 = key(0, 0x04), Key7 = key(0, 0x08),
|
||||
Key9 = key(0, 0x10), KeyPlus = key(0, 0x20), KeyGBP = key(0, 0x40), KeyDelete = key(0, 0x80),
|
||||
|
||||
KeyRestore = 0xfffd
|
||||
// Virtual keys.
|
||||
KeyUp = 0xfff0,
|
||||
KeyLeft = 0xfff1,
|
||||
KeyF2 = 0xfff2,
|
||||
KeyF4 = 0xfff3,
|
||||
KeyF6 = 0xfff4,
|
||||
KeyF8 = 0xfff5,
|
||||
|
||||
// Physical keys not within the usual matrix.
|
||||
KeyRestore = 0xfffd,
|
||||
#undef key
|
||||
};
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ class Joystick: public Inputs::ConcreteJoystick {
|
||||
user_port_via_port_handler_(user_port_via_port_handler),
|
||||
keyboard_via_port_handler_(keyboard_via_port_handler) {}
|
||||
|
||||
void did_set_input(const Input &digital_input, bool is_active) override {
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
JoystickInput mapped_input;
|
||||
switch(digital_input.type) {
|
||||
default: return;
|
||||
@@ -287,7 +287,7 @@ class ConcreteMachine:
|
||||
public Configurable::Device,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
|
||||
public Utility::TypeRecipient,
|
||||
public Utility::TypeRecipient<CharacterMapper>,
|
||||
public Storage::Tape::BinaryTapePlayer::Delegate,
|
||||
public Machine,
|
||||
public ClockingHint::Observer,
|
||||
@@ -397,9 +397,10 @@ class ConcreteMachine:
|
||||
memset(processor_write_memory_map_, 0, sizeof(processor_write_memory_map_));
|
||||
memset(mos6560_bus_handler_.video_memory_map, 0, sizeof(mos6560_bus_handler_.video_memory_map));
|
||||
|
||||
#define set_ram(baseaddr, length) \
|
||||
write_to_map(processor_read_memory_map_, &ram_[baseaddr], baseaddr, length); \
|
||||
write_to_map(processor_write_memory_map_, &ram_[baseaddr], baseaddr, length);
|
||||
#define set_ram(baseaddr, length) { \
|
||||
write_to_map(processor_read_memory_map_, &ram_[baseaddr], baseaddr, length); \
|
||||
write_to_map(processor_write_memory_map_, &ram_[baseaddr], baseaddr, length); \
|
||||
}
|
||||
|
||||
// Add 6502-visible RAM as requested.
|
||||
set_ram(0x0000, 0x0400);
|
||||
@@ -453,7 +454,7 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) override final {
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if(!media.tapes.empty()) {
|
||||
tape_->set_tape(media.tapes.front());
|
||||
}
|
||||
@@ -477,18 +478,36 @@ class ConcreteMachine:
|
||||
return !media.tapes.empty() || (!media.disks.empty() && c1540_ != nullptr) || !media.cartridges.empty();
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool is_pressed) override final {
|
||||
if(key != KeyRestore)
|
||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||||
if(key < 0xfff0) {
|
||||
keyboard_via_port_handler_->set_key_state(key, is_pressed);
|
||||
else
|
||||
user_port_via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !is_pressed);
|
||||
} else {
|
||||
switch(key) {
|
||||
case KeyRestore:
|
||||
user_port_via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !is_pressed);
|
||||
break;
|
||||
#define ShiftedMap(source, target) \
|
||||
case source: \
|
||||
keyboard_via_port_handler_->set_key_state(KeyLShift, is_pressed); \
|
||||
keyboard_via_port_handler_->set_key_state(target, is_pressed); \
|
||||
break;
|
||||
|
||||
ShiftedMap(KeyUp, KeyDown);
|
||||
ShiftedMap(KeyLeft, KeyRight);
|
||||
ShiftedMap(KeyF2, KeyF1);
|
||||
ShiftedMap(KeyF4, KeyF3);
|
||||
ShiftedMap(KeyF6, KeyF5);
|
||||
ShiftedMap(KeyF8, KeyF7);
|
||||
#undef ShiftedMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clear_all_keys() override final {
|
||||
void clear_all_keys() final {
|
||||
keyboard_via_port_handler_->clear_all_keys();
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
@@ -618,45 +637,53 @@ class ConcreteMachine:
|
||||
mos6560_.flush();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override final {
|
||||
void run_for(const Cycles cycles) final {
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
mos6560_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override final {
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return mos6560_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
mos6560_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override final {
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return mos6560_.get_speaker();
|
||||
}
|
||||
|
||||
void mos6522_did_change_interrupt_status(void *mos6522) override final {
|
||||
void mos6522_did_change_interrupt_status(void *mos6522) final {
|
||||
m6502_.set_nmi_line(user_port_via_.get_interrupt_line());
|
||||
m6502_.set_irq_line(keyboard_via_.get_interrupt_line());
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) override final {
|
||||
Utility::TypeRecipient::add_typer(string, std::make_unique<CharacterMapper>());
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
|
||||
}
|
||||
|
||||
void tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape) override final {
|
||||
bool can_type(char c) final {
|
||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
||||
}
|
||||
|
||||
void tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape) final {
|
||||
keyboard_via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !tape->get_input());
|
||||
}
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return Commodore::Vic20::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
bool quickload;
|
||||
if(Configurable::get_quick_load_tape(selections_by_option, quickload)) {
|
||||
allow_fast_tape_hack_ = quickload;
|
||||
@@ -669,27 +696,27 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, false);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, true);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::SVideo);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override {
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
|
||||
tape_is_sleeping_ = clocking == ClockingHint::Preference::None;
|
||||
set_use_fast_tape();
|
||||
}
|
||||
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
if(c1540_) c1540_->set_activity_observer(observer);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class ConcreteMachine:
|
||||
public Configurable::Device,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Tape::Delegate,
|
||||
public Utility::TypeRecipient,
|
||||
public Utility::TypeRecipient<CharacterMapper>,
|
||||
public Activity::Source {
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Acorn::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
@@ -62,7 +62,7 @@ class ConcreteMachine:
|
||||
set_clock_rate(2000000);
|
||||
|
||||
speaker_.set_input_rate(2000000 / SoundGenerator::clock_rate_divider);
|
||||
speaker_.set_high_frequency_cutoff(7000);
|
||||
speaker_.set_high_frequency_cutoff(6000);
|
||||
|
||||
const std::string machine_name = "Electron";
|
||||
std::vector<ROMMachine::ROM> required_roms = {
|
||||
@@ -114,23 +114,46 @@ class ConcreteMachine:
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool isPressed) override final {
|
||||
if(key == KeyBreak) {
|
||||
m6502_.set_reset_line(isPressed);
|
||||
} else {
|
||||
if(isPressed)
|
||||
key_states_[key >> 4] |= key&0xf;
|
||||
else
|
||||
key_states_[key >> 4] &= ~(key&0xf);
|
||||
void set_key_state(uint16_t key, bool isPressed) final {
|
||||
switch(key) {
|
||||
default:
|
||||
if(isPressed)
|
||||
key_states_[key >> 4] |= key&0xf;
|
||||
else
|
||||
key_states_[key >> 4] &= ~(key&0xf);
|
||||
break;
|
||||
|
||||
case KeyBreak:
|
||||
m6502_.set_reset_line(isPressed);
|
||||
break;
|
||||
|
||||
#define FuncShiftedKey(source, dest) \
|
||||
case source: \
|
||||
set_key_state(KeyFunc, isPressed); \
|
||||
set_key_state(dest, isPressed); \
|
||||
break;
|
||||
|
||||
FuncShiftedKey(KeyF1, Key1);
|
||||
FuncShiftedKey(KeyF2, Key2);
|
||||
FuncShiftedKey(KeyF3, Key3);
|
||||
FuncShiftedKey(KeyF4, Key4);
|
||||
FuncShiftedKey(KeyF5, Key5);
|
||||
FuncShiftedKey(KeyF6, Key6);
|
||||
FuncShiftedKey(KeyF7, Key7);
|
||||
FuncShiftedKey(KeyF8, Key8);
|
||||
FuncShiftedKey(KeyF9, Key9);
|
||||
FuncShiftedKey(KeyF0, Key0);
|
||||
|
||||
#undef FuncShiftedKey
|
||||
}
|
||||
}
|
||||
|
||||
void clear_all_keys() override final {
|
||||
void clear_all_keys() final {
|
||||
memset(key_states_, 0, sizeof(key_states_));
|
||||
if(is_holding_shift_) set_key_state(KeyShift, true);
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) override final {
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if(!media.tapes.empty()) {
|
||||
tape_.set_tape(media.tapes.front());
|
||||
}
|
||||
@@ -346,10 +369,10 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
cycles_since_display_update_ += Cycles(static_cast<int>(cycles));
|
||||
cycles_since_audio_update_ += Cycles(static_cast<int>(cycles));
|
||||
cycles_since_display_update_ += Cycles(int(cycles));
|
||||
cycles_since_audio_update_ += Cycles(int(cycles));
|
||||
if(cycles_since_audio_update_ > Cycles(16384)) update_audio();
|
||||
tape_.run_for(Cycles(static_cast<int>(cycles)));
|
||||
tape_.run_for(Cycles(int(cycles)));
|
||||
|
||||
cycles_until_display_interrupt_ -= cycles;
|
||||
if(cycles_until_display_interrupt_ < 0) {
|
||||
@@ -358,8 +381,8 @@ class ConcreteMachine:
|
||||
queue_next_display_interrupt();
|
||||
}
|
||||
|
||||
if(typer_) typer_->run_for(Cycles(static_cast<int>(cycles)));
|
||||
if(plus3_) plus3_->run_for(Cycles(4*static_cast<int>(cycles)));
|
||||
if(typer_) typer_->run_for(Cycles(int(cycles)));
|
||||
if(plus3_) plus3_->run_for(Cycles(4*int(cycles)));
|
||||
if(shift_restart_counter_) {
|
||||
shift_restart_counter_ -= cycles;
|
||||
if(shift_restart_counter_ <= 0) {
|
||||
@@ -379,49 +402,57 @@ class ConcreteMachine:
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_output_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_output_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_output_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override final {
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override final {
|
||||
void run_for(const Cycles cycles) final {
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void tape_did_change_interrupt_status(Tape *tape) override final {
|
||||
void tape_did_change_interrupt_status(Tape *tape) final {
|
||||
interrupt_status_ = (interrupt_status_ & ~(Interrupt::TransmitDataEmpty | Interrupt::ReceiveDataFull | Interrupt::HighToneDetect)) | tape_.get_interrupt_status();
|
||||
evaluate_interrupts();
|
||||
}
|
||||
|
||||
HalfCycles get_typer_delay() override final {
|
||||
return m6502_.get_is_resetting() ? Cycles(625*25*128) : Cycles(0); // wait one second if resetting
|
||||
HalfCycles get_typer_delay() final {
|
||||
return m6502_.get_is_resetting() ? Cycles(750'000) : Cycles(0);
|
||||
}
|
||||
|
||||
HalfCycles get_typer_frequency() override final {
|
||||
return Cycles(625*128*2); // accept a new character every two frames
|
||||
HalfCycles get_typer_frequency() final {
|
||||
return Cycles(60'000);
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) override final {
|
||||
Utility::TypeRecipient::add_typer(string, std::make_unique<CharacterMapper>());
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
|
||||
}
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
bool can_type(char c) final {
|
||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
||||
}
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return Electron::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
bool quickload;
|
||||
if(Configurable::get_quick_load_tape(selections_by_option, quickload)) {
|
||||
allow_fast_tape_hack_ = quickload;
|
||||
@@ -434,14 +465,14 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, false);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, true);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||
@@ -449,7 +480,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
activity_observer_ = observer;
|
||||
if(activity_observer_) {
|
||||
activity_observer_->register_led(caps_led);
|
||||
|
||||
@@ -13,7 +13,9 @@ using namespace Electron;
|
||||
uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
#define BIND(source, dest) case Inputs::Keyboard::Key::source: return Electron::Key::dest
|
||||
switch(key) {
|
||||
default: return KeyCopy;
|
||||
default: break;
|
||||
|
||||
BIND(BackTick, KeyCopy);
|
||||
|
||||
BIND(k0, Key0); BIND(k1, Key1); BIND(k2, Key2); BIND(k3, Key3); BIND(k4, Key4);
|
||||
BIND(k5, Key5); BIND(k6, Key6); BIND(k7, Key7); BIND(k8, Key8); BIND(k9, Key9);
|
||||
@@ -50,8 +52,21 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
BIND(KeypadMinus, KeyMinus); BIND(KeypadPlus, KeyColon);
|
||||
|
||||
BIND(Space, KeySpace);
|
||||
|
||||
// Virtual mappings.
|
||||
BIND(F1, KeyF1);
|
||||
BIND(F2, KeyF2);
|
||||
BIND(F3, KeyF3);
|
||||
BIND(F4, KeyF4);
|
||||
BIND(F5, KeyF5);
|
||||
BIND(F6, KeyF6);
|
||||
BIND(F7, KeyF7);
|
||||
BIND(F8, KeyF8);
|
||||
BIND(F9, KeyF9);
|
||||
BIND(F10, KeyF0);
|
||||
}
|
||||
#undef BIND
|
||||
return KeyboardMachine::MappedMachine::KeyNotMapped;
|
||||
}
|
||||
|
||||
uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
@@ -66,7 +81,7 @@ uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
/* ACK */ X, /* BEL */ X,
|
||||
/* BS */ KEYS(KeyDelete), /* HT */ X,
|
||||
/* LF */ KEYS(KeyReturn), /* VT */ X,
|
||||
/* FF */ X, /* CR */ X,
|
||||
/* FF */ X, /* CR */ KEYS(KeyReturn),
|
||||
/* SO */ X, /* SI */ X,
|
||||
/* DLE */ X, /* DC1 */ X,
|
||||
/* DC2 */ X, /* DC3 */ X,
|
||||
@@ -131,3 +146,7 @@ uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
|
||||
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
|
||||
}
|
||||
|
||||
bool CharacterMapper::needs_pause_after_key(uint16_t key) {
|
||||
return key != KeyControl && key != KeyShift && key != KeyFunc;
|
||||
}
|
||||
|
||||
@@ -30,15 +30,21 @@ enum Key: uint16_t {
|
||||
KeyZ = 0x00c0 | 0x08, KeyA = 0x00c0 | 0x04, KeyQ = 0x00c0 | 0x02, Key1 = 0x00c0 | 0x01,
|
||||
KeyShift = 0x00d0 | 0x08, KeyControl = 0x00d0 | 0x04, KeyFunc = 0x00d0 | 0x02, KeyEscape = 0x00d0 | 0x01,
|
||||
|
||||
// Virtual keys.
|
||||
KeyF1 = 0xfff0, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, KeyF7, KeyF8, KeyF9, KeyF0,
|
||||
|
||||
KeyBreak = 0xfffd,
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) override;
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) final;
|
||||
};
|
||||
|
||||
struct CharacterMapper: public ::Utility::CharacterMapper {
|
||||
uint16_t *sequence_for_character(char character);
|
||||
uint16_t *sequence_for_character(char character) override;
|
||||
|
||||
bool needs_pause_after_reset_all_keys() override { return false; }
|
||||
bool needs_pause_after_key(uint16_t key) override;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
@@ -11,13 +11,12 @@
|
||||
using namespace Electron;
|
||||
|
||||
Plus3::Plus3() : WD1770(P1770) {
|
||||
drives_.emplace_back(new Storage::Disk::Drive(8000000, 300, 2));
|
||||
drives_.emplace_back(new Storage::Disk::Drive(8000000, 300, 2));
|
||||
emplace_drives(2, 8000000, 300, 2);
|
||||
set_control_register(last_control_, 0xff);
|
||||
}
|
||||
|
||||
void Plus3::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
drives_[drive]->set_disk(disk);
|
||||
get_drive(drive).set_disk(disk);
|
||||
}
|
||||
|
||||
void Plus3::set_control_register(uint8_t control) {
|
||||
@@ -33,16 +32,15 @@ void Plus3::set_control_register(uint8_t control) {
|
||||
|
||||
void Plus3::set_control_register(uint8_t control, uint8_t changes) {
|
||||
if(changes&3) {
|
||||
switch(control&3) {
|
||||
case 0: selected_drive_ = -1; set_drive(nullptr); break;
|
||||
default: selected_drive_ = 0; set_drive(drives_[0]); break;
|
||||
case 2: selected_drive_ = 1; set_drive(drives_[1]); break;
|
||||
}
|
||||
set_drive(control&3);
|
||||
}
|
||||
|
||||
// Select the side on both drives at once.
|
||||
if(changes & 0x04) {
|
||||
drives_[0]->set_head((control & 0x04) ? 1 : 0);
|
||||
drives_[1]->set_head((control & 0x04) ? 1 : 0);
|
||||
get_drive(0).set_head((control & 0x04) ? 1 : 0);
|
||||
get_drive(1).set_head((control & 0x04) ? 1 : 0);
|
||||
}
|
||||
|
||||
if(changes & 0x08) set_is_double_density(!(control & 0x08));
|
||||
}
|
||||
|
||||
@@ -53,9 +51,7 @@ void Plus3::set_motor_on(bool on) {
|
||||
}
|
||||
|
||||
void Plus3::set_activity_observer(Activity::Observer *observer) {
|
||||
size_t index = 0;
|
||||
for(const auto &drive: drives_) {
|
||||
drive->set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
|
||||
++index;
|
||||
}
|
||||
for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
|
||||
drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ class Plus3 : public WD::WD1770 {
|
||||
|
||||
private:
|
||||
void set_control_register(uint8_t control, uint8_t changes);
|
||||
std::vector<std::shared_ptr<Storage::Disk::Drive>> drives_;
|
||||
int selected_drive_ = 0;
|
||||
uint8_t last_control_ = 0;
|
||||
|
||||
void set_motor_on(bool on);
|
||||
|
||||
@@ -36,13 +36,13 @@ void SoundGenerator::skip_samples(std::size_t number_of_samples) {
|
||||
}
|
||||
|
||||
void SoundGenerator::set_divider(uint8_t divider) {
|
||||
audio_queue_.defer([=]() {
|
||||
audio_queue_.defer([this, divider]() {
|
||||
divider_ = divider * 32 / clock_rate_divider;
|
||||
});
|
||||
}
|
||||
|
||||
void SoundGenerator::set_is_enabled(bool is_enabled) {
|
||||
audio_queue_.defer([=]() {
|
||||
audio_queue_.defer([this, is_enabled]() {
|
||||
is_enabled_ = is_enabled;
|
||||
counter_ = 0;
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ class SoundGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
void skip_samples(std::size_t number_of_samples);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
@@ -56,6 +56,10 @@ void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus VideoOutput::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / float(crt_cycles_multiplier);
|
||||
}
|
||||
|
||||
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ class VideoOutput {
|
||||
/// Sets the destination for output.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@ MappedMachine::MappedMachine(const std::set<Inputs::Keyboard::Key> &essential_mo
|
||||
keyboard_.set_delegate(this);
|
||||
}
|
||||
|
||||
void MappedMachine::keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) {
|
||||
bool MappedMachine::keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) {
|
||||
uint16_t mapped_key = get_keyboard_mapper()->mapped_key_for_key(key);
|
||||
if(mapped_key != KeyNotMapped) set_key_state(mapped_key, is_pressed);
|
||||
if(mapped_key == KeyNotMapped) return false;
|
||||
set_key_state(mapped_key, is_pressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MappedMachine::reset_all_keys(Inputs::Keyboard *keyboard) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef KeyboardMachine_h
|
||||
#define KeyboardMachine_h
|
||||
|
||||
#include <bitset>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <set>
|
||||
@@ -45,10 +46,61 @@ class Machine: public KeyActions {
|
||||
*/
|
||||
virtual void type_string(const std::string &);
|
||||
|
||||
/*!
|
||||
@returns @c true if this machine can type the character @c c as part of a @c type_string; @c false otherwise.
|
||||
*/
|
||||
virtual bool can_type(char c) { return false; }
|
||||
|
||||
/*!
|
||||
Provides a destination for keyboard input.
|
||||
*/
|
||||
virtual Inputs::Keyboard &get_keyboard() = 0;
|
||||
|
||||
/*!
|
||||
Provides a standard bundle of logic for hosts that are able to correlate typed symbols
|
||||
with keypresses. Specifically:
|
||||
|
||||
If map_logically is false:
|
||||
|
||||
(i) initially try to set @c key as @c is_pressed;
|
||||
(ii) if this machine doesn't map @c key to anything but @c symbol is a printable ASCII character, attempt to @c type_string it.
|
||||
|
||||
If map_logically is true:
|
||||
|
||||
(i) if @c symbol can be typed and this is a key down, @c type_string it;
|
||||
(ii) if @c symbol cannot be typed, set @c key as @c is_pressed
|
||||
*/
|
||||
bool apply_key(Inputs::Keyboard::Key key, char symbol, bool is_pressed, bool map_logically) {
|
||||
Inputs::Keyboard &keyboard = get_keyboard();
|
||||
|
||||
if(!map_logically) {
|
||||
// Try a regular keypress first, and stop if that works.
|
||||
if(keyboard.set_key_pressed(key, symbol, is_pressed)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// That having failed, if a symbol has been supplied then try typing it.
|
||||
if(is_pressed && symbol && can_type(symbol)) {
|
||||
char string[2] = { symbol, 0 };
|
||||
type_string(string);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else {
|
||||
// Try to type first.
|
||||
if(is_pressed && symbol && can_type(symbol)) {
|
||||
char string[2] = { symbol, 0 };
|
||||
type_string(string);
|
||||
return true;
|
||||
}
|
||||
|
||||
// That didn't work. Forward as a keypress. As, either:
|
||||
// (i) this is a key down, but doesn't have a symbol, or is an untypeable symbol; or
|
||||
// (ii) this is a key up, which it won't be an issue to miscommunicate.
|
||||
return keyboard.set_key_pressed(key, symbol, is_pressed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -90,7 +142,7 @@ class MappedMachine: public Inputs::Keyboard::Delegate, public Machine {
|
||||
virtual Inputs::Keyboard &get_keyboard() override;
|
||||
|
||||
private:
|
||||
void keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) override;
|
||||
bool keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) override;
|
||||
void reset_all_keys(Inputs::Keyboard *keyboard) override;
|
||||
Inputs::Keyboard keyboard_;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ class ASCII16kbROMSlotHandler: public ROMSlotHandler {
|
||||
ASCII16kbROMSlotHandler(MSX::MemoryMap &map, int slot) :
|
||||
map_(map), slot_(slot) {}
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override {
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||
switch(address >> 11) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
@@ -39,7 +39,7 @@ class ASCII16kbROMSlotHandler: public ROMSlotHandler {
|
||||
}
|
||||
}
|
||||
|
||||
virtual std::string debug_type() override {
|
||||
virtual std::string debug_type() final {
|
||||
return "A16";
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class ASCII8kbROMSlotHandler: public ROMSlotHandler {
|
||||
ASCII8kbROMSlotHandler(MSX::MemoryMap &map, int slot) :
|
||||
map_(map), slot_(slot) {}
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override {
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||
switch(address >> 11) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
@@ -51,7 +51,7 @@ class ASCII8kbROMSlotHandler: public ROMSlotHandler {
|
||||
}
|
||||
}
|
||||
|
||||
virtual std::string debug_type() override {
|
||||
virtual std::string debug_type() final {
|
||||
return "A8";
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class KonamiROMSlotHandler: public ROMSlotHandler {
|
||||
KonamiROMSlotHandler(MSX::MemoryMap &map, int slot) :
|
||||
map_(map), slot_(slot) {}
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override {
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||
switch(address >> 13) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
@@ -45,7 +45,7 @@ class KonamiROMSlotHandler: public ROMSlotHandler {
|
||||
}
|
||||
}
|
||||
|
||||
virtual std::string debug_type() override {
|
||||
virtual std::string debug_type() final {
|
||||
return "K";
|
||||
}
|
||||
private:
|
||||
|
||||
@@ -20,7 +20,7 @@ class KonamiWithSCCROMSlotHandler: public ROMSlotHandler {
|
||||
KonamiWithSCCROMSlotHandler(MSX::MemoryMap &map, int slot, Konami::SCC &scc) :
|
||||
map_(map), slot_(slot), scc_(scc) {}
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override {
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||
switch(address >> 11) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
@@ -66,7 +66,7 @@ class KonamiWithSCCROMSlotHandler: public ROMSlotHandler {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t read(uint16_t address) override {
|
||||
uint8_t read(uint16_t address) final {
|
||||
if(scc_is_visible_ && address >= 0x9800 && address < 0xa000) {
|
||||
confidence_counter_.add_hit();
|
||||
return scc_.read(address);
|
||||
@@ -75,7 +75,7 @@ class KonamiWithSCCROMSlotHandler: public ROMSlotHandler {
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
virtual std::string debug_type() override {
|
||||
virtual std::string debug_type() final {
|
||||
return "KSCC";
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ using namespace MSX;
|
||||
DiskROM::DiskROM(const std::vector<uint8_t> &rom) :
|
||||
WD1770(P1793),
|
||||
rom_(rom) {
|
||||
drives_[0] = std::make_shared<Storage::Disk::Drive>(8000000, 300, 2);
|
||||
drives_[1] = std::make_shared<Storage::Disk::Drive>(8000000, 300, 2);
|
||||
emplace_drives(2, 8000000, 300, 2);
|
||||
set_is_double_density(true);
|
||||
}
|
||||
|
||||
@@ -23,18 +22,19 @@ void DiskROM::write(uint16_t address, uint8_t value, bool pc_is_outside_bios) {
|
||||
case 0x7ff8: case 0x7ff9: case 0x7ffa: case 0x7ffb:
|
||||
WD::WD1770::write(address, value);
|
||||
break;
|
||||
case 0x7ffc:
|
||||
selected_head_ = value & 1;
|
||||
drives_[0]->set_head(selected_head_);
|
||||
drives_[1]->set_head(selected_head_);
|
||||
break;
|
||||
case 0x7ffc: {
|
||||
const int selected_head = value & 1;
|
||||
for_all_drives([selected_head] (Storage::Disk::Drive &drive, size_t index) {
|
||||
drive.set_head(selected_head);
|
||||
});
|
||||
} break;
|
||||
case 0x7ffd: {
|
||||
selected_drive_ = value & 1;
|
||||
set_drive(drives_[selected_drive_]);
|
||||
set_drive(1 << (value & 1));
|
||||
|
||||
bool drive_motor = !!(value & 0x80);
|
||||
drives_[0]->set_motor_on(drive_motor);
|
||||
drives_[1]->set_motor_on(drive_motor);
|
||||
const bool drive_motor = value & 0x80;
|
||||
for_all_drives([drive_motor] (Storage::Disk::Drive &drive, size_t index) {
|
||||
drive.set_motor_on(drive_motor);
|
||||
});
|
||||
} break;
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ void DiskROM::run_for(HalfCycles half_cycles) {
|
||||
}
|
||||
|
||||
void DiskROM::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
drives_[drive]->set_disk(disk);
|
||||
get_drive(drive).set_disk(disk);
|
||||
}
|
||||
|
||||
void DiskROM::set_head_load_request(bool head_load) {
|
||||
@@ -68,9 +68,7 @@ void DiskROM::set_head_load_request(bool head_load) {
|
||||
}
|
||||
|
||||
void DiskROM::set_activity_observer(Activity::Observer *observer) {
|
||||
size_t c = 1;
|
||||
for(auto &drive: drives_) {
|
||||
drive->set_activity_observer(observer, "Drive " + std::to_string(c), true);
|
||||
++c;
|
||||
}
|
||||
for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
|
||||
drive.set_activity_observer(observer, "Drive " + std::to_string(index), true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ class DiskROM: public ROMSlotHandler, public WD::WD1770 {
|
||||
public:
|
||||
DiskROM(const std::vector<uint8_t> &rom);
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override;
|
||||
uint8_t read(uint16_t address) override;
|
||||
void run_for(HalfCycles half_cycles) override;
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final;
|
||||
uint8_t read(uint16_t address) final;
|
||||
void run_for(HalfCycles half_cycles) final;
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
@@ -36,11 +36,8 @@ class DiskROM: public ROMSlotHandler, public WD::WD1770 {
|
||||
const std::vector<uint8_t> &rom_;
|
||||
|
||||
long int controller_cycles_ = 0;
|
||||
size_t selected_drive_ = 0;
|
||||
int selected_head_ = 0;
|
||||
std::array<std::shared_ptr<Storage::Disk::Drive>, 2> drives_;
|
||||
|
||||
void set_head_load_request(bool head_load) override;
|
||||
void set_head_load_request(bool head_load) final;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ class AYPortHandler: public GI::AY38910::PortHandler {
|
||||
Input(Input::Fire, 1),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, bool is_active) override {
|
||||
void did_set_input(const Input &input, bool is_active) final {
|
||||
uint8_t mask = 0;
|
||||
switch(input.type) {
|
||||
default: return;
|
||||
@@ -278,23 +278,27 @@ class ConcreteMachine:
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return vdp_->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
void run_for(const Cycles cycles) final {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
|
||||
float get_confidence() override {
|
||||
float get_confidence() final {
|
||||
if(performed_unmapped_access_ || pc_zero_accesses_ > 1) return 0.0f;
|
||||
if(memory_slots_[1].handler) {
|
||||
return memory_slots_[1].handler->get_confidence();
|
||||
@@ -302,14 +306,14 @@ class ConcreteMachine:
|
||||
return 0.5f;
|
||||
}
|
||||
|
||||
std::string debug_type() override {
|
||||
std::string debug_type() final {
|
||||
if(memory_slots_[1].handler) {
|
||||
return "MSX:" + memory_slots_[1].handler->debug_type();
|
||||
}
|
||||
return "MSX";
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if(!media.cartridges.empty()) {
|
||||
const auto &segment = media.cartridges.front()->get_segments().front();
|
||||
memory_slots_[1].source = segment.data;
|
||||
@@ -356,7 +360,7 @@ class ConcreteMachine:
|
||||
return true;
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) override final {
|
||||
void type_string(const std::string &string) final {
|
||||
std::transform(
|
||||
string.begin(),
|
||||
string.end(),
|
||||
@@ -365,8 +369,13 @@ class ConcreteMachine:
|
||||
);
|
||||
}
|
||||
|
||||
bool can_type(char c) final {
|
||||
// Make an effort to type the entire printable ASCII range.
|
||||
return c >= 32 && c < 127;
|
||||
}
|
||||
|
||||
// MARK: MSX::MemoryMap
|
||||
void map(int slot, std::size_t source_address, uint16_t destination_address, std::size_t length) override {
|
||||
void map(int slot, std::size_t source_address, uint16_t destination_address, std::size_t length) final {
|
||||
assert(!(destination_address & 8191));
|
||||
assert(!(length & 8191));
|
||||
assert(static_cast<std::size_t>(destination_address) + length <= 65536);
|
||||
@@ -381,7 +390,7 @@ class ConcreteMachine:
|
||||
page_memory(paged_memory_);
|
||||
}
|
||||
|
||||
void unmap(int slot, uint16_t destination_address, std::size_t length) override {
|
||||
void unmap(int slot, uint16_t destination_address, std::size_t length) final {
|
||||
assert(!(destination_address & 8191));
|
||||
assert(!(length & 8191));
|
||||
assert(static_cast<std::size_t>(destination_address) + length <= 65536);
|
||||
@@ -624,26 +633,26 @@ class ConcreteMachine:
|
||||
return key_states_[selected_key_line_];
|
||||
}
|
||||
|
||||
void clear_all_keys() override {
|
||||
void clear_all_keys() final {
|
||||
std::memset(key_states_, 0xff, sizeof(key_states_));
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool is_pressed) override {
|
||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||||
int mask = 1 << (key & 7);
|
||||
int line = key >> 4;
|
||||
if(is_pressed) key_states_[line] &= ~mask; else key_states_[line] |= mask;
|
||||
}
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return MSX::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
bool quickload;
|
||||
if(Configurable::get_quick_load_tape(selections_by_option, quickload)) {
|
||||
allow_fast_tape_ = quickload;
|
||||
@@ -656,14 +665,14 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, false);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, true);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||
@@ -671,13 +680,13 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Sleeper
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override {
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
|
||||
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
|
||||
set_use_fast_tape();
|
||||
}
|
||||
|
||||
// MARK: - Activity::Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
DiskROM *disk_rom = get_disk_rom();
|
||||
if(disk_rom) {
|
||||
disk_rom->set_activity_observer(observer);
|
||||
@@ -686,7 +695,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Joysticks
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return ay_port_handler_.get_joysticks();
|
||||
}
|
||||
|
||||
@@ -756,11 +765,11 @@ class ConcreteMachine:
|
||||
Intel::i8255::i8255<i8255PortHandler> i8255_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910 ay_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
Konami::SCC scc_;
|
||||
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910, Audio::Toggle, Konami::SCC> mixer_;
|
||||
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910, Audio::Toggle, Konami::SCC>> speaker_;
|
||||
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC> mixer_;
|
||||
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC>> speaker_;
|
||||
|
||||
Storage::Tape::BinaryTapePlayer tape_player_;
|
||||
bool tape_player_is_sleeping_ = false;
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
|
||||
#define LOG_PREFIX "[SMS] "
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
#include "../../Analyser/Static/Sega/Target.hpp"
|
||||
@@ -54,7 +56,7 @@ class Joystick: public Inputs::ConcreteJoystick {
|
||||
Input(Input::Fire, 1)
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &digital_input, bool is_active) override {
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
switch(digital_input.type) {
|
||||
default: return;
|
||||
|
||||
@@ -168,7 +170,7 @@ class ConcreteMachine:
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
vdp_->set_tv_standard(
|
||||
(region_ == Target::Region::Europe) ?
|
||||
TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC);
|
||||
@@ -177,15 +179,19 @@ class ConcreteMachine:
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return vdp_->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
void run_for(const Cycles cycles) final {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
|
||||
@@ -340,45 +346,51 @@ class ConcreteMachine:
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
// MARK: - Keyboard (i.e. the pause and reset buttons).
|
||||
Inputs::Keyboard &get_keyboard() override {
|
||||
Inputs::Keyboard &get_keyboard() final {
|
||||
return keyboard_;
|
||||
}
|
||||
|
||||
void keyboard_did_change_key(Inputs::Keyboard *, Inputs::Keyboard::Key key, bool is_pressed) override {
|
||||
bool keyboard_did_change_key(Inputs::Keyboard *, Inputs::Keyboard::Key key, bool is_pressed) final {
|
||||
if(key == Inputs::Keyboard::Key::Enter) {
|
||||
pause_is_pressed_ = is_pressed;
|
||||
} else if(key == Inputs::Keyboard::Key::Escape) {
|
||||
reset_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(key == Inputs::Keyboard::Key::Escape) {
|
||||
reset_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void reset_all_keys(Inputs::Keyboard *) override {
|
||||
void reset_all_keys(Inputs::Keyboard *) final {
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return Sega::MasterSystem::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
Configurable::Display display;
|
||||
if(Configurable::get_display(selections_by_option, display)) {
|
||||
set_video_signal_configurable(display);
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||
return selection_set;
|
||||
|
||||
113
Machines/Oric/BD500.cpp
Normal file
113
Machines/Oric/BD500.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// BD500.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/01/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "BD500.hpp"
|
||||
|
||||
using namespace Oric;
|
||||
|
||||
/*
|
||||
Notes on the code below: the Byte Drive 500 isn't well documented; this implementation is based on
|
||||
experimentation without access to real hardware as documented in http://forum.defence-force.org/viewtopic.php?f=25&t=2055
|
||||
and with additional guidance from iss of Oricutron (amongst other things) elsewhere,
|
||||
e.g. http://forum.defence-force.org/viewtopic.php?f=22&t=1698&start=105#p21468
|
||||
*/
|
||||
|
||||
BD500::BD500() : DiskController(P1793, 9000000, Storage::Disk::Drive::ReadyType::ShugartModifiedRDY) {
|
||||
disable_basic_rom_ = true;
|
||||
select_paged_item();
|
||||
set_is_double_density(true);
|
||||
set_drive(1);
|
||||
}
|
||||
|
||||
void BD500::write(int address, uint8_t value) {
|
||||
access(address);
|
||||
|
||||
if(address >= 0x0320 && address <= 0x0323) {
|
||||
// if(address == 0x320) printf("Command %02x\n", value);
|
||||
WD::WD1770::write(address, value);
|
||||
}
|
||||
|
||||
if(address == 0x031a) {
|
||||
// Drive select; kudos to iss of Oricutron for figuring this one out;
|
||||
// cf. http://forum.defence-force.org/viewtopic.php?f=25&p=21409#p21393
|
||||
switch(value & 0xe0) {
|
||||
default: set_drive(0); break;
|
||||
case 0x20: set_drive(1); break;
|
||||
case 0x40: set_drive(2); break;
|
||||
case 0x80: set_drive(4); break;
|
||||
case 0xc0: set_drive(8); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t BD500::read(int address) {
|
||||
access(address);
|
||||
|
||||
switch(address) {
|
||||
default: return 0xff;
|
||||
|
||||
case 0x0320: case 0x0321: case 0x0322: case 0x0323:
|
||||
return WD::WD1770::read(address);
|
||||
|
||||
case 0x312: return (get_data_request_line() ? 0x80 : 0x00) | (get_interrupt_request_line() ? 0x40 : 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
void BD500::access(int address) {
|
||||
// Determine whether to perform a command.
|
||||
switch(address) {
|
||||
case 0x0320: case 0x0321: case 0x0322: case 0x0323: case 0x0312:
|
||||
return;
|
||||
|
||||
case 0x311: disable_basic_rom_ = true; break;
|
||||
case 0x313: enable_overlay_ram_ = false; break;
|
||||
case 0x314: enable_overlay_ram_ = true; break;
|
||||
case 0x317: disable_basic_rom_ = false; break;
|
||||
|
||||
default:
|
||||
// printf("Switch %04x???\n", address);
|
||||
break;
|
||||
}
|
||||
|
||||
select_paged_item();
|
||||
}
|
||||
|
||||
void BD500::set_head_load_request(bool head_load) {
|
||||
// Turn all motors on or off; if off then unload the head instantly.
|
||||
is_loading_head_ |= head_load;
|
||||
for_all_drives([head_load] (Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_motor_on(head_load);
|
||||
});
|
||||
if(!head_load) set_head_loaded(false);
|
||||
}
|
||||
|
||||
void BD500::run_for(const Cycles cycles) {
|
||||
// If a head load is in progress and the selected drive is now ready,
|
||||
// declare head loaded.
|
||||
if(is_loading_head_ && get_drive().get_is_ready()) {
|
||||
set_head_loaded(true);
|
||||
is_loading_head_ = false;
|
||||
}
|
||||
|
||||
WD::WD1770::run_for(cycles);
|
||||
}
|
||||
|
||||
void BD500::set_activity_observer(Activity::Observer *observer) {
|
||||
observer_ = observer;
|
||||
if(observer) {
|
||||
observer->register_led("BD-500");
|
||||
observer_->set_led_status("BD-500", get_head_loaded());
|
||||
}
|
||||
}
|
||||
|
||||
void BD500::set_head_loaded(bool loaded) {
|
||||
WD::WD1770::set_head_loaded(loaded);
|
||||
if(observer_) {
|
||||
observer_->set_led_status("BD-500", loaded);
|
||||
}
|
||||
}
|
||||
43
Machines/Oric/BD500.hpp
Normal file
43
Machines/Oric/BD500.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// BD500.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/01/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef BD500_hpp
|
||||
#define BD500_hpp
|
||||
|
||||
#include "../../Components/1770/1770.hpp"
|
||||
#include "../../Activity/Observer.hpp"
|
||||
#include "DiskController.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
namespace Oric {
|
||||
|
||||
class BD500: public DiskController {
|
||||
public:
|
||||
BD500();
|
||||
|
||||
void write(int address, uint8_t value);
|
||||
uint8_t read(int address);
|
||||
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
private:
|
||||
void set_head_load_request(bool head_load) final;
|
||||
bool is_loading_head_ = false;
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
|
||||
void access(int address);
|
||||
void set_head_loaded(bool loaded);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
#endif /* BD500_hpp */
|
||||
75
Machines/Oric/DiskController.hpp
Normal file
75
Machines/Oric/DiskController.hpp
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// DiskController.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/01/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef DiskController_h
|
||||
#define DiskController_h
|
||||
|
||||
namespace Oric {
|
||||
|
||||
class DiskController: public WD::WD1770 {
|
||||
public:
|
||||
DiskController(WD::WD1770::Personality personality, int clock_rate, Storage::Disk::Drive::ReadyType ready_type) :
|
||||
WD::WD1770(personality), clock_rate_(clock_rate), ready_type_(ready_type) {
|
||||
emplace_drives(4, clock_rate_, 300, 2, ready_type_);
|
||||
// TODO: don't assume four drives?
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int d) {
|
||||
get_drive(size_t(d)).set_disk(disk);
|
||||
}
|
||||
|
||||
enum class PagedItem {
|
||||
DiskROM,
|
||||
BASIC,
|
||||
RAM
|
||||
};
|
||||
|
||||
struct Delegate: public WD1770::Delegate {
|
||||
virtual void disk_controller_did_change_paged_item(DiskController *controller) = 0;
|
||||
};
|
||||
inline void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
WD1770::set_delegate(delegate);
|
||||
if(delegate) delegate->disk_controller_did_change_paged_item(this);
|
||||
}
|
||||
inline PagedItem get_paged_item() {
|
||||
return paged_item_;
|
||||
}
|
||||
|
||||
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) {
|
||||
if(paged_item_ == item) return;
|
||||
paged_item_ = item;
|
||||
if(delegate_) {
|
||||
delegate_->disk_controller_did_change_paged_item(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif /* DiskController_h */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user