mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
263 Commits
2020-01-01
...
2020-02-16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
cdb31b1c2b | ||
|
|
6a44936a7c | ||
|
|
45afb13a54 | ||
|
|
3ced31043a | ||
|
|
7361e7ec34 | ||
|
|
533729638c | ||
|
|
9f30be1c13 | ||
|
|
09289f383d | ||
|
|
20b25ce866 | ||
|
|
c1bae49a92 | ||
|
|
b3f806201b | ||
|
|
9f2f547932 | ||
|
|
f0d5bbecf2 | ||
|
|
3d7ef43293 | ||
|
|
4578b65487 | ||
|
|
a28c52c250 | ||
|
|
e4349f5e05 | ||
|
|
7b2777ac08 | ||
|
|
0fbcbfc61b | ||
|
|
3ab4fb8c79 | ||
|
|
42a9585321 | ||
|
|
937cba8978 | ||
|
|
627d3c28ea | ||
|
|
19ddfae6d6 | ||
|
|
56ebd08af0 | ||
|
|
7de1181213 | ||
|
|
c7a5b054db | ||
|
|
ca12ba297b | ||
|
|
7abf527084 | ||
|
|
c0b5bfe726 | ||
|
|
414b0cc234 | ||
|
|
134e828336 | ||
|
|
455e831b87 | ||
|
|
617e0bada9 | ||
|
|
7dea99b1cc | ||
|
|
42ccf48966 | ||
|
|
2f8078db22 |
@@ -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_;
|
||||
|
||||
@@ -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;
|
||||
void 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,10 @@ 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;
|
||||
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,6 +102,49 @@ static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) {
|
||||
return !std::memcmp(signature, first_sample.data(), sizeof(signature));
|
||||
}
|
||||
|
||||
bool is_400_loader(Storage::Encodings::MFM::Parser &parser, uint16_t range_start, uint16_t range_end) {
|
||||
/*
|
||||
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;
|
||||
|
||||
// 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 =
|
||||
Analyser::Static::MOS6502::Disassemble(first_sample, Analyser::Static::Disassembler::OffsetMapper(0x400), {0x400});
|
||||
|
||||
// Check for references to the Jasmin registers.
|
||||
int register_hits = 0;
|
||||
for(auto list : {disassembly.external_stores, disassembly.external_loads, disassembly.external_modifies}) {
|
||||
for(auto address : list) {
|
||||
register_hits += (address >= range_start && address <= range_end);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -115,12 +160,10 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
|
||||
for(const auto &file : tape_files) {
|
||||
if(file.data_type == File::MachineCode) {
|
||||
std::vector<uint16_t> entry_points = {file.starting_address};
|
||||
Analyser::Static::MOS6502::Disassembly disassembly =
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,12 +173,22 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
|
||||
}
|
||||
|
||||
if(!media.disks.empty()) {
|
||||
// Only the Microdisc is emulated right now, so accept only disks that it can boot from.
|
||||
// 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(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +26,15 @@ struct Target: public ::Analyser::Static::Target {
|
||||
enum class DiskInterface {
|
||||
Microdisc,
|
||||
Pravetz,
|
||||
Jasmin,
|
||||
BD500,
|
||||
None
|
||||
};
|
||||
|
||||
ROM rom = ROM::BASIC11;
|
||||
DiskInterface disk_interface = DiskInterface::None;
|
||||
std::string loading_command;
|
||||
bool should_start_jasmin = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ WD1770::WD1770(Personality p) :
|
||||
posit_event(int(Event1770::Command));
|
||||
}
|
||||
|
||||
void WD1770::set_register(int address, uint8_t value) {
|
||||
void WD1770::write(int address, uint8_t value) {
|
||||
switch(address&3) {
|
||||
case 0: {
|
||||
if((value&0xf0) == 0xd0) {
|
||||
@@ -54,7 +54,7 @@ void WD1770::set_register(int address, uint8_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t WD1770::get_register(int address) {
|
||||
uint8_t WD1770::read(int address) {
|
||||
switch(address&3) {
|
||||
default: {
|
||||
update_status([] (Status &status) {
|
||||
@@ -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();
|
||||
|
||||
@@ -36,29 +36,29 @@ class WD1770: public Storage::Disk::MFMController {
|
||||
using Storage::Disk::MFMController::set_is_double_density;
|
||||
|
||||
/// Writes @c value to the register at @c address. Only the low two bits of the address are decoded.
|
||||
void set_register(int address, uint8_t value);
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/// Fetches the value of the register @c address. Only the low two bits of the address are decoded.
|
||||
uint8_t get_register(int address);
|
||||
uint8_t read(int address);
|
||||
|
||||
/// Runs the controller for @c number_of_cycles cycles.
|
||||
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); }
|
||||
|
||||
@@ -94,10 +94,10 @@ template <class T> class MOS6522: public MOS6522Storage {
|
||||
MOS6522(const MOS6522 &) = delete;
|
||||
|
||||
/*! Sets a register value. */
|
||||
void set_register(int address, uint8_t value);
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*! Gets a register value. */
|
||||
uint8_t get_register(int address);
|
||||
uint8_t read(int address);
|
||||
|
||||
/*! @returns the bus handler. */
|
||||
T &bus_handler();
|
||||
|
||||
@@ -30,7 +30,7 @@ template <typename T> void MOS6522<T>::access(int address) {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) {
|
||||
template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
access(address);
|
||||
switch(address) {
|
||||
@@ -155,7 +155,7 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value)
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> uint8_t MOS6522<T>::get_register(int address) {
|
||||
template <typename T> uint8_t MOS6522<T>::read(int address) {
|
||||
address &= 0xf;
|
||||
access(address);
|
||||
switch(address) {
|
||||
|
||||
@@ -32,7 +32,7 @@ template <class T> class MOS6532 {
|
||||
inline void set_ram(uint16_t address, uint8_t value) { ram_[address&0x7f] = value; }
|
||||
inline uint8_t get_ram(uint16_t address) { return ram_[address & 0x7f]; }
|
||||
|
||||
inline void set_register(int address, uint8_t value) {
|
||||
inline void write(int address, uint8_t value) {
|
||||
const uint8_t decodedAddress = address & 0x07;
|
||||
switch(decodedAddress) {
|
||||
// Port output
|
||||
@@ -63,7 +63,7 @@ template <class T> class MOS6532 {
|
||||
}
|
||||
}
|
||||
|
||||
inline uint8_t get_register(int address) {
|
||||
inline uint8_t read(int address) {
|
||||
const uint8_t decodedAddress = address & 0x7;
|
||||
switch(decodedAddress) {
|
||||
// Port input
|
||||
|
||||
@@ -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_;
|
||||
@@ -58,7 +59,7 @@ enum class OutputMode {
|
||||
To run the VIC for a cycle, the caller should call @c get_address, make the requested bus access
|
||||
and call @c set_graphics_value with the result.
|
||||
|
||||
@c set_register and @c get_register provide register access.
|
||||
@c write and @c read provide register access.
|
||||
*/
|
||||
template <class BusHandler> class MOS6560 {
|
||||
public:
|
||||
@@ -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) {
|
||||
@@ -353,7 +355,7 @@ template <class BusHandler> class MOS6560 {
|
||||
/*!
|
||||
Writes to a 6560 register.
|
||||
*/
|
||||
void set_register(int address, uint8_t value) {
|
||||
void write(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
registers_.direct_values[address] = value;
|
||||
switch(address) {
|
||||
@@ -417,7 +419,7 @@ template <class BusHandler> class MOS6560 {
|
||||
/*
|
||||
Reads from a 6560 register.
|
||||
*/
|
||||
uint8_t get_register(int address) {
|
||||
uint8_t read(int address) {
|
||||
address &= 0xf;
|
||||
switch(address) {
|
||||
default: return registers_.direct_values[address];
|
||||
|
||||
@@ -27,7 +27,7 @@ template <class T> class i8255 {
|
||||
Stores the value @c value to the register at @c address. If this causes a change in 8255 output
|
||||
then the PortHandler will be informed.
|
||||
*/
|
||||
void set_register(int address, uint8_t value) {
|
||||
void write(int address, uint8_t value) {
|
||||
switch(address & 3) {
|
||||
case 0:
|
||||
if(!(control_ & 0x10)) {
|
||||
@@ -60,7 +60,7 @@ template <class T> class i8255 {
|
||||
Obtains the current value for the register at @c address. If this provides a reading
|
||||
of input then the PortHandler will be queried.
|
||||
*/
|
||||
uint8_t get_register(int address) {
|
||||
uint8_t read(int address) {
|
||||
switch(address & 3) {
|
||||
case 0: return (control_ & 0x10) ? port_handler_.get_value(0) : outputs_[0];
|
||||
case 1: return (control_ & 0x02) ? port_handler_.get_value(1) : outputs_[1];
|
||||
|
||||
@@ -163,7 +163,7 @@ void i8272::run_for(Cycles cycles) {
|
||||
if(is_sleeping_) update_clocking_observer();
|
||||
}
|
||||
|
||||
void i8272::set_register(int address, uint8_t value) {
|
||||
void i8272::write(int address, uint8_t value) {
|
||||
// don't consider attempted sets to the status register
|
||||
if(!address) return;
|
||||
|
||||
@@ -181,7 +181,7 @@ void i8272::set_register(int address, uint8_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t i8272::get_register(int address) {
|
||||
uint8_t i8272::read(int address) {
|
||||
if(address) {
|
||||
if(result_stack_.empty()) return 0xff;
|
||||
uint8_t result = result_stack_.back();
|
||||
@@ -865,7 +865,7 @@ void i8272::posit_event(int event_type) {
|
||||
SetDataRequest();
|
||||
SetDataDirectionToProcessor();
|
||||
|
||||
// The actual stuff of unwinding result_stack_ is handled by ::get_register; wait
|
||||
// The actual stuff of unwinding result_stack_ is handled by ::read; wait
|
||||
// until the processor has read all result bytes.
|
||||
WAIT_FOR_EVENT(Event8272::ResultEmpty);
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ class i8272 : public Storage::Disk::MFMController {
|
||||
void set_data_input(uint8_t value);
|
||||
uint8_t get_data_output();
|
||||
|
||||
void set_register(int address, uint8_t value);
|
||||
uint8_t get_register(int address);
|
||||
void write(int address, uint8_t value);
|
||||
uint8_t read(int address);
|
||||
|
||||
void set_dma_acknowledge(bool dack);
|
||||
void set_terminal_count(bool tc);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -492,7 +500,7 @@ void Base::output_border(int cycles, uint32_t cram_dot) {
|
||||
}
|
||||
}
|
||||
|
||||
void TMS9918::set_register(int address, uint8_t value) {
|
||||
void TMS9918::write(int address, uint8_t value) {
|
||||
// Writes to address 0 are writes to the video RAM. Store
|
||||
// the value and return.
|
||||
if(!(address & 1)) {
|
||||
@@ -670,7 +678,7 @@ void TMS9918::latch_horizontal_counter() {
|
||||
latched_column_ = write_pointer_.column;
|
||||
}
|
||||
|
||||
uint8_t TMS9918::get_register(int address) {
|
||||
uint8_t TMS9918::read(int address) {
|
||||
write_phase_ = false;
|
||||
|
||||
// Reads from address 0 read video RAM, via the read-ahead buffer.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -54,10 +57,10 @@ class TMS9918: public Base {
|
||||
void run_for(const HalfCycles cycles);
|
||||
|
||||
/*! Sets a register value. */
|
||||
void set_register(int address, uint8_t value);
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*! Gets a register value. */
|
||||
uint8_t get_register(int address);
|
||||
uint8_t read(int address);
|
||||
|
||||
/*! Gets the current scan line; provided by the Master System only. */
|
||||
uint8_t get_current_line();
|
||||
@@ -69,8 +72,8 @@ class TMS9918: public Base {
|
||||
void latch_horizontal_counter();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until get_interrupt_line would next return true if
|
||||
there are no interceding calls to set_register or get_register.
|
||||
Returns the amount of time until @c get_interrupt_line would next return true if
|
||||
there are no interceding calls to @c write or to @c read.
|
||||
|
||||
If get_interrupt_line is true now, returns zero. If get_interrupt_line would
|
||||
never return true, returns -1.
|
||||
|
||||
@@ -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,17 +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));
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -92,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++;
|
||||
}
|
||||
@@ -134,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_++;
|
||||
}
|
||||
@@ -143,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:
|
||||
@@ -161,10 +190,20 @@ void AY38910::evaluate_output_volume() {
|
||||
};
|
||||
#undef level
|
||||
|
||||
// This remapping table seeks to map 'channel volumes', i.e. the levels produced from the
|
||||
// 16-step progammatic volumes set per channel to 'envelope volumes', i.e. the 32-step
|
||||
// volumes that are produced by the envelope generators (on a YM at least). My reading of
|
||||
// the data sheet is that '0' is still off, but 15 should be as loud as peak envelope. So
|
||||
// I've thrown in the discontinuity at the low end, where it'll be very quiet.
|
||||
const int channel_volumes[] = {
|
||||
0, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31
|
||||
};
|
||||
static_assert(sizeof(channel_volumes) == 16*sizeof(int));
|
||||
|
||||
// Channel volume is a simple selection: if the bit at 0x10 is set, use the envelope volume; otherwise use the lower four bits,
|
||||
// mapped to the range 1–31 in case this is a YM.
|
||||
#define channel_volume(c) \
|
||||
((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * (((output_registers_[c]&0xf) << 1) + 1)
|
||||
((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * channel_volumes[output_registers_[c]&0xf]
|
||||
|
||||
const int volumes[3] = {
|
||||
channel_volume(8),
|
||||
@@ -173,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) {
|
||||
@@ -209,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;
|
||||
}
|
||||
@@ -224,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:
|
||||
@@ -262,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] = {
|
||||
@@ -276,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.
|
||||
@@ -303,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.
|
||||
@@ -319,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_) {
|
||||
@@ -344,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);
|
||||
|
||||
@@ -48,7 +48,7 @@ void SN76489::set_sample_volume_range(std::int16_t range) {
|
||||
evaluate_output_volume();
|
||||
}
|
||||
|
||||
void SN76489::set_register(uint8_t value) {
|
||||
void SN76489::write(uint8_t value) {
|
||||
task_queue_.defer([value, this] () {
|
||||
if(value & 0x80) {
|
||||
active_register_ = value;
|
||||
|
||||
@@ -26,12 +26,13 @@ class SN76489: public Outputs::Speaker::SampleSource {
|
||||
SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1);
|
||||
|
||||
/// Writes a new value to the SN76489.
|
||||
void set_register(uint8_t value);
|
||||
void write(uint8_t value);
|
||||
|
||||
// As per 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,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 +156,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_;
|
||||
};
|
||||
|
||||
@@ -335,6 +338,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);
|
||||
@@ -627,7 +635,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 +664,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 +680,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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -924,14 +930,14 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
|
||||
// Check for an 8255 PIO access
|
||||
if(!(address & 0x800)) {
|
||||
i8255_.set_register((address >> 8) & 3, *cycle.value);
|
||||
i8255_.write((address >> 8) & 3, *cycle.value);
|
||||
}
|
||||
|
||||
if constexpr (has_fdc) {
|
||||
// Check for an FDC access
|
||||
if((address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
fdc_.set_register(address & 1, *cycle.value);
|
||||
fdc_.write(address & 1, *cycle.value);
|
||||
}
|
||||
|
||||
// Check for a disk motor access
|
||||
@@ -947,14 +953,14 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
|
||||
// Check for a PIO access
|
||||
if(!(address & 0x800)) {
|
||||
*cycle.value &= i8255_.get_register((address >> 8) & 3);
|
||||
*cycle.value &= i8255_.read((address >> 8) & 3);
|
||||
}
|
||||
|
||||
// Check for an FDC access
|
||||
if constexpr (has_fdc) {
|
||||
if((address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
*cycle.value &= fdc_.get_register(address & 1);
|
||||
*cycle.value &= fdc_.read(address & 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1002,26 +1008,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 +1049,70 @@ 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 {
|
||||
void type_string(const std::string &string) final {
|
||||
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper());
|
||||
Utility::TypeRecipient::add_typer(string, std::move(mapper));
|
||||
}
|
||||
|
||||
HalfCycles get_typer_delay() override final {
|
||||
HalfCycles get_typer_delay() final {
|
||||
return Cycles(4000000); // Wait 1 second before typing.
|
||||
}
|
||||
|
||||
HalfCycles get_typer_frequency() override final {
|
||||
HalfCycles get_typer_frequency() final {
|
||||
return Cycles(160000); // Type one character 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,15 +803,15 @@ 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 {
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) final {
|
||||
switch(key) {
|
||||
default: break;
|
||||
case Key::F12:
|
||||
@@ -846,38 +850,38 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 +890,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,14 +226,14 @@ 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.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = via_.get_register(register_address);
|
||||
cycle.value->halves.low = via_.read(register_address);
|
||||
} else {
|
||||
via_.set_register(register_address, cycle.value->halves.low);
|
||||
via_.write(register_address, cycle.value->halves.low);
|
||||
}
|
||||
|
||||
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
|
||||
@@ -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) {
|
||||
@@ -181,9 +181,9 @@ template<class T> class Cartridge:
|
||||
if((address&0x1280) == 0x280) {
|
||||
update_6532();
|
||||
if(isReadOperation(operation)) {
|
||||
returnValue &= mos6532_.get_register(address);
|
||||
returnValue &= mos6532_.read(address);
|
||||
} else {
|
||||
mos6532_.set_register(address, *value);
|
||||
mos6532_.write(address, *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_;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "../../../Activity/Source.hpp"
|
||||
|
||||
//#define LOG_TRACE
|
||||
//bool should_log = false;
|
||||
#include "../../../Processors/68000/68000.hpp"
|
||||
|
||||
#include "../../../Components/AY38910/AY38910.hpp"
|
||||
@@ -100,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;
|
||||
}
|
||||
|
||||
@@ -138,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);
|
||||
}
|
||||
@@ -240,7 +247,7 @@ class ConcreteMachine:
|
||||
|
||||
case BusDevice::ROM:
|
||||
memory = rom_.data();
|
||||
address %= rom_.size();
|
||||
address -= rom_start_;
|
||||
break;
|
||||
|
||||
case BusDevice::Floating:
|
||||
@@ -263,11 +270,11 @@ class ConcreteMachine:
|
||||
return delay;
|
||||
|
||||
case BusDevice::IO:
|
||||
switch(address >> 1) {
|
||||
switch(address & 0xfffe) { // TODO: surely it's going to be even less precise than this?
|
||||
default:
|
||||
// assert(false);
|
||||
|
||||
case 0x7fc000:
|
||||
case 0x8000:
|
||||
/* Memory controller configuration:
|
||||
b0, b1: bank 1
|
||||
b2, b3: bank 0
|
||||
@@ -279,8 +286,64 @@ class ConcreteMachine:
|
||||
*/
|
||||
break;
|
||||
|
||||
case 0x7fc400: /* PSG: write to select register, read to read register. */
|
||||
case 0x7fc401: /* PSG: write to write register. */
|
||||
// Video controls.
|
||||
case 0x8200: case 0x8202: case 0x8204: case 0x8206:
|
||||
case 0x8208: case 0x820a: case 0x820c: case 0x820e:
|
||||
case 0x8210: case 0x8212: case 0x8214: case 0x8216:
|
||||
case 0x8218: case 0x821a: case 0x821c: case 0x821e:
|
||||
case 0x8220: case 0x8222: case 0x8224: case 0x8226:
|
||||
case 0x8228: case 0x822a: case 0x822c: case 0x822e:
|
||||
case 0x8230: case 0x8232: case 0x8234: case 0x8236:
|
||||
case 0x8238: case 0x823a: case 0x823c: case 0x823e:
|
||||
case 0x8240: case 0x8242: case 0x8244: case 0x8246:
|
||||
case 0x8248: case 0x824a: case 0x824c: case 0x824e:
|
||||
case 0x8250: case 0x8252: case 0x8254: case 0x8256:
|
||||
case 0x8258: case 0x825a: case 0x825c: case 0x825e:
|
||||
case 0x8260: case 0x8262:
|
||||
if(!cycle.data_select_active()) return delay;
|
||||
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.set_value16(video_->read(int(address >> 1)));
|
||||
} else {
|
||||
video_->write(int(address >> 1), cycle.value16());
|
||||
}
|
||||
break;
|
||||
|
||||
// DMA.
|
||||
case 0x8604: case 0x8606: case 0x8608: case 0x860a: case 0x860c:
|
||||
if(!cycle.data_select_active()) return delay;
|
||||
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.set_value16(dma_->read(int(address >> 1)));
|
||||
} else {
|
||||
dma_->write(int(address >> 1), cycle.value16());
|
||||
}
|
||||
break;
|
||||
|
||||
// Audio.
|
||||
//
|
||||
// Re: mirrors, Dan Hollis' hardware register list asserts:
|
||||
//
|
||||
// "Note: PSG Registers are now fixed at these addresses. All other addresses are masked out on the Falcon. Any
|
||||
// writes to the shadow registers $8804-$88FF will cause bus errors.", which I am taking to imply that those shadow
|
||||
// registers exist on the Atari ST.
|
||||
case 0x8800: case 0x8802: case 0x8804: case 0x8806: case 0x8808: case 0x880a: case 0x880c: case 0x880e:
|
||||
case 0x8810: case 0x8812: case 0x8814: case 0x8816: case 0x8818: case 0x881a: case 0x881c: case 0x881e:
|
||||
case 0x8820: case 0x8822: case 0x8824: case 0x8826: case 0x8828: case 0x882a: case 0x882c: case 0x882e:
|
||||
case 0x8830: case 0x8832: case 0x8834: case 0x8836: case 0x8838: case 0x883a: case 0x883c: case 0x883e:
|
||||
case 0x8840: case 0x8842: case 0x8844: case 0x8846: case 0x8848: case 0x884a: case 0x884c: case 0x884e:
|
||||
case 0x8850: case 0x8852: case 0x8854: case 0x8856: case 0x8858: case 0x885a: case 0x885c: case 0x885e:
|
||||
case 0x8860: case 0x8862: case 0x8864: case 0x8866: case 0x8868: case 0x886a: case 0x886c: case 0x886e:
|
||||
case 0x8870: case 0x8872: case 0x8874: case 0x8876: case 0x8878: case 0x887a: case 0x887c: case 0x887e:
|
||||
case 0x8880: case 0x8882: case 0x8884: case 0x8886: case 0x8888: case 0x888a: case 0x888c: case 0x888e:
|
||||
case 0x8890: case 0x8892: case 0x8894: case 0x8896: case 0x8898: case 0x889a: case 0x889c: case 0x889e:
|
||||
case 0x88a0: case 0x88a2: case 0x88a4: case 0x88a6: case 0x88a8: case 0x88aa: case 0x88ac: case 0x88ae:
|
||||
case 0x88b0: case 0x88b2: case 0x88b4: case 0x88b6: case 0x88b8: case 0x88ba: case 0x88bc: case 0x88be:
|
||||
case 0x88c0: case 0x88c2: case 0x88c4: case 0x88c6: case 0x88c8: case 0x88ca: case 0x88cc: case 0x88ce:
|
||||
case 0x88d0: case 0x88d2: case 0x88d4: case 0x88d6: case 0x88d8: case 0x88da: case 0x88dc: case 0x88de:
|
||||
case 0x88e0: case 0x88e2: case 0x88e4: case 0x88e6: case 0x88e8: case 0x88ea: case 0x88ec: case 0x88ee:
|
||||
case 0x88f0: case 0x88f2: case 0x88f4: case 0x88f6: case 0x88f8: case 0x88fa: case 0x88fc: case 0x88fe:
|
||||
|
||||
if(!cycle.data_select_active()) return delay;
|
||||
|
||||
advance_time(HalfCycles(2));
|
||||
@@ -291,25 +354,26 @@ class ConcreteMachine:
|
||||
cycle.set_value8_high(ay_.get_data_output());
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
} else {
|
||||
if((address >> 1) == 0x7fc400) {
|
||||
ay_.set_control_lines(GI::AY38910::BC1);
|
||||
} else {
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR));
|
||||
}
|
||||
// Net effect here: addresses with bit 1 set write to a register,
|
||||
// addresses with bit 1 clear select a register.
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(
|
||||
GI::AY38910::BC2 | GI::AY38910::BDIR
|
||||
| ((address&2) ? 0 : GI::AY38910::BC1)
|
||||
));
|
||||
ay_.set_data_input(cycle.value8_high());
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
}
|
||||
return delay + HalfCycles(2);
|
||||
|
||||
// The MFP block:
|
||||
case 0x7ffd00: case 0x7ffd01: case 0x7ffd02: case 0x7ffd03:
|
||||
case 0x7ffd04: case 0x7ffd05: case 0x7ffd06: case 0x7ffd07:
|
||||
case 0x7ffd08: case 0x7ffd09: case 0x7ffd0a: case 0x7ffd0b:
|
||||
case 0x7ffd0c: case 0x7ffd0d: case 0x7ffd0e: case 0x7ffd0f:
|
||||
case 0x7ffd10: case 0x7ffd11: case 0x7ffd12: case 0x7ffd13:
|
||||
case 0x7ffd14: case 0x7ffd15: case 0x7ffd16: case 0x7ffd17:
|
||||
case 0x7ffd18: case 0x7ffd19: case 0x7ffd1a: case 0x7ffd1b:
|
||||
case 0x7ffd1c: case 0x7ffd1d: case 0x7ffd1e: case 0x7ffd1f:
|
||||
case 0xfa00: case 0xfa02: case 0xfa04: case 0xfa06:
|
||||
case 0xfa08: case 0xfa0a: case 0xfa0c: case 0xfa0e:
|
||||
case 0xfa10: case 0xfa12: case 0xfa14: case 0xfa16:
|
||||
case 0xfa18: case 0xfa1a: case 0xfa1c: case 0xfa1e:
|
||||
case 0xfa20: case 0xfa22: case 0xfa24: case 0xfa26:
|
||||
case 0xfa28: case 0xfa2a: case 0xfa2c: case 0xfa2e:
|
||||
case 0xfa30: case 0xfa32: case 0xfa34: case 0xfa36:
|
||||
case 0xfa38: case 0xfa3a: case 0xfa3c: case 0xfa3e:
|
||||
if(!cycle.data_select_active()) return delay;
|
||||
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
@@ -319,53 +383,19 @@ class ConcreteMachine:
|
||||
}
|
||||
break;
|
||||
|
||||
// Video controls.
|
||||
case 0x7fc100: case 0x7fc101: case 0x7fc102: case 0x7fc103:
|
||||
case 0x7fc104: case 0x7fc105: case 0x7fc106: case 0x7fc107:
|
||||
case 0x7fc108: case 0x7fc109: case 0x7fc10a: case 0x7fc10b:
|
||||
case 0x7fc10c: case 0x7fc10d: case 0x7fc10e: case 0x7fc10f:
|
||||
case 0x7fc110: case 0x7fc111: case 0x7fc112: case 0x7fc113:
|
||||
case 0x7fc114: case 0x7fc115: case 0x7fc116: case 0x7fc117:
|
||||
case 0x7fc118: case 0x7fc119: case 0x7fc11a: case 0x7fc11b:
|
||||
case 0x7fc11c: case 0x7fc11d: case 0x7fc11e: case 0x7fc11f:
|
||||
case 0x7fc120: case 0x7fc121: case 0x7fc122: case 0x7fc123:
|
||||
case 0x7fc124: case 0x7fc125: case 0x7fc126: case 0x7fc127:
|
||||
case 0x7fc128: case 0x7fc129: case 0x7fc12a: case 0x7fc12b:
|
||||
case 0x7fc12c: case 0x7fc12d: case 0x7fc12e: case 0x7fc12f:
|
||||
case 0x7fc130: case 0x7fc131:
|
||||
if(!cycle.data_select_active()) return delay;
|
||||
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.set_value16(video_->read(int(address >> 1)));
|
||||
} else {
|
||||
video_->write(int(address >> 1), cycle.value16());
|
||||
}
|
||||
break;
|
||||
|
||||
// ACIAs.
|
||||
case 0x7ffe00: case 0x7ffe01: case 0x7ffe02: case 0x7ffe03: {
|
||||
case 0xfc00: case 0xfc02: case 0xfc04: case 0xfc06: {
|
||||
// Set VPA.
|
||||
mc68000_.set_is_peripheral_address(!cycle.data_select_active());
|
||||
if(!cycle.data_select_active()) return delay;
|
||||
|
||||
const auto acia_ = ((address >> 1) < 0x7ffe02) ? &keyboard_acia_ : &midi_acia_;
|
||||
const auto acia_ = (address & 4) ? &midi_acia_ : &keyboard_acia_;
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.set_value8_high((*acia_)->read(int(address >> 1)));
|
||||
} else {
|
||||
(*acia_)->write(int(address >> 1), cycle.value8_high());
|
||||
}
|
||||
} break;
|
||||
|
||||
// DMA.
|
||||
case 0x7fc302: case 0x7fc303: case 0x7fc304: case 0x7fc305: case 0x7fc306:
|
||||
if(!cycle.data_select_active()) return delay;
|
||||
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.set_value16(dma_->read(int(address >> 1)));
|
||||
} else {
|
||||
dma_->write(int(address >> 1), cycle.value16());
|
||||
}
|
||||
break;
|
||||
}
|
||||
return HalfCycles(0);
|
||||
}
|
||||
@@ -411,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;
|
||||
@@ -432,7 +463,7 @@ class ConcreteMachine:
|
||||
mfp_.flush();
|
||||
}
|
||||
|
||||
if(dma_is_realtime_) {
|
||||
if(dma_clocking_preference_ == ClockingHint::Preference::RealTime) {
|
||||
dma_.flush();
|
||||
}
|
||||
|
||||
@@ -441,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();
|
||||
@@ -465,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_;
|
||||
@@ -476,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.
|
||||
@@ -499,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).
|
||||
@@ -508,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.
|
||||
@@ -539,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).
|
||||
@@ -634,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ uint16_t DMAController::read(int address) {
|
||||
if(control_ & Control::CPUTarget) {
|
||||
return 0xffff;
|
||||
} else {
|
||||
return 0xff00 | fdc_.get_register(control_ >> 1);
|
||||
return 0xff00 | fdc_.read(control_ >> 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -78,7 +78,7 @@ void DMAController::write(int address, uint16_t value) {
|
||||
if(control_ & Control::CPUTarget) {
|
||||
// TODO: HDC.
|
||||
} else {
|
||||
fdc_.set_register(control_ >> 1, uint8_t(value));
|
||||
fdc_.write(control_ >> 1, uint8_t(value));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -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) {
|
||||
@@ -153,7 +153,7 @@ void DMAController::wd1770_did_change_output(WD::WD1770 *) {
|
||||
|
||||
// Read from the data register into the active buffer.
|
||||
if(bytes_received_ < 16) {
|
||||
buffer_[active_buffer_].contents[bytes_received_] = fdc_.get_register(3);
|
||||
buffer_[active_buffer_].contents[bytes_received_] = fdc_.read(3);
|
||||
++bytes_received_;
|
||||
}
|
||||
if(bytes_received_ == 16) {
|
||||
@@ -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;
|
||||
|
||||
@@ -100,13 +100,46 @@ void IntelligentKeyboard::run_for(HalfCycles duration) {
|
||||
// machine advertises as joystick 1 is mapped to the Atari ST's joystick 2, so as to
|
||||
// maintain both the normal emulation expections that the first joystick is the primary
|
||||
// one and the Atari ST's convention that the main joystick is in port 2.
|
||||
for(size_t c = 0; c < 2; ++c) {
|
||||
const auto joystick = static_cast<Joystick *>(joysticks_[c ^ 1].get());
|
||||
if(joystick->has_event()) {
|
||||
output_bytes({
|
||||
uint8_t(0xfe | c),
|
||||
joystick->get_state()
|
||||
});
|
||||
if(joystick_mode_ == JoystickMode::Event || joystick_mode_ == JoystickMode::KeyCode) {
|
||||
for(size_t c = 0; c < 2; ++c) {
|
||||
const auto joystick = static_cast<Joystick *>(joysticks_[c ^ 1].get());
|
||||
if(joystick->has_event()) {
|
||||
|
||||
if(joystick_mode_ == JoystickMode::Event) {
|
||||
// Event mode: forward a joystick event message.
|
||||
output_bytes({
|
||||
uint8_t(0xfe | c),
|
||||
joystick->get_state()
|
||||
});
|
||||
} else {
|
||||
// Key code mode: decompose the joystick event into
|
||||
// instantaneous key events.
|
||||
const auto event_mask = joystick->event_mask();
|
||||
const auto new_state = joystick->get_state();
|
||||
const auto new_presses = (event_mask ^ new_state) & new_state;
|
||||
|
||||
// Send cursor keys for the movement.
|
||||
const Key keys[] = {Key::Up, Key::Down, Key::Left, Key::Right};
|
||||
for(int key = 0; key < 4; ++key) {
|
||||
if(new_presses & (1 << key)) {
|
||||
output_bytes({
|
||||
uint8_t(keys[key]),
|
||||
uint8_t(0x80 | uint8_t(keys[key]))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check also for fire, but the key to send depends
|
||||
// on the joystick.
|
||||
if(new_presses & 0x80) {
|
||||
const Key fire_buttons[] = {Key::Joystick1Button, Key::Joystick2Button};
|
||||
output_bytes({
|
||||
uint8_t(fire_buttons[c]),
|
||||
uint8_t(0x80 | uint8_t(fire_buttons[c]))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,21 +458,45 @@ void IntelligentKeyboard::disable_joysticks() {
|
||||
|
||||
void IntelligentKeyboard::set_joystick_event_mode() {
|
||||
joystick_mode_ = JoystickMode::Event;
|
||||
clear_joystick_events();
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_joystick_interrogation_mode() {
|
||||
joystick_mode_ = JoystickMode::Interrogation;
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::interrogate_joysticks() {
|
||||
void IntelligentKeyboard::set_joystick_keycode_mode(VelocityThreshold horizontal, VelocityThreshold vertical) {
|
||||
joystick_mode_ = JoystickMode::KeyCode;
|
||||
clear_joystick_events();
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::clear_joystick_events() {
|
||||
const auto joystick1 = static_cast<Joystick *>(joysticks_[0].get());
|
||||
const auto joystick2 = static_cast<Joystick *>(joysticks_[1].get());
|
||||
joystick1->get_state();
|
||||
joystick2->get_state();
|
||||
}
|
||||
|
||||
output_bytes({
|
||||
0xfd,
|
||||
joystick2->get_state(),
|
||||
joystick1->get_state()
|
||||
});
|
||||
void IntelligentKeyboard::interrogate_joysticks() {
|
||||
if(joystick_mode_ != JoystickMode::Interrogation) {
|
||||
// Joystick::get_state() implicitly clears Joystick::has_event,
|
||||
// so don't permit interrogation if the user isn't in interrogation
|
||||
// mode because it might cause dropped events.
|
||||
output_bytes({
|
||||
0xfd,
|
||||
0x00,
|
||||
0x00
|
||||
});
|
||||
} else {
|
||||
const auto joystick1 = static_cast<Joystick *>(joysticks_[0].get());
|
||||
const auto joystick2 = static_cast<Joystick *>(joysticks_[1].get());
|
||||
|
||||
output_bytes({
|
||||
0xfd,
|
||||
joystick2->get_state(),
|
||||
joystick1->get_state()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_joystick_monitoring_mode(uint8_t rate) {
|
||||
@@ -449,7 +506,3 @@ void IntelligentKeyboard::set_joystick_monitoring_mode(uint8_t rate) {
|
||||
void IntelligentKeyboard::set_joystick_fire_button_monitoring_mode() {
|
||||
LOG("Unimplemented: joystick fire button monitoring mode");
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_joystick_keycode_mode(VelocityThreshold horizontal, VelocityThreshold vertical) {
|
||||
LOG("Unimplemented: joystick keycode mode");
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ enum class Key: uint16_t {
|
||||
Insert = 0x52, Delete,
|
||||
ISO = 0x60, Undo, Help, KeypadOpenBracket, KeypadCloseBracket, KeypadDivide, KeypadMultiply,
|
||||
Keypad7, Keypad8, Keypad9, Keypad4, Keypad5, Keypad6, Keypad1, Keypad2, Keypad3, Keypad0, KeypadDecimalPoint,
|
||||
KeypadEnter
|
||||
KeypadEnter,
|
||||
Joystick1Button = 0x74, // These keycodes are used only in joystick keycode mode.
|
||||
Joystick2Button = 0x75,
|
||||
};
|
||||
static_assert(uint16_t(Key::RightShift) == 0x36, "RightShift should have key code 0x36; check intermediate entries");
|
||||
static_assert(uint16_t(Key::F10) == 0x44, "F10 should have key code 0x44; check intermediate entries");
|
||||
@@ -143,8 +145,10 @@ class IntelligentKeyboard:
|
||||
void set_joystick_keycode_mode(VelocityThreshold horizontal, VelocityThreshold vertical);
|
||||
void interrogate_joysticks();
|
||||
|
||||
void clear_joystick_events();
|
||||
|
||||
enum class JoystickMode {
|
||||
Disabled, Event, Interrogation
|
||||
Disabled, Event, Interrogation, KeyCode
|
||||
} joystick_mode_ = JoystickMode::Event;
|
||||
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
@@ -158,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;
|
||||
@@ -181,6 +185,10 @@ class IntelligentKeyboard:
|
||||
return returned_state_ != state_;
|
||||
}
|
||||
|
||||
uint8_t event_mask() {
|
||||
return returned_state_ ^ state_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t state_ = 0x00;
|
||||
uint8_t returned_state_ = 0x00;
|
||||
|
||||
@@ -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
|
||||
@@ -94,15 +104,16 @@ struct Checker {
|
||||
} checker;
|
||||
#endif
|
||||
|
||||
const int de_delay_period = CYCLE(28); // Number of half cycles after DE that observed DE changes.
|
||||
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, 216, 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;
|
||||
@@ -267,33 +277,28 @@ void Video::advance(HalfCycles duration) {
|
||||
next_vertical_.enable = true;
|
||||
} else if(y_ == vertical_timings.reset_enable) {
|
||||
next_vertical_.enable = false;
|
||||
} else if(next_y_ == vertical_timings.height) {
|
||||
} else if(next_y_ == vertical_timings.height - 2) {
|
||||
next_vertical_.sync_schedule = VerticalState::SyncSchedule::Begin;
|
||||
} else if(next_y_ == vertical_timings.height) {
|
||||
next_y_ = 0;
|
||||
} else if(next_y_ == 2) {
|
||||
} else if(y_ == 0) {
|
||||
next_vertical_.sync_schedule = VerticalState::SyncSchedule::End;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -305,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_;
|
||||
@@ -328,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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,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.
|
||||
@@ -418,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_);
|
||||
@@ -464,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();
|
||||
});
|
||||
@@ -554,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.
|
||||
@@ -609,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;
|
||||
}
|
||||
@@ -710,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();
|
||||
}
|
||||
}
|
||||
@@ -720,20 +741,23 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
void Video::VideoStream::flush_pixels() {
|
||||
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;
|
||||
// Flush only if there's something to flush.
|
||||
if(pixel_pointer_) {
|
||||
switch(bpp_) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
pixel_pointer_ = 0;
|
||||
@@ -741,13 +765,25 @@ void Video::VideoStream::flush_pixels() {
|
||||
}
|
||||
|
||||
void Video::VideoStream::set_bpp(OutputBpp bpp) {
|
||||
// Terminate the allocated block of memory (if any).
|
||||
flush_pixels();
|
||||
|
||||
// Reset the shifter.
|
||||
// TODO: is flushing like this correct?
|
||||
output_shifter_ = 0;
|
||||
|
||||
// Store the new BPP.
|
||||
bpp_ = 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);
|
||||
}
|
||||
|
||||
@@ -256,7 +260,7 @@ class ConcreteMachine:
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
switch((address >> 5) & 7) {
|
||||
case 5:
|
||||
*cycle.value = vdp_->get_register(address);
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
@@ -299,14 +303,14 @@ class ConcreteMachine:
|
||||
break;
|
||||
|
||||
case 5:
|
||||
vdp_->set_register(address, *cycle.value);
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 7:
|
||||
update_audio();
|
||||
sn76489_.set_register(*cycle.value);
|
||||
sn76489_.write(*cycle.value);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -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;
|
||||
@@ -86,14 +86,14 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
|
||||
}
|
||||
} else if(address >= 0x1800 && address <= 0x180f) {
|
||||
if(isReadOperation(operation))
|
||||
*value = serial_port_VIA_.get_register(address);
|
||||
*value = serial_port_VIA_.read(address);
|
||||
else
|
||||
serial_port_VIA_.set_register(address, *value);
|
||||
serial_port_VIA_.write(address, *value);
|
||||
} else if(address >= 0x1c00 && address <= 0x1c0f) {
|
||||
if(isReadOperation(operation))
|
||||
*value = drive_VIA_.get_register(address);
|
||||
*value = drive_VIA_.read(address);
|
||||
else
|
||||
drive_VIA_.set_register(address, *value);
|
||||
drive_VIA_.write(address, *value);
|
||||
}
|
||||
|
||||
serial_port_VIA_.run_for(Cycles(1));
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
@@ -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,18 @@ 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 {
|
||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||||
if(key != KeyRestore)
|
||||
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);
|
||||
}
|
||||
|
||||
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_;
|
||||
}
|
||||
|
||||
@@ -503,10 +504,10 @@ class ConcreteMachine:
|
||||
if((address&0xfc00) == 0x9000) {
|
||||
if(!(address&0x100)) {
|
||||
update_video();
|
||||
result &= mos6560_.get_register(address);
|
||||
result &= mos6560_.read(address);
|
||||
}
|
||||
if(address & 0x10) result &= user_port_via_.get_register(address);
|
||||
if(address & 0x20) result &= keyboard_via_.get_register(address);
|
||||
if(address & 0x10) result &= user_port_via_.read(address);
|
||||
if(address & 0x20) result &= keyboard_via_.read(address);
|
||||
}
|
||||
*value = result;
|
||||
|
||||
@@ -590,12 +591,12 @@ class ConcreteMachine:
|
||||
// The VIC is selected by bit 8 = 0
|
||||
if(!(address&0x100)) {
|
||||
update_video();
|
||||
mos6560_.set_register(address, *value);
|
||||
mos6560_.write(address, *value);
|
||||
}
|
||||
// The first VIA is selected by bit 4 = 1.
|
||||
if(address & 0x10) user_port_via_.set_register(address, *value);
|
||||
if(address & 0x10) user_port_via_.write(address, *value);
|
||||
// The second VIA is selected by bit 5 = 1.
|
||||
if(address & 0x20) keyboard_via_.set_register(address, *value);
|
||||
if(address & 0x20) keyboard_via_.write(address, *value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,45 +619,49 @@ 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 {
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient::add_typer(string, std::make_unique<CharacterMapper>());
|
||||
}
|
||||
|
||||
void tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape) override final {
|
||||
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 +674,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ class ConcreteMachine:
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool isPressed) override final {
|
||||
void set_key_state(uint16_t key, bool isPressed) final {
|
||||
if(key == KeyBreak) {
|
||||
m6502_.set_reset_line(isPressed);
|
||||
} else {
|
||||
@@ -125,12 +125,12 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -204,7 +204,7 @@ class ConcreteMachine:
|
||||
case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f:
|
||||
if(!isReadOperation(operation)) {
|
||||
update_display();
|
||||
video_output_.set_register(address, *value);
|
||||
video_output_.write(address, *value);
|
||||
video_access_range_ = video_output_.get_memory_access_range();
|
||||
queue_next_display_interrupt();
|
||||
}
|
||||
@@ -260,9 +260,9 @@ class ConcreteMachine:
|
||||
set_key_state(KeyShift, false);
|
||||
}
|
||||
if(isReadOperation(operation))
|
||||
*value = plus3_->get_register(address);
|
||||
*value = plus3_->read(address);
|
||||
else
|
||||
plus3_->set_register(address, *value);
|
||||
plus3_->write(address, *value);
|
||||
}
|
||||
break;
|
||||
case 0xfc00:
|
||||
@@ -379,49 +379,53 @@ 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 {
|
||||
HalfCycles get_typer_delay() final {
|
||||
return m6502_.get_is_resetting() ? Cycles(625*25*128) : Cycles(0); // wait one second if resetting
|
||||
}
|
||||
|
||||
HalfCycles get_typer_frequency() override final {
|
||||
HalfCycles get_typer_frequency() final {
|
||||
return Cycles(625*128*2); // accept a new character every two frames
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) override final {
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient::add_typer(string, std::make_unique<CharacterMapper>());
|
||||
}
|
||||
|
||||
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 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 +438,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 +453,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);
|
||||
|
||||
@@ -34,7 +34,7 @@ enum Key: uint16_t {
|
||||
};
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -250,7 +254,7 @@ void VideoOutput::run_for(const Cycles cycles) {
|
||||
|
||||
// MARK: - Register hub
|
||||
|
||||
void VideoOutput::set_register(int address, uint8_t value) {
|
||||
void VideoOutput::write(int address, uint8_t value) {
|
||||
switch(address & 0xf) {
|
||||
case 0x02:
|
||||
start_screen_address_ = (start_screen_address_ & 0xfe00) | static_cast<uint16_t>((value & 0xe0) << 1);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -46,7 +49,7 @@ class VideoOutput {
|
||||
Writes @c value to the register at @c address. May mutate the results of @c get_next_interrupt,
|
||||
@c get_cycles_until_next_ram_availability and @c get_memory_access_range.
|
||||
*/
|
||||
void set_register(int address, uint8_t value);
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*!
|
||||
Describes an interrupt the video hardware will generate by its identity and scheduling time.
|
||||
@@ -62,7 +65,7 @@ class VideoOutput {
|
||||
The time until signalling returned is the number of cycles after the final one triggered
|
||||
by the most recent call to @c run_for.
|
||||
|
||||
This result may be mutated by calls to @c set_register.
|
||||
This result may be mutated by calls to @c write.
|
||||
*/
|
||||
Interrupt get_next_interrupt();
|
||||
|
||||
|
||||
@@ -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,35 +13,35 @@ 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);
|
||||
}
|
||||
|
||||
void DiskROM::write(uint16_t address, uint8_t value, bool pc_is_outside_bios) {
|
||||
switch(address) {
|
||||
case 0x7ff8: case 0x7ff9: case 0x7ffa: case 0x7ffb:
|
||||
set_register(address, value);
|
||||
break;
|
||||
case 0x7ffc:
|
||||
selected_head_ = value & 1;
|
||||
drives_[0]->set_head(selected_head_);
|
||||
drives_[1]->set_head(selected_head_);
|
||||
WD::WD1770::write(address, value);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t DiskROM::read(uint16_t address) {
|
||||
if(address >= 0x7ff8 && address < 0x7ffc) {
|
||||
return get_register(address);
|
||||
return WD::WD1770::read(address);
|
||||
}
|
||||
if(address == 0x7fff) {
|
||||
return (get_data_request_line() ? 0x00 : 0x80) | (get_interrupt_request_line() ? 0x00 : 0x40);
|
||||
@@ -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(),
|
||||
@@ -366,7 +370,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
// 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 +385,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);
|
||||
@@ -428,7 +432,7 @@ class ConcreteMachine:
|
||||
// TAPION
|
||||
|
||||
// Enable the tape motor.
|
||||
i8255_.set_register(0xab, 0x8);
|
||||
i8255_.write(0xab, 0x8);
|
||||
|
||||
// Disable interrupts.
|
||||
z80_.set_value_of_register(CPU::Z80::Register::IFF1, 0);
|
||||
@@ -509,7 +513,7 @@ class ConcreteMachine:
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
switch(address & 0xff) {
|
||||
case 0x98: case 0x99:
|
||||
*cycle.value = vdp_->get_register(address);
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
@@ -523,7 +527,7 @@ class ConcreteMachine:
|
||||
|
||||
case 0xa8: case 0xa9:
|
||||
case 0xaa: case 0xab:
|
||||
*cycle.value = i8255_.get_register(address);
|
||||
*cycle.value = i8255_.read(address);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -536,7 +540,7 @@ class ConcreteMachine:
|
||||
const int port = address & 0xff;
|
||||
switch(port) {
|
||||
case 0x98: case 0x99:
|
||||
vdp_->set_register(address, *cycle.value);
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
@@ -550,7 +554,7 @@ class ConcreteMachine:
|
||||
|
||||
case 0xa8: case 0xa9:
|
||||
case 0xaa: case 0xab:
|
||||
i8255_.set_register(address, *cycle.value);
|
||||
i8255_.write(address, *cycle.value);
|
||||
break;
|
||||
|
||||
case 0xfc: case 0xfd: case 0xfe: case 0xff:
|
||||
@@ -624,26 +628,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 +660,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 +675,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 +690,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 +760,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);
|
||||
}
|
||||
|
||||
@@ -240,7 +246,7 @@ class ConcreteMachine:
|
||||
*cycle.value = vdp_.last_valid()->get_latched_horizontal_counter();
|
||||
break;
|
||||
case 0x80: case 0x81:
|
||||
*cycle.value = vdp_->get_register(address);
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
@@ -288,10 +294,10 @@ class ConcreteMachine:
|
||||
} break;
|
||||
case 0x40: case 0x41:
|
||||
update_audio();
|
||||
sn76489_.set_register(*cycle.value);
|
||||
sn76489_.write(*cycle.value);
|
||||
break;
|
||||
case 0x80: case 0x81:
|
||||
vdp_->set_register(address, *cycle.value);
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
@@ -340,16 +346,16 @@ 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 {
|
||||
void 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) {
|
||||
@@ -357,28 +363,28 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
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 */
|
||||
81
Machines/Oric/Jasmin.cpp
Normal file
81
Machines/Oric/Jasmin.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// Jasmin.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 05/01/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Jasmin.hpp"
|
||||
|
||||
using namespace Oric;
|
||||
|
||||
// NB: there's some controversy here on WD1770 versus WD1772, but between those two I think
|
||||
// the only difference is stepping rates, and it says 1770 on the schematic I'm looking at.
|
||||
Jasmin::Jasmin() : DiskController(P1770, 8000000, Storage::Disk::Drive::ReadyType::ShugartRDY) {
|
||||
set_is_double_density(true);
|
||||
select_paged_item();
|
||||
}
|
||||
|
||||
void Jasmin::write(int address, uint8_t value) {
|
||||
switch(address) {
|
||||
// Set side.
|
||||
case 0x3f8: {
|
||||
const int head = value & 1;
|
||||
for_all_drives([head] (Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_head(head);
|
||||
});
|
||||
} break;
|
||||
|
||||
case 0x3f9:
|
||||
/* TODO: reset. */
|
||||
break;
|
||||
|
||||
case 0x3fa: {
|
||||
// If b0, enable overlay RAM.
|
||||
enable_overlay_ram_ = value & 1;
|
||||
select_paged_item();
|
||||
} break;
|
||||
|
||||
case 0x3fb:
|
||||
// If b0, disable BASIC ROM.
|
||||
disable_basic_rom_ = value & 1;
|
||||
select_paged_item();
|
||||
break;
|
||||
|
||||
case 0x3fc: case 0x3fd: case 0x3fe: case 0x3ff: {
|
||||
// Updated selected drives mask.
|
||||
const uint8_t mask = uint8_t(1 << (address - 0x3fc));
|
||||
selected_drives_ = (selected_drives_ & ~mask) | (mask * (value & 1));
|
||||
|
||||
// Select drive.
|
||||
set_drive(selected_drives_);
|
||||
|
||||
// Update motor inputs: apply the motor only to selected drives.
|
||||
// Which may or may not be correct.
|
||||
for_all_drives([mask, this] (Storage::Disk::Drive &drive, size_t index) {
|
||||
const uint8_t shift_mask = uint8_t(1 << index);
|
||||
drive.set_motor_on( (mask & shift_mask) ? motor_on_ : false );
|
||||
});
|
||||
} break;
|
||||
|
||||
default:
|
||||
return WD::WD1770::write(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
void Jasmin::set_motor_on(bool on) {
|
||||
motor_on_ = on;
|
||||
get_drive().set_motor_on(motor_on_);
|
||||
if(observer_) {
|
||||
observer_->set_led_status("Jasmin", on);
|
||||
}
|
||||
}
|
||||
|
||||
void Jasmin::set_activity_observer(Activity::Observer *observer) {
|
||||
observer_ = observer;
|
||||
if(observer) {
|
||||
observer->register_led("Jasmin");
|
||||
observer_->set_led_status("Jasmin", motor_on_);
|
||||
}
|
||||
}
|
||||
36
Machines/Oric/Jasmin.hpp
Normal file
36
Machines/Oric/Jasmin.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Jasmin.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 05/01/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Jasmin_hpp
|
||||
#define Jasmin_hpp
|
||||
|
||||
#include "../../Components/1770/1770.hpp"
|
||||
#include "../../Activity/Observer.hpp"
|
||||
#include "DiskController.hpp"
|
||||
|
||||
namespace Oric {
|
||||
|
||||
class Jasmin: public DiskController {
|
||||
public:
|
||||
Jasmin();
|
||||
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
private:
|
||||
void set_motor_on(bool on) final;
|
||||
bool motor_on_ = false;
|
||||
uint8_t selected_drives_ = 0;
|
||||
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
#endif /* Jasmin_hpp */
|
||||
@@ -45,6 +45,9 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
|
||||
BIND(Space, KeySpace);
|
||||
BIND(Enter, KeyReturn);
|
||||
|
||||
BIND(F12, KeyNMI);
|
||||
BIND(F1, KeyJasminReset);
|
||||
}
|
||||
#undef BIND
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user