mirror of
https://github.com/TomHarte/CLK.git
synced 2025-09-25 08:25:09 +00:00
Compare commits
435 Commits
2019-11-17
...
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 | ||
|
ea45ae78d1 | ||
|
cb7d6c185c | ||
|
5be30b1f7b | ||
|
0bf1a87f4c | ||
|
b184426f2b | ||
|
2456fb120d | ||
|
23ed9ad2de | ||
|
017681a97c | ||
|
153f60735d | ||
|
90b899c00e | ||
|
5ce8d7c0e5 | ||
|
c11fe25537 | ||
|
c4edd635c5 | ||
|
0a12893d63 | ||
|
8e777c299f | ||
|
09513ec14c | ||
|
e23d1a2958 | ||
|
6449403f6a | ||
|
c8fe66092b | ||
|
b33218c61e | ||
|
8ce26e7182 | ||
|
47068ee081 | ||
|
5361ee2526 | ||
|
214b6a254a | ||
|
93f6964d8a | ||
|
13f11e071a | ||
|
f7825dd2a2 | ||
|
a9d1f5d925 | ||
|
2757e5d600 | ||
|
5026de9653 | ||
|
5fa8e046d8 | ||
|
ec9357e080 | ||
|
f8dd33b645 | ||
|
de43e86310 | ||
|
314973a5ef | ||
|
d26ce65236 | ||
|
1de4f179c0 | ||
|
3cb5684d95 | ||
|
a9a92de954 | ||
|
daacd6805e | ||
|
54fe01b532 | ||
|
42dd70dbff | ||
|
e59de71d79 | ||
|
a8ba3607b7 | ||
|
4205e95883 | ||
|
f633cf4c3f | ||
|
dfa6b11737 | ||
|
42926e72cc | ||
|
80cb06eb33 | ||
|
5068328a15 | ||
|
adc2b77833 | ||
|
99415217dc | ||
|
48d519d475 | ||
|
ed831e5912 | ||
|
1db7c7989b | ||
|
b2bed82da6 | ||
|
afae1443b4 | ||
|
0dae608da5 | ||
|
8a1fe99fa4 | ||
|
ac604b30f3 | ||
|
b035b92f33 | ||
|
d25b48878c | ||
|
34a3790e11 | ||
|
f3378f3e3e | ||
|
78accc1db1 | ||
|
a756985e18 | ||
|
30e0d4aa30 | ||
|
de72c66c64 | ||
|
6edd3c9698 | ||
|
5456a4a39d | ||
|
66d9b60b98 | ||
|
274867579b | ||
|
a847654ef2 | ||
|
05d77d3297 | ||
|
e9318efeb6 | ||
|
25da5ebdae | ||
|
cf16f41939 | ||
|
08f2877382 | ||
|
6f4444d834 | ||
|
993dfeae1b | ||
|
b4fd506361 | ||
|
e5440a4146 | ||
|
57ce10418f | ||
|
47508d50a7 | ||
|
56cc191a8b | ||
|
2a1520c04e | ||
|
3d83f5ab49 | ||
|
0007dc23b3 | ||
|
416d68ab3a | ||
|
ed7f171736 | ||
|
0e066f0f70 | ||
|
3e6f51f5cf | ||
|
797abae4b3 | ||
|
4605b1b264 | ||
|
d802e8aee3 | ||
|
206ab380c7 | ||
|
d85ae21b2f | ||
|
470cc572fd | ||
|
f0d9d8542b | ||
|
d2390fcb11 | ||
|
5ce612cb38 | ||
|
ec7aa2d355 | ||
|
9464658d1e | ||
|
a3e64cae41 | ||
|
e969b386f1 | ||
|
af9c0aca97 | ||
|
8a2ac87209 | ||
|
096b447b4b | ||
|
0d23f141d6 | ||
|
84167af54f | ||
|
8be26502c4 | ||
|
ba2436206f | ||
|
60a9b260b1 | ||
|
e603fc6aaa | ||
|
81cc278b98 | ||
|
4c068e9bb8 | ||
|
f23c5ada31 | ||
|
dc1abd874e | ||
|
1bf4686c59 | ||
|
a500fbcd73 | ||
|
d0ef41f11e | ||
|
adf6723bf6 | ||
|
37e26c0c37 | ||
|
ac1575be27 | ||
|
923287bf01 | ||
|
77fe14cdb3 | ||
|
c00ae7ce6a | ||
|
d5b2e6514a | ||
|
fc7f46006e | ||
|
41503d7253 | ||
|
f88c942fef | ||
|
4bcf217324 | ||
|
f6f2b4b90f | ||
|
95b5db4d87 | ||
|
de4403e021 | ||
|
0a405d1c06 | ||
|
768b3709b8 | ||
|
7cc5d0b209 | ||
|
c2646a415f | ||
|
e1c7a140d0 | ||
|
7cd11ecb7f | ||
|
4dd235f677 | ||
|
a7cfb840ef | ||
|
acfe2c63b8 | ||
|
b192381928 | ||
|
c785797da6 | ||
|
0408592ada | ||
|
407cc78c78 | ||
|
4536c6a224 | ||
|
0ed87c61bd | ||
|
332f0d6167 | ||
|
08a27bdec7 | ||
|
288cabbad1 | ||
|
7ff57f8cdf | ||
|
06edeea866 | ||
|
3c77d3bda0 | ||
|
72cb3a1cf6 | ||
|
e0ceab6642 | ||
|
894066984c | ||
|
c91495d068 | ||
|
e787c03530 | ||
|
b12136691a | ||
|
c04d2f6c6e | ||
|
6990abc0d3 | ||
|
0ce5057fd9 | ||
|
ade8df7217 | ||
|
b98703bd5b | ||
|
82c984afa4 | ||
|
1202b0a65f | ||
|
facc0a1976 | ||
|
25da8b7787 | ||
|
253dd84109 |
2
.github/workflows/ccpp.yml
vendored
2
.github/workflows/ccpp.yml
vendored
@@ -17,6 +17,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install libsdl2-dev scons
|
||||
run: sudo apt-get --allow-releaseinfo-change update; sudo apt-get install libsdl2-dev scons
|
||||
- name: Make
|
||||
run: cd OSBindings/SDL; scons
|
||||
|
@@ -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>
|
||||
|
||||
@@ -18,7 +18,7 @@ using namespace Analyser::Static::Acorn;
|
||||
|
||||
std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
// c.f. http://beebwiki.mdfs.net/Acorn_DFS_disc_format
|
||||
std::unique_ptr<Catalogue> catalogue(new Catalogue);
|
||||
auto catalogue = std::make_unique<Catalogue>();
|
||||
Storage::Encodings::MFM::Parser parser(false, disk);
|
||||
|
||||
Storage::Encodings::MFM::Sector *names = parser.get_sector(0, 0, 0);
|
||||
@@ -75,7 +75,7 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s
|
||||
return catalogue;
|
||||
}
|
||||
std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
std::unique_ptr<Catalogue> catalogue(new Catalogue);
|
||||
auto catalogue = std::make_unique<Catalogue>();
|
||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||
|
||||
Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.get_sector(0, 0, 1);
|
||||
|
@@ -58,7 +58,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
}
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::Electron;
|
||||
target->confidence = 0.5; // TODO: a proper estimation
|
||||
target->has_dfs = false;
|
||||
|
@@ -10,13 +10,13 @@
|
||||
|
||||
#include <deque>
|
||||
|
||||
#include "../../../NumberTheory/CRC.hpp"
|
||||
#include "../../../Numeric/CRC.hpp"
|
||||
#include "../../../Storage/Tape/Parsers/Acorn.hpp"
|
||||
|
||||
using namespace Analyser::Static::Acorn;
|
||||
|
||||
static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::Tape::Tape> &tape, Storage::Tape::Acorn::Parser &parser) {
|
||||
std::unique_ptr<File::Chunk> new_chunk(new File::Chunk);
|
||||
auto new_chunk = std::make_unique<File::Chunk>();
|
||||
int shift_register = 0;
|
||||
|
||||
// TODO: move this into the parser
|
||||
@@ -90,7 +90,7 @@ static std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &chunks) {
|
||||
if(!chunks.size()) return nullptr;
|
||||
|
||||
// accumulate chunks for as long as block number is sequential and the end-of-file bit isn't set
|
||||
std::unique_ptr<File> file(new File);
|
||||
auto file = std::make_unique<File>();
|
||||
|
||||
uint16_t block_number = 0;
|
||||
|
||||
|
@@ -181,7 +181,7 @@ static bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, co
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
TargetList destination;
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::AmstradCPC;
|
||||
target->confidence = 0.5;
|
||||
|
||||
|
@@ -10,7 +10,7 @@
|
||||
#include "Target.hpp"
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::AppleII::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
auto target = std::unique_ptr<Target>(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::AppleII;
|
||||
target->media = media;
|
||||
|
||||
|
@@ -183,7 +183,7 @@ static void DeterminePagingForCartridge(Target &target, const Storage::Cartridge
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Atari2600::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
// TODO: sanity checking; is this image really for an Atari 2600?
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::Atari2600;
|
||||
target->confidence = 0.5;
|
||||
target->media.cartridges = media.cartridges;
|
||||
|
@@ -54,7 +54,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Coleco::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
TargetList targets;
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::ColecoVision;
|
||||
target->confidence = 1.0f - 1.0f / 32768.0f;
|
||||
target->media.cartridges = ColecoCartridgesFrom(media.cartridges);
|
||||
|
@@ -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.reset(new 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_;
|
||||
@@ -125,7 +127,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||
}
|
||||
|
||||
std::shared_ptr<Sector> get_next_sector() {
|
||||
std::shared_ptr<Sector> sector(new Sector);
|
||||
auto sector = std::make_shared<Sector>();
|
||||
const int max_index_count = index_count_ + 2;
|
||||
|
||||
while(index_count_ < max_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;
|
||||
|
@@ -16,6 +16,7 @@
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
using namespace Analyser::Static::Commodore;
|
||||
@@ -44,7 +45,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
TargetList destination;
|
||||
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::Vic20; // TODO: machine estimation
|
||||
target->confidence = 0.5; // TODO: a proper estimation
|
||||
|
||||
@@ -78,7 +79,7 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
||||
}
|
||||
|
||||
if(!files.empty()) {
|
||||
target->memory_model = Target::MemoryModel::Unexpanded;
|
||||
auto memory_model = Target::MemoryModel::Unexpanded;
|
||||
std::ostringstream string_stream;
|
||||
string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ",";
|
||||
if(files.front().is_basic()) {
|
||||
@@ -94,16 +95,18 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
||||
default:
|
||||
LOG("Unrecognised loading address for Commodore program: " << PADHEX(4) << files.front().starting_address);
|
||||
case 0x1001:
|
||||
target->memory_model = Target::MemoryModel::Unexpanded;
|
||||
memory_model = Target::MemoryModel::Unexpanded;
|
||||
break;
|
||||
case 0x1201:
|
||||
target->memory_model = Target::MemoryModel::ThirtyTwoKB;
|
||||
memory_model = Target::MemoryModel::ThirtyTwoKB;
|
||||
break;
|
||||
case 0x0401:
|
||||
target->memory_model = Target::MemoryModel::EightKB;
|
||||
memory_model = Target::MemoryModel::EightKB;
|
||||
break;
|
||||
}
|
||||
|
||||
target->set_memory_model(memory_model);
|
||||
|
||||
// General approach: increase memory size conservatively such that the largest file found will fit.
|
||||
// for(File &file : files) {
|
||||
// std::size_t file_size = file.data.size();
|
||||
@@ -145,13 +148,52 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
||||
}
|
||||
|
||||
if(!target->media.empty()) {
|
||||
// Inspect filename for a region hint.
|
||||
// Inspect filename for configuration hints.
|
||||
std::string lowercase_name = file_name;
|
||||
std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower);
|
||||
|
||||
// Hint 1: 'ntsc' anywhere in the name implies America.
|
||||
if(lowercase_name.find("ntsc") != std::string::npos) {
|
||||
target->region = Analyser::Static::Commodore::Target::Region::American;
|
||||
}
|
||||
|
||||
// Potential additional hints: check for TheC64 tags.
|
||||
auto final_underscore = lowercase_name.find_last_of('_');
|
||||
if(final_underscore != std::string::npos) {
|
||||
auto iterator = lowercase_name.begin() + ssize_t(final_underscore) + 1;
|
||||
|
||||
while(iterator != lowercase_name.end()) {
|
||||
// Grab the next tag.
|
||||
char next_tag[3] = {0, 0, 0};
|
||||
next_tag[0] = *iterator++;
|
||||
if(iterator == lowercase_name.end()) break;
|
||||
next_tag[1] = *iterator++;
|
||||
|
||||
// Exit early if attempting to read another tag has run over the file extension.
|
||||
if(next_tag[0] == '.' || next_tag[1] == '.') break;
|
||||
|
||||
// Check whether it's anything.
|
||||
target->enabled_ram.bank0 |= !strcmp(next_tag, "b0");
|
||||
target->enabled_ram.bank1 |= !strcmp(next_tag, "b1");
|
||||
target->enabled_ram.bank2 |= !strcmp(next_tag, "b2");
|
||||
target->enabled_ram.bank3 |= !strcmp(next_tag, "b3");
|
||||
target->enabled_ram.bank5 |= !strcmp(next_tag, "b5");
|
||||
if(!strcmp(next_tag, "tn")) { // i.e. NTSC.
|
||||
target->region = Analyser::Static::Commodore::Target::Region::American;
|
||||
}
|
||||
if(!strcmp(next_tag, "tp")) { // i.e. PAL.
|
||||
target->region = Analyser::Static::Commodore::Target::Region::European;
|
||||
}
|
||||
|
||||
// Unhandled:
|
||||
//
|
||||
// M6: this is a C64 file.
|
||||
// MV: this is a Vic-20 file.
|
||||
// J1/J2: this C64 file should have the primary joystick in slot 1/2.
|
||||
// RO: this disk image should be treated as read-only.
|
||||
}
|
||||
}
|
||||
|
||||
// Attach a 1540 if there are any disks here.
|
||||
target->has_c1540 = !target->media.disks.empty();
|
||||
|
||||
|
@@ -31,7 +31,26 @@ struct Target: public ::Analyser::Static::Target {
|
||||
Swedish
|
||||
};
|
||||
|
||||
MemoryModel memory_model = MemoryModel::Unexpanded;
|
||||
/// Maps from a named memory model to a bank enabled/disabled set.
|
||||
void set_memory_model(MemoryModel memory_model) {
|
||||
// This is correct for unexpanded and 32kb memory models.
|
||||
enabled_ram.bank0 = enabled_ram.bank1 =
|
||||
enabled_ram.bank2 = enabled_ram.bank3 =
|
||||
enabled_ram.bank5 = memory_model == MemoryModel::ThirtyTwoKB;
|
||||
|
||||
// Bank 0 will need to be enabled if this is an 8kb machine.
|
||||
enabled_ram.bank0 |= memory_model == MemoryModel::EightKB;
|
||||
}
|
||||
struct {
|
||||
bool bank0 = false;
|
||||
bool bank1 = false;
|
||||
bool bank2 = false;
|
||||
bool bank3 = false;
|
||||
bool bank5 = false;
|
||||
// Sic. There is no bank 4; this is because the area that logically would be
|
||||
// bank 4 is occupied by the character ROM, colour RAM, hardware registers, etc.
|
||||
} enabled_ram;
|
||||
|
||||
Region region = Region::European;
|
||||
bool has_c1540 = false;
|
||||
std::string loading_command;
|
||||
|
@@ -34,7 +34,7 @@ static std::unique_ptr<Analyser::Static::Target> CartridgeTarget(
|
||||
output_segments.emplace_back(start_address, segment.data);
|
||||
}
|
||||
|
||||
std::unique_ptr<Analyser::Static::MSX::Target> target(new Analyser::Static::MSX::Target);
|
||||
auto target = std::make_unique<Analyser::Static::MSX::Target>();
|
||||
target->machine = Analyser::Machine::MSX;
|
||||
target->confidence = confidence;
|
||||
|
||||
@@ -269,7 +269,7 @@ Analyser::Static::TargetList Analyser::Static::MSX::GetTargets(const Media &medi
|
||||
std::move(cartridge_targets.begin(), cartridge_targets.end(), std::back_inserter(destination));
|
||||
|
||||
// Consider building a target for disks and/or tapes.
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
|
||||
// Check tapes for loadable files.
|
||||
for(auto &tape : media.tapes) {
|
||||
|
@@ -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,8 +102,51 @@ 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) {
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::Oric;
|
||||
target->confidence = 0.5;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &med
|
||||
return {};
|
||||
|
||||
TargetList targets;
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
|
||||
target->machine = Machine::MasterSystem;
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -137,7 +137,7 @@ template <class T> class WrappedInt {
|
||||
// bool operator () is not supported because it offers an implicit cast to int, which is prone silently to permit misuse
|
||||
|
||||
/// @returns The underlying int, cast to an integral type of your choosing.
|
||||
template<typename Type = IntType> forceinline constexpr Type as() { return Type(length_); }
|
||||
template<typename Type = IntType> forceinline constexpr Type as() const { return Type(length_); }
|
||||
|
||||
/// @returns The underlying int, in its native form.
|
||||
forceinline constexpr IntType as_integral() const { return length_; }
|
||||
|
@@ -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 */
|
||||
|
@@ -29,7 +29,7 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
|
||||
/// Adds time to the actor.
|
||||
forceinline void operator += (const LocalTimeScale &rhs) {
|
||||
if(multiplier != 1) {
|
||||
if constexpr (multiplier != 1) {
|
||||
time_since_update_ += rhs * multiplier;
|
||||
} else {
|
||||
time_since_update_ += rhs;
|
||||
@@ -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_;
|
||||
@@ -52,8 +59,9 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
forceinline void flush() {
|
||||
if(!is_flushed_) {
|
||||
is_flushed_ = true;
|
||||
if(divider == 1) {
|
||||
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
||||
if constexpr (divider == 1) {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -18,19 +18,19 @@ using namespace WD;
|
||||
WD1770::WD1770(Personality p) :
|
||||
Storage::Disk::MFMController(8000000),
|
||||
personality_(p),
|
||||
interesting_event_mask_(static_cast<int>(Event1770::Command)) {
|
||||
interesting_event_mask_(int(Event1770::Command)) {
|
||||
set_is_double_density(false);
|
||||
posit_event(static_cast<int>(Event1770::Command));
|
||||
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) {
|
||||
if(value == 0xd0) {
|
||||
// Force interrupt **immediately**.
|
||||
LOG("Force interrupt immediately");
|
||||
posit_event(static_cast<int>(Event1770::ForceInterrupt));
|
||||
posit_event(int(Event1770::ForceInterrupt));
|
||||
} else {
|
||||
ERROR("!!!TODO: force interrupt!!!");
|
||||
update_status([] (Status &status) {
|
||||
@@ -39,7 +39,7 @@ void WD1770::set_register(int address, uint8_t value) {
|
||||
}
|
||||
} else {
|
||||
command_ = value;
|
||||
posit_event(static_cast<int>(Event1770::Command));
|
||||
posit_event(int(Event1770::Command));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -54,27 +54,35 @@ 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) {
|
||||
status.interrupt_request = false;
|
||||
});
|
||||
uint8_t status =
|
||||
(status_.write_protect ? Flag::WriteProtect : 0) |
|
||||
(status_.crc_error ? Flag::CRCError : 0) |
|
||||
(status_.busy ? Flag::Busy : 0);
|
||||
(status_.crc_error ? Flag::CRCError : 0) |
|
||||
(status_.busy ? Flag::Busy : 0);
|
||||
|
||||
// Per Jean Louis-Guérin's documentation:
|
||||
//
|
||||
// * the write-protect bit is locked into place by a type 2 or type 3 command, but is
|
||||
// read live after a type 1.
|
||||
// * the track 0 bit is captured during a type 1 instruction and lost upon any other type,
|
||||
// it is not live sampled.
|
||||
switch(status_.type) {
|
||||
case Status::One:
|
||||
status |=
|
||||
(get_drive().get_is_track_zero() ? Flag::TrackZero : 0) |
|
||||
(status_.seek_error ? Flag::SeekError : 0);
|
||||
// TODO: index hole
|
||||
(status_.track_zero ? Flag::TrackZero : 0) |
|
||||
(status_.seek_error ? Flag::SeekError : 0) |
|
||||
(get_drive().get_is_read_only() ? Flag::WriteProtect : 0) |
|
||||
(get_drive().get_index_pulse() ? Flag::Index : 0);
|
||||
break;
|
||||
|
||||
case Status::Two:
|
||||
case Status::Three:
|
||||
status |=
|
||||
(status_.write_protect ? Flag::WriteProtect : 0) |
|
||||
(status_.record_type ? Flag::RecordType : 0) |
|
||||
(status_.lost_data ? Flag::LostData : 0) |
|
||||
(status_.data_request ? Flag::DataRequest : 0) |
|
||||
@@ -91,10 +99,15 @@ uint8_t WD1770::get_register(int address) {
|
||||
if(status_.type == Status::One)
|
||||
status |= (status_.spin_up ? Flag::SpinUp : 0);
|
||||
}
|
||||
// LOG("Returned status " << PADHEX(2) << int(status) << " of type " << 1+int(status_.type));
|
||||
return status;
|
||||
}
|
||||
case 1: return track_;
|
||||
case 2: return sector_;
|
||||
case 1:
|
||||
LOG("Returned track " << int(track_));
|
||||
return track_;
|
||||
case 2:
|
||||
LOG("Returned sector " << int(sector_));
|
||||
return sector_;
|
||||
case 3:
|
||||
update_status([] (Status &status) {
|
||||
status.data_request = false;
|
||||
@@ -110,25 +123,27 @@ void WD1770::run_for(const Cycles cycles) {
|
||||
const auto number_of_cycles = cycles.as_integral();
|
||||
if(delay_time_ <= number_of_cycles) {
|
||||
delay_time_ = 0;
|
||||
posit_event(static_cast<int>(Event1770::Timer));
|
||||
posit_event(int(Event1770::Timer));
|
||||
} else {
|
||||
delay_time_ -= number_of_cycles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(mask); return; case __LINE__:
|
||||
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = int(mask); return; case __LINE__:
|
||||
#define WAIT_FOR_TIME(ms) resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer);
|
||||
#define WAIT_FOR_BYTES(count) resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = static_cast<int>(Event::Token); return; }
|
||||
#define WAIT_FOR_BYTES(count) resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = int(Event::Token); return; }
|
||||
#define BEGIN_SECTION() switch(resume_point_) { default:
|
||||
#define END_SECTION() (void)0; }
|
||||
|
||||
#define READ_ID() \
|
||||
if(new_event_type == static_cast<int>(Event::Token)) { \
|
||||
if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; } \
|
||||
else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { \
|
||||
if(new_event_type == int(Event::Token)) { \
|
||||
if(!distance_into_section_ && get_latest_token().type == Token::ID) {\
|
||||
set_data_mode(DataMode::Reading); \
|
||||
++distance_into_section_; \
|
||||
} else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { \
|
||||
header_[distance_into_section_ - 1] = get_latest_token().byte_value; \
|
||||
distance_into_section_++; \
|
||||
++distance_into_section_; \
|
||||
} \
|
||||
}
|
||||
|
||||
@@ -161,10 +176,10 @@ void WD1770::run_for(const Cycles cycles) {
|
||||
// +--------+----------+-------------------------+
|
||||
|
||||
void WD1770::posit_event(int new_event_type) {
|
||||
if(new_event_type == static_cast<int>(Event::IndexHole)) {
|
||||
if(new_event_type == int(Event::IndexHole)) {
|
||||
index_hole_count_++;
|
||||
if(index_hole_count_target_ == index_hole_count_) {
|
||||
posit_event(static_cast<int>(Event1770::IndexHoleTarget));
|
||||
posit_event(int(Event1770::IndexHoleTarget));
|
||||
index_hole_count_target_ = -1;
|
||||
}
|
||||
|
||||
@@ -179,15 +194,16 @@ void WD1770::posit_event(int new_event_type) {
|
||||
}
|
||||
}
|
||||
|
||||
if(new_event_type == static_cast<int>(Event1770::ForceInterrupt)) {
|
||||
if(new_event_type == int(Event1770::ForceInterrupt)) {
|
||||
interesting_event_mask_ = 0;
|
||||
resume_point_ = 0;
|
||||
update_status([] (Status &status) {
|
||||
status.type = Status::One;
|
||||
status.data_request = false;
|
||||
status.spin_up = false;
|
||||
});
|
||||
} else {
|
||||
if(!(interesting_event_mask_ & static_cast<int>(new_event_type))) return;
|
||||
if(!(interesting_event_mask_ & int(new_event_type))) return;
|
||||
interesting_event_mask_ &= ~new_event_type;
|
||||
}
|
||||
|
||||
@@ -210,6 +226,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
update_status([] (Status &status) {
|
||||
status.busy = true;
|
||||
status.interrupt_request = false;
|
||||
status.track_zero = false; // Always reset by a non-type 1; so reset regardless and set properly later.
|
||||
});
|
||||
|
||||
LOG("Starting " << PADHEX(2) << int(command_));
|
||||
@@ -242,6 +259,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
status.data_request = false;
|
||||
});
|
||||
|
||||
LOG("Step/Seek/Restore with track " << int(track_) << " data " << int(data_));
|
||||
if(!has_motor_on_line() && !has_head_load_line()) goto test_type1_type;
|
||||
|
||||
if(has_motor_on_line()) goto begin_type1_spin_up;
|
||||
@@ -274,16 +292,16 @@ void WD1770::posit_event(int new_event_type) {
|
||||
}
|
||||
|
||||
perform_seek_or_restore_command:
|
||||
if(track_ == data_) goto verify;
|
||||
if(track_ == data_) goto verify_seek;
|
||||
step_direction_ = (data_ > track_);
|
||||
|
||||
adjust_track:
|
||||
if(step_direction_) track_++; else track_--;
|
||||
if(step_direction_) ++track_; else --track_;
|
||||
|
||||
perform_step:
|
||||
if(!step_direction_ && get_drive().get_is_track_zero()) {
|
||||
track_ = 0;
|
||||
goto verify;
|
||||
goto verify_seek;
|
||||
}
|
||||
get_drive().step(Storage::Disk::HeadPosition(step_direction_ ? 1 : -1));
|
||||
Cycles::IntType time_to_wait;
|
||||
@@ -295,14 +313,17 @@ void WD1770::posit_event(int new_event_type) {
|
||||
case 3: time_to_wait = (personality_ == P1772) ? 3 : 30; break;
|
||||
}
|
||||
WAIT_FOR_TIME(time_to_wait);
|
||||
if(command_ >> 5) goto verify;
|
||||
if(command_ >> 5) goto verify_seek;
|
||||
goto perform_seek_or_restore_command;
|
||||
|
||||
perform_step_command:
|
||||
if(command_ & 0x10) goto adjust_track;
|
||||
goto perform_step;
|
||||
|
||||
verify:
|
||||
verify_seek:
|
||||
update_status([this] (Status &status) {
|
||||
status.track_zero = get_drive().get_is_track_zero();
|
||||
});
|
||||
if(!(command_ & 0x04)) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
@@ -311,17 +332,20 @@ void WD1770::posit_event(int new_event_type) {
|
||||
distance_into_section_ = 0;
|
||||
|
||||
verify_read_data:
|
||||
WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token));
|
||||
WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token));
|
||||
READ_ID();
|
||||
|
||||
if(index_hole_count_ == 6) {
|
||||
LOG("Nothing found to verify");
|
||||
update_status([] (Status &status) {
|
||||
status.seek_error = true;
|
||||
});
|
||||
goto wait_for_command;
|
||||
}
|
||||
if(distance_into_section_ == 7) {
|
||||
distance_into_section_ = 0;
|
||||
set_data_mode(DataMode::Scanning);
|
||||
|
||||
if(get_crc_generator().get_value()) {
|
||||
update_status([] (Status &status) {
|
||||
status.crc_error = true;
|
||||
@@ -336,8 +360,6 @@ void WD1770::posit_event(int new_event_type) {
|
||||
});
|
||||
goto wait_for_command;
|
||||
}
|
||||
|
||||
distance_into_section_ = 0;
|
||||
}
|
||||
goto verify_read_data;
|
||||
|
||||
@@ -394,8 +416,11 @@ void WD1770::posit_event(int new_event_type) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
|
||||
distance_into_section_ = 0;
|
||||
set_data_mode(DataMode::Scanning);
|
||||
|
||||
type2_get_header:
|
||||
WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token));
|
||||
WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token));
|
||||
READ_ID();
|
||||
|
||||
if(index_hole_count_ == 5) {
|
||||
@@ -406,8 +431,10 @@ void WD1770::posit_event(int new_event_type) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
if(distance_into_section_ == 7) {
|
||||
LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||
distance_into_section_ = 0;
|
||||
set_data_mode(DataMode::Scanning);
|
||||
|
||||
LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||
if( header_[0] == track_ && header_[2] == sector_ &&
|
||||
(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) {
|
||||
LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||
@@ -424,7 +451,6 @@ void WD1770::posit_event(int new_event_type) {
|
||||
});
|
||||
goto type2_read_or_write_data;
|
||||
}
|
||||
distance_into_section_ = 0;
|
||||
}
|
||||
goto type2_get_header;
|
||||
|
||||
@@ -455,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;
|
||||
}
|
||||
@@ -467,6 +493,9 @@ void WD1770::posit_event(int new_event_type) {
|
||||
header_[distance_into_section_] = get_latest_token().byte_value;
|
||||
distance_into_section_++;
|
||||
if(distance_into_section_ == 2) {
|
||||
distance_into_section_ = 0;
|
||||
set_data_mode(DataMode::Scanning);
|
||||
|
||||
if(get_crc_generator().get_value()) {
|
||||
LOG("CRC error; terminating");
|
||||
update_status([this] (Status &status) {
|
||||
@@ -475,11 +504,13 @@ void WD1770::posit_event(int new_event_type) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
|
||||
LOG("Finished reading sector " << std::dec << int(sector_));
|
||||
|
||||
if(command_ & 0x10) {
|
||||
sector_++;
|
||||
LOG("Advancing to search for sector " << std::dec << int(sector_));
|
||||
goto test_type2_write_protection;
|
||||
}
|
||||
LOG("Finished reading sector " << std::dec << int(sector_));
|
||||
goto wait_for_command;
|
||||
}
|
||||
goto type2_check_crc;
|
||||
@@ -533,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;
|
||||
}
|
||||
|
||||
@@ -612,8 +643,8 @@ void WD1770::posit_event(int new_event_type) {
|
||||
distance_into_section_ = 0;
|
||||
|
||||
read_address_get_header:
|
||||
WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token));
|
||||
if(new_event_type == static_cast<int>(Event::Token)) {
|
||||
WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token));
|
||||
if(new_event_type == int(Event::Token)) {
|
||||
if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; }
|
||||
else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {
|
||||
if(status_.data_request) {
|
||||
@@ -627,9 +658,11 @@ void WD1770::posit_event(int new_event_type) {
|
||||
update_status([] (Status &status) {
|
||||
status.data_request = true;
|
||||
});
|
||||
distance_into_section_++;
|
||||
++distance_into_section_;
|
||||
|
||||
if(distance_into_section_ == 7) {
|
||||
distance_into_section_ = 0;
|
||||
|
||||
if(get_crc_generator().get_value()) {
|
||||
update_status([] (Status &status) {
|
||||
status.crc_error = true;
|
||||
@@ -653,7 +686,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
index_hole_count_ = 0;
|
||||
|
||||
read_track_read_byte:
|
||||
WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole));
|
||||
WAIT_FOR_EVENT(int(Event::Token) | int(Event::IndexHole));
|
||||
if(index_hole_count_) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
@@ -720,7 +753,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
case 0xfd: case 0xfe:
|
||||
// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022
|
||||
write_raw_short(
|
||||
static_cast<uint16_t>(
|
||||
uint16_t(
|
||||
0xa022 |
|
||||
((data_ & 0x80) << 7) |
|
||||
((data_ & 0x40) << 6) |
|
||||
@@ -788,7 +821,11 @@ void WD1770::set_motor_on(bool motor_on) {}
|
||||
|
||||
void WD1770::set_head_loaded(bool head_loaded) {
|
||||
head_is_loaded_ = head_loaded;
|
||||
if(head_loaded) posit_event(static_cast<int>(Event1770::HeadLoad));
|
||||
if(head_loaded) posit_event(int(Event1770::HeadLoad));
|
||||
}
|
||||
|
||||
bool WD1770::get_head_loaded() {
|
||||
return head_is_loaded_;
|
||||
}
|
||||
|
||||
ClockingHint::Preference WD1770::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); }
|
||||
@@ -96,6 +99,7 @@ class WD1770: public Storage::Disk::MFMController {
|
||||
bool data_request = false;
|
||||
bool interrupt_request = false;
|
||||
bool busy = false;
|
||||
bool track_zero = false;
|
||||
enum {
|
||||
One, Two, Three
|
||||
} type = One;
|
||||
|
@@ -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];
|
||||
|
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "6850.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
using namespace Motorola::ACIA;
|
||||
|
||||
const HalfCycles ACIA::SameAsTransmit;
|
||||
@@ -21,6 +23,7 @@ ACIA::ACIA(HalfCycles transmit_clock_rate, HalfCycles receive_clock_rate) :
|
||||
|
||||
uint8_t ACIA::read(int address) {
|
||||
if(address&1) {
|
||||
overran_ = false;
|
||||
received_data_ |= NoValueMask;
|
||||
update_interrupt_line();
|
||||
return uint8_t(received_data_);
|
||||
@@ -29,6 +32,20 @@ uint8_t ACIA::read(int address) {
|
||||
}
|
||||
}
|
||||
|
||||
void ACIA::reset() {
|
||||
transmit.reset_writing();
|
||||
transmit.write(true);
|
||||
request_to_send.reset_writing();
|
||||
|
||||
bits_received_ = bits_incoming_ = 0;
|
||||
receive_interrupt_enabled_ = transmit_interrupt_enabled_ = false;
|
||||
overran_ = false;
|
||||
next_transmission_ = received_data_ = NoValueMask;
|
||||
|
||||
update_interrupt_line();
|
||||
assert(!interrupt_line_);
|
||||
}
|
||||
|
||||
void ACIA::write(int address, uint8_t value) {
|
||||
if(address&1) {
|
||||
next_transmission_ = value;
|
||||
@@ -36,9 +53,7 @@ void ACIA::write(int address, uint8_t value) {
|
||||
update_interrupt_line();
|
||||
} else {
|
||||
if((value&3) == 3) {
|
||||
transmit.reset_writing();
|
||||
transmit.write(true);
|
||||
request_to_send.reset_writing();
|
||||
reset();
|
||||
} else {
|
||||
switch(value & 3) {
|
||||
default:
|
||||
@@ -143,6 +158,7 @@ bool ACIA::serial_line_did_produce_bit(Serial::Line *line, int bit) {
|
||||
const int bit_target = expected_bits();
|
||||
if(bits_received_ >= bit_target) {
|
||||
bits_received_ = 0;
|
||||
overran_ |= get_status() & 1;
|
||||
received_data_ = uint8_t(bits_incoming_ >> (12 - bit_target));
|
||||
update_interrupt_line();
|
||||
update_clocking_observer();
|
||||
@@ -186,8 +202,9 @@ void ACIA::update_interrupt_line() {
|
||||
(receive_interrupt_enabled_ && (status & 0x25)) ||
|
||||
(transmit_interrupt_enabled_ && (status & 0x02));
|
||||
|
||||
if(interrupt_delegate_ && old_line != interrupt_line_)
|
||||
if(interrupt_delegate_ && old_line != interrupt_line_) {
|
||||
interrupt_delegate_->acia6850_did_change_interrupt_status(this);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ACIA::get_status() {
|
||||
@@ -196,6 +213,7 @@ uint8_t ACIA::get_status() {
|
||||
((next_transmission_ == NoValueMask) ? 0x02 : 0x00) |
|
||||
// (data_carrier_detect.read() ? 0x04 : 0x00) |
|
||||
// (clear_to_send.read() ? 0x08 : 0x00) |
|
||||
(overran_ ? 0x20 : 0x00) |
|
||||
(interrupt_line_ ? 0x80 : 0x00)
|
||||
;
|
||||
|
||||
|
@@ -74,6 +74,7 @@ class ACIA: public ClockingHint::Source, private Serial::Line::ReadDelegate {
|
||||
}
|
||||
|
||||
bool get_interrupt_line() const;
|
||||
void reset();
|
||||
|
||||
// Input lines.
|
||||
Serial::Line receive;
|
||||
@@ -99,12 +100,13 @@ class ACIA: public ClockingHint::Source, private Serial::Line::ReadDelegate {
|
||||
} parity_ = Parity::None;
|
||||
int data_bits_ = 7, stop_bits_ = 2;
|
||||
|
||||
static const int NoValueMask = 0x100;
|
||||
static constexpr int NoValueMask = 0x100;
|
||||
int next_transmission_ = NoValueMask;
|
||||
int received_data_ = NoValueMask;
|
||||
|
||||
int bits_received_ = 0;
|
||||
int bits_incoming_ = 0;
|
||||
bool overran_ = false;
|
||||
|
||||
void consider_transmission();
|
||||
int expected_bits();
|
||||
|
@@ -8,10 +8,14 @@
|
||||
|
||||
#include "MFP68901.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define NDEBUG
|
||||
#endif
|
||||
|
||||
#define LOG_PREFIX "[MFP] "
|
||||
//#define NDEBUG
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
using namespace Motorola::MFP68901;
|
||||
@@ -109,7 +113,7 @@ void MFP68901::write(int address, uint8_t value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int timer_prescales[] = {
|
||||
constexpr int timer_prescales[] = {
|
||||
1, 4, 10, 16, 50, 64, 100, 200
|
||||
};
|
||||
|
||||
@@ -178,28 +182,29 @@ void MFP68901::write(int address, uint8_t value) {
|
||||
void MFP68901::run_for(HalfCycles time) {
|
||||
cycles_left_ += time;
|
||||
|
||||
// TODO: this is the stupidest possible implementation. Improve.
|
||||
int cycles = int(cycles_left_.flush<Cycles>().as_integral());
|
||||
while(cycles--) {
|
||||
for(int c = 0; c < 4; ++c) {
|
||||
if(timers_[c].mode >= TimerMode::Delay) {
|
||||
--timers_[c].divisor;
|
||||
if(!timers_[c].divisor) {
|
||||
timers_[c].divisor = timers_[c].prescale;
|
||||
decrement_timer(c, 1);
|
||||
}
|
||||
const int cycles = int(cycles_left_.flush<Cycles>().as_integral());
|
||||
if(!cycles) return;
|
||||
|
||||
for(int c = 0; c < 4; ++c) {
|
||||
if(timers_[c].mode >= TimerMode::Delay) {
|
||||
// This code applies the timer prescaling only. prescale_count is used to count
|
||||
// upwards rather than downwards for simplicity, but on the real hardware it's
|
||||
// pretty safe to assume it actually counted downwards. So the clamp to 0 is
|
||||
// because gymnastics may need to occur when the prescale value is altered, e.g.
|
||||
// if a prescale of 256 is set and the prescale_count is currently 2 then the
|
||||
// counter should roll over in 254 cycles. If the user at that point changes the
|
||||
// prescale_count to 1 then the counter will need to be altered to -253 and
|
||||
// allowed to keep counting up until it crosses both 0 and 1.
|
||||
const int dividend = timers_[c].prescale_count + cycles;
|
||||
const int decrements = std::max(dividend / timers_[c].prescale, 0);
|
||||
if(decrements) {
|
||||
decrement_timer(c, decrements);
|
||||
timers_[c].prescale_count = dividend % timers_[c].prescale;
|
||||
} else {
|
||||
timers_[c].prescale_count += cycles;
|
||||
}
|
||||
}
|
||||
}
|
||||
// const int cycles = int(cycles_left_.flush<Cycles>().as_integral());
|
||||
// for(int c = 0; c < 4; ++c) {
|
||||
// if(timers_[c].mode >= TimerMode::Delay) {
|
||||
// const int dividend = (cycles + timers_[c].prescale - timers_[c].divisor);
|
||||
// const int decrements = dividend / timers_[c].prescale;
|
||||
// timers_[c].divisor = timers_[c].prescale - (dividend % timers_[c].prescale);
|
||||
// if(decrements) decrement_timer(c, decrements);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
HalfCycles MFP68901::get_next_sequence_point() {
|
||||
@@ -209,12 +214,19 @@ HalfCycles MFP68901::get_next_sequence_point() {
|
||||
// MARK: - Timers
|
||||
|
||||
void MFP68901::set_timer_mode(int timer, TimerMode mode, int prescale, bool reset_timer) {
|
||||
LOG("Timer " << timer << " mode set: " << int(mode) << "; prescale: " << prescale);
|
||||
timers_[timer].mode = mode;
|
||||
timers_[timer].prescale = prescale;
|
||||
if(reset_timer) {
|
||||
timers_[timer].divisor = prescale;
|
||||
timers_[timer].prescale_count = 0;
|
||||
timers_[timer].value = timers_[timer].reload_value;
|
||||
} else {
|
||||
// This hoop is because the prescale_count here goes upward but I'm assuming it goes downward in
|
||||
// real hardware. Therefore this deals with the "switched to a lower prescaling" case whereby the
|
||||
// old cycle should be allowed naturally to expire.
|
||||
timers_[timer].prescale_count = prescale - (timers_[timer].prescale - timers_[timer].prescale_count);
|
||||
}
|
||||
|
||||
timers_[timer].prescale = prescale;
|
||||
}
|
||||
|
||||
void MFP68901::set_timer_data(int timer, uint8_t value) {
|
||||
@@ -232,9 +244,21 @@ void MFP68901::set_timer_event_input(int channel, bool value) {
|
||||
if(timers_[channel].event_input == value) return;
|
||||
|
||||
timers_[channel].event_input = value;
|
||||
if(timers_[channel].mode == TimerMode::EventCount && !value) { /* TODO: which edge is counted? "as defined by the associated Interrupt Channel’s edge bit"? */
|
||||
if(timers_[channel].mode == TimerMode::EventCount && (value == !!(gpip_active_edge_ & (0x10 >> channel)))) {
|
||||
// "The active state of the signal on TAI or TBI is dependent upon the associated
|
||||
// Interrupt Channel’s edge bit (GPIP 4 for TAI and GPIP 3 for TBI [...] ).
|
||||
// If the edge bit associated with the TAI or TBI input is a one, it will be active high.
|
||||
decrement_timer(channel, 1);
|
||||
}
|
||||
|
||||
// TODO:
|
||||
//
|
||||
// Altering the edge bit while the timer is in the event count mode can produce a count pulse.
|
||||
// The interrupt channel associated with the input (I3 for I4 for TAI) is allowed to function normally.
|
||||
// To count transitions reliably, the input must remain in each state (1/O) for a length of time equal
|
||||
// to four periods of the timer clock.
|
||||
//
|
||||
// (the final bit probably explains 13 cycles of the DE to interrupt latency; not sure about the other ~15)
|
||||
}
|
||||
|
||||
void MFP68901::decrement_timer(int timer, int amount) {
|
||||
@@ -247,7 +271,10 @@ void MFP68901::decrement_timer(int timer, int amount) {
|
||||
case 2: begin_interrupts(Interrupt::TimerC); break;
|
||||
case 3: begin_interrupts(Interrupt::TimerD); break;
|
||||
}
|
||||
if(timers_[timer].mode == TimerMode::Delay) {
|
||||
|
||||
// Re: reloading when in event counting mode; I found the data sheet thoroughly unclear on
|
||||
// this, but it appears empirically to be correct. See e.g. Pompey Pirates menu 27.
|
||||
if(timers_[timer].mode == TimerMode::Delay || timers_[timer].mode == TimerMode::EventCount) {
|
||||
timers_[timer].value += timers_[timer].reload_value; // TODO: properly.
|
||||
}
|
||||
}
|
||||
@@ -261,7 +288,7 @@ void MFP68901::set_port_input(uint8_t input) {
|
||||
}
|
||||
|
||||
uint8_t MFP68901::get_port_output() {
|
||||
return 0xff;
|
||||
return 0xff; // TODO.
|
||||
}
|
||||
|
||||
void MFP68901::reevaluate_gpip_interrupts() {
|
||||
@@ -311,8 +338,6 @@ void MFP68901::update_interrupts() {
|
||||
|
||||
// Update the delegate if necessary.
|
||||
if(interrupt_delegate_ && interrupt_line_ != old_interrupt_line) {
|
||||
if(interrupt_line_)
|
||||
LOG("Generating interrupt: " << std::hex << interrupt_pending_ << " / " << std::hex << interrupt_mask_ << " : " << std::hex << interrupt_in_service_);
|
||||
interrupt_delegate_->mfp68901_did_change_interrupt_status(this);
|
||||
}
|
||||
}
|
||||
@@ -340,7 +365,7 @@ int MFP68901::acknowledge_interrupt() {
|
||||
|
||||
int selected = 0;
|
||||
while((1 << selected) != mask) ++selected;
|
||||
LOG("Interrupt acknowledged: " << selected);
|
||||
// LOG("Interrupt acknowledged: " << selected);
|
||||
return (interrupt_vector_ & 0xf0) | uint8_t(selected);
|
||||
}
|
||||
|
||||
|
@@ -21,28 +21,58 @@ class PortHandler {
|
||||
// TODO: announce changes in output.
|
||||
};
|
||||
|
||||
/*!
|
||||
Models the Motorola 68901 Multi-Function Peripheral ('MFP').
|
||||
*/
|
||||
class MFP68901: public ClockingHint::Source {
|
||||
public:
|
||||
/// @returns the result of a read from @c address.
|
||||
uint8_t read(int address);
|
||||
|
||||
/// Performs a write of @c value to @c address.
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/// Advances the MFP by the supplied number of HalfCycles.
|
||||
void run_for(HalfCycles);
|
||||
|
||||
/// @returns the number of cycles until the next possible sequence point — the next time
|
||||
/// at which the interrupt line _might_ change. This object conforms to ClockingHint::Source
|
||||
/// so that mechanism can also be used to reduce the quantity of calls into this class.
|
||||
///
|
||||
/// @discussion TODO, alas.
|
||||
HalfCycles get_next_sequence_point();
|
||||
|
||||
/// Sets the current level of either of the timer event inputs — TAI and TBI in datasheet terms.
|
||||
void set_timer_event_input(int channel, bool value);
|
||||
|
||||
/// Sets a port handler, a receiver that will be notified upon any change in GPIP output.
|
||||
///
|
||||
/// @discussion TODO.
|
||||
void set_port_handler(PortHandler *);
|
||||
|
||||
/// Sets the current input GPIP values.
|
||||
void set_port_input(uint8_t);
|
||||
|
||||
/// @returns the current GPIP output values.
|
||||
///
|
||||
/// @discussion TODO.
|
||||
uint8_t get_port_output();
|
||||
|
||||
/// @returns @c true if the interrupt output is currently active; @c false otherwise.s
|
||||
bool get_interrupt_line();
|
||||
|
||||
static const int NoAcknowledgement = 0x100;
|
||||
static constexpr int NoAcknowledgement = 0x100;
|
||||
|
||||
/// Communicates an interrupt acknowledge cycle.
|
||||
///
|
||||
/// @returns the vector placed on the bus if any; @c NoAcknowledgement if nothing is loaded.
|
||||
int acknowledge_interrupt();
|
||||
|
||||
struct InterruptDelegate {
|
||||
/// Informs the delegate of a change in the interrupt line of the nominated MFP.
|
||||
virtual void mfp68901_did_change_interrupt_status(MFP68901 *) = 0;
|
||||
};
|
||||
/// Sets a delegate that will be notified upon any change in the interrupt line.
|
||||
void set_interrupt_delegate(InterruptDelegate *delegate);
|
||||
|
||||
// ClockingHint::Source.
|
||||
@@ -63,7 +93,7 @@ class MFP68901: public ClockingHint::Source {
|
||||
uint8_t value = 0;
|
||||
uint8_t reload_value = 0;
|
||||
int prescale = 1;
|
||||
int divisor = 1;
|
||||
int prescale_count = 1;
|
||||
bool event_input = false;
|
||||
} timers_[4];
|
||||
uint8_t timer_ab_control_[2] = { 0, 0 };
|
||||
|
@@ -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();
|
||||
@@ -292,7 +292,7 @@ void i8272::posit_event(int event_type) {
|
||||
WAIT_FOR_EVENT(Event8272::CommandByte)
|
||||
SetBusy();
|
||||
|
||||
static const std::size_t required_lengths[32] = {
|
||||
static constexpr std::size_t required_lengths[32] = {
|
||||
0, 0, 9, 3, 2, 9, 9, 2,
|
||||
1, 9, 2, 0, 9, 6, 0, 3,
|
||||
0, 9, 0, 0, 0, 0, 0, 0,
|
||||
@@ -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;
|
||||
|
@@ -17,16 +17,16 @@ using namespace TI::TMS;
|
||||
|
||||
namespace {
|
||||
|
||||
const uint8_t StatusInterrupt = 0x80;
|
||||
const uint8_t StatusSpriteOverflow = 0x40;
|
||||
constexpr uint8_t StatusInterrupt = 0x80;
|
||||
constexpr uint8_t StatusSpriteOverflow = 0x40;
|
||||
|
||||
const int StatusSpriteCollisionShift = 5;
|
||||
const uint8_t StatusSpriteCollision = 0x20;
|
||||
constexpr int StatusSpriteCollisionShift = 5;
|
||||
constexpr uint8_t StatusSpriteCollision = 0x20;
|
||||
|
||||
// 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole
|
||||
// line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368.
|
||||
const unsigned int CRTCyclesPerLine = 1365;
|
||||
const unsigned int CRTCyclesDivider = 4;
|
||||
constexpr unsigned int CRTCyclesPerLine = 1365;
|
||||
constexpr unsigned int CRTCyclesDivider = 4;
|
||||
|
||||
struct ReverseTable {
|
||||
std::uint8_t map[256];
|
||||
@@ -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);
|
||||
}
|
||||
@@ -352,8 +360,7 @@ void TMS9918::run_for(const HalfCycles cycles) {
|
||||
// Output video stream.
|
||||
// --------------------
|
||||
|
||||
#define intersect(left, right, code) \
|
||||
{ \
|
||||
#define intersect(left, right, code) { \
|
||||
const int start = std::max(read_pointer_.column, left); \
|
||||
const int end = std::min(end_column, right); \
|
||||
if(end > start) {\
|
||||
@@ -493,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)) {
|
||||
@@ -625,7 +632,7 @@ void TMS9918::set_register(int address, uint8_t value) {
|
||||
|
||||
uint8_t TMS9918::get_current_line() {
|
||||
// Determine the row to return.
|
||||
static const int row_change_position = 63; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality.
|
||||
constexpr int row_change_position = 63; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality.
|
||||
int source_row =
|
||||
(write_pointer_.column < row_change_position)
|
||||
? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines
|
||||
@@ -671,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.
|
||||
@@ -830,8 +837,8 @@ void Base::draw_tms_character(int start, int end) {
|
||||
int sprite_collision = 0;
|
||||
memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0]));
|
||||
|
||||
static const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff};
|
||||
static const int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
|
||||
constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff};
|
||||
constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
|
||||
|
||||
// Draw all sprites into the sprite buffer.
|
||||
const int shifter_target = sprites_16x16_ ? 32 : 16;
|
||||
|
@@ -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,51 +6,66 @@
|
||||
// Copyright 2016 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "AY38910.hpp"
|
||||
|
||||
#include <cmath>
|
||||
//namespace GI {
|
||||
//namespace AY38910 {
|
||||
|
||||
using namespace GI::AY38910;
|
||||
|
||||
AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
|
||||
// set up envelope lookup tables
|
||||
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;
|
||||
|
||||
// Set up envelope lookup tables.
|
||||
for(int c = 0; c < 16; c++) {
|
||||
for(int p = 0; p < 32; p++) {
|
||||
for(int p = 0; p < 64; p++) {
|
||||
switch(c) {
|
||||
case 0: case 1: case 2: case 3: case 9:
|
||||
envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0;
|
||||
envelope_overflow_masks_[c] = 0x1f;
|
||||
/* Envelope: \____ */
|
||||
envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0;
|
||||
envelope_overflow_masks_[c] = 0x3f;
|
||||
break;
|
||||
case 4: case 5: case 6: case 7: case 15:
|
||||
envelope_shapes_[c][p] = (p < 16) ? p : 0;
|
||||
envelope_overflow_masks_[c] = 0x1f;
|
||||
/* Envelope: /____ */
|
||||
envelope_shapes_[c][p] = (p < 32) ? p : 0;
|
||||
envelope_overflow_masks_[c] = 0x3f;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
envelope_shapes_[c][p] = (p & 0xf) ^ 0xf;
|
||||
/* Envelope: \\\\\\\\ */
|
||||
envelope_shapes_[c][p] = (p & 0x1f) ^ 0x1f;
|
||||
envelope_overflow_masks_[c] = 0x00;
|
||||
break;
|
||||
case 12:
|
||||
envelope_shapes_[c][p] = (p & 0xf);
|
||||
/* Envelope: //////// */
|
||||
envelope_shapes_[c][p] = (p & 0x1f);
|
||||
envelope_overflow_masks_[c] = 0x00;
|
||||
break;
|
||||
|
||||
case 10:
|
||||
envelope_shapes_[c][p] = (p & 0xf) ^ ((p < 16) ? 0xf : 0x0);
|
||||
/* Envelope: \/\/\/\/ */
|
||||
envelope_shapes_[c][p] = (p & 0x1f) ^ ((p < 32) ? 0x1f : 0x0);
|
||||
envelope_overflow_masks_[c] = 0x00;
|
||||
break;
|
||||
case 14:
|
||||
envelope_shapes_[c][p] = (p & 0xf) ^ ((p < 16) ? 0x0 : 0xf);
|
||||
/* Envelope: /\/\/\/\ */
|
||||
envelope_shapes_[c][p] = (p & 0x1f) ^ ((p < 32) ? 0x0 : 0x1f);
|
||||
envelope_overflow_masks_[c] = 0x00;
|
||||
break;
|
||||
|
||||
case 11:
|
||||
envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0xf;
|
||||
envelope_overflow_masks_[c] = 0x1f;
|
||||
/* Envelope: \------ (if - is high) */
|
||||
envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0x1f;
|
||||
envelope_overflow_masks_[c] = 0x3f;
|
||||
break;
|
||||
case 13:
|
||||
envelope_shapes_[c][p] = (p < 16) ? p : 0xf;
|
||||
envelope_overflow_masks_[c] = 0x1f;
|
||||
/* Envelope: /------- */
|
||||
envelope_shapes_[c][p] = (p < 32) ? p : 0x1f;
|
||||
envelope_overflow_masks_[c] = 0x3f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -59,21 +74,50 @@ AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_
|
||||
set_sample_volume_range(0);
|
||||
}
|
||||
|
||||
void AY38910::set_sample_volume_range(std::int16_t range) {
|
||||
// set up volume lookup table
|
||||
const float max_volume = static_cast<float>(range) / 3.0f; // As there are three channels.
|
||||
const float root_two = sqrtf(2.0f);
|
||||
for(int v = 0; v < 16; v++) {
|
||||
volumes_[v] = static_cast<int>(max_volume / powf(root_two, static_cast<float>(v ^ 0xf)));
|
||||
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) / 3.18f));
|
||||
}
|
||||
volumes_[0] = 0;
|
||||
|
||||
// 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.
|
||||
// Therefore this class implements a divider of 4 and doubles the tone
|
||||
// and noise periods. The envelope ticks along at the divide-by-four rate,
|
||||
// but if this is an AY rather than a YM then its lowest bit is forced to 1,
|
||||
// matching the YM datasheet's depiction of envelope level 31 as equal to
|
||||
// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc.
|
||||
|
||||
std::size_t c = 0;
|
||||
while((master_divider_&7) && c < number_of_samples) {
|
||||
target[c] = output_volume_;
|
||||
while((master_divider_&3) && c < number_of_samples) {
|
||||
if constexpr (is_stereo) {
|
||||
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||
} else {
|
||||
target[c] = int16_t(output_volume_);
|
||||
}
|
||||
master_divider_++;
|
||||
c++;
|
||||
}
|
||||
@@ -83,49 +127,53 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
if(tone_counters_[c]) tone_counters_[c]--;\
|
||||
else {\
|
||||
tone_outputs_[c] ^= 1;\
|
||||
tone_counters_[c] = tone_periods_[c];\
|
||||
tone_counters_[c] = tone_periods_[c] << 1;\
|
||||
}
|
||||
|
||||
// update the tone channels
|
||||
// Update the tone channels.
|
||||
step_channel(0);
|
||||
step_channel(1);
|
||||
step_channel(2);
|
||||
|
||||
#undef step_channel
|
||||
|
||||
// ... the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
|
||||
// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
|
||||
// it into the official 17 upon divider underflow.
|
||||
if(noise_counter_) noise_counter_--;
|
||||
else {
|
||||
noise_counter_ = noise_period_;
|
||||
noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes.
|
||||
noise_output_ ^= noise_shift_register_&1;
|
||||
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
|
||||
noise_shift_register_ >>= 1;
|
||||
}
|
||||
|
||||
// ... and the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
|
||||
// implementing non-repeating patterns by locking them to table position 0x1f.
|
||||
// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
|
||||
// implementing non-repeating patterns by locking them to the final table position.
|
||||
if(envelope_divider_) envelope_divider_--;
|
||||
else {
|
||||
envelope_divider_ = envelope_period_;
|
||||
envelope_position_ ++;
|
||||
if(envelope_position_ == 32) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
|
||||
if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
|
||||
}
|
||||
|
||||
evaluate_output_volume();
|
||||
|
||||
for(int ic = 0; ic < 8 && c < number_of_samples; ic++) {
|
||||
target[c] = output_volume_;
|
||||
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
|
||||
if constexpr (is_stereo) {
|
||||
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||
} else {
|
||||
target[c] = int16_t(output_volume_);
|
||||
}
|
||||
c++;
|
||||
master_divider_++;
|
||||
}
|
||||
}
|
||||
|
||||
master_divider_ &= 7;
|
||||
master_divider_ &= 3;
|
||||
}
|
||||
|
||||
void AY38910::evaluate_output_volume() {
|
||||
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_];
|
||||
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:
|
||||
// 1 if neither tone nor noise is enabled;
|
||||
@@ -142,9 +190,20 @@ void AY38910::evaluate_output_volume() {
|
||||
};
|
||||
#undef level
|
||||
|
||||
// Channel volume is a simple selection: if the bit at 0x10 is set, use the envelope volume; otherwise use the lower four bits
|
||||
// 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)
|
||||
((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),
|
||||
@@ -153,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) {
|
||||
@@ -189,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;
|
||||
}
|
||||
@@ -204,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:
|
||||
@@ -242,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] = {
|
||||
@@ -256,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.
|
||||
@@ -283,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.
|
||||
@@ -299,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_) {
|
||||
@@ -324,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>;
|
||||
|
@@ -52,15 +52,24 @@ enum ControlLines {
|
||||
BDIR = (1 << 2)
|
||||
};
|
||||
|
||||
enum class Personality {
|
||||
/// Provides 16 volume levels to envelopes.
|
||||
AY38910,
|
||||
/// Provides 32 volume levels to envelopes.
|
||||
YM2149F
|
||||
};
|
||||
|
||||
/*!
|
||||
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(Concurrency::DeferringAsyncTaskQueue &task_queue);
|
||||
AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &);
|
||||
|
||||
/// Sets the value the AY would read from its data lines if it were not outputting.
|
||||
void set_data_input(uint8_t r);
|
||||
@@ -84,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_;
|
||||
@@ -109,11 +131,11 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
|
||||
int envelope_period_ = 0;
|
||||
int envelope_divider_ = 0;
|
||||
int envelope_position_ = 0;
|
||||
int envelope_shapes_[16][32];
|
||||
int envelope_position_ = 0, envelope_position_mask_ = 0;
|
||||
int envelope_shapes_[16][64];
|
||||
int envelope_overflow_masks_[16];
|
||||
|
||||
int volumes_[16];
|
||||
int volumes_[32];
|
||||
|
||||
enum ControlState {
|
||||
Inactive,
|
||||
@@ -128,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 {
|
||||
@@ -48,7 +48,7 @@ class DiskII final:
|
||||
The value returned by @c read_address if accessing that address
|
||||
didn't cause the disk II to place anything onto the bus.
|
||||
*/
|
||||
const int DidNotLoad = -1;
|
||||
static constexpr int DidNotLoad = -1;
|
||||
|
||||
/// Advances the controller by @c cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
@@ -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;
|
||||
|
||||
|
@@ -13,15 +13,15 @@
|
||||
using namespace Apple;
|
||||
|
||||
namespace {
|
||||
const int CA0 = 1 << 0;
|
||||
const int CA1 = 1 << 1;
|
||||
const int CA2 = 1 << 2;
|
||||
const int LSTRB = 1 << 3;
|
||||
const int ENABLE = 1 << 4;
|
||||
const int DRIVESEL = 1 << 5; /* This means drive select, like on the original Disk II. */
|
||||
const int Q6 = 1 << 6;
|
||||
const int Q7 = 1 << 7;
|
||||
const int SEL = 1 << 8; /* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */
|
||||
constexpr int CA0 = 1 << 0;
|
||||
constexpr int CA1 = 1 << 1;
|
||||
constexpr int CA2 = 1 << 2;
|
||||
constexpr int LSTRB = 1 << 3;
|
||||
constexpr int ENABLE = 1 << 4;
|
||||
constexpr int DRIVESEL = 1 << 5; /* This means drive select, like on the original Disk II. */
|
||||
constexpr int Q6 = 1 << 6;
|
||||
constexpr int Q7 = 1 << 7;
|
||||
constexpr int SEL = 1 << 8; /* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */
|
||||
}
|
||||
|
||||
IWM::IWM(int clock_rate) :
|
||||
|
@@ -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;
|
||||
|
@@ -18,7 +18,7 @@ AsyncTaskQueue::AsyncTaskQueue()
|
||||
#ifdef __APPLE__
|
||||
serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL);
|
||||
#else
|
||||
thread_.reset(new std::thread([this]() {
|
||||
thread_ = std::make_unique<std::thread>([this]() {
|
||||
while(!should_destruct_) {
|
||||
std::function<void(void)> next_function;
|
||||
|
||||
@@ -39,7 +39,7 @@ AsyncTaskQueue::AsyncTaskQueue()
|
||||
processing_condition_.wait(lock);
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ void AsyncTaskQueue::flush() {
|
||||
#ifdef __APPLE__
|
||||
dispatch_sync(serial_dispatch_queue_, ^{});
|
||||
#else
|
||||
std::shared_ptr<std::mutex> flush_mutex(new std::mutex);
|
||||
std::shared_ptr<std::condition_variable> flush_condition(new std::condition_variable);
|
||||
auto flush_mutex = std::make_shared<std::mutex>();
|
||||
auto flush_condition = std::make_shared<std::condition_variable>();
|
||||
std::unique_lock<std::mutex> lock(*flush_mutex);
|
||||
enqueue([=] () {
|
||||
std::unique_lock<std::mutex> inner_lock(*flush_mutex);
|
||||
@@ -88,7 +88,7 @@ DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() {
|
||||
|
||||
void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
|
||||
if(!deferred_tasks_) {
|
||||
deferred_tasks_.reset(new std::list<std::function<void(void)>>);
|
||||
deferred_tasks_ = std::make_shared<std::list<std::function<void(void)>>>();
|
||||
}
|
||||
deferred_tasks_->push_back(function);
|
||||
}
|
||||
|
@@ -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_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ namespace {
|
||||
Appends a Boolean selection of @c selection for option @c name to @c selection_set.
|
||||
*/
|
||||
void append_bool(Configurable::SelectionSet &selection_set, const std::string &name, bool selection) {
|
||||
selection_set[name] = std::unique_ptr<Configurable::Selection>(new Configurable::BooleanSelection(selection));
|
||||
selection_set[name] = std::make_unique<Configurable::BooleanSelection>(selection);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -64,7 +64,7 @@ void Configurable::append_display_selection(Configurable::SelectionSet &selectio
|
||||
case Display::CompositeMonochrome: string_selection = "composite-mono"; break;
|
||||
case Display::CompositeColour: string_selection = "composite"; break;
|
||||
}
|
||||
selection_set["display"] = std::unique_ptr<Configurable::Selection>(new Configurable::ListSelection(string_selection));
|
||||
selection_set["display"] = std::make_unique<Configurable::ListSelection>(string_selection);
|
||||
}
|
||||
|
||||
void Configurable::append_quick_boot_selection(Configurable::SelectionSet &selection_set, bool selection) {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -124,8 +124,11 @@ class InterruptTimer {
|
||||
class AYDeferrer {
|
||||
public:
|
||||
/// Constructs a new AY instance and sets its clock rate.
|
||||
AYDeferrer() : ay_(audio_queue_), speaker_(ay_) {
|
||||
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);
|
||||
@@ -509,7 +517,7 @@ class CRTCBusHandler {
|
||||
|
||||
uint8_t mapped_palette_value(uint8_t colour) {
|
||||
#define COL(r, g, b) (r << 4) | (g << 2) | b
|
||||
static const uint8_t mapping[32] = {
|
||||
constexpr uint8_t mapping[32] = {
|
||||
COL(1, 1, 1), COL(1, 1, 1), COL(0, 2, 1), COL(2, 2, 1),
|
||||
COL(0, 0, 1), COL(2, 0, 1), COL(0, 1, 1), COL(2, 1, 1),
|
||||
COL(2, 0, 1), COL(2, 2, 1), COL(2, 2, 0), COL(2, 2, 2),
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -877,8 +883,10 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
// Pump the AY
|
||||
ay_.run_for(cycle.length);
|
||||
|
||||
// Clock the FDC, if connected, using a lazy scale by two
|
||||
time_since_fdc_update_ += cycle.length;
|
||||
if constexpr (has_fdc) {
|
||||
// Clock the FDC, if connected, using a lazy scale by two
|
||||
time_since_fdc_update_ += cycle.length;
|
||||
}
|
||||
|
||||
// Update typing activity
|
||||
if(typer_) typer_->run_for(cycle.length);
|
||||
@@ -904,9 +912,11 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Check for an upper ROM selection
|
||||
if(has_fdc && !(address&0x2000)) {
|
||||
upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC;
|
||||
if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data();
|
||||
if constexpr (has_fdc) {
|
||||
if(!(address&0x2000)) {
|
||||
upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC;
|
||||
if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a CRTC access
|
||||
@@ -920,19 +930,21 @@ 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);
|
||||
}
|
||||
|
||||
// Check for an FDC access
|
||||
if(has_fdc && (address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
fdc_.set_register(address & 1, *cycle.value);
|
||||
}
|
||||
if constexpr (has_fdc) {
|
||||
// Check for an FDC access
|
||||
if((address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
fdc_.write(address & 1, *cycle.value);
|
||||
}
|
||||
|
||||
// Check for a disk motor access
|
||||
if(has_fdc && !(address & 0x580)) {
|
||||
flush_fdc();
|
||||
fdc_.set_motor_on(!!(*cycle.value));
|
||||
// Check for a disk motor access
|
||||
if(!(address & 0x580)) {
|
||||
flush_fdc();
|
||||
fdc_.set_motor_on(!!(*cycle.value));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
@@ -941,13 +953,15 @@ 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(has_fdc && (address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
*cycle.value &= fdc_.get_register(address & 1);
|
||||
if constexpr (has_fdc) {
|
||||
if((address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
*cycle.value &= fdc_.read(address & 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a CRTC access; the below is not a typo, the CRTC can be selected
|
||||
@@ -994,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());
|
||||
@@ -1030,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 {
|
||||
if(has_fdc) fdc_.set_activity_observer(observer);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1155,11 +1174,13 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
FDC fdc_;
|
||||
HalfCycles time_since_fdc_update_;
|
||||
void flush_fdc() {
|
||||
// Clock the FDC, if connected, using a lazy scale by two
|
||||
if(has_fdc && !fdc_is_sleeping_) {
|
||||
fdc_.run_for(Cycles(time_since_fdc_update_.as_integral()));
|
||||
if constexpr (has_fdc) {
|
||||
// Clock the FDC, if connected, using a lazy scale by two
|
||||
if(!fdc_is_sleeping_) {
|
||||
fdc_.run_for(Cycles(time_since_fdc_update_.as_integral()));
|
||||
}
|
||||
time_since_fdc_update_ = HalfCycles(0);
|
||||
}
|
||||
time_since_fdc_update_ = HalfCycles(0);
|
||||
}
|
||||
|
||||
InterruptTimer interrupt_timer_;
|
||||
|
@@ -79,7 +79,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
void update_video() {
|
||||
video_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
static const int audio_divider = 8;
|
||||
static constexpr int audio_divider = 8;
|
||||
void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -329,7 +329,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
audio_toggle_(audio_queue_),
|
||||
speaker_(audio_toggle_) {
|
||||
// The system's master clock rate.
|
||||
const float master_clock = 14318180.0;
|
||||
constexpr float master_clock = 14318180.0;
|
||||
|
||||
// This is where things get slightly convoluted: establish the machine as having a clock rate
|
||||
// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
|
||||
@@ -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 {
|
||||
string_serialiser_.reset(new Utility::StringSerialiser(string, true));
|
||||
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);
|
||||
|
||||
@@ -203,7 +206,7 @@ class VideoBase {
|
||||
std::array<uint8_t, 40> auxiliary_stream_;
|
||||
|
||||
bool is_iie_ = false;
|
||||
static const int flash_length = 8406;
|
||||
static constexpr int flash_length = 8406;
|
||||
|
||||
// Describes the current text mode mapping from in-memory character index
|
||||
// to output character.
|
||||
@@ -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) {}
|
||||
|
||||
/*!
|
||||
@@ -339,9 +342,9 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
|
||||
A frame is oriented around 65 cycles across, 262 lines down.
|
||||
*/
|
||||
static const int first_sync_line = 220; // A complete guess. Information needed.
|
||||
static const int first_sync_column = 49; // Also a guess.
|
||||
static const int sync_length = 4; // One of the two likely candidates.
|
||||
constexpr int first_sync_line = 220; // A complete guess. Information needed.
|
||||
constexpr int first_sync_column = 49; // Also a guess.
|
||||
constexpr int sync_length = 4; // One of the two likely candidates.
|
||||
|
||||
int int_cycles = int(cycles.as_integral());
|
||||
while(int_cycles) {
|
||||
|
@@ -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_;
|
||||
|
@@ -18,7 +18,7 @@
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
static const uint16_t KeypadMask = 0x100;
|
||||
constexpr uint16_t KeypadMask = 0x100;
|
||||
|
||||
/*!
|
||||
Defines the keycodes that could be passed directly to a Macintosh via set_key_pressed.
|
||||
|
@@ -49,7 +49,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
const int CLOCK_RATE = 7833600;
|
||||
constexpr int CLOCK_RATE = 7833600;
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -152,7 +152,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
scc_.set_delegate(this);
|
||||
|
||||
// Also watch for changes in clocking requirement from the SCSI chip.
|
||||
if(model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
scsi_bus_.set_clocking_hint_observer(this);
|
||||
}
|
||||
|
||||
@@ -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,49 +513,51 @@ 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(model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
scsi_bus_.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 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.
|
||||
@@ -642,7 +642,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
}
|
||||
|
||||
// Update the SCSI if currently active.
|
||||
if(model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
if(scsi_bus_is_clocked_) scsi_bus_.run_for(duration);
|
||||
}
|
||||
}
|
||||
@@ -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_;
|
||||
|
@@ -17,11 +17,11 @@
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
static const HalfCycles line_length(704);
|
||||
static const int number_of_lines = 370;
|
||||
static const HalfCycles frame_length(line_length * HalfCycles(number_of_lines));
|
||||
static const int sync_start = 36;
|
||||
static const int sync_end = 38;
|
||||
constexpr HalfCycles line_length(704);
|
||||
constexpr int number_of_lines = 370;
|
||||
constexpr HalfCycles frame_length(line_length * HalfCycles(number_of_lines));
|
||||
constexpr int sync_start = 36;
|
||||
constexpr int sync_end = 38;
|
||||
|
||||
/*!
|
||||
Models the 68000-era Macintosh video hardware, producing a 512x348 pixel image,
|
||||
@@ -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,58 +77,57 @@ 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;
|
||||
switch(target.paging_model) {
|
||||
case PagingModel::ActivisionStack: bus_.reset(new Cartridge::Cartridge<Cartridge::ActivisionStack>(rom)); break;
|
||||
case PagingModel::CBSRamPlus: bus_.reset(new Cartridge::Cartridge<Cartridge::CBSRAMPlus>(rom)); break;
|
||||
case PagingModel::CommaVid: bus_.reset(new Cartridge::Cartridge<Cartridge::CommaVid>(rom)); break;
|
||||
case PagingModel::MegaBoy: bus_.reset(new Cartridge::Cartridge<Cartridge::MegaBoy>(rom)); break;
|
||||
case PagingModel::MNetwork: bus_.reset(new Cartridge::Cartridge<Cartridge::MNetwork>(rom)); break;
|
||||
case PagingModel::None: bus_.reset(new Cartridge::Cartridge<Cartridge::Unpaged>(rom)); break;
|
||||
case PagingModel::ParkerBros: bus_.reset(new Cartridge::Cartridge<Cartridge::ParkerBros>(rom)); break;
|
||||
case PagingModel::Pitfall2: bus_.reset(new Cartridge::Cartridge<Cartridge::Pitfall2>(rom)); break;
|
||||
case PagingModel::Tigervision: bus_.reset(new Cartridge::Cartridge<Cartridge::Tigervision>(rom)); break;
|
||||
case PagingModel::ActivisionStack: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::ActivisionStack>>(rom); break;
|
||||
case PagingModel::CBSRamPlus: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::CBSRAMPlus>>(rom); break;
|
||||
case PagingModel::CommaVid: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::CommaVid>>(rom); break;
|
||||
case PagingModel::MegaBoy: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::MegaBoy>>(rom); break;
|
||||
case PagingModel::MNetwork: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::MNetwork>>(rom); break;
|
||||
case PagingModel::None: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Unpaged>>(rom); break;
|
||||
case PagingModel::ParkerBros: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::ParkerBros>>(rom); break;
|
||||
case PagingModel::Pitfall2: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Pitfall2>>(rom); break;
|
||||
case PagingModel::Tigervision: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Tigervision>>(rom); break;
|
||||
|
||||
case PagingModel::Atari8k:
|
||||
if(target.uses_superchip) {
|
||||
bus_.reset(new Cartridge::Cartridge<Cartridge::Atari8kSuperChip>(rom));
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8kSuperChip>>(rom);
|
||||
} else {
|
||||
bus_.reset(new Cartridge::Cartridge<Cartridge::Atari8k>(rom));
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8k>>(rom);
|
||||
}
|
||||
break;
|
||||
case PagingModel::Atari16k:
|
||||
if(target.uses_superchip) {
|
||||
bus_.reset(new Cartridge::Cartridge<Cartridge::Atari16kSuperChip>(rom));
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16kSuperChip>>(rom);
|
||||
} else {
|
||||
bus_.reset(new Cartridge::Cartridge<Cartridge::Atari16k>(rom));
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16k>>(rom);
|
||||
}
|
||||
break;
|
||||
case PagingModel::Atari32k:
|
||||
if(target.uses_superchip) {
|
||||
bus_.reset(new Cartridge::Cartridge<Cartridge::Atari32kSuperChip>(rom));
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32kSuperChip>>(rom);
|
||||
} else {
|
||||
bus_.reset(new Cartridge::Cartridge<Cartridge::Atari32k>(rom));
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32k>>(rom);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
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();
|
||||
|
@@ -13,11 +13,11 @@
|
||||
|
||||
using namespace Atari2600;
|
||||
namespace {
|
||||
const int cycles_per_line = 228;
|
||||
const int first_pixel_cycle = 68;
|
||||
constexpr int cycles_per_line = 228;
|
||||
constexpr int first_pixel_cycle = 68;
|
||||
|
||||
const int sync_flag = 0x1;
|
||||
const int blank_flag = 0x2;
|
||||
constexpr int sync_flag = 0x1;
|
||||
constexpr int blank_flag = 0x2;
|
||||
|
||||
uint8_t reverse_table[256];
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,7 @@ namespace Atari2600 {
|
||||
|
||||
// This should be a divisor of 38; audio counters are updated every 38 cycles, though lesser dividers
|
||||
// will give greater resolution to changes in audio state. 1, 2 and 19 are the only divisors of 38.
|
||||
const int CPUTicksPerAudioTick = 2;
|
||||
constexpr int CPUTicksPerAudioTick = 2;
|
||||
|
||||
class TIASound: public Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
@@ -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"
|
||||
@@ -28,8 +29,11 @@
|
||||
|
||||
#include "../../../ClockReceiver/JustInTime.hpp"
|
||||
#include "../../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../../Configurable/StandardOptions.hpp"
|
||||
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
|
||||
#define LOG_PREFIX "[ST] "
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include "../../Utility/MemoryPacker.hpp"
|
||||
@@ -38,7 +42,13 @@
|
||||
namespace Atari {
|
||||
namespace ST {
|
||||
|
||||
const int CLOCK_RATE = 8021247;
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour | Configurable::QuickLoadTape)
|
||||
);
|
||||
}
|
||||
|
||||
constexpr int CLOCK_RATE = 8021247;
|
||||
|
||||
using Target = Analyser::Static::Target;
|
||||
class ConcreteMachine:
|
||||
@@ -54,20 +64,22 @@ class ConcreteMachine:
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public Activity::Source,
|
||||
public MediaTarget::Machine,
|
||||
public GI::AY38910::PortHandler {
|
||||
public GI::AY38910::PortHandler,
|
||||
public Configurable::Device,
|
||||
public Video::RangeObserver {
|
||||
public:
|
||||
ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
mc68000_(*this),
|
||||
keyboard_acia_(Cycles(500000)),
|
||||
midi_acia_(Cycles(500000)),
|
||||
ay_(audio_queue_),
|
||||
ay_(GI::AY38910::Personality::YM2149F, audio_queue_),
|
||||
speaker_(ay_),
|
||||
ikbd_(keyboard_acia_->transmit, keyboard_acia_->receive) {
|
||||
set_clock_rate(CLOCK_RATE);
|
||||
speaker_.set_input_rate(CLOCK_RATE / 4);
|
||||
speaker_.set_input_rate(float(CLOCK_RATE) / 4.0f);
|
||||
|
||||
ram_.resize(512 * 512); // i.e. 512kb
|
||||
video_->set_ram(ram_.data(), ram_.size());
|
||||
ram_.resize(512 * 1024); // i.e. 512kb
|
||||
video_->set_ram(reinterpret_cast<uint16_t *>(ram_.data()), ram_.size());
|
||||
Memory::Fuzz(ram_);
|
||||
|
||||
std::vector<ROMMachine::ROM> rom_descriptions = {
|
||||
@@ -83,13 +95,16 @@ class ConcreteMachine:
|
||||
// Set up basic memory map.
|
||||
memory_map_[0] = BusDevice::MostlyRAM;
|
||||
int c = 1;
|
||||
for(; c < 0x08; ++c) memory_map_[c] = BusDevice::RAM;
|
||||
for(; c < int(ram_.size() >> 16); ++c) memory_map_[c] = BusDevice::RAM;
|
||||
for(; c < 0x40; ++c) memory_map_[c] = BusDevice::Floating;
|
||||
for(; c < 0xff; ++c) memory_map_[c] = BusDevice::Unassigned;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -111,6 +126,8 @@ class ConcreteMachine:
|
||||
|
||||
set_gpip_input();
|
||||
|
||||
video_->set_range_observer(this);
|
||||
|
||||
// Insert any supplied media.
|
||||
insert_media(target.media);
|
||||
}
|
||||
@@ -124,6 +141,14 @@ 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);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
@@ -147,13 +172,21 @@ class ConcreteMachine:
|
||||
// Advance time.
|
||||
advance_time(cycle.length);
|
||||
|
||||
// Check for assertion of reset.
|
||||
if(cycle.operation & Microcycle::Reset) {
|
||||
LOG("Unhandled Reset");
|
||||
}
|
||||
|
||||
// A null cycle leaves nothing else to do.
|
||||
if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return HalfCycles(0);
|
||||
|
||||
// An interrupt acknowledge, perhaps?
|
||||
if(cycle.operation & Microcycle::InterruptAcknowledge) {
|
||||
// Current implementation: everything other than 6 (i.e. the MFP is autovectored.
|
||||
if((cycle.word_address()&7) != 6) {
|
||||
const int interrupt_level = cycle.word_address()&7;
|
||||
if(interrupt_level != 6) {
|
||||
video_interrupts_pending_ &= ~interrupt_level;
|
||||
update_interrupt_input();
|
||||
mc68000_.set_is_peripheral_address(true);
|
||||
return HalfCycles(0);
|
||||
} else {
|
||||
@@ -170,42 +203,56 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
auto address = cycle.host_endian_byte_address();
|
||||
|
||||
// If this is a new strobing of the address signal, test for bus error and pre-DTack delay.
|
||||
HalfCycles delay(0);
|
||||
if(cycle.operation & Microcycle::NewAddress) {
|
||||
// DTack will be implicit; work out how long until that should be,
|
||||
// and apply bus error constraints.
|
||||
const int i_phase = bus_phase_.as<int>() & 7;
|
||||
if(i_phase < 4) {
|
||||
delay = HalfCycles(4 - i_phase);
|
||||
advance_time(delay);
|
||||
// Bus error test.
|
||||
if(
|
||||
// Anything unassigned should generate a bus error.
|
||||
(memory_map_[address >> 16] == BusDevice::Unassigned) ||
|
||||
|
||||
// Bus errors also apply to unprivileged access to the first 0x800 bytes, or the IO area.
|
||||
(!is_supervisor && (address < 0x800 || memory_map_[address >> 16] == BusDevice::IO))
|
||||
) {
|
||||
mc68000_.set_bus_error(true);
|
||||
return delay; // TODO: there should be an extra delay here.
|
||||
}
|
||||
|
||||
// TODO: presumably test is if(after declared memory size and (not supervisor or before hardware space)) bus_error?
|
||||
// DTack delay rule: if accessing RAM or the shifter, align with the two cycles next available
|
||||
// for the CPU to access that side of the bus.
|
||||
if(address < ram_.size() || (address == 0xff8260)) {
|
||||
// DTack will be implicit; work out how long until that should be,
|
||||
// and apply bus error constraints.
|
||||
const int i_phase = bus_phase_.as<int>() & 7;
|
||||
if(i_phase < 4) {
|
||||
delay = HalfCycles(4 - i_phase);
|
||||
advance_time(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto address = cycle.word_address();
|
||||
uint16_t *memory = nullptr;
|
||||
switch(memory_map_[address >> 15]) {
|
||||
uint8_t *memory = nullptr;
|
||||
switch(memory_map_[address >> 16]) {
|
||||
default:
|
||||
case BusDevice::MostlyRAM:
|
||||
if(address < 4) {
|
||||
if(address < 8) {
|
||||
memory = rom_.data();
|
||||
break;
|
||||
}
|
||||
case BusDevice::RAM:
|
||||
memory = ram_.data();
|
||||
address &= ram_.size() - 1;
|
||||
// TODO: align with the next access window.
|
||||
break;
|
||||
|
||||
case BusDevice::ROM:
|
||||
memory = rom_.data();
|
||||
address %= rom_.size();
|
||||
address -= rom_start_;
|
||||
break;
|
||||
|
||||
case BusDevice::Floating:
|
||||
// TODO: provide vapour reads here. But: will these always be of the last video fetch?
|
||||
case BusDevice::Unassigned:
|
||||
// TODO: figure out the rules about bus errors.
|
||||
case BusDevice::Cartridge:
|
||||
/*
|
||||
TOS 1.0 appears to attempt to read from the catridge before it has setup
|
||||
@@ -223,11 +270,11 @@ class ConcreteMachine:
|
||||
return delay;
|
||||
|
||||
case BusDevice::IO:
|
||||
switch(address) {
|
||||
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
|
||||
@@ -239,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));
|
||||
@@ -251,81 +354,48 @@ class ConcreteMachine:
|
||||
cycle.set_value8_high(ay_.get_data_output());
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
} else {
|
||||
if(address == 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) {
|
||||
cycle.set_value8_low(mfp_->read(int(address)));
|
||||
cycle.set_value8_low(mfp_->read(int(address >> 1)));
|
||||
} else {
|
||||
mfp_->write(int(address), cycle.value8_low());
|
||||
}
|
||||
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)));
|
||||
} else {
|
||||
video_->write(int(address), cycle.value16());
|
||||
mfp_->write(int(address >> 1), cycle.value8_low());
|
||||
}
|
||||
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 < 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)));
|
||||
cycle.set_value8_high((*acia_)->read(int(address >> 1)));
|
||||
} else {
|
||||
(*acia_)->write(int(address), cycle.value8_high());
|
||||
(*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)));
|
||||
} else {
|
||||
dma_->write(int(address), cycle.value16());
|
||||
}
|
||||
break;
|
||||
}
|
||||
return HalfCycles(0);
|
||||
}
|
||||
@@ -336,21 +406,20 @@ class ConcreteMachine:
|
||||
break;
|
||||
|
||||
case Microcycle::SelectWord | Microcycle::Read:
|
||||
cycle.value->full = memory[address];
|
||||
cycle.value->full = *reinterpret_cast<uint16_t *>(&memory[address]);
|
||||
break;
|
||||
case Microcycle::SelectByte | Microcycle::Read:
|
||||
cycle.value->halves.low = uint8_t(memory[address] >> cycle.byte_shift());
|
||||
cycle.value->halves.low = memory[address];
|
||||
break;
|
||||
case Microcycle::SelectWord:
|
||||
video_.flush(); // TODO: (and below), a range check to determine whether this is really necesary.
|
||||
memory[address] = cycle.value->full;
|
||||
if(address >= video_range_.low_address && address < video_range_.high_address)
|
||||
video_.flush();
|
||||
*reinterpret_cast<uint16_t *>(&memory[address]) = cycle.value->full;
|
||||
break;
|
||||
case Microcycle::SelectByte:
|
||||
video_.flush();
|
||||
memory[address] = uint16_t(
|
||||
(cycle.value->halves.low << cycle.byte_shift()) |
|
||||
(memory[address] & cycle.untouched_byte_mask())
|
||||
);
|
||||
if(address >= video_range_.low_address && address < video_range_.high_address)
|
||||
video_.flush();
|
||||
memory[address] = cycle.value->halves.low;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -372,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;
|
||||
@@ -393,7 +463,7 @@ class ConcreteMachine:
|
||||
mfp_.flush();
|
||||
}
|
||||
|
||||
if(dma_is_realtime_) {
|
||||
if(dma_clocking_preference_ == ClockingHint::Preference::RealTime) {
|
||||
dma_.flush();
|
||||
}
|
||||
|
||||
@@ -402,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();
|
||||
@@ -426,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_;
|
||||
@@ -435,11 +506,25 @@ class ConcreteMachine:
|
||||
HalfCycles cycles_since_ikbd_update_;
|
||||
IntelligentKeyboard ikbd_;
|
||||
|
||||
std::vector<uint16_t> ram_;
|
||||
std::vector<uint16_t> rom_;
|
||||
std::vector<uint8_t> ram_;
|
||||
std::vector<uint8_t> rom_;
|
||||
uint32_t rom_start_ = 0;
|
||||
|
||||
enum class BusDevice {
|
||||
MostlyRAM, RAM, ROM, Cartridge, IO, Unassigned
|
||||
/// A mostly RAM page is one that returns ROM for the first 8 bytes, RAM elsewhere.
|
||||
MostlyRAM,
|
||||
/// Allows reads and writes to ram_.
|
||||
RAM,
|
||||
/// Nothing is mapped to this area, and it also doesn't trigger an exception upon access.
|
||||
Floating,
|
||||
/// Allows reading from rom_; writes do nothing.
|
||||
ROM,
|
||||
/// Allows interaction with a cartrige_.
|
||||
Cartridge,
|
||||
/// Marks the IO page, in which finer decoding will occur.
|
||||
IO,
|
||||
/// An unassigned page has nothing below it, in a way that triggers exceptions.
|
||||
Unassigned
|
||||
};
|
||||
BusDevice memory_map_[256];
|
||||
|
||||
@@ -447,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).
|
||||
@@ -456,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.
|
||||
@@ -470,7 +555,7 @@ class ConcreteMachine:
|
||||
// that's implemented, just offers magical zero-cost DMA insertion and
|
||||
// extrication.
|
||||
if(dma_->get_bus_request_line()) {
|
||||
dma_->bus_grant(ram_.data(), ram_.size());
|
||||
dma_->bus_grant(reinterpret_cast<uint16_t *>(ram_.data()), ram_.size() >> 1);
|
||||
}
|
||||
}
|
||||
void set_gpip_input() {
|
||||
@@ -487,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).
|
||||
@@ -503,12 +588,27 @@ class ConcreteMachine:
|
||||
update_interrupt_input();
|
||||
}
|
||||
|
||||
int video_interrupts_pending_ = 0;
|
||||
bool previous_hsync_ = false, previous_vsync_ = false;
|
||||
void update_interrupt_input() {
|
||||
// Complete guess: set video interrupts pending if/when hsync of vsync
|
||||
// go inactive. Reset upon IACK.
|
||||
const bool hsync = video_.last_valid()->hsync();
|
||||
const bool vsync = video_.last_valid()->vsync();
|
||||
if(previous_hsync_ != hsync && previous_hsync_) {
|
||||
video_interrupts_pending_ |= 2;
|
||||
}
|
||||
if(previous_vsync_ != vsync && previous_vsync_) {
|
||||
video_interrupts_pending_ |= 4;
|
||||
}
|
||||
previous_vsync_ = vsync;
|
||||
previous_hsync_ = hsync;
|
||||
|
||||
if(mfp_->get_interrupt_line()) {
|
||||
mc68000_.set_interrupt_level(6);
|
||||
} else if(video_->vsync()) {
|
||||
} else if(video_interrupts_pending_ & 4) {
|
||||
mc68000_.set_interrupt_level(4);
|
||||
} else if(video_->hsync()) {
|
||||
} else if(video_interrupts_pending_ & 2) {
|
||||
mc68000_.set_interrupt_level(2);
|
||||
} else {
|
||||
mc68000_.set_interrupt_level(0);
|
||||
@@ -541,7 +641,7 @@ class ConcreteMachine:
|
||||
// TODO: ?
|
||||
} else {
|
||||
/*
|
||||
TODO: Port A:
|
||||
Port A:
|
||||
b7: reserved
|
||||
b6: "freely usable output (monitor jack)"
|
||||
b5: centronics strobe
|
||||
@@ -567,9 +667,39 @@ 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);
|
||||
}
|
||||
|
||||
// MARK: - Video Range
|
||||
Video::Range video_range_;
|
||||
void video_did_change_access_range(Video *video) final {
|
||||
video_range_ = video->get_memory_access_range();
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return Atari::ST::get_options();
|
||||
}
|
||||
|
||||
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() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||
return selection_set;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -16,6 +16,9 @@
|
||||
namespace Atari {
|
||||
namespace ST {
|
||||
|
||||
/// @returns The options available for an Atari ST.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options();
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
|
@@ -8,6 +8,9 @@
|
||||
|
||||
#include "DMAController.hpp"
|
||||
|
||||
#define LOG_PREFIX "[DMA] "
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using namespace Atari::ST;
|
||||
@@ -42,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;
|
||||
@@ -75,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;
|
||||
@@ -102,19 +105,28 @@ void DMAController::write(int address, uint16_t value) {
|
||||
control_ = value;
|
||||
break;
|
||||
|
||||
// DMA addressing.
|
||||
// DMA addressing; cf. http://www.atari-forum.com/viewtopic.php?t=30289 on a hardware
|
||||
// feature emulated here: 'carry' will ripple upwards if a write resets the top bit
|
||||
// of the byte it is adjusting.
|
||||
case 4: address_ = int((address_ & 0x00ffff) | ((value & 0xff) << 16)); break;
|
||||
case 5: address_ = int((address_ & 0xff00ff) | ((value & 0xff) << 8)); break;
|
||||
case 6: address_ = int((address_ & 0xffff00) | ((value & 0xff) << 0)); break;
|
||||
case 5:
|
||||
if(((value << 8) ^ address_) & ~(value << 8) & 0x8000) address_ += 0x10000;
|
||||
address_ = int((address_ & 0xff00ff) | ((value & 0xff) << 8));
|
||||
break;
|
||||
case 6:
|
||||
if((value ^ address_) & ~value & 0x80) address_ += 0x100;
|
||||
address_ = int((address_ & 0xffff00) | ((value & 0xfe) << 0));
|
||||
break; // Lowest bit: discarded.
|
||||
}
|
||||
}
|
||||
|
||||
void DMAController::set_floppy_drive_selection(bool drive1, bool drive2, bool side2) {
|
||||
// LOG("Selected: " << (drive1 ? "1" : "-") << (drive2 ? "2" : "-") << (side2 ? "s" : "-"));
|
||||
fdc_.set_floppy_drive_selection(drive1, drive2, side2);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -141,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) {
|
||||
@@ -171,6 +183,7 @@ int DMAController::bus_grant(uint16_t *ram, size_t size) {
|
||||
bus_request_line_ = false;
|
||||
if(delegate_) delegate_->dma_controller_did_change_output(this);
|
||||
|
||||
size <<= 1; // Convert to bytes.
|
||||
if(control_ & Control::Direction) {
|
||||
// TODO: writes.
|
||||
return 0;
|
||||
@@ -178,11 +191,19 @@ int DMAController::bus_grant(uint16_t *ram, size_t size) {
|
||||
// Check that the older buffer is full; stop if not.
|
||||
if(!buffer_[active_buffer_ ^ 1].is_full) return 0;
|
||||
|
||||
#define b(i, n) " " << PADHEX(2) << int(buffer_[i].contents[n])
|
||||
#define b2(i, n) b(i, n) << b(i, n+1)
|
||||
#define b4(i, n) b2(i, n) << b2(i, n+2)
|
||||
#define b16(i) b4(i, 0) << b4(i, 4) << b4(i, 8) << b4(i, 12)
|
||||
// LOG("[1] to " << PADHEX(6) << address_ << b16(active_buffer_ ^ 1));
|
||||
|
||||
for(int c = 0; c < 8; ++c) {
|
||||
ram[size_t(address_ >> 1) & (size - 1)] = uint16_t(
|
||||
(buffer_[active_buffer_ ^ 1].contents[(c << 1) + 0] << 8) |
|
||||
(buffer_[active_buffer_ ^ 1].contents[(c << 1) + 1] << 0)
|
||||
);
|
||||
if(size_t(address_) < size) {
|
||||
ram[address_ >> 1] = uint16_t(
|
||||
(buffer_[active_buffer_ ^ 1].contents[(c << 1) + 0] << 8) |
|
||||
(buffer_[active_buffer_ ^ 1].contents[(c << 1) + 1] << 0)
|
||||
);
|
||||
}
|
||||
address_ += 2;
|
||||
}
|
||||
buffer_[active_buffer_ ^ 1].is_full = false;
|
||||
@@ -190,11 +211,19 @@ int DMAController::bus_grant(uint16_t *ram, size_t size) {
|
||||
// Check that the newer buffer is full; stop if not.
|
||||
if(!buffer_[active_buffer_ ].is_full) return 8;
|
||||
|
||||
// LOG("[2] to " << PADHEX(6) << address_ << b16(active_buffer_));
|
||||
#undef b16
|
||||
#undef b4
|
||||
#undef b2
|
||||
#undef b
|
||||
|
||||
for(int c = 0; c < 8; ++c) {
|
||||
ram[size_t(address_ >> 1) & (size - 1)] = uint16_t(
|
||||
(buffer_[active_buffer_].contents[(c << 1) + 0] << 8) |
|
||||
(buffer_[active_buffer_].contents[(c << 1) + 1] << 0)
|
||||
);
|
||||
if(size_t(address_) < size) {
|
||||
ram[address_ >> 1] = uint16_t(
|
||||
(buffer_[active_buffer_].contents[(c << 1) + 0] << 8) |
|
||||
(buffer_[active_buffer_].contents[(c << 1) + 1] << 0)
|
||||
);
|
||||
}
|
||||
address_ += 2;
|
||||
}
|
||||
buffer_[active_buffer_].is_full = false;
|
||||
@@ -227,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;
|
||||
|
@@ -8,6 +8,11 @@
|
||||
|
||||
#include "IntelligentKeyboard.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#define LOG_PREFIX "[IKYB] "
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
using namespace Atari::ST;
|
||||
|
||||
IntelligentKeyboard::IntelligentKeyboard(Serial::Line &input, Serial::Line &output) : output_line_(output) {
|
||||
@@ -19,6 +24,7 @@ IntelligentKeyboard::IntelligentKeyboard(Serial::Line &input, Serial::Line &outp
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
|
||||
mouse_button_state_ = 0;
|
||||
mouse_button_events_ = 0;
|
||||
mouse_movement_[0] = 0;
|
||||
mouse_movement_[1] = 0;
|
||||
}
|
||||
@@ -46,20 +52,38 @@ ClockingHint::Preference IntelligentKeyboard::preferred_clocking() {
|
||||
void IntelligentKeyboard::run_for(HalfCycles duration) {
|
||||
// Take this opportunity to check for joystick, mouse and keyboard events,
|
||||
// which will have been received asynchronously.
|
||||
if(mouse_mode_ == MouseMode::Relative) {
|
||||
const int captured_movement[2] = { mouse_movement_[0].load(), mouse_movement_[1].load() };
|
||||
const int captured_button_state = mouse_button_state_;
|
||||
if(
|
||||
(posted_button_state_ != captured_button_state) ||
|
||||
(abs(captured_movement[0]) >= mouse_threshold_[0]) ||
|
||||
(abs(captured_movement[1]) >= mouse_threshold_[1]) ) {
|
||||
mouse_movement_[0] -= captured_movement[0];
|
||||
mouse_movement_[1] -= captured_movement[1];
|
||||
const int captured_movement[2] = { mouse_movement_[0].load(), mouse_movement_[1].load() };
|
||||
switch(mouse_mode_) {
|
||||
case MouseMode::Relative: {
|
||||
const int captured_button_state = mouse_button_state_;
|
||||
if(
|
||||
(posted_button_state_ != captured_button_state) ||
|
||||
(abs(captured_movement[0]) >= mouse_threshold_[0]) ||
|
||||
(abs(captured_movement[1]) >= mouse_threshold_[1]) ) {
|
||||
mouse_movement_[0] -= captured_movement[0];
|
||||
mouse_movement_[1] -= captured_movement[1];
|
||||
|
||||
post_relative_mouse_event(captured_movement[0], captured_movement[1]);
|
||||
}
|
||||
} else {
|
||||
// TODO: absolute-mode mouse updates.
|
||||
post_relative_mouse_event(captured_movement[0], captured_movement[1] * mouse_y_multiplier_);
|
||||
}
|
||||
} break;
|
||||
|
||||
case MouseMode::Absolute: {
|
||||
const int scaled_movement[2] = { captured_movement[0] / mouse_scale_[0], captured_movement[1] / mouse_scale_[1] };
|
||||
mouse_position_[0] += scaled_movement[0];
|
||||
mouse_position_[1] += mouse_y_multiplier_ * scaled_movement[1];
|
||||
|
||||
// Clamp to range.
|
||||
mouse_position_[0] = std::min(std::max(mouse_position_[0], 0), mouse_range_[0]);
|
||||
mouse_position_[1] = std::min(std::max(mouse_position_[1], 0), mouse_range_[1]);
|
||||
|
||||
mouse_movement_[0] -= scaled_movement[0] * mouse_scale_[0];
|
||||
mouse_movement_[1] -= scaled_movement[1] * mouse_scale_[1];
|
||||
} break;
|
||||
|
||||
case MouseMode::Disabled:
|
||||
mouse_movement_[0] = 0;
|
||||
mouse_movement_[1] = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Forward key changes; implicit assumption here: mutexs are cheap while there's
|
||||
@@ -72,14 +96,50 @@ void IntelligentKeyboard::run_for(HalfCycles duration) {
|
||||
key_queue_.clear();
|
||||
}
|
||||
|
||||
// Check for joystick changes.
|
||||
for(size_t c = 0; c < 2; ++c) {
|
||||
const auto joystick = static_cast<Joystick *>(joysticks_[c].get());
|
||||
if(joystick->has_event()) {
|
||||
output_bytes({
|
||||
uint8_t(0xfe | c),
|
||||
joystick->get_state()
|
||||
});
|
||||
// Check for joystick changes; slight complexity here: the joystick that the emulated
|
||||
// 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.
|
||||
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]))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,49 +265,68 @@ void IntelligentKeyboard::reset() {
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::resume() {
|
||||
LOG("Unimplemented: resume");
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::pause() {
|
||||
LOG("Unimplemented: pause");
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::disable_mouse() {
|
||||
mouse_mode_ = MouseMode::Disabled;
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_relative_mouse_position_reporting() {
|
||||
mouse_mode_ = MouseMode::Relative;
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_absolute_mouse_position_reporting(uint16_t max_x, uint16_t max_y) {
|
||||
mouse_mode_ = MouseMode::Absolute;
|
||||
mouse_range_[0] = int(max_x);
|
||||
mouse_range_[1] = int(max_y);
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_mouse_position(uint16_t x, uint16_t y) {
|
||||
mouse_position_[0] = std::min(int(x), mouse_range_[0]);
|
||||
mouse_position_[1] = std::min(int(y), mouse_range_[1]);
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_mouse_keycode_reporting(uint8_t delta_x, uint8_t delta_y) {
|
||||
LOG("Unimplemented: set mouse keycode reporting");
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_mouse_threshold(uint8_t x, uint8_t y) {
|
||||
mouse_threshold_[0] = x;
|
||||
mouse_threshold_[1] = y;
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_mouse_scale(uint8_t x, uint8_t y) {
|
||||
mouse_scale_[0] = x;
|
||||
mouse_scale_[1] = y;
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_mouse_y_downward() {
|
||||
mouse_y_multiplier_ = 1;
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_mouse_y_upward() {
|
||||
mouse_y_multiplier_ = -1;
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_mouse_button_actions(uint8_t actions) {
|
||||
LOG("Unimplemented: set mouse button actions");
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::interrogate_mouse_position() {
|
||||
const int captured_mouse_button_events_ = mouse_button_events_;
|
||||
mouse_button_events_ &= ~captured_mouse_button_events_;
|
||||
output_bytes({
|
||||
0xf7, // Beginning of mouse response.
|
||||
0x00, // 0000dcba; a = right button down since last interrogation, b = right button up since, c/d = left button.
|
||||
0x00, // x motion: MSB, LSB
|
||||
0x00,
|
||||
0x00, // y motion: MSB, LSB
|
||||
0x00
|
||||
0xf7, // Beginning of mouse response.
|
||||
uint8_t(captured_mouse_button_events_), // 0000dcba; a = right button down since last interrogation, b = right button up since, c/d = left button.
|
||||
uint8_t(mouse_position_[0] >> 8), // x position: MSB, LSB
|
||||
uint8_t(mouse_position_[0] & 0xff),
|
||||
uint8_t(mouse_position_[1] >> 8), // y position: MSB, LSB
|
||||
uint8_t(mouse_position_[1] & 0xff)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -355,11 +434,16 @@ int IntelligentKeyboard::get_number_of_buttons() {
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_button_pressed(int index, bool is_pressed) {
|
||||
const auto mask = 1 << (index ^ 1); // The primary button is b1; the secondary is b0.
|
||||
index ^= 1; // The primary button is b1; the secondary is b0.
|
||||
|
||||
const auto mask = 1 << index;
|
||||
const auto event_mask = 1 << (index << 1);
|
||||
if(is_pressed) {
|
||||
mouse_button_state_ |= mask;
|
||||
mouse_button_events_ |= event_mask;
|
||||
} else {
|
||||
mouse_button_state_ &= ~mask;
|
||||
mouse_button_events_ |= event_mask << 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,28 +458,51 @@ 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,
|
||||
joystick1->get_state(),
|
||||
joystick2->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) {
|
||||
LOG("Unimplemented: joystick monitoring mode");
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_joystick_fire_button_monitoring_mode() {
|
||||
}
|
||||
|
||||
void IntelligentKeyboard::set_joystick_keycode_mode(VelocityThreshold horizontal, VelocityThreshold vertical) {
|
||||
LOG("Unimplemented: joystick fire button monitoring 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");
|
||||
@@ -110,12 +112,14 @@ class IntelligentKeyboard:
|
||||
void reset_all_buttons() final;
|
||||
|
||||
enum class MouseMode {
|
||||
Relative, Absolute
|
||||
Relative, Absolute, Disabled
|
||||
} mouse_mode_ = MouseMode::Relative;
|
||||
|
||||
// Absolute positioning state.
|
||||
int mouse_range_[2] = {0, 0};
|
||||
int mouse_scale_[2] = {0, 0};
|
||||
int mouse_range_[2] = {320, 200};
|
||||
int mouse_scale_[2] = {1, 1};
|
||||
int mouse_position_[2] = {0, 0};
|
||||
int mouse_y_multiplier_ = 1;
|
||||
|
||||
// Relative positioning state.
|
||||
int posted_button_state_ = 0;
|
||||
@@ -125,6 +129,7 @@ class IntelligentKeyboard:
|
||||
// Received mouse state.
|
||||
std::atomic<int> mouse_movement_[2];
|
||||
std::atomic<int> mouse_button_state_;
|
||||
std::atomic<int> mouse_button_events_;
|
||||
|
||||
// MARK: - Joystick.
|
||||
void disable_joysticks();
|
||||
@@ -140,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 {
|
||||
@@ -155,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;
|
||||
@@ -178,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,15 +31,14 @@ 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.
|
||||
const VerticalParams &vertical_parameters(FieldFrequency frequency) {
|
||||
const VerticalParams &vertical_parameters(Video::FieldFrequency frequency) {
|
||||
return vertical_params[int(frequency)];
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
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
|
||||
@@ -56,14 +57,24 @@ const struct HorizontalParams {
|
||||
const int set_blank;
|
||||
const int reset_blank;
|
||||
|
||||
const int length;
|
||||
const int vertical_decision;
|
||||
|
||||
LineLength length;
|
||||
} horizontal_params[3] = {
|
||||
{56*2, 376*2, 450*2, 28*2, 512*2},
|
||||
{52*2, 372*2, 450*2, 24*2, 508*2},
|
||||
{4*2, 164*2, 184*2, 2*2, 224*2}
|
||||
{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.
|
||||
};
|
||||
|
||||
const HorizontalParams &horizontal_parameters(FieldFrequency frequency) {
|
||||
// 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)];
|
||||
}
|
||||
|
||||
@@ -72,14 +83,20 @@ struct Checker {
|
||||
Checker() {
|
||||
for(int c = 0; c < 3; ++c) {
|
||||
// Expected horizontal order of events: reset blank, enable display, disable display, enable blank (at least 50 before end of line), end of line
|
||||
const auto horizontal = horizontal_parameters(FieldFrequency(c));
|
||||
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);
|
||||
const auto horizontal = horizontal_parameters(Video::FieldFrequency(c));
|
||||
|
||||
if(c < 2) {
|
||||
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.length);
|
||||
} else {
|
||||
assert(horizontal.set_enable < horizontal.reset_enable);
|
||||
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
|
||||
const auto vertical = vertical_parameters(FieldFrequency(c));
|
||||
const auto vertical = vertical_parameters(Video::FieldFrequency(c));
|
||||
assert(vertical.set_enable < vertical.reset_enable);
|
||||
assert(vertical.reset_enable < vertical.height);
|
||||
}
|
||||
@@ -87,15 +104,30 @@ struct Checker {
|
||||
} checker;
|
||||
#endif
|
||||
|
||||
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 line_length_latch_position = CYCLE(54);
|
||||
|
||||
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.
|
||||
|
||||
}
|
||||
|
||||
Video::Video() :
|
||||
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
|
||||
shifter_(crt_, palette_) {
|
||||
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) {
|
||||
@@ -106,14 +138,31 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void Video::run_for(HalfCycles duration) {
|
||||
const auto horizontal_timings = horizontal_parameters(field_frequency_);
|
||||
const auto vertical_timings = vertical_parameters(field_frequency_);
|
||||
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) {
|
||||
int integer_duration = int(duration.as_integral());
|
||||
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);
|
||||
@@ -122,29 +171,57 @@ void Video::run_for(HalfCycles duration) {
|
||||
if(horizontal_timings.set_enable > x_) next_event = std::min(next_event, horizontal_timings.set_enable);
|
||||
|
||||
// Check for events that are relative to existing latched state.
|
||||
if(line_length_ - 50*2 > x_) next_event = std::min(next_event, line_length_ - 50*2);
|
||||
if(line_length_ - 10*2 > x_) next_event = std::min(next_event, line_length_ - 10*2);
|
||||
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_ < 30*2 && next_event >= 30*2) {
|
||||
next_event = 30*2;
|
||||
if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && x_ < vsync_x_position && next_event >= vsync_x_position) {
|
||||
next_event = vsync_x_position;
|
||||
}
|
||||
|
||||
// Determine current output mode and number of cycles to output for.
|
||||
const int run_length = std::min(integer_duration, next_event - x_);
|
||||
const bool display_enable = vertical_.enable && horizontal_.enable;
|
||||
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_;
|
||||
|
||||
if(horizontal_.sync || vertical_.sync) {
|
||||
shifter_.output_sync(run_length);
|
||||
} else if(horizontal_.blank || vertical_.blank) {
|
||||
shifter_.output_blank(run_length);
|
||||
} else if(!vertical_.enable || !horizontal_.enable) {
|
||||
shifter_.output_border(run_length, output_bpp_);
|
||||
} else {
|
||||
// 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 = x_ >> 3;
|
||||
const int end_column = (x_ + 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];
|
||||
data_latch_position_ = (data_latch_position_ + 1) & 127;
|
||||
++current_address_;
|
||||
++start_column;
|
||||
}
|
||||
}
|
||||
|
||||
if(horizontal_.sync || vertical_.sync) {
|
||||
video_stream_.output(run_length, VideoStream::OutputMode::Sync);
|
||||
} else if(horizontal_.blank || vertical_.blank) {
|
||||
video_stream_.output(run_length, VideoStream::OutputMode::Blank);
|
||||
} else if(!load_) {
|
||||
video_stream_.output(run_length, VideoStream::OutputMode::Pixels);
|
||||
} else {
|
||||
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 = start >> 3;
|
||||
const int end_column = end >> 3;
|
||||
const int start_offset = start & 7;
|
||||
const int end_offset = end & 7;
|
||||
|
||||
// Rules obeyed below:
|
||||
//
|
||||
@@ -153,106 +230,164 @@ void Video::run_for(HalfCycles duration) {
|
||||
// was reloaded by the fetch depends on the FIFO.
|
||||
|
||||
if(start_column == end_column) {
|
||||
shifter_.output_pixels(run_length, output_bpp_);
|
||||
if(!start_offset) {
|
||||
push_latched_data();
|
||||
}
|
||||
video_stream_.output(run_length, VideoStream::OutputMode::Pixels);
|
||||
} else {
|
||||
// Continue the current column if partway across.
|
||||
if(x_&7) {
|
||||
if(start_offset) {
|
||||
// If at least one column boundary is crossed, complete this column.
|
||||
shifter_.output_pixels(8 - (x_ & 7), output_bpp_);
|
||||
video_stream_.output(8 - start_offset, VideoStream::OutputMode::Pixels);
|
||||
++start_column; // This starts a new column, so latch a new word.
|
||||
latch_word();
|
||||
}
|
||||
|
||||
// Run for all columns that have their starts in this time period.
|
||||
int complete_columns = end_column - start_column;
|
||||
while(complete_columns--) {
|
||||
shifter_.output_pixels(8, output_bpp_);
|
||||
latch_word();
|
||||
push_latched_data();
|
||||
video_stream_.output(8, VideoStream::OutputMode::Pixels);
|
||||
}
|
||||
|
||||
// Output the start of the next column, if necessary.
|
||||
if(start_column != end_column && (x_ + run_length) & 7) {
|
||||
shifter_.output_pixels((x_ + run_length) & 7, output_bpp_);
|
||||
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_ <= 54*2 && (x_ + run_length) > 54*2) 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_ <= 502*2 && (x_ + run_length) > 502*2) {
|
||||
// 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;
|
||||
|
||||
// Use vertical_parameters to get parameters for the current output frequency.
|
||||
if(next_y_ == vertical_timings.set_enable) {
|
||||
// Use vertical_parameters to get parameters for the current output frequency;
|
||||
// quick note: things other than the total frame size are counted in terms
|
||||
// of the line they're evaluated on — i.e. the test is this line, not the next
|
||||
// one. The total height constraint is obviously whether the next one would be
|
||||
// too far.
|
||||
if(y_ == vertical_timings.set_enable) {
|
||||
next_vertical_.enable = true;
|
||||
} else if(next_y_ == vertical_timings.reset_enable) {
|
||||
} else if(y_ == vertical_timings.reset_enable) {
|
||||
next_vertical_.enable = false;
|
||||
} 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;
|
||||
next_vertical_.sync_schedule = VerticalState::SyncSchedule::Begin;
|
||||
current_address_ = base_address_ >> 1;
|
||||
} else if(next_y_ == 3) {
|
||||
} 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.
|
||||
// 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_ - 50*2 == x_) horizontal_.sync = true;
|
||||
else if(line_length_ - 10*2 == x_) horizontal_.sync = false;
|
||||
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_ == 30*2) {
|
||||
if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && x_ == vsync_x_position) {
|
||||
vertical_.sync = vertical_.sync_schedule == VerticalState::SyncSchedule::Begin;
|
||||
vertical_.enable &= !vertical_.sync;
|
||||
|
||||
reset_fifo(); // TODO: remove this, probably, once otherwise stable?
|
||||
}
|
||||
|
||||
// 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_;
|
||||
}
|
||||
|
||||
// The address is reloaded during the entire period of vertical sync.
|
||||
// Cf. http://www.atari-forum.com/viewtopic.php?t=31954&start=50#p324730
|
||||
if(vertical_.sync) {
|
||||
current_address_ = base_address_ >> 1;
|
||||
|
||||
// Consider a shout out to the range observer.
|
||||
if(previous_base_address_ != base_address_) {
|
||||
previous_base_address_ = base_address_;
|
||||
if(range_observer_) {
|
||||
range_observer_->video_did_change_access_range(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 load line.
|
||||
deferrer_.defer(load_delay_period, [this, next_display_enable] {
|
||||
this->load_ = next_display_enable;
|
||||
this->load_base_ = this->x_;
|
||||
});
|
||||
|
||||
// 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.
|
||||
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.
|
||||
deferrer_.defer(vsync_delay_period, [this, next_vertical_sync = vertical_.sync] {
|
||||
this->public_state_.vsync = next_vertical_sync;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Video::latch_word() {
|
||||
data_latch_[data_latch_position_] = ram_[current_address_ & 262143];
|
||||
++current_address_;
|
||||
++data_latch_position_;
|
||||
if(data_latch_position_ == 4) {
|
||||
data_latch_position_ = 0;
|
||||
shifter_.load(
|
||||
(uint64_t(data_latch_[0]) << 48) |
|
||||
(uint64_t(data_latch_[1]) << 32) |
|
||||
(uint64_t(data_latch_[2]) << 16) |
|
||||
uint64_t(data_latch_[3])
|
||||
void Video::push_latched_data() {
|
||||
data_latch_read_position_ = (data_latch_read_position_ + 1) & 127;
|
||||
|
||||
if(!(data_latch_read_position_ & 3)) {
|
||||
video_stream_.load(
|
||||
(uint64_t(data_latch_[(data_latch_read_position_ - 4) & 127]) << 48) |
|
||||
(uint64_t(data_latch_[(data_latch_read_position_ - 3) & 127]) << 32) |
|
||||
(uint64_t(data_latch_[(data_latch_read_position_ - 2) & 127]) << 16) |
|
||||
uint64_t(data_latch_[(data_latch_read_position_ - 1) & 127])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void Video::reset_fifo() {
|
||||
data_latch_read_position_ = data_latch_position_ = 0;
|
||||
}
|
||||
|
||||
bool Video::hsync() {
|
||||
return horizontal_.sync;
|
||||
return public_state_.hsync;
|
||||
}
|
||||
|
||||
bool Video::vsync() {
|
||||
return vertical_.sync;
|
||||
return public_state_.vsync;
|
||||
}
|
||||
|
||||
bool Video::display_enabled() {
|
||||
return horizontal_.enable && vertical_.enable;
|
||||
return public_state_.display_enable;
|
||||
}
|
||||
|
||||
HalfCycles Video::get_next_sequence_point() {
|
||||
@@ -273,29 +408,45 @@ HalfCycles Video::get_next_sequence_point() {
|
||||
// visible area.
|
||||
|
||||
const auto horizontal_timings = horizontal_parameters(field_frequency_);
|
||||
// const auto vertical_timings = vertical_parameters(field_frequency_);
|
||||
|
||||
// If this is a vertically-enabled line, check for the display enable boundaries.
|
||||
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.
|
||||
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.
|
||||
if(vertical_.enable) {
|
||||
// TODO: what if there's a sync event scheduled for this line?
|
||||
if(x_ < horizontal_timings.set_enable) return HalfCycles(horizontal_timings.set_enable - x_);
|
||||
if(x_ < horizontal_timings.reset_enable) return HalfCycles(horizontal_timings.reset_enable - x_);
|
||||
} else {
|
||||
if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && (x_ < 30*2)) {
|
||||
return HalfCycles(30*2 - x_);
|
||||
if(x_ < horizontal_timings.set_enable + de_delay_period) {
|
||||
event_time = std::min(event_time, horizontal_timings.set_enable + de_delay_period);
|
||||
}
|
||||
else if(x_ < horizontal_timings.reset_enable + de_delay_period) {
|
||||
event_time = std::min(event_time, horizontal_timings.reset_enable + de_delay_period);
|
||||
}
|
||||
}
|
||||
|
||||
// Test for beginning and end of sync.
|
||||
if(x_ < line_length_ - 50) return HalfCycles(line_length_ - 50 - x_);
|
||||
if(x_ < line_length_ - 10) return HalfCycles(line_length_ - 10 - x_);
|
||||
// If a vertical sync event is scheduled, test for that.
|
||||
if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && (x_ < vsync_x_position)) {
|
||||
event_time = std::min(event_time, vsync_x_position);
|
||||
}
|
||||
|
||||
// Okay, then, it depends on the next line. If the next line is the start or end of vertical sync,
|
||||
// it's that.
|
||||
// if(y_+1 == vertical_timings.height || y_+1 == 3) return HalfCycles(line_length_ - x_);
|
||||
// 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_end + hsync_delay_period) {
|
||||
event_time = std::min(line_length_.hsync_end + hsync_delay_period, event_time);
|
||||
}
|
||||
|
||||
// It wasn't any of those, so as a temporary expedient, just supply end of line.
|
||||
return HalfCycles(line_length_ - x_);
|
||||
// Also factor in the line length latching time.
|
||||
if(x_ < line_length_latch_position) {
|
||||
event_time = std::min(line_length_latch_position, event_time);
|
||||
}
|
||||
|
||||
// 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_);
|
||||
}
|
||||
|
||||
// MARK: - IO dispatch
|
||||
@@ -333,8 +484,11 @@ void Video::write(int address, uint16_t value) {
|
||||
|
||||
// Sync mode and pixel mode.
|
||||
case 0x05:
|
||||
sync_mode_ = value;
|
||||
update_output_mode();
|
||||
// Writes to sync mode have a one-cycle delay in effect.
|
||||
deferrer_.defer(HalfCycles(2), [this, value] {
|
||||
sync_mode_ = value;
|
||||
update_output_mode();
|
||||
});
|
||||
break;
|
||||
case 0x30:
|
||||
video_mode_ = value;
|
||||
@@ -346,6 +500,8 @@ void Video::write(int address, uint16_t value) {
|
||||
case 0x24: case 0x25: case 0x26: case 0x27:
|
||||
case 0x28: case 0x29: case 0x2a: case 0x2b:
|
||||
case 0x2c: case 0x2d: case 0x2e: case 0x2f: {
|
||||
if(address == 0x20) video_stream_.will_change_border_colour();
|
||||
|
||||
raw_palette_[address - 0x20] = value;
|
||||
uint8_t *const entry = reinterpret_cast<uint8_t *>(&palette_[address - 0x20]);
|
||||
entry[0] = uint8_t((value & 0x700) >> 7);
|
||||
@@ -355,146 +511,195 @@ void Video::write(int address, uint16_t value) {
|
||||
}
|
||||
|
||||
void Video::update_output_mode() {
|
||||
const auto old_bpp_ = output_bpp_;
|
||||
|
||||
// If this is black and white mode, that's that.
|
||||
switch((video_mode_ >> 8) & 3) {
|
||||
default:
|
||||
case 0: output_bpp_ = OutputBpp::Four; break;
|
||||
case 1: output_bpp_ = OutputBpp::Two; break;
|
||||
|
||||
// 1bpp mode ignores the otherwise-programmed frequency.
|
||||
case 2:
|
||||
output_bpp_ = OutputBpp::One;
|
||||
field_frequency_ = FieldFrequency::SeventyTwo;
|
||||
return;
|
||||
default:
|
||||
case 2: output_bpp_ = OutputBpp::One; break;
|
||||
}
|
||||
|
||||
field_frequency_ = (sync_mode_ & 0x200) ? FieldFrequency::Fifty : FieldFrequency::Sixty;
|
||||
// 1bpp mode ignores the otherwise-programmed frequency.
|
||||
if(output_bpp_ == OutputBpp::One) {
|
||||
field_frequency_ = FieldFrequency::SeventyTwo;
|
||||
} else {
|
||||
field_frequency_ = (sync_mode_ & 0x200) ? FieldFrequency::Fifty : FieldFrequency::Sixty;
|
||||
}
|
||||
if(output_bpp_ != old_bpp_) {
|
||||
// "the 71-Hz-switch does something like a shifter-reset." (and some people use a high-low resolutions switch instead)
|
||||
reset_fifo();
|
||||
video_stream_.set_bpp(output_bpp_);
|
||||
}
|
||||
|
||||
// const int freqs[] = {50, 60, 72};
|
||||
// printf("%d, %d -> %d [%d %d]\n", x_ / 2, y_, freqs[int(field_frequency_)], horizontal_.enable, vertical_.enable);
|
||||
}
|
||||
|
||||
// MARK: - The shifter
|
||||
|
||||
void Video::Shifter::flush_output(OutputMode next_mode) {
|
||||
switch(output_mode_) {
|
||||
case OutputMode::Sync: crt_.output_sync(duration_); break;
|
||||
case OutputMode::Blank: crt_.output_blank(duration_); break;
|
||||
case OutputMode::Border: {
|
||||
// if(!border_colour_) {
|
||||
// crt_.output_blank(duration_);
|
||||
// } else {
|
||||
uint16_t *const colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = border_colour_;
|
||||
crt_.output_level(duration_);
|
||||
// }
|
||||
} break;
|
||||
case OutputMode::Pixels: {
|
||||
crt_.output_data(duration_, pixel_pointer_);
|
||||
pixel_buffer_ = nullptr;
|
||||
pixel_pointer_ = 0;
|
||||
} break;
|
||||
}
|
||||
duration_ = 0;
|
||||
output_mode_ = next_mode;
|
||||
}
|
||||
|
||||
|
||||
void Video::Shifter::output_blank(int duration) {
|
||||
if(output_mode_ != OutputMode::Blank) {
|
||||
flush_output(OutputMode::Blank);
|
||||
}
|
||||
duration_ += duration;
|
||||
}
|
||||
|
||||
void Video::Shifter::output_sync(int duration) {
|
||||
if(output_mode_ != OutputMode::Sync) {
|
||||
flush_output(OutputMode::Sync);
|
||||
}
|
||||
duration_ += duration;
|
||||
}
|
||||
|
||||
void Video::Shifter::output_border(int duration, OutputBpp bpp) {
|
||||
// If there's still anything in the shifter, redirect this to an output_pixels call.
|
||||
if(output_shifter_) {
|
||||
// This doesn't take an opinion on how much of the shifter remains populated;
|
||||
// it assumes the worst case.
|
||||
const int pixel_length = std::min(32, duration);
|
||||
output_pixels(pixel_length, bpp);
|
||||
duration -= pixel_length;
|
||||
if(!duration) {
|
||||
return;
|
||||
}
|
||||
void Video::VideoStream::output(int duration, OutputMode mode) {
|
||||
// If this is a transition from sync to blank, actually transition to colour burst.
|
||||
if(output_mode_ == OutputMode::Sync && mode == OutputMode::Blank) {
|
||||
mode = OutputMode::ColourBurst;
|
||||
}
|
||||
|
||||
// Flush anything that isn't level output *in the current border colour*.
|
||||
if(output_mode_ != OutputMode::Border || border_colour_ != palette_[0]) {
|
||||
flush_output(OutputMode::Border);
|
||||
border_colour_ = palette_[0];
|
||||
}
|
||||
duration_ += duration;
|
||||
}
|
||||
// If this is seeming a transition from blank to colour burst, obey it only if/when
|
||||
// sufficient colour burst has been output.
|
||||
if(output_mode_ == OutputMode::Blank && mode == OutputMode::ColourBurst) {
|
||||
if(duration_ + duration >= 40) {
|
||||
const int overage = duration + duration_ - 40;
|
||||
duration_ = 40;
|
||||
|
||||
void Video::Shifter::output_pixels(int duration, OutputBpp bpp) {
|
||||
// If the shifter is empty and there's no pixel buffer at present,
|
||||
// redirect this to an output_level call. Otherwise, do a quick
|
||||
// memset-type fill, since the special case has been detected anyway.
|
||||
if(!output_shifter_) {
|
||||
if(!pixel_buffer_) {
|
||||
output_border(duration, bpp);
|
||||
generate(overage, OutputMode::ColourBurst, true);
|
||||
} else {
|
||||
duration_ += duration;
|
||||
|
||||
switch(bpp_) {
|
||||
case OutputBpp::One: {
|
||||
const size_t pixels = size_t(duration << 1);
|
||||
memset(&pixel_buffer_[pixel_pointer_], 0, pixels * sizeof(uint16_t));
|
||||
pixel_pointer_ += pixels;
|
||||
} break;
|
||||
|
||||
default:
|
||||
case OutputBpp::Four:
|
||||
assert(!(duration & 1));
|
||||
duration >>= 1;
|
||||
case OutputBpp::Two: {
|
||||
while(duration--) {
|
||||
pixel_buffer_[pixel_pointer_] = palette_[0];
|
||||
++pixel_pointer_;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
mode = OutputMode::ColourBurst;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a transition, or if we're doing pixels, output whatever has been accumulated.
|
||||
if(mode != output_mode_ || output_mode_ == OutputMode::Pixels) {
|
||||
generate(duration, output_mode_, mode != output_mode_);
|
||||
} else {
|
||||
duration_ += duration;
|
||||
}
|
||||
|
||||
// Accumulate time in the current mode.
|
||||
output_mode_ = mode;
|
||||
}
|
||||
|
||||
void Video::VideoStream::generate(int duration, OutputMode mode, bool is_terminal) {
|
||||
// Three of these are trivial; deal with them upfront. They don't care about the duration of
|
||||
// whatever is new, just about how much was accumulated prior to now.
|
||||
if(mode != OutputMode::Pixels) {
|
||||
switch(mode) {
|
||||
default:
|
||||
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.
|
||||
duration_ = duration;
|
||||
|
||||
// The shifter should keep running, so throw away the proper amount of content.
|
||||
shift(duration_);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Flush anything that isn't pixel output in the proper bpp; also flush if there's nowhere
|
||||
// left to put pixels.
|
||||
if(output_mode_ != OutputMode::Pixels || bpp_ != bpp || pixel_pointer_ >= 320) {
|
||||
flush_output(OutputMode::Pixels);
|
||||
bpp_ = bpp;
|
||||
pixel_buffer_ = reinterpret_cast<uint16_t *>(crt_.begin_data(320 + 32));
|
||||
}
|
||||
duration_ += duration;
|
||||
// If the shifter is empty, accumulate in duration_ a promise to draw border later.
|
||||
if(!output_shifter_) {
|
||||
if(pixel_pointer_) {
|
||||
flush_pixels();
|
||||
}
|
||||
|
||||
duration_ += duration;
|
||||
|
||||
// If this is terminal, we'll need to draw now. But if it isn't, job done.
|
||||
if(is_terminal) {
|
||||
flush_border();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// There's definitely some pixels to convey, but perhaps there's some border first?
|
||||
if(duration_) {
|
||||
flush_border();
|
||||
}
|
||||
|
||||
// Time to do some pixels!
|
||||
output_pixels(duration);
|
||||
|
||||
// If was terminal, make sure any transient storage is output.
|
||||
if(is_terminal) {
|
||||
flush_pixels();
|
||||
}
|
||||
}
|
||||
|
||||
void Video::VideoStream::will_change_border_colour() {
|
||||
// Flush the accumulated border if it'd be adversely affected.
|
||||
if(duration_ && output_mode_ == OutputMode::Pixels) {
|
||||
flush_border();
|
||||
}
|
||||
}
|
||||
|
||||
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_*2);
|
||||
|
||||
duration_ = 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
constexpr int upper = 0;
|
||||
#else
|
||||
constexpr int upper = 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Video::VideoStream::shift(int duration) {
|
||||
switch(bpp_) {
|
||||
case OutputBpp::One: {
|
||||
int pixels = duration << 1;
|
||||
if(pixel_buffer_) {
|
||||
while(pixels--) {
|
||||
case OutputBpp::One:
|
||||
output_shifter_ <<= (duration << 1);
|
||||
break;
|
||||
case OutputBpp::Two:
|
||||
while(duration--) {
|
||||
shifter_halves_[upper] = (shifter_halves_[upper] << 1) & 0xfffefffe;
|
||||
shifter_halves_[upper] |= (shifter_halves_[upper^1] & 0x80008000) >> 15;
|
||||
shifter_halves_[upper^1] = (shifter_halves_[upper^1] << 1) & 0xfffefffe;
|
||||
}
|
||||
break;
|
||||
case OutputBpp::Four:
|
||||
while(duration) {
|
||||
output_shifter_ = (output_shifter_ << 1) & 0xfffefffefffefffe;
|
||||
duration -= 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: turn this into a template on current BPP, perhaps? Would avoid reevaluation of the conditional.
|
||||
void Video::VideoStream::output_pixels(int duration) {
|
||||
constexpr int allocation_size = 352; // i.e. 320 plus a spare 32.
|
||||
|
||||
// Convert from duration to pixels.
|
||||
int pixels = duration;
|
||||
switch(bpp_) {
|
||||
case OutputBpp::One: pixels <<= 1; break;
|
||||
default: break;
|
||||
case OutputBpp::Four: pixels >>= 1; break;
|
||||
}
|
||||
|
||||
while(pixels) {
|
||||
// If no buffer is currently available, attempt to allocate one.
|
||||
if(!pixel_buffer_) {
|
||||
pixel_buffer_ = reinterpret_cast<uint16_t *>(crt_.begin_data(allocation_size, 2));
|
||||
|
||||
// Stop the loop if no buffer is available.
|
||||
if(!pixel_buffer_) break;
|
||||
}
|
||||
|
||||
int pixels_to_draw = std::min(allocation_size - pixel_pointer_, pixels);
|
||||
pixels -= pixels_to_draw;
|
||||
|
||||
switch(bpp_) {
|
||||
case OutputBpp::One:
|
||||
while(pixels_to_draw--) {
|
||||
pixel_buffer_[pixel_pointer_] = ((output_shifter_ >> 63) & 1) * 0xffff;
|
||||
output_shifter_ <<= 1;
|
||||
|
||||
++pixel_pointer_;
|
||||
}
|
||||
} else {
|
||||
pixel_pointer_ += size_t(pixels);
|
||||
output_shifter_ <<= pixels;
|
||||
}
|
||||
} break;
|
||||
case OutputBpp::Two: {
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
const int upper = 0;
|
||||
#else
|
||||
const int upper = 1;
|
||||
#endif
|
||||
if(pixel_buffer_) {
|
||||
while(duration--) {
|
||||
break;
|
||||
|
||||
case OutputBpp::Two:
|
||||
while(pixels_to_draw--) {
|
||||
pixel_buffer_[pixel_pointer_] = palette_[
|
||||
((output_shifter_ >> 63) & 1) |
|
||||
((output_shifter_ >> 46) & 2)
|
||||
@@ -508,20 +713,10 @@ void Video::Shifter::output_pixels(int duration, OutputBpp bpp) {
|
||||
|
||||
++pixel_pointer_;
|
||||
}
|
||||
} else {
|
||||
pixel_pointer_ += size_t(duration);
|
||||
while(duration--) {
|
||||
shifter_halves_[upper] = (shifter_halves_[upper] << 1) & 0xfffefffe;
|
||||
shifter_halves_[upper] |= (shifter_halves_[upper^1] & 0x80008000) >> 15;
|
||||
shifter_halves_[upper^1] = (shifter_halves_[upper^1] << 1) & 0xfffefffe;
|
||||
}
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
case OutputBpp::Four:
|
||||
assert(!(duration & 1));
|
||||
if(pixel_buffer_) {
|
||||
while(duration) {
|
||||
break;
|
||||
|
||||
case OutputBpp::Four:
|
||||
while(pixels_to_draw--) {
|
||||
pixel_buffer_[pixel_pointer_] = palette_[
|
||||
((output_shifter_ >> 63) & 1) |
|
||||
((output_shifter_ >> 46) & 2) |
|
||||
@@ -529,20 +724,80 @@ void Video::Shifter::output_pixels(int duration, OutputBpp bpp) {
|
||||
((output_shifter_ >> 12) & 8)
|
||||
];
|
||||
output_shifter_ = (output_shifter_ << 1) & 0xfffefffefffefffe;
|
||||
|
||||
++pixel_pointer_;
|
||||
duration -= 2;
|
||||
}
|
||||
} else {
|
||||
pixel_pointer_ += size_t(duration >> 1);
|
||||
while(duration) {
|
||||
output_shifter_ = (output_shifter_ << 1) & 0xfffefffefffefffe;
|
||||
duration -= 2;
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check whether the limit has been reached.
|
||||
if(pixel_pointer_ >= allocation_size - 32) {
|
||||
flush_pixels();
|
||||
}
|
||||
}
|
||||
|
||||
// If duration remains, that implies no buffer was available, so
|
||||
// just do the corresponding shifting and provide proper timing to the CRT.
|
||||
if(pixels) {
|
||||
int leftover_duration = pixels;
|
||||
switch(bpp_) {
|
||||
default: leftover_duration >>= 1; break;
|
||||
case OutputBpp::Two: break;
|
||||
case OutputBpp::Four: leftover_duration <<= 1; break;
|
||||
}
|
||||
shift(leftover_duration);
|
||||
crt_.output_data(leftover_duration*2);
|
||||
}
|
||||
}
|
||||
|
||||
void Video::Shifter::load(uint64_t value) {
|
||||
output_shifter_ = value;
|
||||
void Video::VideoStream::flush_pixels() {
|
||||
// Flush only if there's something to flush.
|
||||
if(pixel_pointer_) {
|
||||
switch(bpp_) {
|
||||
case OutputBpp::One: crt_.output_data(pixel_pointer_); 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;
|
||||
pixel_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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.
|
||||
|
||||
Video::Range Video::get_memory_access_range() {
|
||||
Range range;
|
||||
range.low_address = uint32_t(previous_base_address_);
|
||||
range.high_address = range.low_address + 56994;
|
||||
// 56994 is pessimistic but unscientific, being derived from the resolution of the largest
|
||||
// fullscreen demo I could quickly find documentation of. TODO: calculate real number.
|
||||
return range;
|
||||
}
|
||||
|
||||
void Video::set_range_observer(RangeObserver *observer) {
|
||||
range_observer_ = observer;
|
||||
observer->video_did_change_access_range(this);
|
||||
}
|
||||
|
@@ -11,55 +11,132 @@
|
||||
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../ClockReceiver/DeferredQueue.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
// Testing hook; not for any other user.
|
||||
class VideoTester;
|
||||
|
||||
namespace Atari {
|
||||
namespace ST {
|
||||
|
||||
enum class FieldFrequency {
|
||||
Fifty = 0, Sixty = 1, SeventyTwo = 2
|
||||
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
|
||||
(hopefully) to a subsystem.
|
||||
*/
|
||||
class Video {
|
||||
public:
|
||||
Video();
|
||||
|
||||
/*!
|
||||
Sets the memory pool that provides video, and its size.
|
||||
*/
|
||||
void set_ram(uint16_t *, size_t size);
|
||||
|
||||
/*!
|
||||
Sets the target device for video data.
|
||||
*/
|
||||
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);
|
||||
|
||||
/*!
|
||||
Produces the next @c duration period of pixels.
|
||||
*/
|
||||
void run_for(HalfCycles duration);
|
||||
|
||||
|
||||
/*!
|
||||
@returns the number of cycles until there is next a change in the hsync,
|
||||
vsync or display_enable outputs.
|
||||
*/
|
||||
HalfCycles get_next_sequence_point();
|
||||
|
||||
/*!
|
||||
@returns @c true if the horizontal sync output is currently active; @c false otherwise.
|
||||
|
||||
@discussion On an Atari ST, this generates a VPA-style interrupt, which is often erroneously
|
||||
documented as being triggered by horizontal blank.
|
||||
*/
|
||||
bool hsync();
|
||||
|
||||
/*!
|
||||
@returns @c true if the vertical sync output is currently active; @c false otherwise.
|
||||
|
||||
@discussion On an Atari ST, this generates a VPA-style interrupt, which is often erroneously
|
||||
documented as being triggered by vertical blank.
|
||||
*/
|
||||
bool vsync();
|
||||
|
||||
/*!
|
||||
@returns @c true if the display enabled output is currently active; @c false otherwise.
|
||||
|
||||
@discussion On an Atari ST this is fed to the MFP. The documentation that I've been able to
|
||||
find implies a total 28-cycle delay between the real delay enabled signal changing and its effect
|
||||
on the 68000 interrupt input via the MFP. As I have yet to determine how much delay is caused
|
||||
by the MFP a full 28-cycle delay is applied by this class. This should be dialled down when the
|
||||
MFP's responsibility is clarified.
|
||||
*/
|
||||
bool display_enabled();
|
||||
|
||||
void set_ram(uint16_t *, size_t size);
|
||||
|
||||
/// @returns the effect of reading from @c address; only the low 6 bits are decoded.
|
||||
uint16_t read(int address);
|
||||
|
||||
/// Writes @c value to @c address, of which only the low 6 bits are decoded.
|
||||
void write(int address, uint16_t value);
|
||||
|
||||
/// Used internally to track state.
|
||||
enum class FieldFrequency {
|
||||
Fifty = 0, Sixty = 1, SeventyTwo = 2
|
||||
};
|
||||
|
||||
struct RangeObserver {
|
||||
/// Indicates to the observer that the memory access range has changed.
|
||||
virtual void video_did_change_access_range(Video *) = 0;
|
||||
};
|
||||
|
||||
/// Sets a range observer, which is an actor that will be notified if the memory access range changes.
|
||||
void set_range_observer(RangeObserver *);
|
||||
|
||||
struct Range {
|
||||
uint32_t low_address, high_address;
|
||||
};
|
||||
/*!
|
||||
@returns the range of addresses that the video might read from.
|
||||
*/
|
||||
Range get_memory_access_range();
|
||||
|
||||
private:
|
||||
DeferredQueue<HalfCycles> deferrer_;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
RangeObserver *range_observer_ = nullptr;
|
||||
|
||||
uint16_t raw_palette_[16];
|
||||
uint16_t palette_[16];
|
||||
int base_address_ = 0;
|
||||
int previous_base_address_ = 0;
|
||||
int current_address_ = 0;
|
||||
|
||||
uint16_t *ram_;
|
||||
uint16_t line_buffer_[256];
|
||||
uint16_t *ram_ = nullptr;
|
||||
|
||||
int x_ = 0, y_ = 0, next_y_ = 0;
|
||||
bool load_ = false;
|
||||
int load_base_ = 0;
|
||||
|
||||
uint16_t video_mode_ = 0;
|
||||
uint16_t sync_mode_ = 0;
|
||||
@@ -67,7 +144,7 @@ class Video {
|
||||
FieldFrequency field_frequency_ = FieldFrequency::Fifty;
|
||||
enum class OutputBpp {
|
||||
One, Two, Four
|
||||
} output_bpp_;
|
||||
} output_bpp_ = OutputBpp::Four;
|
||||
void update_output_mode();
|
||||
|
||||
struct HorizontalState {
|
||||
@@ -89,42 +166,81 @@ class Video {
|
||||
} sync_schedule = SyncSchedule::None;
|
||||
bool sync = false;
|
||||
} vertical_, next_vertical_;
|
||||
int line_length_ = 1024;
|
||||
LineLength line_length_;
|
||||
|
||||
int data_latch_position_ = 0;
|
||||
uint16_t data_latch_[4];
|
||||
void latch_word();
|
||||
int data_latch_read_position_ = 0;
|
||||
uint16_t data_latch_[128];
|
||||
void push_latched_data();
|
||||
|
||||
class Shifter {
|
||||
void reset_fifo();
|
||||
|
||||
/*!
|
||||
Provides a target for control over the output video stream, which is considered to be
|
||||
a permanently shifting shifter, that you need to reload when appropriate, which can be
|
||||
overridden by the blank and sync levels.
|
||||
|
||||
This stream will automatically insert a colour burst.
|
||||
*/
|
||||
class VideoStream {
|
||||
public:
|
||||
Shifter(Outputs::CRT::CRT &crt, uint16_t *palette) : crt_(crt), palette_(palette) {}
|
||||
void output_blank(int duration);
|
||||
void output_sync(int duration);
|
||||
void output_border(int duration, OutputBpp bpp);
|
||||
void output_pixels(int duration, OutputBpp bpp);
|
||||
VideoStream(Outputs::CRT::CRT &crt, uint16_t *palette) : crt_(crt), palette_(palette) {}
|
||||
|
||||
enum class OutputMode {
|
||||
Sync, Blank, ColourBurst, Pixels,
|
||||
};
|
||||
|
||||
/// Sets the current data format for the shifter. Changes in output BPP flush the shifter.
|
||||
void set_bpp(OutputBpp bpp);
|
||||
|
||||
/// Outputs signal of type @c mode for @c duration.
|
||||
void output(int duration, OutputMode mode);
|
||||
|
||||
/// Warns the video stream that the border colour, included in the palette that it holds a pointer to,
|
||||
/// will change momentarily. This should be called after the relevant @c output() updates, and
|
||||
/// is used to help elide border-regio output.
|
||||
void will_change_border_colour();
|
||||
|
||||
/// Loads 64 bits into the Shifter. The shifter shifts continuously. If you also declare
|
||||
/// a pixels region then whatever is being shifted will reach the display, in a form that
|
||||
/// depends on the current output BPP.
|
||||
void load(uint64_t value);
|
||||
|
||||
private:
|
||||
// The target CRT and the palette to use.
|
||||
Outputs::CRT::CRT &crt_;
|
||||
uint16_t *palette_ = nullptr;
|
||||
|
||||
// Internal stateful processes.
|
||||
void generate(int duration, OutputMode mode, bool is_terminal);
|
||||
|
||||
void flush_border();
|
||||
void flush_pixels();
|
||||
void shift(int duration);
|
||||
void output_pixels(int duration);
|
||||
|
||||
// Internal state that is a function of output intent.
|
||||
int duration_ = 0;
|
||||
enum class OutputMode {
|
||||
Sync, Blank, Border, Pixels
|
||||
} output_mode_ = OutputMode::Sync;
|
||||
uint16_t border_colour_;
|
||||
OutputBpp bpp_;
|
||||
OutputMode output_mode_ = OutputMode::Sync;
|
||||
OutputBpp bpp_ = OutputBpp::Four;
|
||||
union {
|
||||
uint64_t output_shifter_;
|
||||
uint32_t shifter_halves_[2];
|
||||
};
|
||||
|
||||
void flush_output(OutputMode next_mode);
|
||||
// Internal state for handling output serialisation.
|
||||
uint16_t *pixel_buffer_ = nullptr;
|
||||
int pixel_pointer_ = 0;
|
||||
} video_stream_;
|
||||
|
||||
uint16_t *pixel_buffer_;
|
||||
size_t pixel_pointer_ = 0;
|
||||
/// Contains copies of the various observeable fields, after the relevant propagation delay.
|
||||
struct PublicState {
|
||||
bool display_enable = false;
|
||||
bool hsync = false;
|
||||
bool vsync = false;
|
||||
} public_state_;
|
||||
|
||||
Outputs::CRT::CRT &crt_;
|
||||
uint16_t *palette_ = nullptr;
|
||||
} shifter_;
|
||||
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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@
|
||||
#include "../../Analyser/Dynamic/ConfidenceCounter.hpp"
|
||||
|
||||
namespace {
|
||||
const int sn76489_divider = 2;
|
||||
constexpr int sn76489_divider = 2;
|
||||
}
|
||||
|
||||
namespace Coleco {
|
||||
@@ -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;
|
||||
|
||||
@@ -123,7 +123,7 @@ class ConcreteMachine:
|
||||
z80_(*this),
|
||||
vdp_(TI::TMS::TMS9918A),
|
||||
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, sn76489_divider),
|
||||
ay_(audio_queue_),
|
||||
ay_(GI::AY38910::Personality::AY38910, audio_queue_),
|
||||
mixer_(sn76489_, ay_),
|
||||
speaker_(mixer_) {
|
||||
speaker_.set_input_rate(3579545.0f / static_cast<float>(sn76489_divider));
|
||||
@@ -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;
|
||||
@@ -320,7 +320,7 @@ class ConcreteMachine:
|
||||
tape_->set_delegate(this);
|
||||
tape_->set_clocking_hint_observer(this);
|
||||
|
||||
// install a joystick
|
||||
// Install a joystick.
|
||||
joysticks_.emplace_back(new Joystick(*user_port_via_port_handler_, *keyboard_via_port_handler_));
|
||||
|
||||
const std::string machine_name = "Vic20";
|
||||
@@ -369,7 +369,7 @@ class ConcreteMachine:
|
||||
|
||||
if(target.has_c1540) {
|
||||
// construct the 1540
|
||||
c1540_.reset(new ::Commodore::C1540::Machine(Commodore::C1540::Personality::C1540, rom_fetcher));
|
||||
c1540_ = std::make_unique<::Commodore::C1540::Machine>(Commodore::C1540::Personality::C1540, rom_fetcher);
|
||||
|
||||
// attach it to the serial bus
|
||||
c1540_->set_serial_bus(serial_bus_);
|
||||
@@ -397,26 +397,19 @@ 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
|
||||
switch(target.memory_model) {
|
||||
case Analyser::Static::Commodore::Target::MemoryModel::Unexpanded:
|
||||
// The default Vic-20 memory map has 1kb at address 0 and another 4kb at address 0x1000.
|
||||
set_ram(0x0000, 0x0400);
|
||||
set_ram(0x1000, 0x1000);
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::MemoryModel::EightKB:
|
||||
// An 8kb Vic-20 fills in the gap between the two blocks of RAM on an unexpanded machine.
|
||||
set_ram(0x0000, 0x2000);
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::MemoryModel::ThirtyTwoKB:
|
||||
// A 32kb Vic-20 fills the entire lower 32kb with RAM.
|
||||
set_ram(0x0000, 0x8000);
|
||||
break;
|
||||
}
|
||||
// Add 6502-visible RAM as requested.
|
||||
set_ram(0x0000, 0x0400);
|
||||
set_ram(0x1000, 0x1000); // Built-in RAM.
|
||||
if(target.enabled_ram.bank0) set_ram(0x0400, 0x0c00); // Bank 0: 0x0400 -> 0x1000.
|
||||
if(target.enabled_ram.bank1) set_ram(0x2000, 0x2000); // Bank 1: 0x2000 -> 0x4000.
|
||||
if(target.enabled_ram.bank2) set_ram(0x4000, 0x2000); // Bank 2: 0x4000 -> 0x6000.
|
||||
if(target.enabled_ram.bank3) set_ram(0x6000, 0x2000); // Bank 3: 0x6000 -> 0x8000.
|
||||
if(target.enabled_ram.bank5) set_ram(0xa000, 0x2000); // Bank 5: 0xa000 -> 0xc000.
|
||||
|
||||
#undef set_ram
|
||||
|
||||
@@ -453,13 +446,15 @@ class ConcreteMachine:
|
||||
write_to_map(mos6560_bus_handler_.video_memory_map, character_rom_.data(), 0x0000, static_cast<uint16_t>(character_rom_.size()));
|
||||
write_to_map(processor_read_memory_map_, kernel_rom_.data(), 0xe000, static_cast<uint16_t>(kernel_rom_.size()));
|
||||
|
||||
// The insert_media occurs last, so if there's a conflict between cartridges and RAM,
|
||||
// the cartridge wins.
|
||||
insert_media(target.media);
|
||||
if(!target.loading_command.empty()) {
|
||||
type_string(target.loading_command);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -483,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_;
|
||||
}
|
||||
|
||||
@@ -509,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;
|
||||
|
||||
@@ -596,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,46 +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 {
|
||||
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper());
|
||||
Utility::TypeRecipient::add_typer(string, std::move(mapper));
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient::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;
|
||||
@@ -676,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);
|
||||
}
|
||||
|
||||
@@ -712,7 +710,7 @@ class ConcreteMachine:
|
||||
|
||||
std::vector<uint8_t> rom_;
|
||||
uint16_t rom_address_, rom_length_;
|
||||
uint8_t ram_[0x8000];
|
||||
uint8_t ram_[0x10000];
|
||||
uint8_t colour_ram_[0x0400];
|
||||
|
||||
uint8_t *processor_read_memory_map_[64];
|
||||
|
@@ -88,7 +88,7 @@ class ConcreteMachine:
|
||||
set_rom(ROM::OS, *roms[1], false);
|
||||
|
||||
if(target.has_dfs || target.has_adfs) {
|
||||
plus3_.reset(new Plus3);
|
||||
plus3_ = std::make_unique<Plus3>();
|
||||
|
||||
if(target.has_dfs) {
|
||||
set_rom(ROM::Slot0, *roms[dfs_rom_position], true);
|
||||
@@ -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,50 +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 {
|
||||
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper());
|
||||
Utility::TypeRecipient::add_typer(string, std::move(mapper));
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient::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;
|
||||
@@ -435,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);
|
||||
@@ -450,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 {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user