mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-30 14:16:04 +00:00
Compare commits
814 Commits
2019-08-01
...
2020-02-16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e1b245cd8 | ||
|
|
5400c47f07 | ||
|
|
4153442703 | ||
|
|
5e4b721e97 | ||
|
|
aca41ac089 | ||
|
|
01a883e669 | ||
|
|
1e4356f83a | ||
|
|
545a6177bb | ||
|
|
50d356be2f | ||
|
|
9835e800ec | ||
|
|
5242362f31 | ||
|
|
808e4e8537 | ||
|
|
43740a4b2f | ||
|
|
f99d672237 | ||
|
|
337cb4fb86 | ||
|
|
90856a0e7a | ||
|
|
ea1c8a3b81 | ||
|
|
d55d077a95 | ||
|
|
f760a68173 | ||
|
|
e66a3523b6 | ||
|
|
89d6b85b83 | ||
|
|
e02d109864 | ||
|
|
743981e9ad | ||
|
|
49b8e771b5 | ||
|
|
dde672701f | ||
|
|
9ca2d8f9f2 | ||
|
|
fd786412aa | ||
|
|
eb88c7cfba | ||
|
|
e1892ff370 | ||
|
|
763159a6f6 | ||
|
|
6810a6ee58 | ||
|
|
65e6c3a9fe | ||
|
|
dcbbf988c1 | ||
|
|
199cafebcf | ||
|
|
555d807d76 | ||
|
|
003c6ad11b | ||
|
|
dc77d87427 | ||
|
|
cfc44cf778 | ||
|
|
3df99788ff | ||
|
|
3600d2d193 | ||
|
|
5f661adb7f | ||
|
|
109d072cb6 | ||
|
|
0c1c5a0ab8 | ||
|
|
e01c66fd65 | ||
|
|
9f32fa7f5b | ||
|
|
91a3d42919 | ||
|
|
3cb6bbf771 | ||
|
|
452e281009 | ||
|
|
3da948db52 | ||
|
|
0c2f77305f | ||
|
|
05bcd73f82 | ||
|
|
654f5b0478 | ||
|
|
886d923e30 | ||
|
|
6624cb7a78 | ||
|
|
6147134423 | ||
|
|
bf6bc7c684 | ||
|
|
0b0a7e241b | ||
|
|
705d14259c | ||
|
|
f1cd35fa16 | ||
|
|
6bda4034c6 | ||
|
|
b04daca98e | ||
|
|
85dcdbfe9e | ||
|
|
24340d1d4f | ||
|
|
6ae42d07a7 | ||
|
|
2ea1e059a8 | ||
|
|
b5d6126a2d | ||
|
|
dac217c98c | ||
|
|
c26c8992ae | ||
|
|
b76a5870b3 | ||
|
|
7c0f3bb237 | ||
|
|
f615d096ca | ||
|
|
09132306e4 | ||
|
|
f95b07efea | ||
|
|
14d976eecb | ||
|
|
e1cbad0b6d | ||
|
|
e7410b8ed8 | ||
|
|
5caf74b930 | ||
|
|
b41920990f | ||
|
|
709c229cd7 | ||
|
|
01fd1b1a2e | ||
|
|
96769c52f6 | ||
|
|
cf9729c74f | ||
|
|
0f2783075f | ||
|
|
256f4a6679 | ||
|
|
0310f94f0c | ||
|
|
085529ed72 | ||
|
|
8aabf1b374 | ||
|
|
ff39f71ca0 | ||
|
|
019474300d | ||
|
|
af976b8b3d | ||
|
|
f3db1a0c60 | ||
|
|
ce28213a5e | ||
|
|
f9ce50d2bb | ||
|
|
ee16095863 | ||
|
|
f0a6e0f3d5 | ||
|
|
8c4fb0f688 | ||
|
|
baa51853c4 | ||
|
|
0e29c6b0ab | ||
|
|
1b27eedf6b | ||
|
|
8b1f183198 | ||
|
|
4766ec55fe | ||
|
|
c5edc879b6 | ||
|
|
65309e60c4 | ||
|
|
5c4623e9f7 | ||
|
|
2c0cab9e4d | ||
|
|
d0117556d1 | ||
|
|
b1ff031b54 | ||
|
|
7e8405e68a | ||
|
|
c8fd00217d | ||
|
|
9d340599a6 | ||
|
|
8e094598ca | ||
|
|
189122ab84 | ||
|
|
4b53f6a9f0 | ||
|
|
561e149058 | ||
|
|
5975fc8e63 | ||
|
|
7316a3aa88 | ||
|
|
50be991415 | ||
|
|
52e49439a6 | ||
|
|
6bcdd3177d | ||
|
|
83dbd257e1 | ||
|
|
b514756272 | ||
|
|
7e4c13c43e | ||
|
|
79bb0f8222 | ||
|
|
43bf6aca67 | ||
|
|
03d23aad41 | ||
|
|
c398aa60c1 | ||
|
|
9666193c67 | ||
|
|
3f57020b00 | ||
|
|
294e09f275 | ||
|
|
ba516387ba | ||
|
|
2103e1b470 | ||
|
|
7bac439e95 | ||
|
|
9136917f00 | ||
|
|
6802318784 | ||
|
|
428d141bc9 | ||
|
|
a86fb33789 | ||
|
|
beefb70f75 | ||
|
|
3c6a00dc3c | ||
|
|
8404409c0d | ||
|
|
a5f285b4ce | ||
|
|
9d97a294a7 | ||
|
|
56448373ae | ||
|
|
a71c5946f0 | ||
|
|
e7fff6e123 | ||
|
|
82e5def7c4 | ||
|
|
d97a073d1b | ||
|
|
e74f37d6ed | ||
|
|
3aa2c297a2 | ||
|
|
290db67f09 | ||
|
|
4de121142b | ||
|
|
3c760e585a | ||
|
|
8adb2283b5 | ||
|
|
cb61e84868 | ||
|
|
8349005c4b | ||
|
|
a2847f4f8e | ||
|
|
add3ebcb44 | ||
|
|
98daad45c7 | ||
|
|
1b4b6b0aee | ||
|
|
8f94da9daf | ||
|
|
357137918d | ||
|
|
b0f7b762af | ||
|
|
da3ee381f4 | ||
|
|
d27d14d2b0 | ||
|
|
b0326530d6 | ||
|
|
c2bd5be51a | ||
|
|
84f5feab70 | ||
|
|
4b2c68c3d3 | ||
|
|
5391a699a4 | ||
|
|
f3f8345e5e | ||
|
|
c755411636 | ||
|
|
f02759b76b | ||
|
|
f34ddce28f | ||
|
|
50348c9fe7 | ||
|
|
3bfeebf2a1 | ||
|
|
dca79ea10e | ||
|
|
b7fd4de32f | ||
|
|
78d08278ed | ||
|
|
d4be052e76 | ||
|
|
d674fd0e67 | ||
|
|
229b7b36ed | ||
|
|
8a8b8db5d1 | ||
|
|
d30f83871d | ||
|
|
1422f8a93a | ||
|
|
f0da75f8e9 | ||
|
|
cb8a7a4137 | ||
|
|
efd684dc56 | ||
|
|
aeac6b5888 | ||
|
|
9bb294a023 | ||
|
|
1972ca00a4 | ||
|
|
6a185a574a | ||
|
|
c606931c93 | ||
|
|
93cecf0882 | ||
|
|
aac3d27c10 | ||
|
|
99122efbbc | ||
|
|
30e856b9e4 | ||
|
|
91fae86e73 | ||
|
|
f5c194386c | ||
|
|
98f7662185 | ||
|
|
62c3720c97 | ||
|
|
6b08239199 | ||
|
|
f258fc2971 | ||
|
|
6b84ae3095 | ||
|
|
5dd8c677f1 | ||
|
|
1cbcd5355f | ||
|
|
9799250f2c | ||
|
|
ecb5807ec0 | ||
|
|
942986aadc | ||
|
|
1f539822ee | ||
|
|
fab35b360a | ||
|
|
80fcf5b5c0 | ||
|
|
b3b2e18c4b | ||
|
|
2d233b6358 | ||
|
|
83ed36eb08 | ||
|
|
89f4032ffc | ||
|
|
8c90ec4636 | ||
|
|
514141f8c5 | ||
|
|
8e3a618619 | ||
|
|
6df6af09de | ||
|
|
f42655a0fc | ||
|
|
f81a7f0faf | ||
|
|
2b4c924399 | ||
|
|
64517a02b7 | ||
|
|
b4befd57a9 | ||
|
|
2c742a051e | ||
|
|
6595f8f527 | ||
|
|
985b36da73 | ||
|
|
cdb31b1c2b | ||
|
|
6a44936a7c | ||
|
|
45afb13a54 | ||
|
|
3ced31043a | ||
|
|
7361e7ec34 | ||
|
|
533729638c | ||
|
|
9f30be1c13 | ||
|
|
09289f383d | ||
|
|
20b25ce866 | ||
|
|
c1bae49a92 | ||
|
|
b3f806201b | ||
|
|
9f2f547932 | ||
|
|
f0d5bbecf2 | ||
|
|
3d7ef43293 | ||
|
|
4578b65487 | ||
|
|
a28c52c250 | ||
|
|
e4349f5e05 | ||
|
|
7b2777ac08 | ||
|
|
0fbcbfc61b | ||
|
|
3ab4fb8c79 | ||
|
|
42a9585321 | ||
|
|
937cba8978 | ||
|
|
627d3c28ea | ||
|
|
19ddfae6d6 | ||
|
|
56ebd08af0 | ||
|
|
7de1181213 | ||
|
|
c7a5b054db | ||
|
|
ca12ba297b | ||
|
|
7abf527084 | ||
|
|
c0b5bfe726 | ||
|
|
414b0cc234 | ||
|
|
134e828336 | ||
|
|
455e831b87 | ||
|
|
617e0bada9 | ||
|
|
7dea99b1cc | ||
|
|
42ccf48966 | ||
|
|
2f8078db22 | ||
|
|
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 | ||
|
|
9d07765823 | ||
|
|
11de0e198f | ||
|
|
f16f0897d5 | ||
|
|
3d4d45ef62 | ||
|
|
04c4f5f321 | ||
|
|
fa900d22e8 | ||
|
|
aee2890b25 | ||
|
|
40e1ec28fb | ||
|
|
c6e2b1237c | ||
|
|
efdd27a435 | ||
|
|
2c4f372872 | ||
|
|
74be876d72 | ||
|
|
e8e166eec5 | ||
|
|
4ec8fa0d20 | ||
|
|
b019c6f8dd | ||
|
|
6ec3c47cc0 | ||
|
|
ccce127f13 | ||
|
|
f4cfca0451 | ||
|
|
eb287605f7 | ||
|
|
2026761a07 | ||
|
|
6a82c87320 | ||
|
|
f0478225f0 | ||
|
|
d6edfa5c6d | ||
|
|
e7253a8713 | ||
|
|
c6f6bc68e1 | ||
|
|
ab34fad8ca | ||
|
|
e4c77614c1 | ||
|
|
072b0266af | ||
|
|
7ae0902103 | ||
|
|
8e9428623e | ||
|
|
2c25135d8a | ||
|
|
3741cba88c | ||
|
|
860837d894 | ||
|
|
cef07038c1 | ||
|
|
0bf61c9c99 | ||
|
|
837dfd1ab8 | ||
|
|
0204003680 | ||
|
|
5fc4e57db7 | ||
|
|
c4fefe1eb3 | ||
|
|
77ef7dc8fc | ||
|
|
e3abbc9966 | ||
|
|
70c6010fe0 | ||
|
|
8c736a639a | ||
|
|
cc7ff1ec9e | ||
|
|
9d12ca691f | ||
|
|
db03b03276 | ||
|
|
45375fb5d0 | ||
|
|
d2324e413d | ||
|
|
e0c15f43bb | ||
|
|
8b0d550b81 | ||
|
|
d1259f829e | ||
|
|
7caef46c05 | ||
|
|
6902251d8b | ||
|
|
0b683b0360 | ||
|
|
5d5dc79f2c | ||
|
|
68f9084d9e | ||
|
|
b7c407be10 | ||
|
|
f45798faf0 | ||
|
|
7c66d7a13c | ||
|
|
f9a35c6636 | ||
|
|
5e1570258d | ||
|
|
fc8021c0b0 | ||
|
|
c9cd56915e | ||
|
|
1fd19c5786 | ||
|
|
ce66b5fd9c | ||
|
|
8aa425c9d8 | ||
|
|
ec68bc5047 | ||
|
|
0971bbeebe | ||
|
|
9292f66c2d | ||
|
|
f3e2e88986 | ||
|
|
6afefa107e | ||
|
|
0ce807805d | ||
|
|
41f3c29e30 | ||
|
|
6c75c60149 | ||
|
|
015f2101f8 | ||
|
|
35f1a7ab10 | ||
|
|
8df1eea955 | ||
|
|
eeafdf2c03 | ||
|
|
befe2c2929 | ||
|
|
46ec3510be | ||
|
|
e9965c2738 | ||
|
|
48b0d8c329 | ||
|
|
07582cee4a | ||
|
|
4dbd2a805a | ||
|
|
20bf425f98 | ||
|
|
0567410bcf | ||
|
|
6d1e09ba55 | ||
|
|
f40dbefa67 | ||
|
|
f93cdd21de | ||
|
|
e1dc3b1915 | ||
|
|
cbf25a16dc | ||
|
|
14e790746b | ||
|
|
bf7e9cfd62 | ||
|
|
a67e0014a4 | ||
|
|
c070f2100c | ||
|
|
75e34b4215 | ||
|
|
a5bbf54a27 | ||
|
|
5309ac7c30 | ||
|
|
731dc350b4 | ||
|
|
635e18a50d | ||
|
|
4857ceb3eb | ||
|
|
1c154131f9 | ||
|
|
fd02b6fc18 | ||
|
|
553f3b6d8b | ||
|
|
1135576a3a | ||
|
|
a5057e6540 | ||
|
|
1d790ec2a9 | ||
|
|
0f2d72c436 | ||
|
|
aa52652027 | ||
|
|
4a1fa8fc13 | ||
|
|
95d3b6e79f | ||
|
|
5f6711b72c | ||
|
|
d44734d105 | ||
|
|
1aaa6331a0 | ||
|
|
de1bfb4e24 | ||
|
|
0cb19421e8 | ||
|
|
92847037b3 | ||
|
|
f4556ef6b0 | ||
|
|
4266264449 | ||
|
|
1aba1db62c | ||
|
|
0fc191c87d | ||
|
|
dc4a0e4e3b | ||
|
|
3794d94b68 | ||
|
|
0082dc4411 | ||
|
|
22754683f8 | ||
|
|
909685d87d | ||
|
|
55710ea00e | ||
|
|
36a9a5288b | ||
|
|
e89be6249d | ||
|
|
ac39fd0235 | ||
|
|
ecc0cea5a1 | ||
|
|
eae11cbf17 | ||
|
|
e96386f572 | ||
|
|
a8d481a764 | ||
|
|
2207638287 | ||
|
|
872897029e | ||
|
|
51b4b5551d | ||
|
|
7a2de47f58 | ||
|
|
f2f98ed60c | ||
|
|
77f14fa638 | ||
|
|
f09a240e6c | ||
|
|
092a61f93e | ||
|
|
e30ba58e0d | ||
|
|
7cb82fccc0 | ||
|
|
ed9a5b0430 | ||
|
|
8f59a73425 | ||
|
|
91223b9ec8 | ||
|
|
83f5f0e2ad | ||
|
|
cf37e9f5de | ||
|
|
e4f7ead894 | ||
|
|
4134463094 | ||
|
|
83d73fb088 | ||
|
|
75c3e2dacd | ||
|
|
cf07982a9b | ||
|
|
313aaa8f95 | ||
|
|
2e86dada1d | ||
|
|
696af5c3a6 | ||
|
|
f08b38d0ae | ||
|
|
9a8352282d | ||
|
|
3d03cce6b1 | ||
|
|
34075a7674 | ||
|
|
f79c87659f | ||
|
|
c10b64e1c0 | ||
|
|
5d5fe52144 | ||
|
|
d461331fd2 | ||
|
|
ff62eb6dce | ||
|
|
374439693e | ||
|
|
c4ef33b23f | ||
|
|
a7ed357569 | ||
|
|
4e5b440145 | ||
|
|
2bd7be13b5 | ||
|
|
4b09d7c41d | ||
|
|
97d44129cb | ||
|
|
b0f5f7bd37 | ||
|
|
d1dd6876b5 | ||
|
|
a59ec9e818 | ||
|
|
4ead905c3c | ||
|
|
127bb043e7 | ||
|
|
42ebe06474 | ||
|
|
74fe32da23 | ||
|
|
780916551f | ||
|
|
305b1211ba | ||
|
|
2cf52fb89c | ||
|
|
6e1b606adf | ||
|
|
3bb0bf9e14 | ||
|
|
87a6d22894 | ||
|
|
484a0ceeb8 | ||
|
|
da1436abd2 | ||
|
|
125f781ced | ||
|
|
c66c484c54 | ||
|
|
345b32d6e3 | ||
|
|
8b397626bf | ||
|
|
0da1881a07 | ||
|
|
d4077afd30 | ||
|
|
95c45b5515 | ||
|
|
684644420a | ||
|
|
735586f5f8 | ||
|
|
ddae086661 | ||
|
|
9c7aa5f3fc | ||
|
|
418cd07e17 | ||
|
|
2ae5739b8b | ||
|
|
e095a622d3 | ||
|
|
9ab49065cd | ||
|
|
ab50f17d87 | ||
|
|
f5a2e180f9 | ||
|
|
f2e1584275 | ||
|
|
0fd8813ddb | ||
|
|
b69180ba01 | ||
|
|
c352d8ae8c | ||
|
|
530e831064 | ||
|
|
3b165a78f2 | ||
|
|
8d87e9eb1c | ||
|
|
f86dc082bb | ||
|
|
d7982aa84e | ||
|
|
516d78f5a8 | ||
|
|
8b50a7d6e3 | ||
|
|
4bf81d3b90 | ||
|
|
cd75978e4e | ||
|
|
fda99d9c5f | ||
|
|
c5ebf75351 | ||
|
|
2581b520af | ||
|
|
52e5296544 | ||
|
|
d7ce2c26e8 | ||
|
|
f88e1b1373 | ||
|
|
021d4dbaf1 | ||
|
|
dbde8f2ee7 | ||
|
|
5d06930df4 | ||
|
|
7722596a3b | ||
|
|
1de1818ebb | ||
|
|
885f890df1 | ||
|
|
e195497ab7 | ||
|
|
fcd2143697 | ||
|
|
3f45cd2380 | ||
|
|
a8a34497ff | ||
|
|
953423cc02 | ||
|
|
a2ca887b99 | ||
|
|
3c5ae9cf8e | ||
|
|
fe621d7e52 | ||
|
|
814bb4ec63 | ||
|
|
e8bc254f3f | ||
|
|
3c146a3fb2 | ||
|
|
b609ce6fcb | ||
|
|
929475d31e | ||
|
|
f14d98452e | ||
|
|
9d17d48bca | ||
|
|
4ac3839185 | ||
|
|
c089d1cd09 | ||
|
|
cb85ec25cc | ||
|
|
fbf95ec2b8 | ||
|
|
6adca98f34 | ||
|
|
48f4d8b875 | ||
|
|
7758f9d0a9 | ||
|
|
7112f0336c | ||
|
|
298694a881 | ||
|
|
7ff4594f09 | ||
|
|
e8bd538182 | ||
|
|
8489e8650f | ||
|
|
114f81941e | ||
|
|
077c7d767f | ||
|
|
8f88addf9f | ||
|
|
f28c124039 | ||
|
|
a416bc0058 | ||
|
|
e78b1dcf3c | ||
|
|
8a14f5d814 | ||
|
|
e5f983fbac | ||
|
|
3e639e96e7 | ||
|
|
61993f0687 | ||
|
|
5f16fa8c08 | ||
|
|
dcea9c9ab2 | ||
|
|
e7bf0799b6 | ||
|
|
e760421f6f | ||
|
|
8ea4c17315 | ||
|
|
2e24da4614 | ||
|
|
e46601872b | ||
|
|
6d0e41b760 | ||
|
|
5a82df837d | ||
|
|
776b819a5a | ||
|
|
1783f6c84b | ||
|
|
2ef2c73efe | ||
|
|
55e003ccc1 | ||
|
|
3d54d55dbb | ||
|
|
72c0a631f7 | ||
|
|
1608a90d5d | ||
|
|
4f8a45a6ce | ||
|
|
4f0f1dcf18 | ||
|
|
839e51d92d | ||
|
|
e470cf23d8 | ||
|
|
8d4a96683a | ||
|
|
f53411a319 | ||
|
|
128a1da626 | ||
|
|
962275c22a | ||
|
|
3002ac8a4a | ||
|
|
ff43674638 | ||
|
|
2f6c366668 | ||
|
|
2ce1f0a3b1 | ||
|
|
210129c3a1 | ||
|
|
934901447a | ||
|
|
960b289e70 | ||
|
|
243e40cd79 | ||
|
|
c849188016 | ||
|
|
87e8dade2f | ||
|
|
6fc5b4e825 | ||
|
|
00ce7f8ae0 | ||
|
|
6e0e9afe2f | ||
|
|
cb0d994827 | ||
|
|
bee782234a | ||
|
|
64dad35026 | ||
|
|
cbd1a8cf78 | ||
|
|
a4ab0afce3 | ||
|
|
1c7e0f3c9d | ||
|
|
318cdb41ea | ||
|
|
2f8e31bc8b | ||
|
|
310c722cc0 | ||
|
|
25956bd90f | ||
|
|
1a60ced61b | ||
|
|
081316c071 | ||
|
|
eafbc12cc1 | ||
|
|
ca08716c52 | ||
|
|
30cef1ee22 | ||
|
|
5598802439 | ||
|
|
1c6720b0db | ||
|
|
404b088199 | ||
|
|
7d61df238a | ||
|
|
c86db12f1c | ||
|
|
ce2e85af8b | ||
|
|
2d82855f26 | ||
|
|
faec516a2c | ||
|
|
8e274ec5d0 | ||
|
|
bb1a0a0b76 | ||
|
|
252650808d | ||
|
|
e3d9254555 | ||
|
|
90cf99b626 | ||
|
|
955e909e61 | ||
|
|
8339e2044c | ||
|
|
0e0c789b02 | ||
|
|
7e001c1d03 | ||
|
|
9047932b81 | ||
|
|
f668e4a54c | ||
|
|
ce1c96d68c | ||
|
|
0f67e490e8 | ||
|
|
895c315fa5 | ||
|
|
a90a74a512 | ||
|
|
3e1286cbef | ||
|
|
949c1e1668 | ||
|
|
bbd4e4d3dc | ||
|
|
4c5f596533 | ||
|
|
4859d3781b | ||
|
|
bac0461f7f | ||
|
|
f26a200d78 | ||
|
|
28ccb7b54e | ||
|
|
b6e4c8209b | ||
|
|
16548f0765 | ||
|
|
6a80832140 | ||
|
|
c6cf0e914b | ||
|
|
35b1a55c12 | ||
|
|
e3794c0c0e | ||
|
|
f88dc23c71 | ||
|
|
0e293e4983 | ||
|
|
e334abfe20 | ||
|
|
fd2fbe0e59 | ||
|
|
330b27d085 | ||
|
|
478f2533b5 | ||
|
|
b96972a4b9 | ||
|
|
f2b083f4de | ||
|
|
80f6d665d9 | ||
|
|
a07488cf1b | ||
|
|
d67c5145c0 | ||
|
|
5e76d593af | ||
|
|
83393e8e91 | ||
|
|
e08a64d455 | ||
|
|
b93f9b3973 | ||
|
|
9c517d07d4 | ||
|
|
f45de5b87a | ||
|
|
011d76175c | ||
|
|
96005261c7 | ||
|
|
c8177af45a | ||
|
|
97eff5b16d | ||
|
|
917520fb1e | ||
|
|
335dda3d55 |
22
.github/workflows/ccpp.yml
vendored
Normal file
22
.github/workflows/ccpp.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: SDL/Ubuntu
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt-get --allow-releaseinfo-change update; sudo apt-get install libsdl2-dev scons
|
||||||
|
- name: Make
|
||||||
|
run: cd OSBindings/SDL; scons
|
||||||
13
.travis.yml
13
.travis.yml
@@ -1,13 +0,0 @@
|
|||||||
# language: objective-c
|
|
||||||
# osx_image: xcode8.2
|
|
||||||
# xcode_project: OSBindings/Mac/Clock Signal.xcodeproj
|
|
||||||
# xcode_scheme: Clock Signal
|
|
||||||
# xcode_sdk: macosx10.12
|
|
||||||
|
|
||||||
language: cpp
|
|
||||||
before_install:
|
|
||||||
- sudo apt-get install libsdl2-dev
|
|
||||||
script: cd OSBindings/SDL && scons
|
|
||||||
compiler:
|
|
||||||
- clang
|
|
||||||
- gcc
|
|
||||||
@@ -22,7 +22,7 @@ namespace Dynamic {
|
|||||||
class ConfidenceCounter: public ConfidenceSource {
|
class ConfidenceCounter: public ConfidenceSource {
|
||||||
public:
|
public:
|
||||||
/*! @returns The computed probability, based on the history of events. */
|
/*! @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. */
|
/*! Records an event that implies this is the appropriate class: pushes probability up towards 1.0. */
|
||||||
void add_hit();
|
void add_hit();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ConfidenceSummary: public ConfidenceSource {
|
|||||||
const std::vector<float> &weights);
|
const std::vector<float> &weights);
|
||||||
|
|
||||||
/*! @returns The weighted sum of all sources. */
|
/*! @returns The weighted sum of all sources. */
|
||||||
float get_confidence() override;
|
float get_confidence() final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<ConfidenceSource *> sources_;
|
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);
|
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() {
|
Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() {
|
||||||
return speaker_;
|
return speaker_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MultiCRTMachine::run_for(Time::Seconds duration) {
|
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);
|
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() {
|
void MultiCRTMachine::did_change_machine_order() {
|
||||||
if(scan_target_) scan_target_->will_change_owner();
|
if(scan_target_) scan_target_->will_change_owner();
|
||||||
|
|
||||||
perform_serial([=](::CRTMachine::Machine *machine) {
|
perform_serial([](::CRTMachine::Machine *machine) {
|
||||||
machine->set_scan_target(nullptr);
|
machine->set_scan_target(nullptr);
|
||||||
});
|
});
|
||||||
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
|
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.
|
// Below is the standard CRTMachine::Machine interface; see there for documentation.
|
||||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override;
|
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final;
|
||||||
Outputs::Speaker::Speaker *get_speaker() override;
|
Outputs::Display::ScanStatus get_scan_status() const final;
|
||||||
void run_for(Time::Seconds duration) override;
|
Outputs::Speaker::Speaker *get_speaker() final;
|
||||||
|
void run_for(Time::Seconds duration) final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void run_for(const Cycles cycles) override {}
|
void run_for(const Cycles cycles) final {}
|
||||||
const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_;
|
const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_;
|
||||||
std::recursive_mutex &machines_mutex_;
|
std::recursive_mutex &machines_mutex_;
|
||||||
std::vector<Concurrency::AsyncTaskQueue> queues_;
|
std::vector<Concurrency::AsyncTaskQueue> queues_;
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ class MultiConfigurable: public Configurable::Device {
|
|||||||
MultiConfigurable(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
MultiConfigurable(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||||
|
|
||||||
// Below is the standard Configurable::Device interface; see there for documentation.
|
// Below is the standard Configurable::Device interface; see there for documentation.
|
||||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override;
|
std::vector<std::unique_ptr<Configurable::Option>> get_options() final;
|
||||||
void set_selections(const Configurable::SelectionSet &selection_by_option) override;
|
void set_selections(const Configurable::SelectionSet &selection_by_option) final;
|
||||||
Configurable::SelectionSet get_accurate_selections() override;
|
Configurable::SelectionSet get_accurate_selections() final;
|
||||||
Configurable::SelectionSet get_user_friendly_selections() override;
|
Configurable::SelectionSet get_user_friendly_selections() final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<Configurable::Device *> devices_;
|
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()) {
|
if(inputs.empty()) {
|
||||||
for(const auto &joystick: joysticks_) {
|
for(const auto &joystick: joysticks_) {
|
||||||
std::vector<Input> joystick_inputs = joystick->get_inputs();
|
std::vector<Input> joystick_inputs = joystick->get_inputs();
|
||||||
@@ -40,19 +40,19 @@ class MultiJoystick: public Inputs::Joystick {
|
|||||||
return inputs;
|
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_) {
|
for(const auto &joystick: joysticks_) {
|
||||||
joystick->set_input(digital_input, is_active);
|
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_) {
|
for(const auto &joystick: joysticks_) {
|
||||||
joystick->set_input(digital_input, value);
|
joystick->set_input(digital_input, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset_all_inputs() override {
|
void reset_all_inputs() final {
|
||||||
for(const auto &joystick: joysticks_) {
|
for(const auto &joystick: joysticks_) {
|
||||||
joystick->reset_all_inputs();
|
joystick->reset_all_inputs();
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,6 @@ MultiJoystickMachine::MultiJoystickMachine(const std::vector<std::unique_ptr<::M
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::unique_ptr<Inputs::Joystick>> &MultiJoystickMachine::get_joysticks() {
|
const std::vector<std::unique_ptr<Inputs::Joystick>> &MultiJoystickMachine::get_joysticks() {
|
||||||
return joysticks_;
|
return joysticks_;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class MultiJoystickMachine: public JoystickMachine::Machine {
|
|||||||
MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||||
|
|
||||||
// Below is the standard JoystickMachine::Machine interface; see there for documentation.
|
// Below is the standard JoystickMachine::Machine interface; see there for documentation.
|
||||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override;
|
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine {
|
|||||||
public:
|
public:
|
||||||
MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines);
|
MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines);
|
||||||
|
|
||||||
void set_key_pressed(Key key, char value, bool is_pressed) override;
|
void set_key_pressed(Key key, char value, bool is_pressed) final;
|
||||||
void reset_all_keys() override;
|
void reset_all_keys() final;
|
||||||
const std::set<Key> &observed_keys() override;
|
const std::set<Key> &observed_keys() final;
|
||||||
bool is_exclusive() override;
|
bool is_exclusive() final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::vector<::KeyboardMachine::Machine *> &machines_;
|
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);
|
MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||||
|
|
||||||
// Below is the standard KeyboardMachine::Machine interface; see there for documentation.
|
// Below is the standard KeyboardMachine::Machine interface; see there for documentation.
|
||||||
void clear_all_keys() override;
|
void clear_all_keys() final;
|
||||||
void set_key_state(uint16_t key, bool is_pressed) override;
|
void set_key_state(uint16_t key, bool is_pressed) final;
|
||||||
void type_string(const std::string &) override;
|
void type_string(const std::string &) final;
|
||||||
Inputs::Keyboard &get_keyboard() override;
|
Inputs::Keyboard &get_keyboard() final;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ struct MultiMediaTarget: public MediaTarget::Machine {
|
|||||||
MultiMediaTarget(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
MultiMediaTarget(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||||
|
|
||||||
// Below is the standard MediaTarget::Machine interface; see there for documentation.
|
// 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:
|
private:
|
||||||
std::vector<MediaTarget::Machine *> targets_;
|
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());
|
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_) {
|
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) {
|
void MultiSpeaker::set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) {
|
||||||
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_);
|
std::lock_guard<std::mutex> lock_guard(front_speaker_mutex_);
|
||||||
if(speaker != front_speaker_) return;
|
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) {
|
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.
|
// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation.
|
||||||
float get_ideal_clock_rate_in_range(float minimum, float maximum) override;
|
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;
|
void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override;
|
||||||
|
bool get_is_stereo() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) override;
|
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final;
|
||||||
void speaker_did_change_input_clock(Speaker *speaker) override;
|
void speaker_did_change_input_clock(Speaker *speaker) final;
|
||||||
MultiSpeaker(const std::vector<Outputs::Speaker::Speaker *> &speakers);
|
MultiSpeaker(const std::vector<Outputs::Speaker::Speaker *> &speakers);
|
||||||
|
|
||||||
std::vector<Outputs::Speaker::Speaker *> speakers_;
|
std::vector<Outputs::Speaker::Speaker *> speakers_;
|
||||||
Outputs::Speaker::Speaker *front_speaker_ = nullptr;
|
Outputs::Speaker::Speaker *front_speaker_ = nullptr;
|
||||||
Outputs::Speaker::Speaker::Delegate *delegate_ = nullptr;
|
Outputs::Speaker::Speaker::Delegate *delegate_ = nullptr;
|
||||||
std::mutex front_speaker_mutex_;
|
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);
|
static bool would_collapse(const std::vector<std::unique_ptr<DynamicMachine>> &machines);
|
||||||
MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines);
|
MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines);
|
||||||
|
|
||||||
Activity::Source *activity_source() override;
|
Activity::Source *activity_source() final;
|
||||||
Configurable::Device *configurable_device() override;
|
Configurable::Device *configurable_device() final;
|
||||||
CRTMachine::Machine *crt_machine() override;
|
CRTMachine::Machine *crt_machine() final;
|
||||||
JoystickMachine::Machine *joystick_machine() override;
|
JoystickMachine::Machine *joystick_machine() final;
|
||||||
MouseMachine::Machine *mouse_machine() override;
|
MouseMachine::Machine *mouse_machine() final;
|
||||||
KeyboardMachine::Machine *keyboard_machine() override;
|
KeyboardMachine::Machine *keyboard_machine() final;
|
||||||
MediaTarget::Machine *media_target() override;
|
MediaTarget::Machine *media_target() final;
|
||||||
void *raw_pointer() override;
|
void *raw_pointer() final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void multi_crt_did_run_machines() override;
|
void multi_crt_did_run_machines() final;
|
||||||
|
|
||||||
std::vector<std::unique_ptr<DynamicMachine>> machines_;
|
std::vector<std::unique_ptr<DynamicMachine>> machines_;
|
||||||
std::recursive_mutex machines_mutex_;
|
std::recursive_mutex machines_mutex_;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ enum class Machine {
|
|||||||
AmstradCPC,
|
AmstradCPC,
|
||||||
AppleII,
|
AppleII,
|
||||||
Atari2600,
|
Atari2600,
|
||||||
|
AtariST,
|
||||||
ColecoVision,
|
ColecoVision,
|
||||||
Electron,
|
Electron,
|
||||||
Macintosh,
|
Macintosh,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
#include "../../../Storage/Disk/Controller/DiskController.hpp"
|
#include "../../../Storage/Disk/Controller/DiskController.hpp"
|
||||||
#include "../../../Storage/Disk/Encodings/MFM/Parser.hpp"
|
#include "../../../Storage/Disk/Encodings/MFM/Parser.hpp"
|
||||||
#include "../../../NumberTheory/CRC.hpp"
|
#include "../../../Numeric/CRC.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#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) {
|
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
|
// 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::Parser parser(false, disk);
|
||||||
|
|
||||||
Storage::Encodings::MFM::Sector *names = parser.get_sector(0, 0, 0);
|
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;
|
return catalogue;
|
||||||
}
|
}
|
||||||
std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
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::Parser parser(true, disk);
|
||||||
|
|
||||||
Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.get_sector(0, 0, 1);
|
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) {
|
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->machine = Machine::Electron;
|
||||||
target->confidence = 0.5; // TODO: a proper estimation
|
target->confidence = 0.5; // TODO: a proper estimation
|
||||||
target->has_dfs = false;
|
target->has_dfs = false;
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
|
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
|
||||||
#include "../../../NumberTheory/CRC.hpp"
|
#include "../../../Numeric/CRC.hpp"
|
||||||
#include "../../../Storage/Tape/Parsers/Acorn.hpp"
|
#include "../../../Storage/Tape/Parsers/Acorn.hpp"
|
||||||
|
|
||||||
using namespace Analyser::Static::Acorn;
|
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) {
|
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;
|
int shift_register = 0;
|
||||||
|
|
||||||
// TODO: move this into the parser
|
// 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;
|
if(!chunks.size()) return nullptr;
|
||||||
|
|
||||||
// accumulate chunks for as long as block number is sequential and the end-of-file bit isn't set
|
// 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;
|
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) {
|
Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||||
TargetList destination;
|
TargetList destination;
|
||||||
std::unique_ptr<Target> target(new Target);
|
auto target = std::make_unique<Target>();
|
||||||
target->machine = Machine::AmstradCPC;
|
target->machine = Machine::AmstradCPC;
|
||||||
target->confidence = 0.5;
|
target->confidence = 0.5;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
#include "Target.hpp"
|
#include "Target.hpp"
|
||||||
|
|
||||||
Analyser::Static::TargetList Analyser::Static::AppleII::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
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->machine = Machine::AppleII;
|
||||||
target->media = media;
|
target->media = media;
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,10 @@
|
|||||||
|
|
||||||
#include "../Disassembler/6502.hpp"
|
#include "../Disassembler/6502.hpp"
|
||||||
|
|
||||||
using namespace Analyser::Static::Atari;
|
using namespace Analyser::Static::Atari2600;
|
||||||
|
using Target = Analyser::Static::Atari2600::Target;
|
||||||
|
|
||||||
static void DeterminePagingFor2kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) {
|
static void DeterminePagingFor2kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment) {
|
||||||
// if this is a 2kb cartridge then it's definitely either unpaged or a CommaVid
|
// if this is a 2kb cartridge then it's definitely either unpaged or a CommaVid
|
||||||
uint16_t entry_address, break_address;
|
uint16_t entry_address, break_address;
|
||||||
|
|
||||||
@@ -48,10 +49,10 @@ static void DeterminePagingFor2kCartridge(Analyser::Static::Atari::Target &targe
|
|||||||
// caveat: false positives aren't likely to be problematic; a false positive is a 2KB ROM that always addresses
|
// caveat: false positives aren't likely to be problematic; a false positive is a 2KB ROM that always addresses
|
||||||
// itself so as to land in ROM even if mapped as a CommaVid and this code is on the fence as to whether it
|
// itself so as to land in ROM even if mapped as a CommaVid and this code is on the fence as to whether it
|
||||||
// attempts to modify itself but it probably doesn't
|
// attempts to modify itself but it probably doesn't
|
||||||
if(has_wide_area_store) target.paging_model = Analyser::Static::Atari::Target::PagingModel::CommaVid;
|
if(has_wide_area_store) target.paging_model = Target::PagingModel::CommaVid;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DeterminePagingFor8kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
static void DeterminePagingFor8kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||||
// Activision stack titles have their vectors at the top of the low 4k, not the top, and
|
// Activision stack titles have their vectors at the top of the low 4k, not the top, and
|
||||||
// always list 0xf000 as both vectors; they do not repeat them, and, inexplicably, they all
|
// always list 0xf000 as both vectors; they do not repeat them, and, inexplicably, they all
|
||||||
// issue an SEI as their first instruction (maybe some sort of relic of the development environment?)
|
// issue an SEI as their first instruction (maybe some sort of relic of the development environment?)
|
||||||
@@ -60,12 +61,12 @@ static void DeterminePagingFor8kCartridge(Analyser::Static::Atari::Target &targe
|
|||||||
(segment.data[8191] != 0xf0 || segment.data[8189] != 0xf0 || segment.data[8190] != 0x00 || segment.data[8188] != 0x00) &&
|
(segment.data[8191] != 0xf0 || segment.data[8189] != 0xf0 || segment.data[8190] != 0x00 || segment.data[8188] != 0x00) &&
|
||||||
segment.data[0] == 0x78
|
segment.data[0] == 0x78
|
||||||
) {
|
) {
|
||||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::ActivisionStack;
|
target.paging_model = Target::PagingModel::ActivisionStack;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// make an assumption that this is the Atari paging model
|
// make an assumption that this is the Atari paging model
|
||||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari8k;
|
target.paging_model = Target::PagingModel::Atari8k;
|
||||||
|
|
||||||
std::set<uint16_t> internal_accesses;
|
std::set<uint16_t> internal_accesses;
|
||||||
internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end());
|
internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end());
|
||||||
@@ -85,13 +86,13 @@ static void DeterminePagingFor8kCartridge(Analyser::Static::Atari::Target &targe
|
|||||||
tigervision_access_count += masked_address == 0x3f;
|
tigervision_access_count += masked_address == 0x3f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(parker_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::ParkerBros;
|
if(parker_access_count > atari_access_count) target.paging_model = Target::PagingModel::ParkerBros;
|
||||||
else if(tigervision_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::Tigervision;
|
else if(tigervision_access_count > atari_access_count) target.paging_model = Target::PagingModel::Tigervision;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DeterminePagingFor16kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
static void DeterminePagingFor16kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||||
// make an assumption that this is the Atari paging model
|
// make an assumption that this is the Atari paging model
|
||||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari16k;
|
target.paging_model = Target::PagingModel::Atari16k;
|
||||||
|
|
||||||
std::set<uint16_t> internal_accesses;
|
std::set<uint16_t> internal_accesses;
|
||||||
internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end());
|
internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end());
|
||||||
@@ -106,17 +107,17 @@ static void DeterminePagingFor16kCartridge(Analyser::Static::Atari::Target &targ
|
|||||||
mnetwork_access_count += masked_address >= 0x1fe0 && masked_address < 0x1ffb;
|
mnetwork_access_count += masked_address >= 0x1fe0 && masked_address < 0x1ffb;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mnetwork_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::MNetwork;
|
if(mnetwork_access_count > atari_access_count) target.paging_model = Target::PagingModel::MNetwork;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DeterminePagingFor64kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
static void DeterminePagingFor64kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||||
// make an assumption that this is a Tigervision if there is a write to 3F
|
// make an assumption that this is a Tigervision if there is a write to 3F
|
||||||
target.paging_model =
|
target.paging_model =
|
||||||
(disassembly.external_stores.find(0x3f) != disassembly.external_stores.end()) ?
|
(disassembly.external_stores.find(0x3f) != disassembly.external_stores.end()) ?
|
||||||
Analyser::Static::Atari::Target::PagingModel::Tigervision : Analyser::Static::Atari::Target::PagingModel::MegaBoy;
|
Target::PagingModel::Tigervision : Target::PagingModel::MegaBoy;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) {
|
static void DeterminePagingForCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment) {
|
||||||
if(segment.data.size() == 2048) {
|
if(segment.data.size() == 2048) {
|
||||||
DeterminePagingFor2kCartridge(target, segment);
|
DeterminePagingFor2kCartridge(target, segment);
|
||||||
return;
|
return;
|
||||||
@@ -140,16 +141,16 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target,
|
|||||||
DeterminePagingFor8kCartridge(target, segment, disassembly);
|
DeterminePagingFor8kCartridge(target, segment, disassembly);
|
||||||
break;
|
break;
|
||||||
case 10495:
|
case 10495:
|
||||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::Pitfall2;
|
target.paging_model = Target::PagingModel::Pitfall2;
|
||||||
break;
|
break;
|
||||||
case 12288:
|
case 12288:
|
||||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::CBSRamPlus;
|
target.paging_model = Target::PagingModel::CBSRamPlus;
|
||||||
break;
|
break;
|
||||||
case 16384:
|
case 16384:
|
||||||
DeterminePagingFor16kCartridge(target, segment, disassembly);
|
DeterminePagingFor16kCartridge(target, segment, disassembly);
|
||||||
break;
|
break;
|
||||||
case 32768:
|
case 32768:
|
||||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari32k;
|
target.paging_model = Target::PagingModel::Atari32k;
|
||||||
break;
|
break;
|
||||||
case 65536:
|
case 65536:
|
||||||
DeterminePagingFor64kCartridge(target, segment, disassembly);
|
DeterminePagingFor64kCartridge(target, segment, disassembly);
|
||||||
@@ -161,8 +162,8 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target,
|
|||||||
// check for a Super Chip. Atari ROM images [almost] always have the same value stored over RAM
|
// check for a Super Chip. Atari ROM images [almost] always have the same value stored over RAM
|
||||||
// regions; when they don't they at least seem to have the first 128 bytes be the same as the
|
// regions; when they don't they at least seem to have the first 128 bytes be the same as the
|
||||||
// next 128 bytes. So check for that.
|
// next 128 bytes. So check for that.
|
||||||
if( target.paging_model != Analyser::Static::Atari::Target::PagingModel::CBSRamPlus &&
|
if( target.paging_model != Target::PagingModel::CBSRamPlus &&
|
||||||
target.paging_model != Analyser::Static::Atari::Target::PagingModel::MNetwork) {
|
target.paging_model != Target::PagingModel::MNetwork) {
|
||||||
bool has_superchip = true;
|
bool has_superchip = true;
|
||||||
for(std::size_t address = 0; address < 128; address++) {
|
for(std::size_t address = 0; address < 128; address++) {
|
||||||
if(segment.data[address] != segment.data[address+128]) {
|
if(segment.data[address] != segment.data[address+128]) {
|
||||||
@@ -174,19 +175,19 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check for a Tigervision or Tigervision-esque scheme
|
// check for a Tigervision or Tigervision-esque scheme
|
||||||
if(target.paging_model == Analyser::Static::Atari::Target::PagingModel::None && segment.data.size() > 4096) {
|
if(target.paging_model == Target::PagingModel::None && segment.data.size() > 4096) {
|
||||||
bool looks_like_tigervision = disassembly.external_stores.find(0x3f) != disassembly.external_stores.end();
|
bool looks_like_tigervision = disassembly.external_stores.find(0x3f) != disassembly.external_stores.end();
|
||||||
if(looks_like_tigervision) target.paging_model = Analyser::Static::Atari::Target::PagingModel::Tigervision;
|
if(looks_like_tigervision) target.paging_model = Target::PagingModel::Tigervision;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Analyser::Static::TargetList Analyser::Static::Atari::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
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?
|
// TODO: sanity checking; is this image really for an Atari 2600?
|
||||||
std::unique_ptr<Analyser::Static::Atari::Target> target(new Analyser::Static::Atari::Target);
|
auto target = std::make_unique<Target>();
|
||||||
target->machine = Machine::Atari2600;
|
target->machine = Machine::Atari2600;
|
||||||
target->confidence = 0.5;
|
target->confidence = 0.5;
|
||||||
target->media.cartridges = media.cartridges;
|
target->media.cartridges = media.cartridges;
|
||||||
target->paging_model = Analyser::Static::Atari::Target::PagingModel::None;
|
target->paging_model = Target::PagingModel::None;
|
||||||
target->uses_superchip = false;
|
target->uses_superchip = false;
|
||||||
|
|
||||||
// try to figure out the paging scheme
|
// try to figure out the paging scheme
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
namespace Analyser {
|
namespace Analyser {
|
||||||
namespace Static {
|
namespace Static {
|
||||||
namespace Atari {
|
namespace Atari2600 {
|
||||||
|
|
||||||
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
||||||
|
|
||||||
@@ -6,14 +6,14 @@
|
|||||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#ifndef Analyser_Static_Atari_Target_h
|
#ifndef Analyser_Static_Atari2600_Target_h
|
||||||
#define Analyser_Static_Atari_Target_h
|
#define Analyser_Static_Atari2600_Target_h
|
||||||
|
|
||||||
#include "../StaticAnalyser.hpp"
|
#include "../StaticAnalyser.hpp"
|
||||||
|
|
||||||
namespace Analyser {
|
namespace Analyser {
|
||||||
namespace Static {
|
namespace Static {
|
||||||
namespace Atari {
|
namespace Atari2600 {
|
||||||
|
|
||||||
struct Target: public ::Analyser::Static::Target {
|
struct Target: public ::Analyser::Static::Target {
|
||||||
enum class PagingModel {
|
enum class PagingModel {
|
||||||
26
Analyser/Static/AtariST/StaticAnalyser.cpp
Normal file
26
Analyser/Static/AtariST/StaticAnalyser.cpp
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// StaticAnalyser.cpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 03/10/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "StaticAnalyser.hpp"
|
||||||
|
#include "Target.hpp"
|
||||||
|
|
||||||
|
Analyser::Static::TargetList Analyser::Static::AtariST::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||||
|
// This analyser can comprehend disks and mass-storage devices only.
|
||||||
|
if(media.disks.empty()) return {};
|
||||||
|
|
||||||
|
// As there is at least one usable media image, wave it through.
|
||||||
|
Analyser::Static::TargetList targets;
|
||||||
|
|
||||||
|
using Target = Analyser::Static::Target;
|
||||||
|
auto *target = new Target;
|
||||||
|
target->machine = Analyser::Machine::AtariST;
|
||||||
|
target->media = media;
|
||||||
|
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
27
Analyser/Static/AtariST/StaticAnalyser.hpp
Normal file
27
Analyser/Static/AtariST/StaticAnalyser.hpp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// StaticAnalyser.hpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 03/10/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef Analyser_Static_AtariST_StaticAnalyser_hpp
|
||||||
|
#define Analyser_Static_AtariST_StaticAnalyser_hpp
|
||||||
|
|
||||||
|
#include "../StaticAnalyser.hpp"
|
||||||
|
#include "../../../Storage/TargetPlatforms.hpp"
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Analyser {
|
||||||
|
namespace Static {
|
||||||
|
namespace AtariST {
|
||||||
|
|
||||||
|
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* Analyser_Static_AtariST_StaticAnalyser_hpp */
|
||||||
23
Analyser/Static/AtariST/Target.hpp
Normal file
23
Analyser/Static/AtariST/Target.hpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Target.hpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 03/06/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef Analyser_Static_AtariST_Target_h
|
||||||
|
#define Analyser_Static_AtariST_Target_h
|
||||||
|
|
||||||
|
namespace Analyser {
|
||||||
|
namespace Static {
|
||||||
|
namespace AtariST {
|
||||||
|
|
||||||
|
struct Target: public ::Analyser::Static::Target {
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* Analyser_Static_AtariST_Target_h */
|
||||||
@@ -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) {
|
Analyser::Static::TargetList Analyser::Static::Coleco::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||||
TargetList targets;
|
TargetList targets;
|
||||||
std::unique_ptr<Target> target(new Target);
|
auto target = std::make_unique<Target>();
|
||||||
target->machine = Machine::ColecoVision;
|
target->machine = Machine::ColecoVision;
|
||||||
target->confidence = 1.0f - 1.0f / 32768.0f;
|
target->confidence = 1.0f - 1.0f / 32768.0f;
|
||||||
target->media.cartridges = ColecoCartridgesFrom(media.cartridges);
|
target->media.cartridges = ColecoCartridgesFrom(media.cartridges);
|
||||||
|
|||||||
@@ -19,12 +19,10 @@ using namespace Analyser::Static::Commodore;
|
|||||||
|
|
||||||
class CommodoreGCRParser: public Storage::Disk::Controller {
|
class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||||
public:
|
public:
|
||||||
std::shared_ptr<Storage::Disk::Drive> drive;
|
|
||||||
|
|
||||||
CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) {
|
CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) {
|
||||||
drive.reset(new Storage::Disk::Drive(4000000, 300, 2));
|
emplace_drive(4000000, 300, 2);
|
||||||
set_drive(drive);
|
set_drive(1);
|
||||||
drive->set_motor_on(true);
|
get_drive().set_motor_on(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Sector {
|
struct Sector {
|
||||||
@@ -61,6 +59,10 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
|
|||||||
return get_sector(sector);
|
return get_sector(sector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||||
|
get_drive().set_disk(disk);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
unsigned int shift_register_;
|
unsigned int shift_register_;
|
||||||
int index_count_;
|
int index_count_;
|
||||||
@@ -125,7 +127,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<Sector> get_next_sector() {
|
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;
|
const int max_index_count = index_count_ + 2;
|
||||||
|
|
||||||
while(index_count_ < max_index_count) {
|
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> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||||
std::vector<File> files;
|
std::vector<File> files;
|
||||||
CommodoreGCRParser parser;
|
CommodoreGCRParser parser;
|
||||||
parser.drive->set_disk(disk);
|
parser.set_disk(disk);
|
||||||
|
|
||||||
// find any sector whatsoever to establish the current track
|
// find any sector whatsoever to establish the current track
|
||||||
std::shared_ptr<CommodoreGCRParser::Sector> sector;
|
std::shared_ptr<CommodoreGCRParser::Sector> sector;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "../../../Outputs/Log.hpp"
|
#include "../../../Outputs/Log.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
using namespace Analyser::Static::Commodore;
|
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) {
|
Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||||
TargetList destination;
|
TargetList destination;
|
||||||
|
|
||||||
std::unique_ptr<Target> target(new Target);
|
auto target = std::make_unique<Target>();
|
||||||
target->machine = Machine::Vic20; // TODO: machine estimation
|
target->machine = Machine::Vic20; // TODO: machine estimation
|
||||||
target->confidence = 0.5; // TODO: a proper 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()) {
|
if(!files.empty()) {
|
||||||
target->memory_model = Target::MemoryModel::Unexpanded;
|
auto memory_model = Target::MemoryModel::Unexpanded;
|
||||||
std::ostringstream string_stream;
|
std::ostringstream string_stream;
|
||||||
string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ",";
|
string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ",";
|
||||||
if(files.front().is_basic()) {
|
if(files.front().is_basic()) {
|
||||||
@@ -94,16 +95,18 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
|||||||
default:
|
default:
|
||||||
LOG("Unrecognised loading address for Commodore program: " << PADHEX(4) << files.front().starting_address);
|
LOG("Unrecognised loading address for Commodore program: " << PADHEX(4) << files.front().starting_address);
|
||||||
case 0x1001:
|
case 0x1001:
|
||||||
target->memory_model = Target::MemoryModel::Unexpanded;
|
memory_model = Target::MemoryModel::Unexpanded;
|
||||||
break;
|
break;
|
||||||
case 0x1201:
|
case 0x1201:
|
||||||
target->memory_model = Target::MemoryModel::ThirtyTwoKB;
|
memory_model = Target::MemoryModel::ThirtyTwoKB;
|
||||||
break;
|
break;
|
||||||
case 0x0401:
|
case 0x0401:
|
||||||
target->memory_model = Target::MemoryModel::EightKB;
|
memory_model = Target::MemoryModel::EightKB;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target->set_memory_model(memory_model);
|
||||||
|
|
||||||
// General approach: increase memory size conservatively such that the largest file found will fit.
|
// General approach: increase memory size conservatively such that the largest file found will fit.
|
||||||
// for(File &file : files) {
|
// for(File &file : files) {
|
||||||
// std::size_t file_size = file.data.size();
|
// 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()) {
|
if(!target->media.empty()) {
|
||||||
// Inspect filename for a region hint.
|
// Inspect filename for configuration hints.
|
||||||
std::string lowercase_name = file_name;
|
std::string lowercase_name = file_name;
|
||||||
std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower);
|
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) {
|
if(lowercase_name.find("ntsc") != std::string::npos) {
|
||||||
target->region = Analyser::Static::Commodore::Target::Region::American;
|
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.
|
// Attach a 1540 if there are any disks here.
|
||||||
target->has_c1540 = !target->media.disks.empty();
|
target->has_c1540 = !target->media.disks.empty();
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,26 @@ struct Target: public ::Analyser::Static::Target {
|
|||||||
Swedish
|
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;
|
Region region = Region::European;
|
||||||
bool has_c1540 = false;
|
bool has_c1540 = false;
|
||||||
std::string loading_command;
|
std::string loading_command;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ static std::unique_ptr<Analyser::Static::Target> CartridgeTarget(
|
|||||||
output_segments.emplace_back(start_address, segment.data);
|
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->machine = Analyser::Machine::MSX;
|
||||||
target->confidence = confidence;
|
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));
|
std::move(cartridge_targets.begin(), cartridge_targets.end(), std::back_inserter(destination));
|
||||||
|
|
||||||
// Consider building a target for disks and/or tapes.
|
// 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.
|
// Check tapes for loadable files.
|
||||||
for(auto &tape : media.tapes) {
|
for(auto &tape : media.tapes) {
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
#include "Target.hpp"
|
#include "Target.hpp"
|
||||||
|
|
||||||
Analyser::Static::TargetList Analyser::Static::Macintosh::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
Analyser::Static::TargetList Analyser::Static::Macintosh::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||||
// This analyser can comprehend disks only.
|
// This analyser can comprehend disks and mass-storage devices only.
|
||||||
if(media.disks.empty()) return {};
|
if(media.disks.empty() && media.mass_storage_devices.empty()) return {};
|
||||||
|
|
||||||
// If there is at least one disk, wave it through.
|
// As there is at least one usable media image, wave it through.
|
||||||
Analyser::Static::TargetList targets;
|
Analyser::Static::TargetList targets;
|
||||||
|
|
||||||
using Target = Analyser::Static::Macintosh::Target;
|
using Target = Analyser::Static::Macintosh::Target;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct Target: public ::Analyser::Static::Target {
|
|||||||
MacPlus
|
MacPlus
|
||||||
};
|
};
|
||||||
|
|
||||||
Model model = Model::Mac512ke;
|
Model model = Model::MacPlus;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
|
|
||||||
using namespace Analyser::Static::Oric;
|
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;
|
int score = 0;
|
||||||
|
|
||||||
for(const auto address : disassembly.outward_calls) score += (rom_functions.find(address) != rom_functions.end()) ? 1 : -1;
|
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;
|
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 = {
|
const std::set<uint16_t> rom_functions = {
|
||||||
0x0228, 0x022b,
|
0x0228, 0x022b,
|
||||||
0xc3ca, 0xc3f8, 0xc448, 0xc47c, 0xc4b5, 0xc4e3, 0xc4e0, 0xc524, 0xc56f, 0xc5a2, 0xc5f8, 0xc60a, 0xc6a5, 0xc6de, 0xc719, 0xc738,
|
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
|
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 = {
|
const std::set<uint16_t> rom_functions = {
|
||||||
0x0238, 0x023b, 0x023e, 0x0241, 0x0244, 0x0247,
|
0x0238, 0x023b, 0x023e, 0x0241, 0x0244, 0x0247,
|
||||||
0xc3c6, 0xc3f4, 0xc444, 0xc47c, 0xc4a8, 0xc4d3, 0xc4e0, 0xc524, 0xc55f, 0xc592, 0xc5e8, 0xc5fa, 0xc692, 0xc6b3, 0xc6ee, 0xc70d,
|
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
|
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.
|
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));
|
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) {
|
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->machine = Machine::Oric;
|
||||||
target->confidence = 0.5;
|
target->confidence = 0.5;
|
||||||
|
|
||||||
@@ -115,12 +160,10 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
|
|||||||
for(const auto &file : tape_files) {
|
for(const auto &file : tape_files) {
|
||||||
if(file.data_type == File::MachineCode) {
|
if(file.data_type == File::MachineCode) {
|
||||||
std::vector<uint16_t> entry_points = {file.starting_address};
|
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);
|
Analyser::Static::MOS6502::Disassemble(file.data, Analyser::Static::Disassembler::OffsetMapper(file.starting_address), entry_points);
|
||||||
|
|
||||||
int basic10_score = Basic10Score(disassembly);
|
if(basic10_score(disassembly) > basic11_score(disassembly)) ++basic10_votes; else ++basic11_votes;
|
||||||
int basic11_score = Basic11Score(disassembly);
|
|
||||||
if(basic10_score > basic11_score) basic10_votes++; else basic11_votes++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +173,22 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!media.disks.empty()) {
|
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) {
|
for(auto &disk: media.disks) {
|
||||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||||
if(IsMicrodisc(parser)) {
|
|
||||||
|
if(is_microdisc(parser)) {
|
||||||
target->disk_interface = Target::DiskInterface::Microdisc;
|
target->disk_interface = Target::DiskInterface::Microdisc;
|
||||||
target->media.disks.push_back(disk);
|
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 {
|
enum class DiskInterface {
|
||||||
Microdisc,
|
Microdisc,
|
||||||
Pravetz,
|
Pravetz,
|
||||||
|
Jasmin,
|
||||||
|
BD500,
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
ROM rom = ROM::BASIC11;
|
ROM rom = ROM::BASIC11;
|
||||||
DiskInterface disk_interface = DiskInterface::None;
|
DiskInterface disk_interface = DiskInterface::None;
|
||||||
std::string loading_command;
|
std::string loading_command;
|
||||||
|
bool should_start_jasmin = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &med
|
|||||||
return {};
|
return {};
|
||||||
|
|
||||||
TargetList targets;
|
TargetList targets;
|
||||||
std::unique_ptr<Target> target(new Target);
|
auto target = std::make_unique<Target>();
|
||||||
|
|
||||||
target->machine = Machine::MasterSystem;
|
target->machine = Machine::MasterSystem;
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
#include "Acorn/StaticAnalyser.hpp"
|
#include "Acorn/StaticAnalyser.hpp"
|
||||||
#include "AmstradCPC/StaticAnalyser.hpp"
|
#include "AmstradCPC/StaticAnalyser.hpp"
|
||||||
#include "AppleII/StaticAnalyser.hpp"
|
#include "AppleII/StaticAnalyser.hpp"
|
||||||
#include "Atari/StaticAnalyser.hpp"
|
#include "Atari2600/StaticAnalyser.hpp"
|
||||||
|
#include "AtariST/StaticAnalyser.hpp"
|
||||||
#include "Coleco/StaticAnalyser.hpp"
|
#include "Coleco/StaticAnalyser.hpp"
|
||||||
#include "Commodore/StaticAnalyser.hpp"
|
#include "Commodore/StaticAnalyser.hpp"
|
||||||
#include "DiskII/StaticAnalyser.hpp"
|
#include "DiskII/StaticAnalyser.hpp"
|
||||||
@@ -40,12 +41,18 @@
|
|||||||
#include "../../Storage/Disk/DiskImage/Formats/G64.hpp"
|
#include "../../Storage/Disk/DiskImage/Formats/G64.hpp"
|
||||||
#include "../../Storage/Disk/DiskImage/Formats/DMK.hpp"
|
#include "../../Storage/Disk/DiskImage/Formats/DMK.hpp"
|
||||||
#include "../../Storage/Disk/DiskImage/Formats/HFE.hpp"
|
#include "../../Storage/Disk/DiskImage/Formats/HFE.hpp"
|
||||||
|
#include "../../Storage/Disk/DiskImage/Formats/MSA.hpp"
|
||||||
#include "../../Storage/Disk/DiskImage/Formats/MSXDSK.hpp"
|
#include "../../Storage/Disk/DiskImage/Formats/MSXDSK.hpp"
|
||||||
#include "../../Storage/Disk/DiskImage/Formats/NIB.hpp"
|
#include "../../Storage/Disk/DiskImage/Formats/NIB.hpp"
|
||||||
#include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
|
#include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
|
||||||
#include "../../Storage/Disk/DiskImage/Formats/SSD.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"
|
#include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp"
|
||||||
|
|
||||||
|
// Mass Storage Devices (i.e. usually, hard disks)
|
||||||
|
#include "../../Storage/MassStorage/Formats/HFV.hpp"
|
||||||
|
|
||||||
// Tapes
|
// Tapes
|
||||||
#include "../../Storage/Tape/Formats/CAS.hpp"
|
#include "../../Storage/Tape/Formats/CAS.hpp"
|
||||||
#include "../../Storage/Tape/Formats/CommodoreTAP.hpp"
|
#include "../../Storage/Tape/Formats/CommodoreTAP.hpp"
|
||||||
@@ -102,7 +109,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
|||||||
Format("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // DSD
|
Format("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // DSD
|
||||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC) // DSK (Amstrad CPC)
|
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC) // DSK (Amstrad CPC)
|
||||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DSK (Apple II)
|
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DSK (Apple II)
|
||||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // DSK (Macintosh)
|
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // DSK (Macintosh, floppy disk)
|
||||||
|
Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk)
|
||||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX) // DSK (MSX)
|
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX) // DSK (MSX)
|
||||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric) // DSK (Oric)
|
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric) // DSK (Oric)
|
||||||
Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore) // G64
|
Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore) // G64
|
||||||
@@ -113,6 +121,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
|||||||
// HFE (TODO: switch to AllDisk once the MSX stops being so greedy)
|
// HFE (TODO: switch to AllDisk once the MSX stops being so greedy)
|
||||||
Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
||||||
Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
||||||
|
Format("msa", result.disks, Disk::DiskImageHolder<Storage::Disk::MSA>, TargetPlatform::AtariST) // MSA
|
||||||
Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII) // NIB
|
Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII) // NIB
|
||||||
Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O
|
Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O
|
||||||
Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P
|
Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P
|
||||||
@@ -138,6 +147,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
|||||||
Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SG
|
Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SG
|
||||||
Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS
|
Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS
|
||||||
Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // SSD
|
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::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore)
|
||||||
Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric)
|
Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric)
|
||||||
Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX
|
Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX
|
||||||
@@ -160,7 +171,7 @@ Media Analyser::Static::GetMedia(const std::string &file_name) {
|
|||||||
TargetList Analyser::Static::GetTargets(const std::string &file_name) {
|
TargetList Analyser::Static::GetTargets(const std::string &file_name) {
|
||||||
TargetList targets;
|
TargetList targets;
|
||||||
|
|
||||||
// Collect all disks, tapes and ROMs as can be extrapolated from this file, forming the
|
// Collect all disks, tapes ROMs, etc as can be extrapolated from this file, forming the
|
||||||
// union of all platforms this file might be a target for.
|
// union of all platforms this file might be a target for.
|
||||||
TargetPlatform::IntType potential_platforms = 0;
|
TargetPlatform::IntType potential_platforms = 0;
|
||||||
Media media = GetMediaAndPlatforms(file_name, potential_platforms);
|
Media media = GetMediaAndPlatforms(file_name, potential_platforms);
|
||||||
@@ -174,7 +185,8 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) {
|
|||||||
if(potential_platforms & TargetPlatform::Acorn) Append(Acorn);
|
if(potential_platforms & TargetPlatform::Acorn) Append(Acorn);
|
||||||
if(potential_platforms & TargetPlatform::AmstradCPC) Append(AmstradCPC);
|
if(potential_platforms & TargetPlatform::AmstradCPC) Append(AmstradCPC);
|
||||||
if(potential_platforms & TargetPlatform::AppleII) Append(AppleII);
|
if(potential_platforms & TargetPlatform::AppleII) Append(AppleII);
|
||||||
if(potential_platforms & TargetPlatform::Atari2600) Append(Atari);
|
if(potential_platforms & TargetPlatform::Atari2600) Append(Atari2600);
|
||||||
|
if(potential_platforms & TargetPlatform::AtariST) Append(AtariST);
|
||||||
if(potential_platforms & TargetPlatform::ColecoVision) Append(Coleco);
|
if(potential_platforms & TargetPlatform::ColecoVision) Append(Coleco);
|
||||||
if(potential_platforms & TargetPlatform::Commodore) Append(Commodore);
|
if(potential_platforms & TargetPlatform::Commodore) Append(Commodore);
|
||||||
if(potential_platforms & TargetPlatform::DiskII) Append(DiskII);
|
if(potential_platforms & TargetPlatform::DiskII) Append(DiskII);
|
||||||
|
|||||||
@@ -11,9 +11,10 @@
|
|||||||
|
|
||||||
#include "../Machines.hpp"
|
#include "../Machines.hpp"
|
||||||
|
|
||||||
#include "../../Storage/Tape/Tape.hpp"
|
|
||||||
#include "../../Storage/Disk/Disk.hpp"
|
|
||||||
#include "../../Storage/Cartridge/Cartridge.hpp"
|
#include "../../Storage/Cartridge/Cartridge.hpp"
|
||||||
|
#include "../../Storage/Disk/Disk.hpp"
|
||||||
|
#include "../../Storage/MassStorage/MassStorageDevice.hpp"
|
||||||
|
#include "../../Storage/Tape/Tape.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -29,9 +30,10 @@ struct Media {
|
|||||||
std::vector<std::shared_ptr<Storage::Disk::Disk>> disks;
|
std::vector<std::shared_ptr<Storage::Disk::Disk>> disks;
|
||||||
std::vector<std::shared_ptr<Storage::Tape::Tape>> tapes;
|
std::vector<std::shared_ptr<Storage::Tape::Tape>> tapes;
|
||||||
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges;
|
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges;
|
||||||
|
std::vector<std::shared_ptr<Storage::MassStorage::MassStorageDevice>> mass_storage_devices;
|
||||||
|
|
||||||
bool empty() const {
|
bool empty() const {
|
||||||
return disks.empty() && tapes.empty() && cartridges.empty();
|
return disks.empty() && tapes.empty() && cartridges.empty() && mass_storage_devices.empty();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#define ClockReceiver_hpp
|
#define ClockReceiver_hpp
|
||||||
|
|
||||||
#include "ForceInline.hpp"
|
#include "ForceInline.hpp"
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Informal pattern for all classes that run from a clock cycle:
|
Informal pattern for all classes that run from a clock cycle:
|
||||||
@@ -54,7 +55,9 @@
|
|||||||
*/
|
*/
|
||||||
template <class T> class WrappedInt {
|
template <class T> class WrappedInt {
|
||||||
public:
|
public:
|
||||||
forceinline constexpr WrappedInt(int l) noexcept : length_(l) {}
|
using IntType = int64_t;
|
||||||
|
|
||||||
|
forceinline constexpr WrappedInt(IntType l) noexcept : length_(l) {}
|
||||||
forceinline constexpr WrappedInt() noexcept : length_(0) {}
|
forceinline constexpr WrappedInt() noexcept : length_(0) {}
|
||||||
|
|
||||||
forceinline T &operator =(const T &rhs) {
|
forceinline T &operator =(const T &rhs) {
|
||||||
@@ -133,16 +136,20 @@ template <class T> class WrappedInt {
|
|||||||
forceinline constexpr bool operator !() const { return !length_; }
|
forceinline constexpr bool operator !() const { return !length_; }
|
||||||
// bool operator () is not supported because it offers an implicit cast to int, which is prone silently to permit misuse
|
// bool operator () is not supported because it offers an implicit cast to int, which is prone silently to permit misuse
|
||||||
|
|
||||||
forceinline constexpr int as_int() const { return length_; }
|
/// @returns The underlying int, cast to an integral type of your choosing.
|
||||||
|
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_; }
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
Severs from @c this the effect of dividing by @c divisor; @c this will end up with
|
Severs from @c this the effect of dividing by @c divisor; @c this will end up with
|
||||||
the value of @c this modulo @c divisor and @c divided by @c divisor is returned.
|
the value of @c this modulo @c divisor and @c divided by @c divisor is returned.
|
||||||
*/
|
*/
|
||||||
forceinline T divide(const T &divisor) {
|
template <typename Result = T> forceinline Result divide(const T &divisor) {
|
||||||
T result(length_ / divisor.length_);
|
Result r;
|
||||||
length_ %= divisor.length_;
|
static_cast<T *>(this)->fill(r, divisor);
|
||||||
return result;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@@ -161,13 +168,13 @@ template <class T> class WrappedInt {
|
|||||||
// classes that use this template.
|
// classes that use this template.
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int length_;
|
IntType length_;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Describes an integer number of whole cycles: pairs of clock signal transitions.
|
/// Describes an integer number of whole cycles: pairs of clock signal transitions.
|
||||||
class Cycles: public WrappedInt<Cycles> {
|
class Cycles: public WrappedInt<Cycles> {
|
||||||
public:
|
public:
|
||||||
forceinline constexpr Cycles(int l) noexcept : WrappedInt<Cycles>(l) {}
|
forceinline constexpr Cycles(IntType l) noexcept : WrappedInt<Cycles>(l) {}
|
||||||
forceinline constexpr Cycles() noexcept : WrappedInt<Cycles>() {}
|
forceinline constexpr Cycles() noexcept : WrappedInt<Cycles>() {}
|
||||||
forceinline constexpr Cycles(const Cycles &cycles) noexcept : WrappedInt<Cycles>(cycles.length_) {}
|
forceinline constexpr Cycles(const Cycles &cycles) noexcept : WrappedInt<Cycles>(cycles.length_) {}
|
||||||
|
|
||||||
@@ -177,15 +184,20 @@ class Cycles: public WrappedInt<Cycles> {
|
|||||||
result.length_ = length_;
|
result.length_ = length_;
|
||||||
length_ = 0;
|
length_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void fill(Cycles &result, const Cycles &divisor) {
|
||||||
|
result.length_ = length_ / divisor.length_;
|
||||||
|
length_ %= divisor.length_;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Describes an integer number of half cycles: single clock signal transitions.
|
/// Describes an integer number of half cycles: single clock signal transitions.
|
||||||
class HalfCycles: public WrappedInt<HalfCycles> {
|
class HalfCycles: public WrappedInt<HalfCycles> {
|
||||||
public:
|
public:
|
||||||
forceinline constexpr HalfCycles(int l) noexcept : WrappedInt<HalfCycles>(l) {}
|
forceinline constexpr HalfCycles(IntType l) noexcept : WrappedInt<HalfCycles>(l) {}
|
||||||
forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {}
|
forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {}
|
||||||
|
|
||||||
forceinline constexpr HalfCycles(const Cycles &cycles) noexcept : WrappedInt<HalfCycles>(cycles.as_int() * 2) {}
|
forceinline constexpr HalfCycles(const Cycles &cycles) noexcept : WrappedInt<HalfCycles>(cycles.as_integral() * 2) {}
|
||||||
forceinline constexpr HalfCycles(const HalfCycles &half_cycles) noexcept : WrappedInt<HalfCycles>(half_cycles.length_) {}
|
forceinline constexpr HalfCycles(const HalfCycles &half_cycles) noexcept : WrappedInt<HalfCycles>(half_cycles.length_) {}
|
||||||
|
|
||||||
/// @returns The number of whole cycles completely covered by this span of half cycles.
|
/// @returns The number of whole cycles completely covered by this span of half cycles.
|
||||||
@@ -215,6 +227,16 @@ class HalfCycles: public WrappedInt<HalfCycles> {
|
|||||||
result.length_ = length_;
|
result.length_ = length_;
|
||||||
length_ = 0;
|
length_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void fill(Cycles &result, const HalfCycles &divisor) {
|
||||||
|
result = Cycles(length_ / (divisor.length_ << 1));
|
||||||
|
length_ %= (divisor.length_ << 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fill(HalfCycles &result, const HalfCycles &divisor) {
|
||||||
|
result.length_ = length_ / divisor.length_;
|
||||||
|
length_ %= divisor.length_;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a specialisation of WrappedInt::flush for converting HalfCycles to Cycles
|
// Create a specialisation of WrappedInt::flush for converting HalfCycles to Cycles
|
||||||
|
|||||||
@@ -13,62 +13,68 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
A DeferredQueue maintains a list of ordered actions and the times at which
|
Provides the logic to insert into and traverse a list of future scheduled items.
|
||||||
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.
|
|
||||||
*/
|
*/
|
||||||
template <typename TimeUnit> class DeferredQueue {
|
template <typename TimeUnit> class DeferredQueue {
|
||||||
public:
|
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.
|
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) {
|
void defer(TimeUnit delay, const std::function<void(void)> &action) {
|
||||||
pending_actions_.emplace_back(delay, action);
|
// Apply immediately if there's no delay (or a negative delay).
|
||||||
}
|
if(delay <= TimeUnit(0)) {
|
||||||
|
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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Divide the time to run according to the pending actions.
|
if(!pending_actions_.empty()) {
|
||||||
while(length > TimeUnit(0)) {
|
// Otherwise enqueue, having subtracted the delay for any preceding events,
|
||||||
TimeUnit next_period = pending_actions_.empty() ? length : std::min(length, pending_actions_[0].delay);
|
// and subtracting from the subsequent, if any.
|
||||||
target_(next_period);
|
auto insertion_point = pending_actions_.begin();
|
||||||
length -= next_period;
|
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;
|
pending_actions_.emplace(insertion_point, delay, action);
|
||||||
for(auto &action: pending_actions_) {
|
} else {
|
||||||
action.delay -= next_period;
|
pending_actions_.emplace_back(delay, action);
|
||||||
if(!action.delay) {
|
|
||||||
action.action();
|
|
||||||
++performances;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(performances) {
|
|
||||||
pending_actions_.erase(pending_actions_.begin(), pending_actions_.begin() + performances);
|
/*!
|
||||||
|
@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:
|
private:
|
||||||
std::function<void(TimeUnit)> target_;
|
|
||||||
|
|
||||||
// The list of deferred actions.
|
// The list of deferred actions.
|
||||||
struct DeferredAction {
|
struct DeferredAction {
|
||||||
TimeUnit delay;
|
TimeUnit delay;
|
||||||
@@ -79,4 +85,40 @@ template <typename TimeUnit> class DeferredQueue {
|
|||||||
std::vector<DeferredAction> pending_actions_;
|
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 */
|
#endif /* DeferredQueue_h */
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
#ifndef ForceInline_hpp
|
#ifndef ForceInline_hpp
|
||||||
#define ForceInline_hpp
|
#define ForceInline_hpp
|
||||||
|
|
||||||
#ifdef DEBUG
|
#ifndef NDEBUG
|
||||||
|
|
||||||
#define forceinline
|
#define forceinline inline
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#define JustInTime_h
|
#define JustInTime_h
|
||||||
|
|
||||||
#include "../Concurrency/AsyncTaskQueue.hpp"
|
#include "../Concurrency/AsyncTaskQueue.hpp"
|
||||||
|
#include "ForceInline.hpp"
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
A JustInTimeActor holds (i) an embedded object with a run_for method; and (ii) an amount
|
A JustInTimeActor holds (i) an embedded object with a run_for method; and (ii) an amount
|
||||||
@@ -21,32 +22,52 @@
|
|||||||
Machines that accumulate HalfCycle time but supply to a Cycle-counted device may supply a
|
Machines that accumulate HalfCycle time but supply to a Cycle-counted device may supply a
|
||||||
separate @c TargetTimeScale at template declaration.
|
separate @c TargetTimeScale at template declaration.
|
||||||
*/
|
*/
|
||||||
template <class T, class LocalTimeScale, class TargetTimeScale = LocalTimeScale> class JustInTimeActor {
|
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class JustInTimeActor {
|
||||||
public:
|
public:
|
||||||
/// Constructs a new JustInTimeActor using the same construction arguments as the included object.
|
/// Constructs a new JustInTimeActor using the same construction arguments as the included object.
|
||||||
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||||
|
|
||||||
/// Adds time to the actor.
|
/// Adds time to the actor.
|
||||||
inline void operator += (const LocalTimeScale &rhs) {
|
forceinline void operator += (const LocalTimeScale &rhs) {
|
||||||
|
if constexpr (multiplier != 1) {
|
||||||
|
time_since_update_ += rhs * multiplier;
|
||||||
|
} else {
|
||||||
time_since_update_ += rhs;
|
time_since_update_ += rhs;
|
||||||
|
}
|
||||||
is_flushed_ = false;
|
is_flushed_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flushes all accumulated time and returns a pointer to the included object.
|
/// Flushes all accumulated time and returns a pointer to the included object.
|
||||||
inline T *operator->() {
|
forceinline T *operator->() {
|
||||||
flush();
|
flush();
|
||||||
return &object_;
|
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.
|
/// Returns a pointer to the included object without flushing time.
|
||||||
inline T *last_valid() {
|
forceinline T *last_valid() {
|
||||||
return &object_;
|
return &object_;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flushes all accumulated time.
|
/// Flushes all accumulated time.
|
||||||
inline void flush() {
|
forceinline void flush() {
|
||||||
if(!is_flushed_) object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
if(!is_flushed_) {
|
||||||
is_flushed_ = true;
|
is_flushed_ = true;
|
||||||
|
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))
|
||||||
|
object_.run_for(duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -55,12 +76,54 @@ template <class T, class LocalTimeScale, class TargetTimeScale = LocalTimeScale>
|
|||||||
bool is_flushed_ = true;
|
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.
|
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,
|
Any time the amount of accumulated time crosses a threshold provided at construction time,
|
||||||
the object will be updated on the AsyncTaskQueue.
|
the object will be updated on the AsyncTaskQueue.
|
||||||
*/
|
*/
|
||||||
template <class T, class LocalTimeScale, class TargetTimeScale = LocalTimeScale> class AsyncJustInTimeActor {
|
template <class T, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class AsyncJustInTimeActor {
|
||||||
public:
|
public:
|
||||||
/// Constructs a new AsyncJustInTimeActor using the same construction arguments as the included object.
|
/// Constructs a new AsyncJustInTimeActor using the same construction arguments as the included object.
|
||||||
template<typename... Args> AsyncJustInTimeActor(TargetTimeScale threshold, Args&&... args) :
|
template<typename... Args> AsyncJustInTimeActor(TargetTimeScale threshold, Args&&... args) :
|
||||||
|
|||||||
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
|
#ifndef TimeTypes_h
|
||||||
#define TimeTypes_h
|
#define TimeTypes_h
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace Time {
|
namespace Time {
|
||||||
|
|
||||||
typedef double Seconds;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "1770.hpp"
|
#include "1770.hpp"
|
||||||
|
|
||||||
#include "../../Storage/Disk/Encodings/MFM/Constants.hpp"
|
#include "../../Storage/Disk/Encodings/MFM/Constants.hpp"
|
||||||
|
|
||||||
|
#define LOG_PREFIX "[WD FDC] "
|
||||||
#include "../../Outputs/Log.hpp"
|
#include "../../Outputs/Log.hpp"
|
||||||
|
|
||||||
using namespace WD;
|
using namespace WD;
|
||||||
@@ -16,19 +18,19 @@ using namespace WD;
|
|||||||
WD1770::WD1770(Personality p) :
|
WD1770::WD1770(Personality p) :
|
||||||
Storage::Disk::MFMController(8000000),
|
Storage::Disk::MFMController(8000000),
|
||||||
personality_(p),
|
personality_(p),
|
||||||
interesting_event_mask_(static_cast<int>(Event1770::Command)) {
|
interesting_event_mask_(int(Event1770::Command)) {
|
||||||
set_is_double_density(false);
|
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) {
|
switch(address&3) {
|
||||||
case 0: {
|
case 0: {
|
||||||
if((value&0xf0) == 0xd0) {
|
if((value&0xf0) == 0xd0) {
|
||||||
if(value == 0xd0) {
|
if(value == 0xd0) {
|
||||||
// Force interrupt **immediately**.
|
// Force interrupt **immediately**.
|
||||||
LOG("Force interrupt immediately");
|
LOG("Force interrupt immediately");
|
||||||
posit_event(static_cast<int>(Event1770::ForceInterrupt));
|
posit_event(int(Event1770::ForceInterrupt));
|
||||||
} else {
|
} else {
|
||||||
ERROR("!!!TODO: force interrupt!!!");
|
ERROR("!!!TODO: force interrupt!!!");
|
||||||
update_status([] (Status &status) {
|
update_status([] (Status &status) {
|
||||||
@@ -37,7 +39,7 @@ void WD1770::set_register(int address, uint8_t value) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
command_ = value;
|
command_ = value;
|
||||||
posit_event(static_cast<int>(Event1770::Command));
|
posit_event(int(Event1770::Command));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -52,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) {
|
switch(address&3) {
|
||||||
default: {
|
default: {
|
||||||
update_status([] (Status &status) {
|
update_status([] (Status &status) {
|
||||||
status.interrupt_request = false;
|
status.interrupt_request = false;
|
||||||
});
|
});
|
||||||
uint8_t status =
|
uint8_t status =
|
||||||
(status_.write_protect ? Flag::WriteProtect : 0) |
|
|
||||||
(status_.crc_error ? Flag::CRCError : 0) |
|
(status_.crc_error ? Flag::CRCError : 0) |
|
||||||
(status_.busy ? Flag::Busy : 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) {
|
switch(status_.type) {
|
||||||
case Status::One:
|
case Status::One:
|
||||||
status |=
|
status |=
|
||||||
(get_drive().get_is_track_zero() ? Flag::TrackZero : 0) |
|
(status_.track_zero ? Flag::TrackZero : 0) |
|
||||||
(status_.seek_error ? Flag::SeekError : 0);
|
(status_.seek_error ? Flag::SeekError : 0) |
|
||||||
// TODO: index hole
|
(get_drive().get_is_read_only() ? Flag::WriteProtect : 0) |
|
||||||
|
(get_drive().get_index_pulse() ? Flag::Index : 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Status::Two:
|
case Status::Two:
|
||||||
case Status::Three:
|
case Status::Three:
|
||||||
status |=
|
status |=
|
||||||
|
(status_.write_protect ? Flag::WriteProtect : 0) |
|
||||||
(status_.record_type ? Flag::RecordType : 0) |
|
(status_.record_type ? Flag::RecordType : 0) |
|
||||||
(status_.lost_data ? Flag::LostData : 0) |
|
(status_.lost_data ? Flag::LostData : 0) |
|
||||||
(status_.data_request ? Flag::DataRequest : 0) |
|
(status_.data_request ? Flag::DataRequest : 0) |
|
||||||
@@ -89,10 +99,15 @@ uint8_t WD1770::get_register(int address) {
|
|||||||
if(status_.type == Status::One)
|
if(status_.type == Status::One)
|
||||||
status |= (status_.spin_up ? Flag::SpinUp : 0);
|
status |= (status_.spin_up ? Flag::SpinUp : 0);
|
||||||
}
|
}
|
||||||
|
// LOG("Returned status " << PADHEX(2) << int(status) << " of type " << 1+int(status_.type));
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
case 1: return track_;
|
case 1:
|
||||||
case 2: return sector_;
|
LOG("Returned track " << int(track_));
|
||||||
|
return track_;
|
||||||
|
case 2:
|
||||||
|
LOG("Returned sector " << int(sector_));
|
||||||
|
return sector_;
|
||||||
case 3:
|
case 3:
|
||||||
update_status([] (Status &status) {
|
update_status([] (Status &status) {
|
||||||
status.data_request = false;
|
status.data_request = false;
|
||||||
@@ -105,28 +120,30 @@ void WD1770::run_for(const Cycles cycles) {
|
|||||||
Storage::Disk::Controller::run_for(cycles);
|
Storage::Disk::Controller::run_for(cycles);
|
||||||
|
|
||||||
if(delay_time_) {
|
if(delay_time_) {
|
||||||
unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_int());
|
const auto number_of_cycles = cycles.as_integral();
|
||||||
if(delay_time_ <= number_of_cycles) {
|
if(delay_time_ <= number_of_cycles) {
|
||||||
delay_time_ = 0;
|
delay_time_ = 0;
|
||||||
posit_event(static_cast<int>(Event1770::Timer));
|
posit_event(int(Event1770::Timer));
|
||||||
} else {
|
} else {
|
||||||
delay_time_ -= number_of_cycles;
|
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_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 BEGIN_SECTION() switch(resume_point_) { default:
|
||||||
#define END_SECTION() (void)0; }
|
#define END_SECTION() (void)0; }
|
||||||
|
|
||||||
#define READ_ID() \
|
#define READ_ID() \
|
||||||
if(new_event_type == static_cast<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_++; } \
|
if(!distance_into_section_ && get_latest_token().type == Token::ID) {\
|
||||||
else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { \
|
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; \
|
header_[distance_into_section_ - 1] = get_latest_token().byte_value; \
|
||||||
distance_into_section_++; \
|
++distance_into_section_; \
|
||||||
} \
|
} \
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,10 +176,10 @@ void WD1770::run_for(const Cycles cycles) {
|
|||||||
// +--------+----------+-------------------------+
|
// +--------+----------+-------------------------+
|
||||||
|
|
||||||
void WD1770::posit_event(int new_event_type) {
|
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_++;
|
index_hole_count_++;
|
||||||
if(index_hole_count_target_ == 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;
|
index_hole_count_target_ = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,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;
|
interesting_event_mask_ = 0;
|
||||||
resume_point_ = 0;
|
resume_point_ = 0;
|
||||||
update_status([] (Status &status) {
|
update_status([] (Status &status) {
|
||||||
status.type = Status::One;
|
status.type = Status::One;
|
||||||
status.data_request = false;
|
status.data_request = false;
|
||||||
|
status.spin_up = false;
|
||||||
});
|
});
|
||||||
} else {
|
} 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;
|
interesting_event_mask_ &= ~new_event_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +226,7 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
update_status([] (Status &status) {
|
update_status([] (Status &status) {
|
||||||
status.busy = true;
|
status.busy = true;
|
||||||
status.interrupt_request = false;
|
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_));
|
LOG("Starting " << PADHEX(2) << int(command_));
|
||||||
@@ -240,6 +259,7 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
status.data_request = false;
|
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() && !has_head_load_line()) goto test_type1_type;
|
||||||
|
|
||||||
if(has_motor_on_line()) goto begin_type1_spin_up;
|
if(has_motor_on_line()) goto begin_type1_spin_up;
|
||||||
@@ -272,19 +292,19 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
perform_seek_or_restore_command:
|
perform_seek_or_restore_command:
|
||||||
if(track_ == data_) goto verify;
|
if(track_ == data_) goto verify_seek;
|
||||||
step_direction_ = (data_ > track_);
|
step_direction_ = (data_ > track_);
|
||||||
|
|
||||||
adjust_track:
|
adjust_track:
|
||||||
if(step_direction_) track_++; else track_--;
|
if(step_direction_) ++track_; else --track_;
|
||||||
|
|
||||||
perform_step:
|
perform_step:
|
||||||
if(!step_direction_ && get_drive().get_is_track_zero()) {
|
if(!step_direction_ && get_drive().get_is_track_zero()) {
|
||||||
track_ = 0;
|
track_ = 0;
|
||||||
goto verify;
|
goto verify_seek;
|
||||||
}
|
}
|
||||||
get_drive().step(Storage::Disk::HeadPosition(step_direction_ ? 1 : -1));
|
get_drive().step(Storage::Disk::HeadPosition(step_direction_ ? 1 : -1));
|
||||||
unsigned int time_to_wait;
|
Cycles::IntType time_to_wait;
|
||||||
switch(command_ & 3) {
|
switch(command_ & 3) {
|
||||||
default:
|
default:
|
||||||
case 0: time_to_wait = 6; break;
|
case 0: time_to_wait = 6; break;
|
||||||
@@ -293,14 +313,17 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
case 3: time_to_wait = (personality_ == P1772) ? 3 : 30; break;
|
case 3: time_to_wait = (personality_ == P1772) ? 3 : 30; break;
|
||||||
}
|
}
|
||||||
WAIT_FOR_TIME(time_to_wait);
|
WAIT_FOR_TIME(time_to_wait);
|
||||||
if(command_ >> 5) goto verify;
|
if(command_ >> 5) goto verify_seek;
|
||||||
goto perform_seek_or_restore_command;
|
goto perform_seek_or_restore_command;
|
||||||
|
|
||||||
perform_step_command:
|
perform_step_command:
|
||||||
if(command_ & 0x10) goto adjust_track;
|
if(command_ & 0x10) goto adjust_track;
|
||||||
goto perform_step;
|
goto perform_step;
|
||||||
|
|
||||||
verify:
|
verify_seek:
|
||||||
|
update_status([this] (Status &status) {
|
||||||
|
status.track_zero = get_drive().get_is_track_zero();
|
||||||
|
});
|
||||||
if(!(command_ & 0x04)) {
|
if(!(command_ & 0x04)) {
|
||||||
goto wait_for_command;
|
goto wait_for_command;
|
||||||
}
|
}
|
||||||
@@ -309,17 +332,20 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
distance_into_section_ = 0;
|
distance_into_section_ = 0;
|
||||||
|
|
||||||
verify_read_data:
|
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();
|
READ_ID();
|
||||||
|
|
||||||
if(index_hole_count_ == 6) {
|
if(index_hole_count_ == 6) {
|
||||||
|
LOG("Nothing found to verify");
|
||||||
update_status([] (Status &status) {
|
update_status([] (Status &status) {
|
||||||
status.seek_error = true;
|
status.seek_error = true;
|
||||||
});
|
});
|
||||||
goto wait_for_command;
|
goto wait_for_command;
|
||||||
}
|
}
|
||||||
if(distance_into_section_ == 7) {
|
if(distance_into_section_ == 7) {
|
||||||
|
distance_into_section_ = 0;
|
||||||
set_data_mode(DataMode::Scanning);
|
set_data_mode(DataMode::Scanning);
|
||||||
|
|
||||||
if(get_crc_generator().get_value()) {
|
if(get_crc_generator().get_value()) {
|
||||||
update_status([] (Status &status) {
|
update_status([] (Status &status) {
|
||||||
status.crc_error = true;
|
status.crc_error = true;
|
||||||
@@ -334,8 +360,6 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
});
|
});
|
||||||
goto wait_for_command;
|
goto wait_for_command;
|
||||||
}
|
}
|
||||||
|
|
||||||
distance_into_section_ = 0;
|
|
||||||
}
|
}
|
||||||
goto verify_read_data;
|
goto verify_read_data;
|
||||||
|
|
||||||
@@ -392,8 +416,11 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
goto wait_for_command;
|
goto wait_for_command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
distance_into_section_ = 0;
|
||||||
|
set_data_mode(DataMode::Scanning);
|
||||||
|
|
||||||
type2_get_header:
|
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();
|
READ_ID();
|
||||||
|
|
||||||
if(index_hole_count_ == 5) {
|
if(index_hole_count_ == 5) {
|
||||||
@@ -404,8 +431,10 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
goto wait_for_command;
|
goto wait_for_command;
|
||||||
}
|
}
|
||||||
if(distance_into_section_ == 7) {
|
if(distance_into_section_ == 7) {
|
||||||
LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
distance_into_section_ = 0;
|
||||||
set_data_mode(DataMode::Scanning);
|
set_data_mode(DataMode::Scanning);
|
||||||
|
|
||||||
|
LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||||
if( header_[0] == track_ && header_[2] == sector_ &&
|
if( header_[0] == track_ && header_[2] == sector_ &&
|
||||||
(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) {
|
(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) {
|
||||||
LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||||
@@ -422,7 +451,6 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
});
|
});
|
||||||
goto type2_read_or_write_data;
|
goto type2_read_or_write_data;
|
||||||
}
|
}
|
||||||
distance_into_section_ = 0;
|
|
||||||
}
|
}
|
||||||
goto type2_get_header;
|
goto type2_get_header;
|
||||||
|
|
||||||
@@ -453,7 +481,7 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
status.data_request = true;
|
status.data_request = true;
|
||||||
});
|
});
|
||||||
distance_into_section_++;
|
distance_into_section_++;
|
||||||
if(distance_into_section_ == 128 << header_[3]) {
|
if(distance_into_section_ == 128 << (header_[3]&3)) {
|
||||||
distance_into_section_ = 0;
|
distance_into_section_ = 0;
|
||||||
goto type2_check_crc;
|
goto type2_check_crc;
|
||||||
}
|
}
|
||||||
@@ -465,6 +493,9 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
header_[distance_into_section_] = get_latest_token().byte_value;
|
header_[distance_into_section_] = get_latest_token().byte_value;
|
||||||
distance_into_section_++;
|
distance_into_section_++;
|
||||||
if(distance_into_section_ == 2) {
|
if(distance_into_section_ == 2) {
|
||||||
|
distance_into_section_ = 0;
|
||||||
|
set_data_mode(DataMode::Scanning);
|
||||||
|
|
||||||
if(get_crc_generator().get_value()) {
|
if(get_crc_generator().get_value()) {
|
||||||
LOG("CRC error; terminating");
|
LOG("CRC error; terminating");
|
||||||
update_status([this] (Status &status) {
|
update_status([this] (Status &status) {
|
||||||
@@ -473,11 +504,13 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
goto wait_for_command;
|
goto wait_for_command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOG("Finished reading sector " << std::dec << int(sector_));
|
||||||
|
|
||||||
if(command_ & 0x10) {
|
if(command_ & 0x10) {
|
||||||
sector_++;
|
sector_++;
|
||||||
|
LOG("Advancing to search for sector " << std::dec << int(sector_));
|
||||||
goto test_type2_write_protection;
|
goto test_type2_write_protection;
|
||||||
}
|
}
|
||||||
LOG("Finished reading sector " << std::dec << int(sector_));
|
|
||||||
goto wait_for_command;
|
goto wait_for_command;
|
||||||
}
|
}
|
||||||
goto type2_check_crc;
|
goto type2_check_crc;
|
||||||
@@ -531,7 +564,7 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
*/
|
*/
|
||||||
write_byte(data_);
|
write_byte(data_);
|
||||||
distance_into_section_++;
|
distance_into_section_++;
|
||||||
if(distance_into_section_ == 128 << header_[3]) {
|
if(distance_into_section_ == 128 << (header_[3]&3)) {
|
||||||
goto type2_write_crc;
|
goto type2_write_crc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,8 +643,8 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
distance_into_section_ = 0;
|
distance_into_section_ = 0;
|
||||||
|
|
||||||
read_address_get_header:
|
read_address_get_header:
|
||||||
WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token));
|
WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token));
|
||||||
if(new_event_type == static_cast<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_++; }
|
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) {
|
else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {
|
||||||
if(status_.data_request) {
|
if(status_.data_request) {
|
||||||
@@ -625,9 +658,11 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
update_status([] (Status &status) {
|
update_status([] (Status &status) {
|
||||||
status.data_request = true;
|
status.data_request = true;
|
||||||
});
|
});
|
||||||
distance_into_section_++;
|
++distance_into_section_;
|
||||||
|
|
||||||
if(distance_into_section_ == 7) {
|
if(distance_into_section_ == 7) {
|
||||||
|
distance_into_section_ = 0;
|
||||||
|
|
||||||
if(get_crc_generator().get_value()) {
|
if(get_crc_generator().get_value()) {
|
||||||
update_status([] (Status &status) {
|
update_status([] (Status &status) {
|
||||||
status.crc_error = true;
|
status.crc_error = true;
|
||||||
@@ -651,7 +686,7 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
index_hole_count_ = 0;
|
index_hole_count_ = 0;
|
||||||
|
|
||||||
read_track_read_byte:
|
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_) {
|
if(index_hole_count_) {
|
||||||
goto wait_for_command;
|
goto wait_for_command;
|
||||||
}
|
}
|
||||||
@@ -718,7 +753,7 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
case 0xfd: case 0xfe:
|
case 0xfd: case 0xfe:
|
||||||
// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022
|
// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022
|
||||||
write_raw_short(
|
write_raw_short(
|
||||||
static_cast<uint16_t>(
|
uint16_t(
|
||||||
0xa022 |
|
0xa022 |
|
||||||
((data_ & 0x80) << 7) |
|
((data_ & 0x80) << 7) |
|
||||||
((data_ & 0x40) << 6) |
|
((data_ & 0x40) << 6) |
|
||||||
@@ -767,15 +802,18 @@ void WD1770::posit_event(int new_event_type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WD1770::update_status(std::function<void(Status &)> updater) {
|
void WD1770::update_status(std::function<void(Status &)> updater) {
|
||||||
|
const Status old_status = status_;
|
||||||
|
|
||||||
if(delegate_) {
|
if(delegate_) {
|
||||||
Status old_status = status_;
|
|
||||||
updater(status_);
|
updater(status_);
|
||||||
bool did_change =
|
const bool did_change =
|
||||||
(status_.busy != old_status.busy) ||
|
(status_.busy != old_status.busy) ||
|
||||||
(status_.data_request != old_status.data_request);
|
(status_.data_request != old_status.data_request) ||
|
||||||
|
(status_.interrupt_request != old_status.interrupt_request);
|
||||||
if(did_change) delegate_->wd1770_did_change_output(this);
|
if(did_change) delegate_->wd1770_did_change_output(this);
|
||||||
}
|
} else updater(status_);
|
||||||
else updater(status_);
|
|
||||||
|
if(status_.busy != old_status.busy) update_clocking_observer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WD1770::set_head_load_request(bool head_load) {}
|
void WD1770::set_head_load_request(bool head_load) {}
|
||||||
@@ -783,5 +821,14 @@ void WD1770::set_motor_on(bool motor_on) {}
|
|||||||
|
|
||||||
void WD1770::set_head_loaded(bool head_loaded) {
|
void WD1770::set_head_loaded(bool head_loaded) {
|
||||||
head_is_loaded_ = 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() {
|
||||||
|
if(status_.busy) return ClockingHint::Preference::RealTime;
|
||||||
|
return Storage::Disk::MFMController::preferred_clocking();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,29 +36,29 @@ class WD1770: public Storage::Disk::MFMController {
|
|||||||
using Storage::Disk::MFMController::set_is_double_density;
|
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.
|
/// 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.
|
/// 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.
|
/// Runs the controller for @c number_of_cycles cycles.
|
||||||
void run_for(const Cycles cycles);
|
void run_for(const Cycles cycles);
|
||||||
|
|
||||||
enum Flag: uint8_t {
|
enum Flag: uint8_t {
|
||||||
NotReady = 0x80,
|
NotReady = 0x80, // 0x80
|
||||||
MotorOn = 0x80,
|
MotorOn = 0x80,
|
||||||
WriteProtect = 0x40,
|
WriteProtect = 0x40, // 0x40
|
||||||
RecordType = 0x20,
|
RecordType = 0x20, // 0x20
|
||||||
SpinUp = 0x20,
|
SpinUp = 0x20,
|
||||||
HeadLoaded = 0x20,
|
HeadLoaded = 0x20,
|
||||||
RecordNotFound = 0x10,
|
RecordNotFound = 0x10, // 0x10
|
||||||
SeekError = 0x10,
|
SeekError = 0x10,
|
||||||
CRCError = 0x08,
|
CRCError = 0x08, // 0x08
|
||||||
LostData = 0x04,
|
LostData = 0x04, // 0x04
|
||||||
TrackZero = 0x04,
|
TrackZero = 0x04,
|
||||||
DataRequest = 0x02,
|
DataRequest = 0x02, // 0x02
|
||||||
Index = 0x02,
|
Index = 0x02,
|
||||||
Busy = 0x01
|
Busy = 0x01 // 0x01
|
||||||
};
|
};
|
||||||
|
|
||||||
/// @returns The current value of the IRQ line output.
|
/// @returns The current value of the IRQ line output.
|
||||||
@@ -73,11 +73,16 @@ class WD1770: public Storage::Disk::MFMController {
|
|||||||
};
|
};
|
||||||
inline void set_delegate(Delegate *delegate) { delegate_ = delegate; }
|
inline void set_delegate(Delegate *delegate) { delegate_ = delegate; }
|
||||||
|
|
||||||
|
ClockingHint::Preference preferred_clocking() final;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void set_head_load_request(bool head_load);
|
virtual void set_head_load_request(bool head_load);
|
||||||
virtual void set_motor_on(bool motor_on);
|
virtual void set_motor_on(bool motor_on);
|
||||||
void set_head_loaded(bool head_loaded);
|
void set_head_loaded(bool head_loaded);
|
||||||
|
|
||||||
|
/// @returns The last value posted to @c set_head_loaded.
|
||||||
|
bool get_head_loaded();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Personality personality_;
|
Personality personality_;
|
||||||
inline bool has_motor_on_line() { return (personality_ != P1793 ) && (personality_ != P1773); }
|
inline bool has_motor_on_line() { return (personality_ != P1793 ) && (personality_ != P1773); }
|
||||||
@@ -94,6 +99,7 @@ class WD1770: public Storage::Disk::MFMController {
|
|||||||
bool data_request = false;
|
bool data_request = false;
|
||||||
bool interrupt_request = false;
|
bool interrupt_request = false;
|
||||||
bool busy = false;
|
bool busy = false;
|
||||||
|
bool track_zero = false;
|
||||||
enum {
|
enum {
|
||||||
One, Two, Three
|
One, Two, Three
|
||||||
} type = One;
|
} type = One;
|
||||||
@@ -121,7 +127,7 @@ class WD1770: public Storage::Disk::MFMController {
|
|||||||
void posit_event(int type);
|
void posit_event(int type);
|
||||||
int interesting_event_mask_;
|
int interesting_event_mask_;
|
||||||
int resume_point_ = 0;
|
int resume_point_ = 0;
|
||||||
unsigned int delay_time_ = 0;
|
Cycles::IntType delay_time_ = 0;
|
||||||
|
|
||||||
// ID buffer
|
// ID buffer
|
||||||
uint8_t header_[6];
|
uint8_t header_[6];
|
||||||
|
|||||||
312
Components/5380/ncr5380.cpp
Normal file
312
Components/5380/ncr5380.cpp
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
//
|
||||||
|
// ncr5380.cpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 10/08/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "ncr5380.hpp"
|
||||||
|
|
||||||
|
#include "../../Outputs/Log.hpp"
|
||||||
|
|
||||||
|
using namespace NCR::NCR5380;
|
||||||
|
using SCSI::Line;
|
||||||
|
|
||||||
|
NCR5380::NCR5380(SCSI::Bus &bus, int clock_rate) :
|
||||||
|
bus_(bus),
|
||||||
|
clock_rate_(clock_rate) {
|
||||||
|
device_id_ = bus_.add_device();
|
||||||
|
bus_.add_observer(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NCR5380::write(int address, uint8_t value, bool dma_acknowledge) {
|
||||||
|
switch(address & 7) {
|
||||||
|
case 0:
|
||||||
|
// LOG("[SCSI 0] Set current SCSI bus state to " << PADHEX(2) << int(value));
|
||||||
|
data_bus_ = value;
|
||||||
|
|
||||||
|
if(dma_request_ && dma_operation_ == DMAOperation::Send) {
|
||||||
|
// printf("w %02x\n", value);
|
||||||
|
dma_acknowledge_ = true;
|
||||||
|
dma_request_ = false;
|
||||||
|
update_control_output();
|
||||||
|
bus_.set_device_output(device_id_, bus_output_);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: {
|
||||||
|
// LOG("[SCSI 1] Initiator command register set: " << PADHEX(2) << int(value));
|
||||||
|
initiator_command_ = value;
|
||||||
|
|
||||||
|
bus_output_ &= ~(Line::Reset | Line::Acknowledge | Line::Busy | Line::SelectTarget | Line::Attention);
|
||||||
|
if(value & 0x80) bus_output_ |= Line::Reset;
|
||||||
|
if(value & 0x08) bus_output_ |= Line::Busy;
|
||||||
|
if(value & 0x04) bus_output_ |= Line::SelectTarget;
|
||||||
|
|
||||||
|
/* bit 5 = differential enable if this were a 5381 */
|
||||||
|
|
||||||
|
test_mode_ = value & 0x40;
|
||||||
|
assert_data_bus_ = value & 0x01;
|
||||||
|
update_control_output();
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// LOG("[SCSI 2] Set mode: " << PADHEX(2) << int(value));
|
||||||
|
mode_ = value;
|
||||||
|
|
||||||
|
// bit 7: 1 = use block mode DMA mode (if DMA mode is also enabled)
|
||||||
|
// bit 6: 1 = be a SCSI target; 0 = be an initiator
|
||||||
|
// bit 5: 1 = check parity
|
||||||
|
// bit 4: 1 = generate an interrupt if parity checking is enabled and an error is found
|
||||||
|
// bit 3: 1 = generate an interrupt when an EOP is received from the DMA controller
|
||||||
|
// bit 2: 1 = generate an interrupt and reset low 6 bits of register 1 if an unexpected loss of Line::Busy occurs
|
||||||
|
// bit 1: 1 = use DMA mode
|
||||||
|
// bit 0: 1 = begin arbitration mode (device ID should be in register 0)
|
||||||
|
arbitration_in_progress_ = false;
|
||||||
|
switch(mode_ & 0x3) {
|
||||||
|
case 0x0:
|
||||||
|
bus_output_ &= ~SCSI::Line::Busy;
|
||||||
|
dma_request_ = false;
|
||||||
|
set_execution_state(ExecutionState::None);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x1:
|
||||||
|
arbitration_in_progress_ = true;
|
||||||
|
set_execution_state(ExecutionState::WaitingForBusy);
|
||||||
|
lost_arbitration_ = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
assert_data_bus_ = false;
|
||||||
|
set_execution_state(ExecutionState::PerformingDMA);
|
||||||
|
bus_.update_observers();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
update_control_output();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3: {
|
||||||
|
// LOG("[SCSI 3] Set target command: " << PADHEX(2) << int(value));
|
||||||
|
target_command_ = value;
|
||||||
|
update_control_output();
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
// LOG("[SCSI 4] Set select enabled: " << PADHEX(2) << int(value));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
// LOG("[SCSI 5] Start DMA send: " << PADHEX(2) << int(value));
|
||||||
|
dma_operation_ = DMAOperation::Send;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
// LOG("[SCSI 6] Start DMA target receive: " << PADHEX(2) << int(value));
|
||||||
|
dma_operation_ = DMAOperation::TargetReceive;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7:
|
||||||
|
// LOG("[SCSI 7] Start DMA initiator receive: " << PADHEX(2) << int(value));
|
||||||
|
dma_operation_ = DMAOperation::InitiatorReceive;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data is output only if the data bus is asserted.
|
||||||
|
if(assert_data_bus_) {
|
||||||
|
bus_output_ = (bus_output_ & ~SCSI::Line::Data) | data_bus_;
|
||||||
|
} else {
|
||||||
|
bus_output_ &= ~SCSI::Line::Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In test mode, still nothing is output. Otherwise throw out
|
||||||
|
// the current value of bus_output_.
|
||||||
|
if(test_mode_) {
|
||||||
|
bus_.set_device_output(device_id_, SCSI::DefaultBusState);
|
||||||
|
} else {
|
||||||
|
bus_.set_device_output(device_id_, bus_output_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t NCR5380::read(int address, bool dma_acknowledge) {
|
||||||
|
switch(address & 7) {
|
||||||
|
case 0:
|
||||||
|
// LOG("[SCSI 0] Get current SCSI bus state: " << PADHEX(2) << (bus_.get_state() & 0xff));
|
||||||
|
|
||||||
|
if(dma_request_ && dma_operation_ == DMAOperation::InitiatorReceive) {
|
||||||
|
dma_acknowledge_ = true;
|
||||||
|
dma_request_ = false;
|
||||||
|
update_control_output();
|
||||||
|
bus_.set_device_output(device_id_, bus_output_);
|
||||||
|
}
|
||||||
|
return uint8_t(bus_.get_state());
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
// LOG("[SCSI 1] Initiator command register get: " << (arbitration_in_progress_ ? 'p' : '-') << (lost_arbitration_ ? 'l' : '-'));
|
||||||
|
return
|
||||||
|
// Bits repeated as they were set.
|
||||||
|
(initiator_command_ & ~0x60) |
|
||||||
|
|
||||||
|
// Arbitration in progress.
|
||||||
|
(arbitration_in_progress_ ? 0x40 : 0x00) |
|
||||||
|
|
||||||
|
// Lost arbitration.
|
||||||
|
(lost_arbitration_ ? 0x20 : 0x00);
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// LOG("[SCSI 2] Get mode");
|
||||||
|
return mode_;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
// LOG("[SCSI 3] Get target command");
|
||||||
|
return target_command_;
|
||||||
|
|
||||||
|
case 4: {
|
||||||
|
const auto bus_state = bus_.get_state();
|
||||||
|
const uint8_t result =
|
||||||
|
((bus_state & Line::Reset) ? 0x80 : 0x00) |
|
||||||
|
((bus_state & Line::Busy) ? 0x40 : 0x00) |
|
||||||
|
((bus_state & Line::Request) ? 0x20 : 0x00) |
|
||||||
|
((bus_state & Line::Message) ? 0x10 : 0x00) |
|
||||||
|
((bus_state & Line::Control) ? 0x08 : 0x00) |
|
||||||
|
((bus_state & Line::Input) ? 0x04 : 0x00) |
|
||||||
|
((bus_state & Line::SelectTarget) ? 0x02 : 0x00) |
|
||||||
|
((bus_state & Line::Parity) ? 0x01 : 0x00);
|
||||||
|
// LOG("[SCSI 4] Get current bus state: " << PADHEX(2) << int(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 5: {
|
||||||
|
const auto bus_state = bus_.get_state();
|
||||||
|
const bool phase_matches =
|
||||||
|
(target_output() & (Line::Message | Line::Control | Line::Input)) ==
|
||||||
|
(bus_state & (Line::Message | Line::Control | Line::Input));
|
||||||
|
|
||||||
|
const uint8_t result =
|
||||||
|
/* b7 = end of DMA */
|
||||||
|
((dma_request_ && state_ == ExecutionState::PerformingDMA) ? 0x40 : 0x00) |
|
||||||
|
/* b5 = parity error */
|
||||||
|
/* b4 = IRQ active */
|
||||||
|
(phase_matches ? 0x08 : 0x00) |
|
||||||
|
/* b2 = busy error */
|
||||||
|
((bus_state & Line::Attention) ? 0x02 : 0x00) |
|
||||||
|
((bus_state & Line::Acknowledge) ? 0x01 : 0x00);
|
||||||
|
// LOG("[SCSI 5] Get bus and status: " << PADHEX(2) << int(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
// LOG("[SCSI 6] Get input data");
|
||||||
|
return 0xff;
|
||||||
|
|
||||||
|
case 7:
|
||||||
|
// LOG("[SCSI 7] Reset parity/interrupt");
|
||||||
|
return 0xff;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SCSI::BusState NCR5380::target_output() {
|
||||||
|
SCSI::BusState output = SCSI::DefaultBusState;
|
||||||
|
if(target_command_ & 0x08) output |= Line::Request;
|
||||||
|
if(target_command_ & 0x04) output |= Line::Message;
|
||||||
|
if(target_command_ & 0x02) output |= Line::Control;
|
||||||
|
if(target_command_ & 0x01) output |= Line::Input;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NCR5380::update_control_output() {
|
||||||
|
bus_output_ &= ~(Line::Request | Line::Message | Line::Control | Line::Input | Line::Acknowledge | Line::Attention);
|
||||||
|
if(mode_ & 0x40) {
|
||||||
|
// This is a target; C/D, I/O, /MSG and /REQ are signalled on the bus.
|
||||||
|
bus_output_ |= target_output();
|
||||||
|
} else {
|
||||||
|
// This is an initiator; /ATN and /ACK are signalled on the bus.
|
||||||
|
if(
|
||||||
|
(initiator_command_ & 0x10) ||
|
||||||
|
(state_ == ExecutionState::PerformingDMA && dma_acknowledge_)
|
||||||
|
) bus_output_ |= Line::Acknowledge;
|
||||||
|
if(initiator_command_ & 0x02) bus_output_ |= Line::Attention;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NCR5380::scsi_bus_did_change(SCSI::Bus *, SCSI::BusState new_state, double time_since_change) {
|
||||||
|
switch(state_) {
|
||||||
|
default: break;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Official documentation:
|
||||||
|
|
||||||
|
Arbitration is accomplished using a bus-free filter to continuously monitor BSY.
|
||||||
|
If BSY remains inactive for at least 400 nsec then the SCSI bus is considered free
|
||||||
|
and arbitration may begin. Arbitration will begin if the bus is free, SEL is inactive
|
||||||
|
and the ARBITRATION bit (port 2, bit 0) is active. Once arbitration has begun
|
||||||
|
(BSY asserted), an arbitration delay of 2.2 /Lsec must elapse before the data bus
|
||||||
|
can be examined to deter- mine if arbitration has been won. This delay must be
|
||||||
|
implemented in the controlling software driver.
|
||||||
|
|
||||||
|
Personal notes:
|
||||||
|
|
||||||
|
I'm discounting that "arbitratation is accomplished" opening, and assuming that what needs
|
||||||
|
to happen is:
|
||||||
|
|
||||||
|
(i) wait for BSY to be inactive;
|
||||||
|
(ii) count 400 nsec;
|
||||||
|
(iii) check that BSY and SEL are inactive.
|
||||||
|
*/
|
||||||
|
|
||||||
|
case ExecutionState::WaitingForBusy:
|
||||||
|
if(!(new_state & SCSI::Line::Busy) || time_since_change < SCSI::DeskewDelay) return;
|
||||||
|
state_ = ExecutionState::WatchingBusy;
|
||||||
|
|
||||||
|
case ExecutionState::WatchingBusy:
|
||||||
|
if(!(new_state & SCSI::Line::Busy)) {
|
||||||
|
lost_arbitration_ = true;
|
||||||
|
set_execution_state(ExecutionState::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for having hit 400ns (more or less) since BSY was inactive.
|
||||||
|
if(time_since_change >= SCSI::BusSettleDelay) {
|
||||||
|
// arbitration_in_progress_ = false;
|
||||||
|
if(new_state & SCSI::Line::SelectTarget) {
|
||||||
|
lost_arbitration_ = true;
|
||||||
|
set_execution_state(ExecutionState::None);
|
||||||
|
} else {
|
||||||
|
bus_output_ &= ~SCSI::Line::Busy;
|
||||||
|
set_execution_state(ExecutionState::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: there's a bug here, given that the dropping of Busy isn't communicated onward. */
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ExecutionState::PerformingDMA:
|
||||||
|
if(time_since_change < SCSI::DeskewDelay) return;
|
||||||
|
|
||||||
|
// Signal a DMA request if the request line is active, i.e. meaningful data is
|
||||||
|
// on the bus, and this device hasn't yet acknowledged it.
|
||||||
|
switch(new_state & (SCSI::Line::Request | SCSI::Line::Acknowledge)) {
|
||||||
|
case 0:
|
||||||
|
dma_request_ = false;
|
||||||
|
break;
|
||||||
|
case SCSI::Line::Request:
|
||||||
|
dma_request_ = true;
|
||||||
|
break;
|
||||||
|
case SCSI::Line::Request | SCSI::Line::Acknowledge:
|
||||||
|
dma_request_ = false;
|
||||||
|
break;
|
||||||
|
case SCSI::Line::Acknowledge:
|
||||||
|
dma_acknowledge_ = false;
|
||||||
|
dma_request_ = false;
|
||||||
|
update_control_output();
|
||||||
|
bus_.set_device_output(device_id_, bus_output_);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NCR5380::set_execution_state(ExecutionState state) {
|
||||||
|
state_ = state;
|
||||||
|
if(state != ExecutionState::PerformingDMA) dma_operation_ = DMAOperation::Ready;
|
||||||
|
}
|
||||||
75
Components/5380/ncr5380.hpp
Normal file
75
Components/5380/ncr5380.hpp
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
//
|
||||||
|
// ncr5380.hpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 10/08/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef ncr5380_hpp
|
||||||
|
#define ncr5380_hpp
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "../../Storage/MassStorage/SCSI/SCSI.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
namespace NCR {
|
||||||
|
namespace NCR5380 {
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Models the NCR 5380, a SCSI interface chip.
|
||||||
|
*/
|
||||||
|
class NCR5380 final: public SCSI::Bus::Observer {
|
||||||
|
public:
|
||||||
|
NCR5380(SCSI::Bus &bus, int clock_rate);
|
||||||
|
|
||||||
|
/*! Writes @c value to @c address. */
|
||||||
|
void write(int address, uint8_t value, bool dma_acknowledge = false);
|
||||||
|
|
||||||
|
/*! Reads from @c address. */
|
||||||
|
uint8_t read(int address, bool dma_acknowledge = false);
|
||||||
|
|
||||||
|
private:
|
||||||
|
SCSI::Bus &bus_;
|
||||||
|
|
||||||
|
const int clock_rate_;
|
||||||
|
size_t device_id_;
|
||||||
|
|
||||||
|
SCSI::BusState bus_output_ = SCSI::DefaultBusState;
|
||||||
|
SCSI::BusState expected_phase_ = SCSI::DefaultBusState;
|
||||||
|
uint8_t mode_ = 0xff;
|
||||||
|
uint8_t initiator_command_ = 0xff;
|
||||||
|
uint8_t data_bus_ = 0xff;
|
||||||
|
uint8_t target_command_ = 0xff;
|
||||||
|
bool test_mode_ = false;
|
||||||
|
bool assert_data_bus_ = false;
|
||||||
|
bool dma_request_ = false;
|
||||||
|
bool dma_acknowledge_ = false;
|
||||||
|
|
||||||
|
enum class ExecutionState {
|
||||||
|
None,
|
||||||
|
WaitingForBusy,
|
||||||
|
WatchingBusy,
|
||||||
|
PerformingDMA,
|
||||||
|
} state_ = ExecutionState::None;
|
||||||
|
enum class DMAOperation {
|
||||||
|
Ready,
|
||||||
|
Send,
|
||||||
|
TargetReceive,
|
||||||
|
InitiatorReceive
|
||||||
|
} dma_operation_ = DMAOperation::Ready;
|
||||||
|
bool lost_arbitration_ = false, arbitration_in_progress_ = false;
|
||||||
|
|
||||||
|
void set_execution_state(ExecutionState state);
|
||||||
|
|
||||||
|
SCSI::BusState target_output();
|
||||||
|
void update_control_output();
|
||||||
|
|
||||||
|
void scsi_bus_did_change(SCSI::Bus *, SCSI::BusState new_state, double time_since_change) final;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* ncr5380_hpp */
|
||||||
@@ -94,10 +94,10 @@ template <class T> class MOS6522: public MOS6522Storage {
|
|||||||
MOS6522(const MOS6522 &) = delete;
|
MOS6522(const MOS6522 &) = delete;
|
||||||
|
|
||||||
/*! Sets a register value. */
|
/*! Sets a register value. */
|
||||||
void set_register(int address, uint8_t value);
|
void write(int address, uint8_t value);
|
||||||
|
|
||||||
/*! Gets a register value. */
|
/*! Gets a register value. */
|
||||||
uint8_t get_register(int address);
|
uint8_t read(int address);
|
||||||
|
|
||||||
/*! @returns the bus handler. */
|
/*! @returns the bus handler. */
|
||||||
T &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;
|
address &= 0xf;
|
||||||
access(address);
|
access(address);
|
||||||
switch(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;
|
address &= 0xf;
|
||||||
access(address);
|
access(address);
|
||||||
switch(address) {
|
switch(address) {
|
||||||
@@ -346,7 +346,7 @@ template <typename T> void MOS6522<T>::do_phase1() {
|
|||||||
|
|
||||||
/*! Runs for a specified number of half cycles. */
|
/*! Runs for a specified number of half cycles. */
|
||||||
template <typename T> void MOS6522<T>::run_for(const HalfCycles half_cycles) {
|
template <typename T> void MOS6522<T>::run_for(const HalfCycles half_cycles) {
|
||||||
int number_of_half_cycles = half_cycles.as_int();
|
auto number_of_half_cycles = half_cycles.as_integral();
|
||||||
if(!number_of_half_cycles) return;
|
if(!number_of_half_cycles) return;
|
||||||
|
|
||||||
if(is_phase2_) {
|
if(is_phase2_) {
|
||||||
@@ -375,7 +375,7 @@ template <typename T> void MOS6522<T>::flush() {
|
|||||||
|
|
||||||
/*! Runs for a specified number of cycles. */
|
/*! Runs for a specified number of cycles. */
|
||||||
template <typename T> void MOS6522<T>::run_for(const Cycles cycles) {
|
template <typename T> void MOS6522<T>::run_for(const Cycles cycles) {
|
||||||
int number_of_cycles = cycles.as_int();
|
auto number_of_cycles = cycles.as_integral();
|
||||||
while(number_of_cycles--) {
|
while(number_of_cycles--) {
|
||||||
do_phase1();
|
do_phase1();
|
||||||
do_phase2();
|
do_phase2();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ template <class T> class MOS6532 {
|
|||||||
inline void set_ram(uint16_t address, uint8_t value) { ram_[address&0x7f] = value; }
|
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 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;
|
const uint8_t decodedAddress = address & 0x07;
|
||||||
switch(decodedAddress) {
|
switch(decodedAddress) {
|
||||||
// Port output
|
// 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;
|
const uint8_t decodedAddress = address & 0x7;
|
||||||
switch(decodedAddress) {
|
switch(decodedAddress) {
|
||||||
// Port input
|
// Port input
|
||||||
@@ -107,7 +107,7 @@ template <class T> class MOS6532 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void run_for(const Cycles cycles) {
|
inline void run_for(const Cycles cycles) {
|
||||||
unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_int());
|
unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_integral());
|
||||||
|
|
||||||
// permit counting _to_ zero; counting _through_ zero initiates the other behaviour
|
// permit counting _to_ zero; counting _through_ zero initiates the other behaviour
|
||||||
if(timer_.value >= number_of_cycles) {
|
if(timer_.value >= number_of_cycles) {
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ AudioGenerator::AudioGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue
|
|||||||
|
|
||||||
|
|
||||||
void AudioGenerator::set_volume(uint8_t volume) {
|
void AudioGenerator::set_volume(uint8_t volume) {
|
||||||
audio_queue_.defer([=]() {
|
audio_queue_.defer([this, volume]() {
|
||||||
volume_ = static_cast<int16_t>(volume) * range_multiplier_;
|
volume_ = int16_t(volume) * range_multiplier_;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioGenerator::set_control(int channel, uint8_t value) {
|
void AudioGenerator::set_control(int channel, uint8_t value) {
|
||||||
audio_queue_.defer([=]() {
|
audio_queue_.defer([this, channel, value]() {
|
||||||
control_registers_[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 get_samples(std::size_t number_of_samples, int16_t *target);
|
||||||
void skip_samples(std::size_t number_of_samples);
|
void skip_samples(std::size_t number_of_samples);
|
||||||
void set_sample_volume_range(std::int16_t range);
|
void set_sample_volume_range(std::int16_t range);
|
||||||
|
static constexpr bool get_is_stereo() { return false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
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
|
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.
|
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 {
|
template <class BusHandler> class MOS6560 {
|
||||||
public:
|
public:
|
||||||
@@ -84,6 +85,7 @@ template <class BusHandler> class MOS6560 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
|
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); }
|
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
|
||||||
Outputs::Speaker::Speaker *get_speaker() { return &speaker_; }
|
Outputs::Speaker::Speaker *get_speaker() { return &speaker_; }
|
||||||
|
|
||||||
@@ -170,7 +172,7 @@ template <class BusHandler> class MOS6560 {
|
|||||||
// keep track of the amount of time since the speaker was updated; lazy updates are applied
|
// keep track of the amount of time since the speaker was updated; lazy updates are applied
|
||||||
cycles_since_speaker_update_ += cycles;
|
cycles_since_speaker_update_ += cycles;
|
||||||
|
|
||||||
int number_of_cycles = cycles.as_int();
|
auto number_of_cycles = cycles.as_integral();
|
||||||
while(number_of_cycles--) {
|
while(number_of_cycles--) {
|
||||||
// keep an old copy of the vertical count because that test is a cycle later than the actual changes
|
// keep an old copy of the vertical count because that test is a cycle later than the actual changes
|
||||||
int previous_vertical_counter = vertical_counter_;
|
int previous_vertical_counter = vertical_counter_;
|
||||||
@@ -353,7 +355,7 @@ template <class BusHandler> class MOS6560 {
|
|||||||
/*!
|
/*!
|
||||||
Writes to a 6560 register.
|
Writes to a 6560 register.
|
||||||
*/
|
*/
|
||||||
void set_register(int address, uint8_t value) {
|
void write(int address, uint8_t value) {
|
||||||
address &= 0xf;
|
address &= 0xf;
|
||||||
registers_.direct_values[address] = value;
|
registers_.direct_values[address] = value;
|
||||||
switch(address) {
|
switch(address) {
|
||||||
@@ -417,7 +419,7 @@ template <class BusHandler> class MOS6560 {
|
|||||||
/*
|
/*
|
||||||
Reads from a 6560 register.
|
Reads from a 6560 register.
|
||||||
*/
|
*/
|
||||||
uint8_t get_register(int address) {
|
uint8_t read(int address) {
|
||||||
address &= 0xf;
|
address &= 0xf;
|
||||||
switch(address) {
|
switch(address) {
|
||||||
default: return registers_.direct_values[address];
|
default: return registers_.direct_values[address];
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ template <class T> class CRTC6845 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void run_for(Cycles cycles) {
|
void run_for(Cycles cycles) {
|
||||||
int cyles_remaining = cycles.as_int();
|
auto cyles_remaining = cycles.as_integral();
|
||||||
while(cyles_remaining--) {
|
while(cyles_remaining--) {
|
||||||
// check for end of visible characters
|
// check for end of visible characters
|
||||||
if(character_counter_ == registers_[1]) {
|
if(character_counter_ == registers_[1]) {
|
||||||
|
|||||||
228
Components/6850/6850.cpp
Normal file
228
Components/6850/6850.cpp
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
//
|
||||||
|
// 6850.cpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 10/10/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "6850.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
using namespace Motorola::ACIA;
|
||||||
|
|
||||||
|
const HalfCycles ACIA::SameAsTransmit;
|
||||||
|
|
||||||
|
ACIA::ACIA(HalfCycles transmit_clock_rate, HalfCycles receive_clock_rate) :
|
||||||
|
transmit_clock_rate_(transmit_clock_rate),
|
||||||
|
receive_clock_rate_((receive_clock_rate != SameAsTransmit) ? receive_clock_rate : transmit_clock_rate) {
|
||||||
|
transmit.set_writer_clock_rate(transmit_clock_rate);
|
||||||
|
request_to_send.set_writer_clock_rate(transmit_clock_rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t ACIA::read(int address) {
|
||||||
|
if(address&1) {
|
||||||
|
overran_ = false;
|
||||||
|
received_data_ |= NoValueMask;
|
||||||
|
update_interrupt_line();
|
||||||
|
return uint8_t(received_data_);
|
||||||
|
} else {
|
||||||
|
return get_status();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
consider_transmission();
|
||||||
|
update_interrupt_line();
|
||||||
|
} else {
|
||||||
|
if((value&3) == 3) {
|
||||||
|
reset();
|
||||||
|
} else {
|
||||||
|
switch(value & 3) {
|
||||||
|
default:
|
||||||
|
case 0: divider_ = 1; break;
|
||||||
|
case 1: divider_ = 16; break;
|
||||||
|
case 2: divider_ = 64; break;
|
||||||
|
}
|
||||||
|
switch((value >> 2) & 7) {
|
||||||
|
default:
|
||||||
|
case 0: data_bits_ = 7; stop_bits_ = 2; parity_ = Parity::Even; break;
|
||||||
|
case 1: data_bits_ = 7; stop_bits_ = 2; parity_ = Parity::Odd; break;
|
||||||
|
case 2: data_bits_ = 7; stop_bits_ = 1; parity_ = Parity::Even; break;
|
||||||
|
case 3: data_bits_ = 7; stop_bits_ = 1; parity_ = Parity::Odd; break;
|
||||||
|
case 4: data_bits_ = 8; stop_bits_ = 2; parity_ = Parity::None; break;
|
||||||
|
case 5: data_bits_ = 8; stop_bits_ = 1; parity_ = Parity::None; break;
|
||||||
|
case 6: data_bits_ = 8; stop_bits_ = 1; parity_ = Parity::Even; break;
|
||||||
|
case 7: data_bits_ = 8; stop_bits_ = 1; parity_ = Parity::Odd; break;
|
||||||
|
}
|
||||||
|
switch((value >> 5) & 3) {
|
||||||
|
case 0: request_to_send.write(false); transmit_interrupt_enabled_ = false; break;
|
||||||
|
case 1: request_to_send.write(false); transmit_interrupt_enabled_ = true; break;
|
||||||
|
case 2: request_to_send.write(true); transmit_interrupt_enabled_ = false; break;
|
||||||
|
case 3:
|
||||||
|
request_to_send.write(false);
|
||||||
|
transmit_interrupt_enabled_ = false;
|
||||||
|
transmit.reset_writing();
|
||||||
|
transmit.write(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
receive.set_read_delegate(this, Storage::Time(divider_ * 2, int(receive_clock_rate_.as_integral())));
|
||||||
|
receive_interrupt_enabled_ = value & 0x80;
|
||||||
|
|
||||||
|
update_interrupt_line();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_clocking_observer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACIA::consider_transmission() {
|
||||||
|
if(next_transmission_ != NoValueMask && !transmit.write_data_time_remaining()) {
|
||||||
|
// Establish start bit and [7 or 8] data bits.
|
||||||
|
if(data_bits_ == 7) next_transmission_ &= 0x7f;
|
||||||
|
int transmission = next_transmission_ << 1;
|
||||||
|
|
||||||
|
// Add a parity bit, if any.
|
||||||
|
int mask = 0x2 << data_bits_;
|
||||||
|
if(parity_ != Parity::None) {
|
||||||
|
transmission |= parity(uint8_t(next_transmission_)) ? mask : 0;
|
||||||
|
mask <<= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add stop bits.
|
||||||
|
for(int c = 0; c < stop_bits_; ++c) {
|
||||||
|
transmission |= mask;
|
||||||
|
mask <<= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output all that.
|
||||||
|
const int total_bits = expected_bits();
|
||||||
|
transmit.write(divider_ * 2, total_bits, transmission);
|
||||||
|
|
||||||
|
// Mark the transmit register as empty again.
|
||||||
|
next_transmission_ = NoValueMask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClockingHint::Preference ACIA::preferred_clocking() {
|
||||||
|
// Real-time clocking is required if a transmission is ongoing; this is a courtesy for whomever
|
||||||
|
// is on the receiving end.
|
||||||
|
if(transmit.transmission_data_time_remaining() > 0) return ClockingHint::Preference::RealTime;
|
||||||
|
|
||||||
|
// If a bit reception is ongoing that might lead to an interrupt, ask for real-time clocking
|
||||||
|
// because it's unclear when the interrupt might come.
|
||||||
|
if(bits_incoming_ && receive_interrupt_enabled_) return ClockingHint::Preference::RealTime;
|
||||||
|
|
||||||
|
// No clocking required then.
|
||||||
|
return ClockingHint::Preference::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ACIA::get_interrupt_line() const {
|
||||||
|
return interrupt_line_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ACIA::expected_bits() {
|
||||||
|
return 1 + data_bits_ + stop_bits_ + (parity_ != Parity::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t ACIA::parity(uint8_t value) {
|
||||||
|
value ^= value >> 4;
|
||||||
|
value ^= value >> 2;
|
||||||
|
value ^= value >> 1;
|
||||||
|
return value ^ (parity_ == Parity::Even);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ACIA::serial_line_did_produce_bit(Serial::Line *line, int bit) {
|
||||||
|
// Shift this bit into the 11-bit input register; this is big enough to hold
|
||||||
|
// the largest transmission symbol.
|
||||||
|
++bits_received_;
|
||||||
|
bits_incoming_ = (bits_incoming_ >> 1) | (bit << 10);
|
||||||
|
|
||||||
|
// If that's the now-expected number of bits, update.
|
||||||
|
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();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: overrun, and parity.
|
||||||
|
|
||||||
|
// Keep receiving, and consider a potential clocking change.
|
||||||
|
if(bits_received_ == 1) update_clocking_observer();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACIA::set_interrupt_delegate(InterruptDelegate *delegate) {
|
||||||
|
interrupt_delegate_ = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ACIA::update_interrupt_line() {
|
||||||
|
const bool old_line = interrupt_line_;
|
||||||
|
|
||||||
|
/*
|
||||||
|
"Bit 7 of the control register is the rie bit. When the rie bit is high, the rdrf, ndcd,
|
||||||
|
and ovr bits will assert the nirq output. When the rie bit is low, nirq generation is disabled."
|
||||||
|
|
||||||
|
rie = read interrupt enable
|
||||||
|
rdrf = receive data register full (status word bit 0)
|
||||||
|
ndcd = data carrier detect (status word bit 2)
|
||||||
|
over = receiver overrun (status word bit 5)
|
||||||
|
|
||||||
|
"Bit 1 of the status register is the tdre bit. When high, the tdre bit indicates that data has been
|
||||||
|
transferred from the transmitter data register to the output shift register. At this point, the a6850
|
||||||
|
is ready to accept a new transmit data byte. However, if the ncts signal is high, the tdre bit remains
|
||||||
|
low regardless of the status of the transmitter data register. Also, if transmit interrupt is enabled,
|
||||||
|
the nirq output is asserted."
|
||||||
|
|
||||||
|
tdre = transmitter data register empty
|
||||||
|
ncts = clear to send
|
||||||
|
*/
|
||||||
|
const auto status = get_status();
|
||||||
|
interrupt_line_ =
|
||||||
|
(receive_interrupt_enabled_ && (status & 0x25)) ||
|
||||||
|
(transmit_interrupt_enabled_ && (status & 0x02));
|
||||||
|
|
||||||
|
if(interrupt_delegate_ && old_line != interrupt_line_) {
|
||||||
|
interrupt_delegate_->acia6850_did_change_interrupt_status(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t ACIA::get_status() {
|
||||||
|
return
|
||||||
|
((received_data_ & NoValueMask) ? 0x00 : 0x01) |
|
||||||
|
((next_transmission_ == NoValueMask) ? 0x02 : 0x00) |
|
||||||
|
// (data_carrier_detect.read() ? 0x04 : 0x00) |
|
||||||
|
// (clear_to_send.read() ? 0x08 : 0x00) |
|
||||||
|
(overran_ ? 0x20 : 0x00) |
|
||||||
|
(interrupt_line_ ? 0x80 : 0x00)
|
||||||
|
;
|
||||||
|
|
||||||
|
// b0: receive data full.
|
||||||
|
// b1: transmit data empty.
|
||||||
|
// b2: DCD.
|
||||||
|
// b3: CTS.
|
||||||
|
// b4: framing error (i.e. no first stop bit where expected).
|
||||||
|
// b5: receiver overran.
|
||||||
|
// b6: parity error.
|
||||||
|
// b7: IRQ state.
|
||||||
|
}
|
||||||
132
Components/6850/6850.hpp
Normal file
132
Components/6850/6850.hpp
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
//
|
||||||
|
// 6850.hpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 10/10/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef Motorola_ACIA_6850_hpp
|
||||||
|
#define Motorola_ACIA_6850_hpp
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||||
|
#include "../../ClockReceiver/ForceInline.hpp"
|
||||||
|
#include "../../ClockReceiver/ClockingHintSource.hpp"
|
||||||
|
#include "../Serial/Line.hpp"
|
||||||
|
|
||||||
|
namespace Motorola {
|
||||||
|
namespace ACIA {
|
||||||
|
|
||||||
|
class ACIA: public ClockingHint::Source, private Serial::Line::ReadDelegate {
|
||||||
|
public:
|
||||||
|
static constexpr const HalfCycles SameAsTransmit = HalfCycles(0);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Constructs a new instance of ACIA which will receive a transmission clock at a rate of
|
||||||
|
@c transmit_clock_rate, and a receive clock at a rate of @c receive_clock_rate.
|
||||||
|
*/
|
||||||
|
ACIA(HalfCycles transmit_clock_rate, HalfCycles receive_clock_rate = SameAsTransmit);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Reads from the ACIA.
|
||||||
|
|
||||||
|
Bit 0 of the address is used as the ACIA's register select line —
|
||||||
|
so even addresses select control/status registers, odd addresses
|
||||||
|
select transmit/receive data registers.
|
||||||
|
*/
|
||||||
|
uint8_t read(int address);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Writes to the ACIA.
|
||||||
|
|
||||||
|
Bit 0 of the address is used as the ACIA's register select line —
|
||||||
|
so even addresses select control/status registers, odd addresses
|
||||||
|
select transmit/receive data registers.
|
||||||
|
*/
|
||||||
|
void write(int address, uint8_t value);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Advances @c transmission_cycles in time, which should be
|
||||||
|
counted relative to the @c transmit_clock_rate.
|
||||||
|
*/
|
||||||
|
forceinline void run_for(HalfCycles transmission_cycles) {
|
||||||
|
if(transmit.transmission_data_time_remaining() > HalfCycles(0)) {
|
||||||
|
const auto write_data_time_remaining = transmit.write_data_time_remaining();
|
||||||
|
|
||||||
|
// There's at most one further byte available to enqueue, so a single 'if'
|
||||||
|
// rather than a 'while' is correct here. It's the responsibilit of the caller
|
||||||
|
// to ensure run_for lengths are appropriate for longer sequences.
|
||||||
|
if(transmission_cycles >= write_data_time_remaining) {
|
||||||
|
if(next_transmission_ != NoValueMask) {
|
||||||
|
transmit.advance_writer(write_data_time_remaining);
|
||||||
|
consider_transmission();
|
||||||
|
transmit.advance_writer(transmission_cycles - write_data_time_remaining);
|
||||||
|
} else {
|
||||||
|
transmit.advance_writer(transmission_cycles);
|
||||||
|
update_clocking_observer();
|
||||||
|
update_interrupt_line();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transmit.advance_writer(transmission_cycles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_interrupt_line() const;
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// Input lines.
|
||||||
|
Serial::Line receive;
|
||||||
|
Serial::Line clear_to_send;
|
||||||
|
Serial::Line data_carrier_detect;
|
||||||
|
|
||||||
|
// Output lines.
|
||||||
|
Serial::Line transmit;
|
||||||
|
Serial::Line request_to_send;
|
||||||
|
|
||||||
|
// ClockingHint::Source.
|
||||||
|
ClockingHint::Preference preferred_clocking() final;
|
||||||
|
|
||||||
|
struct InterruptDelegate {
|
||||||
|
virtual void acia6850_did_change_interrupt_status(ACIA *acia) = 0;
|
||||||
|
};
|
||||||
|
void set_interrupt_delegate(InterruptDelegate *delegate);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int divider_ = 1;
|
||||||
|
enum class Parity {
|
||||||
|
Even, Odd, None
|
||||||
|
} parity_ = Parity::None;
|
||||||
|
int data_bits_ = 7, stop_bits_ = 2;
|
||||||
|
|
||||||
|
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();
|
||||||
|
uint8_t parity(uint8_t value);
|
||||||
|
|
||||||
|
bool receive_interrupt_enabled_ = false;
|
||||||
|
bool transmit_interrupt_enabled_ = false;
|
||||||
|
|
||||||
|
HalfCycles transmit_clock_rate_;
|
||||||
|
HalfCycles receive_clock_rate_;
|
||||||
|
|
||||||
|
bool serial_line_did_produce_bit(Serial::Line *line, int bit) final;
|
||||||
|
|
||||||
|
bool interrupt_line_ = false;
|
||||||
|
void update_interrupt_line();
|
||||||
|
InterruptDelegate *interrupt_delegate_ = nullptr;
|
||||||
|
uint8_t get_status();
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* Motorola_ACIA_6850_hpp */
|
||||||
374
Components/68901/MFP68901.cpp
Normal file
374
Components/68901/MFP68901.cpp
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
//
|
||||||
|
// MFP68901.cpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 06/10/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "MFP68901.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
#define NDEBUG
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define LOG_PREFIX "[MFP] "
|
||||||
|
#include "../../Outputs/Log.hpp"
|
||||||
|
|
||||||
|
using namespace Motorola::MFP68901;
|
||||||
|
|
||||||
|
ClockingHint::Preference MFP68901::preferred_clocking() {
|
||||||
|
// Rule applied: if any timer is actively running and permitted to produce an
|
||||||
|
// interrupt, request real-time running.
|
||||||
|
return
|
||||||
|
(timers_[0].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerA) ||
|
||||||
|
(timers_[1].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerB) ||
|
||||||
|
(timers_[2].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerC) ||
|
||||||
|
(timers_[3].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerD)
|
||||||
|
? ClockingHint::Preference::RealTime : ClockingHint::Preference::JustInTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t MFP68901::read(int address) {
|
||||||
|
address &= 0x1f;
|
||||||
|
|
||||||
|
// Interrupt block: various bits of state can be read, all passively.
|
||||||
|
if(address >= 0x03 && address <= 0x0b) {
|
||||||
|
const int shift = (address&1) << 3;
|
||||||
|
switch(address) {
|
||||||
|
case 0x03: case 0x04: return uint8_t(interrupt_enable_ >> shift);
|
||||||
|
case 0x05: case 0x06: return uint8_t(interrupt_pending_ >> shift);
|
||||||
|
case 0x07: case 0x08: return uint8_t(interrupt_in_service_ >> shift);
|
||||||
|
case 0x09: case 0x0a: return uint8_t(interrupt_mask_ >> shift);
|
||||||
|
case 0x0b: return interrupt_vector_;
|
||||||
|
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(address) {
|
||||||
|
// GPIP block: input, and configured active edge and direction values.
|
||||||
|
case 0x00: return (gpip_input_ & ~gpip_direction_) | (gpip_output_ & gpip_direction_);
|
||||||
|
case 0x01: return gpip_active_edge_;
|
||||||
|
case 0x02: return gpip_direction_;
|
||||||
|
|
||||||
|
/* Interrupt block dealt with above. */
|
||||||
|
default: break;
|
||||||
|
|
||||||
|
// Timer block: read back A, B and C/D control, and read current timer values.
|
||||||
|
case 0x0c: case 0x0d: return timer_ab_control_[address - 0xc];
|
||||||
|
case 0x0e: return timer_cd_control_;
|
||||||
|
case 0x0f: case 0x10:
|
||||||
|
case 0x11: case 0x12: return get_timer_data(address - 0xf);
|
||||||
|
|
||||||
|
// USART block: TODO.
|
||||||
|
case 0x13: LOG("Read: sync character generator"); break;
|
||||||
|
case 0x14: LOG("Read: USART control"); break;
|
||||||
|
case 0x15: LOG("Read: receiver status"); break;
|
||||||
|
case 0x16: LOG("Read: transmitter status"); break;
|
||||||
|
case 0x17: LOG("Read: USART data"); break;
|
||||||
|
}
|
||||||
|
return 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MFP68901::write(int address, uint8_t value) {
|
||||||
|
address &= 0x1f;
|
||||||
|
|
||||||
|
// Interrupt block: enabled and masked interrupts can be set; pending and in-service interrupts can be masked.
|
||||||
|
if(address >= 0x03 && address <= 0x0b) {
|
||||||
|
const int shift = (address&1) << 3;
|
||||||
|
const int preserve = 0xff00 >> shift;
|
||||||
|
const int word_value = value << shift;
|
||||||
|
|
||||||
|
switch(address) {
|
||||||
|
default: break;
|
||||||
|
case 0x03: case 0x04: // Adjust enabled interrupts; disabled ones also cease to be pending.
|
||||||
|
interrupt_enable_ = (interrupt_enable_ & preserve) | word_value;
|
||||||
|
interrupt_pending_ &= interrupt_enable_;
|
||||||
|
break;
|
||||||
|
case 0x05: case 0x06: // Resolve pending interrupts.
|
||||||
|
interrupt_pending_ &= (preserve | word_value);
|
||||||
|
break;
|
||||||
|
case 0x07: case 0x08: // Resolve in-service interrupts.
|
||||||
|
interrupt_in_service_ &= (preserve | word_value);
|
||||||
|
break;
|
||||||
|
case 0x09: case 0x0a: // Adjust interrupt mask.
|
||||||
|
interrupt_mask_ = (interrupt_mask_ & preserve) | word_value;
|
||||||
|
break;
|
||||||
|
case 0x0b: // Set the interrupt vector, possibly changing end-of-interrupt mode.
|
||||||
|
interrupt_vector_ = value;
|
||||||
|
|
||||||
|
// If automatic end-of-interrupt mode has now been enabled, clear
|
||||||
|
// the in-process mask and re-evaluate.
|
||||||
|
if(interrupt_vector_ & 0x08) return;
|
||||||
|
interrupt_in_service_ = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whatever just happened may have affected the state of the interrupt line.
|
||||||
|
update_interrupts();
|
||||||
|
update_clocking_observer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int timer_prescales[] = {
|
||||||
|
1, 4, 10, 16, 50, 64, 100, 200
|
||||||
|
};
|
||||||
|
|
||||||
|
switch(address) {
|
||||||
|
// GPIP block: output and configuration of active edge and direction values.
|
||||||
|
case 0x00:
|
||||||
|
gpip_output_ = value;
|
||||||
|
break;
|
||||||
|
case 0x01:
|
||||||
|
gpip_active_edge_ = value;
|
||||||
|
reevaluate_gpip_interrupts();
|
||||||
|
break;
|
||||||
|
case 0x02:
|
||||||
|
gpip_direction_ = value;
|
||||||
|
reevaluate_gpip_interrupts();
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Interrupt block dealt with above. */
|
||||||
|
default: break;
|
||||||
|
|
||||||
|
// Timer block.
|
||||||
|
case 0x0c:
|
||||||
|
case 0x0d: {
|
||||||
|
const auto timer = address - 0xc;
|
||||||
|
const bool reset = value & 0x10;
|
||||||
|
timer_ab_control_[timer] = value;
|
||||||
|
switch(value & 0xf) {
|
||||||
|
case 0x0: set_timer_mode(timer, TimerMode::Stopped, 1, reset); break;
|
||||||
|
case 0x1: set_timer_mode(timer, TimerMode::Delay, 4, reset); break;
|
||||||
|
case 0x2: set_timer_mode(timer, TimerMode::Delay, 10, reset); break;
|
||||||
|
case 0x3: set_timer_mode(timer, TimerMode::Delay, 16, reset); break;
|
||||||
|
case 0x4: set_timer_mode(timer, TimerMode::Delay, 50, reset); break;
|
||||||
|
case 0x5: set_timer_mode(timer, TimerMode::Delay, 64, reset); break;
|
||||||
|
case 0x6: set_timer_mode(timer, TimerMode::Delay, 100, reset); break;
|
||||||
|
case 0x7: set_timer_mode(timer, TimerMode::Delay, 200, reset); break;
|
||||||
|
case 0x8: set_timer_mode(timer, TimerMode::EventCount, 1, reset); break;
|
||||||
|
case 0x9: set_timer_mode(timer, TimerMode::PulseWidth, 4, reset); break;
|
||||||
|
case 0xa: set_timer_mode(timer, TimerMode::PulseWidth, 10, reset); break;
|
||||||
|
case 0xb: set_timer_mode(timer, TimerMode::PulseWidth, 16, reset); break;
|
||||||
|
case 0xc: set_timer_mode(timer, TimerMode::PulseWidth, 50, reset); break;
|
||||||
|
case 0xd: set_timer_mode(timer, TimerMode::PulseWidth, 64, reset); break;
|
||||||
|
case 0xe: set_timer_mode(timer, TimerMode::PulseWidth, 100, reset); break;
|
||||||
|
case 0xf: set_timer_mode(timer, TimerMode::PulseWidth, 200, reset); break;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case 0x0e:
|
||||||
|
timer_cd_control_ = value;
|
||||||
|
set_timer_mode(3, (value & 7) ? TimerMode::Delay : TimerMode::Stopped, timer_prescales[value & 7], false);
|
||||||
|
set_timer_mode(2, ((value >> 4) & 7) ? TimerMode::Delay : TimerMode::Stopped, timer_prescales[(value >> 4) & 7], false);
|
||||||
|
break;
|
||||||
|
case 0x0f: case 0x10: case 0x11: case 0x12:
|
||||||
|
set_timer_data(address - 0xf, value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// USART block: TODO.
|
||||||
|
case 0x13: LOG("Write: sync character generator"); break;
|
||||||
|
case 0x14: LOG("Write: USART control"); break;
|
||||||
|
case 0x15: LOG("Write: receiver status"); break;
|
||||||
|
case 0x16: LOG("Write: transmitter status"); break;
|
||||||
|
case 0x17: LOG("Write: USART data"); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_clocking_observer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MFP68901::run_for(HalfCycles time) {
|
||||||
|
cycles_left_ += time;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HalfCycles MFP68901::get_next_sequence_point() {
|
||||||
|
return HalfCycles(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
if(reset_timer) {
|
||||||
|
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) {
|
||||||
|
if(timers_[timer].mode == TimerMode::Stopped) {
|
||||||
|
timers_[timer].value = value;
|
||||||
|
}
|
||||||
|
timers_[timer].reload_value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t MFP68901::get_timer_data(int timer) {
|
||||||
|
return timers_[timer].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == !!(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) {
|
||||||
|
while(amount--) {
|
||||||
|
--timers_[timer].value;
|
||||||
|
if(timers_[timer].value < 1) {
|
||||||
|
switch(timer) {
|
||||||
|
case 0: begin_interrupts(Interrupt::TimerA); break;
|
||||||
|
case 1: begin_interrupts(Interrupt::TimerB); break;
|
||||||
|
case 2: begin_interrupts(Interrupt::TimerC); break;
|
||||||
|
case 3: begin_interrupts(Interrupt::TimerD); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - GPIP
|
||||||
|
void MFP68901::set_port_input(uint8_t input) {
|
||||||
|
gpip_input_ = input;
|
||||||
|
reevaluate_gpip_interrupts();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t MFP68901::get_port_output() {
|
||||||
|
return 0xff; // TODO.
|
||||||
|
}
|
||||||
|
|
||||||
|
void MFP68901::reevaluate_gpip_interrupts() {
|
||||||
|
const uint8_t gpip_state = (gpip_input_ & ~gpip_direction_) ^ gpip_active_edge_;
|
||||||
|
|
||||||
|
// An interrupt is detected on any falling edge.
|
||||||
|
const uint8_t new_interrupt_mask = (gpip_state ^ gpip_interrupt_state_) & gpip_interrupt_state_;
|
||||||
|
if(new_interrupt_mask) {
|
||||||
|
begin_interrupts(
|
||||||
|
(new_interrupt_mask & 0x0f) |
|
||||||
|
((new_interrupt_mask & 0x30) << 2) |
|
||||||
|
((new_interrupt_mask & 0xc0) << 8)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
gpip_interrupt_state_ = gpip_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interrupts
|
||||||
|
|
||||||
|
void MFP68901::begin_interrupts(int interrupt) {
|
||||||
|
interrupt_pending_ |= interrupt & interrupt_enable_;
|
||||||
|
update_interrupts();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MFP68901::end_interrupts(int interrupt) {
|
||||||
|
interrupt_pending_ &= ~interrupt;
|
||||||
|
update_interrupts();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MFP68901::update_interrupts() {
|
||||||
|
const auto old_interrupt_line = interrupt_line_;
|
||||||
|
const auto firing_interrupts = interrupt_pending_ & interrupt_mask_;
|
||||||
|
|
||||||
|
if(!firing_interrupts) {
|
||||||
|
interrupt_line_ = false;
|
||||||
|
} else {
|
||||||
|
if(interrupt_vector_ & 0x8) {
|
||||||
|
// Software interrupt mode: permit only if neither this interrupt
|
||||||
|
// nor a higher interrupt is currently in service.
|
||||||
|
const int highest_bit = msb16(firing_interrupts);
|
||||||
|
interrupt_line_ = !(interrupt_in_service_ & ~(highest_bit + highest_bit - 1));
|
||||||
|
} else {
|
||||||
|
// Auto-interrupt mode; just signal.
|
||||||
|
interrupt_line_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the delegate if necessary.
|
||||||
|
if(interrupt_delegate_ && interrupt_line_ != old_interrupt_line) {
|
||||||
|
interrupt_delegate_->mfp68901_did_change_interrupt_status(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MFP68901::get_interrupt_line() {
|
||||||
|
return interrupt_line_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int MFP68901::acknowledge_interrupt() {
|
||||||
|
if(!(interrupt_pending_ & interrupt_mask_)) {
|
||||||
|
return NoAcknowledgement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int mask = msb16(interrupt_pending_ & interrupt_mask_);
|
||||||
|
|
||||||
|
// Clear the pending bit regardless.
|
||||||
|
interrupt_pending_ &= ~mask;
|
||||||
|
|
||||||
|
// If this is software interrupt mode, set the in-service bit.
|
||||||
|
if(interrupt_vector_ & 0x8) {
|
||||||
|
interrupt_in_service_ |= mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_interrupts();
|
||||||
|
|
||||||
|
int selected = 0;
|
||||||
|
while((1 << selected) != mask) ++selected;
|
||||||
|
// LOG("Interrupt acknowledged: " << selected);
|
||||||
|
return (interrupt_vector_ & 0xf0) | uint8_t(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MFP68901::set_interrupt_delegate(InterruptDelegate *delegate) {
|
||||||
|
interrupt_delegate_ = delegate;
|
||||||
|
}
|
||||||
187
Components/68901/MFP68901.hpp
Normal file
187
Components/68901/MFP68901.hpp
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
//
|
||||||
|
// MFP68901.hpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 06/10/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef MFP68901_hpp
|
||||||
|
#define MFP68901_hpp
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||||
|
#include "../../ClockReceiver/ClockingHintSource.hpp"
|
||||||
|
|
||||||
|
namespace Motorola {
|
||||||
|
namespace MFP68901 {
|
||||||
|
|
||||||
|
class PortHandler {
|
||||||
|
public:
|
||||||
|
// 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 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.
|
||||||
|
ClockingHint::Preference preferred_clocking() final;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// MARK: - Timers
|
||||||
|
enum class TimerMode {
|
||||||
|
Stopped, EventCount, Delay, PulseWidth
|
||||||
|
};
|
||||||
|
void set_timer_mode(int timer, TimerMode, int prescale, bool reset_timer);
|
||||||
|
void set_timer_data(int timer, uint8_t);
|
||||||
|
uint8_t get_timer_data(int timer);
|
||||||
|
void decrement_timer(int timer, int amount);
|
||||||
|
|
||||||
|
struct Timer {
|
||||||
|
TimerMode mode = TimerMode::Stopped;
|
||||||
|
uint8_t value = 0;
|
||||||
|
uint8_t reload_value = 0;
|
||||||
|
int prescale = 1;
|
||||||
|
int prescale_count = 1;
|
||||||
|
bool event_input = false;
|
||||||
|
} timers_[4];
|
||||||
|
uint8_t timer_ab_control_[2] = { 0, 0 };
|
||||||
|
uint8_t timer_cd_control_ = 0;
|
||||||
|
|
||||||
|
HalfCycles cycles_left_;
|
||||||
|
|
||||||
|
// MARK: - GPIP
|
||||||
|
uint8_t gpip_input_ = 0;
|
||||||
|
uint8_t gpip_output_ = 0;
|
||||||
|
uint8_t gpip_active_edge_ = 0;
|
||||||
|
uint8_t gpip_direction_ = 0;
|
||||||
|
uint8_t gpip_interrupt_state_ = 0;
|
||||||
|
|
||||||
|
void reevaluate_gpip_interrupts();
|
||||||
|
|
||||||
|
// MARK: - Interrupts
|
||||||
|
|
||||||
|
InterruptDelegate *interrupt_delegate_ = nullptr;
|
||||||
|
|
||||||
|
// Ad hoc documentation:
|
||||||
|
//
|
||||||
|
// An interrupt becomes pending if it is enabled at the time it occurs.
|
||||||
|
//
|
||||||
|
// If a pending interrupt is enabled in the interrupt mask, a processor
|
||||||
|
// interrupt is generated. Otherwise no processor interrupt is generated.
|
||||||
|
//
|
||||||
|
// (Disabling a bit in the enabled mask also instantaneously clears anything
|
||||||
|
// in the pending mask.)
|
||||||
|
//
|
||||||
|
// The user can write to the pending interrupt register; a write
|
||||||
|
// masks whatever is there — so you can disable bits but you cannot set them.
|
||||||
|
//
|
||||||
|
// If the vector register's 'S' bit is set then software end-of-interrupt mode applies:
|
||||||
|
// Acknowledgement of an interrupt clears that interrupt's pending bit, but also sets
|
||||||
|
// its in-service bit. That bit will remain set until the user writes a zero to its position.
|
||||||
|
// If any bits are set in the in-service register, then they will prevent lower-priority
|
||||||
|
// interrupts from being signalled to the CPU. Further interrupts of the same or a higher
|
||||||
|
// priority may occur.
|
||||||
|
//
|
||||||
|
// If the vector register's 'S' bit is clear then automatic end-of-interrupt mode applies:
|
||||||
|
// Acknowledgement of an interrupt will automatically clear the corresponding
|
||||||
|
// pending bit.
|
||||||
|
//
|
||||||
|
int interrupt_enable_ = 0;
|
||||||
|
int interrupt_pending_ = 0;
|
||||||
|
int interrupt_mask_ = 0;
|
||||||
|
int interrupt_in_service_ = 0;
|
||||||
|
bool interrupt_line_ = false;
|
||||||
|
uint8_t interrupt_vector_ = 0;
|
||||||
|
|
||||||
|
enum Interrupt {
|
||||||
|
GPIP0 = (1 << 0),
|
||||||
|
GPIP1 = (1 << 1),
|
||||||
|
GPIP2 = (1 << 2),
|
||||||
|
GPIP3 = (1 << 3),
|
||||||
|
TimerD = (1 << 4),
|
||||||
|
TimerC = (1 << 5),
|
||||||
|
GPIP4 = (1 << 6),
|
||||||
|
GPIP5 = (1 << 7),
|
||||||
|
|
||||||
|
TimerB = (1 << 8),
|
||||||
|
TransmitError = (1 << 9),
|
||||||
|
TransmitBufferEmpty = (1 << 10),
|
||||||
|
ReceiveError = (1 << 11),
|
||||||
|
ReceiveBufferFull = (1 << 12),
|
||||||
|
TimerA = (1 << 13),
|
||||||
|
GPIP6 = (1 << 14),
|
||||||
|
GPIP7 = (1 << 15),
|
||||||
|
};
|
||||||
|
void begin_interrupts(int interrupt);
|
||||||
|
void end_interrupts(int interrupt);
|
||||||
|
void update_interrupts();
|
||||||
|
|
||||||
|
/// @returns the most significant bit set in v, assuming it is one of the least significant 16.
|
||||||
|
inline static int msb16(int v) {
|
||||||
|
// Saturate all bits below the MSB.
|
||||||
|
v |= v >> 1;
|
||||||
|
v |= v >> 2;
|
||||||
|
v |= v >> 4;
|
||||||
|
v |= v >> 8;
|
||||||
|
|
||||||
|
// Throw away lesser bits.
|
||||||
|
return (v+1) >> 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* MFP68901_hpp */
|
||||||
@@ -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
|
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.
|
then the PortHandler will be informed.
|
||||||
*/
|
*/
|
||||||
void set_register(int address, uint8_t value) {
|
void write(int address, uint8_t value) {
|
||||||
switch(address & 3) {
|
switch(address & 3) {
|
||||||
case 0:
|
case 0:
|
||||||
if(!(control_ & 0x10)) {
|
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
|
Obtains the current value for the register at @c address. If this provides a reading
|
||||||
of input then the PortHandler will be queried.
|
of input then the PortHandler will be queried.
|
||||||
*/
|
*/
|
||||||
uint8_t get_register(int address) {
|
uint8_t read(int address) {
|
||||||
switch(address & 3) {
|
switch(address & 3) {
|
||||||
case 0: return (control_ & 0x10) ? port_handler_.get_value(0) : outputs_[0];
|
case 0: return (control_ & 0x10) ? port_handler_.get_value(0) : outputs_[0];
|
||||||
case 1: return (control_ & 0x02) ? port_handler_.get_value(1) : outputs_[1];
|
case 1: return (control_ & 0x02) ? port_handler_.get_value(1) : outputs_[1];
|
||||||
|
|||||||
@@ -95,11 +95,11 @@ void i8272::run_for(Cycles cycles) {
|
|||||||
|
|
||||||
// check for an expired timer
|
// check for an expired timer
|
||||||
if(delay_time_ > 0) {
|
if(delay_time_ > 0) {
|
||||||
if(cycles.as_int() >= delay_time_) {
|
if(cycles.as_integral() >= delay_time_) {
|
||||||
delay_time_ = 0;
|
delay_time_ = 0;
|
||||||
posit_event(static_cast<int>(Event8272::Timer));
|
posit_event(static_cast<int>(Event8272::Timer));
|
||||||
} else {
|
} else {
|
||||||
delay_time_ -= cycles.as_int();
|
delay_time_ -= cycles.as_integral();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +108,8 @@ void i8272::run_for(Cycles cycles) {
|
|||||||
int drives_left = drives_seeking_;
|
int drives_left = drives_seeking_;
|
||||||
for(int c = 0; c < 4; c++) {
|
for(int c = 0; c < 4; c++) {
|
||||||
if(drives_[c].phase == Drive::Seeking) {
|
if(drives_[c].phase == Drive::Seeking) {
|
||||||
drives_[c].step_rate_counter += cycles.as_int();
|
drives_[c].step_rate_counter += cycles.as_integral();
|
||||||
int steps = drives_[c].step_rate_counter / (8000 * step_rate_time_);
|
auto steps = drives_[c].step_rate_counter / (8000 * step_rate_time_);
|
||||||
drives_[c].step_rate_counter %= (8000 * step_rate_time_);
|
drives_[c].step_rate_counter %= (8000 * step_rate_time_);
|
||||||
while(steps--) {
|
while(steps--) {
|
||||||
// Perform a step.
|
// Perform a step.
|
||||||
@@ -141,12 +141,12 @@ void i8272::run_for(Cycles cycles) {
|
|||||||
int head = c&1;
|
int head = c&1;
|
||||||
|
|
||||||
if(drives_[drive].head_unload_delay[head] > 0) {
|
if(drives_[drive].head_unload_delay[head] > 0) {
|
||||||
if(cycles.as_int() >= drives_[drive].head_unload_delay[head]) {
|
if(cycles.as_integral() >= drives_[drive].head_unload_delay[head]) {
|
||||||
drives_[drive].head_unload_delay[head] = 0;
|
drives_[drive].head_unload_delay[head] = 0;
|
||||||
drives_[drive].head_is_loaded[head] = false;
|
drives_[drive].head_is_loaded[head] = false;
|
||||||
head_timers_running_--;
|
head_timers_running_--;
|
||||||
} else {
|
} else {
|
||||||
drives_[drive].head_unload_delay[head] -= cycles.as_int();
|
drives_[drive].head_unload_delay[head] -= cycles.as_integral();
|
||||||
}
|
}
|
||||||
timers_left--;
|
timers_left--;
|
||||||
if(!timers_left) break;
|
if(!timers_left) break;
|
||||||
@@ -163,7 +163,7 @@ void i8272::run_for(Cycles cycles) {
|
|||||||
if(is_sleeping_) update_clocking_observer();
|
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
|
// don't consider attempted sets to the status register
|
||||||
if(!address) return;
|
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(address) {
|
||||||
if(result_stack_.empty()) return 0xff;
|
if(result_stack_.empty()) return 0xff;
|
||||||
uint8_t result = result_stack_.back();
|
uint8_t result = result_stack_.back();
|
||||||
@@ -292,7 +292,7 @@ void i8272::posit_event(int event_type) {
|
|||||||
WAIT_FOR_EVENT(Event8272::CommandByte)
|
WAIT_FOR_EVENT(Event8272::CommandByte)
|
||||||
SetBusy();
|
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,
|
0, 0, 9, 3, 2, 9, 9, 2,
|
||||||
1, 9, 2, 0, 9, 6, 0, 3,
|
1, 9, 2, 0, 9, 6, 0, 3,
|
||||||
0, 9, 0, 0, 0, 0, 0, 0,
|
0, 9, 0, 0, 0, 0, 0, 0,
|
||||||
@@ -865,7 +865,7 @@ void i8272::posit_event(int event_type) {
|
|||||||
SetDataRequest();
|
SetDataRequest();
|
||||||
SetDataDirectionToProcessor();
|
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.
|
// until the processor has read all result bytes.
|
||||||
WAIT_FOR_EVENT(Event8272::ResultEmpty);
|
WAIT_FOR_EVENT(Event8272::ResultEmpty);
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ class i8272: public Storage::Disk::MFMController {
|
|||||||
void set_data_input(uint8_t value);
|
void set_data_input(uint8_t value);
|
||||||
uint8_t get_data_output();
|
uint8_t get_data_output();
|
||||||
|
|
||||||
void set_register(int address, uint8_t value);
|
void write(int address, uint8_t value);
|
||||||
uint8_t get_register(int address);
|
uint8_t read(int address);
|
||||||
|
|
||||||
void set_dma_acknowledge(bool dack);
|
void set_dma_acknowledge(bool dack);
|
||||||
void set_terminal_count(bool tc);
|
void set_terminal_count(bool tc);
|
||||||
|
|
||||||
ClockingHint::Preference preferred_clocking() override;
|
ClockingHint::Preference preferred_clocking() final;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void select_drive(int number) = 0;
|
virtual void select_drive(int number) = 0;
|
||||||
@@ -67,13 +67,13 @@ class i8272: public Storage::Disk::MFMController {
|
|||||||
ResultEmpty = (1 << 5),
|
ResultEmpty = (1 << 5),
|
||||||
NoLongerReady = (1 << 6)
|
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 interesting_event_mask_ = static_cast<int>(Event8272::CommandByte);
|
||||||
int resume_point_ = 0;
|
int resume_point_ = 0;
|
||||||
bool is_access_command_ = false;
|
bool is_access_command_ = false;
|
||||||
|
|
||||||
// The counter used for ::Timer events.
|
// The counter used for ::Timer events.
|
||||||
int delay_time_ = 0;
|
Cycles::IntType delay_time_ = 0;
|
||||||
|
|
||||||
// The connected drives.
|
// The connected drives.
|
||||||
struct Drive {
|
struct Drive {
|
||||||
@@ -89,12 +89,12 @@ class i8272: public Storage::Disk::MFMController {
|
|||||||
bool seek_failed = false;
|
bool seek_failed = false;
|
||||||
|
|
||||||
// Seeking: transient state.
|
// Seeking: transient state.
|
||||||
int step_rate_counter = 0;
|
Cycles::IntType step_rate_counter = 0;
|
||||||
int steps_taken = 0;
|
int steps_taken = 0;
|
||||||
int target_head_position = 0; // either an actual number, or -1 to indicate to step until track zero
|
int target_head_position = 0; // either an actual number, or -1 to indicate to step until track zero
|
||||||
|
|
||||||
// Head state.
|
// Head state.
|
||||||
int head_unload_delay[2] = {0, 0};
|
Cycles::IntType head_unload_delay[2] = {0, 0};
|
||||||
bool head_is_loaded[2] = {false, false};
|
bool head_is_loaded[2] = {false, false};
|
||||||
|
|
||||||
} drives_[4];
|
} drives_[4];
|
||||||
|
|||||||
@@ -17,16 +17,16 @@ using namespace TI::TMS;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
const uint8_t StatusInterrupt = 0x80;
|
constexpr uint8_t StatusInterrupt = 0x80;
|
||||||
const uint8_t StatusSpriteOverflow = 0x40;
|
constexpr uint8_t StatusSpriteOverflow = 0x40;
|
||||||
|
|
||||||
const int StatusSpriteCollisionShift = 5;
|
constexpr int StatusSpriteCollisionShift = 5;
|
||||||
const uint8_t StatusSpriteCollision = 0x20;
|
constexpr uint8_t StatusSpriteCollision = 0x20;
|
||||||
|
|
||||||
// 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole
|
// 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.
|
// line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368.
|
||||||
const unsigned int CRTCyclesPerLine = 1365;
|
constexpr unsigned int CRTCyclesPerLine = 1365;
|
||||||
const unsigned int CRTCyclesDivider = 4;
|
constexpr unsigned int CRTCyclesDivider = 4;
|
||||||
|
|
||||||
struct ReverseTable {
|
struct ReverseTable {
|
||||||
std::uint8_t map[256];
|
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);
|
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) {
|
void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||||
crt_.set_display_type(display_type);
|
crt_.set_display_type(display_type);
|
||||||
}
|
}
|
||||||
@@ -166,7 +174,7 @@ void TMS9918::run_for(const HalfCycles cycles) {
|
|||||||
// Convert 456 clocked half cycles per line to 342 internal cycles per line;
|
// Convert 456 clocked half cycles per line to 342 internal cycles per line;
|
||||||
// the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised
|
// the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised
|
||||||
// for this part. So multiply by three quarters.
|
// for this part. So multiply by three quarters.
|
||||||
int int_cycles = (cycles.as_int() * 3) + cycles_error_;
|
int int_cycles = int(cycles.as_integral() * 3) + cycles_error_;
|
||||||
cycles_error_ = int_cycles & 3;
|
cycles_error_ = int_cycles & 3;
|
||||||
int_cycles >>= 2;
|
int_cycles >>= 2;
|
||||||
if(!int_cycles) return;
|
if(!int_cycles) return;
|
||||||
@@ -352,8 +360,7 @@ void TMS9918::run_for(const HalfCycles cycles) {
|
|||||||
// Output video stream.
|
// Output video stream.
|
||||||
// --------------------
|
// --------------------
|
||||||
|
|
||||||
#define intersect(left, right, code) \
|
#define intersect(left, right, code) { \
|
||||||
{ \
|
|
||||||
const int start = std::max(read_pointer_.column, left); \
|
const int start = std::max(read_pointer_.column, left); \
|
||||||
const int end = std::min(end_column, right); \
|
const int end = std::min(end_column, right); \
|
||||||
if(end > start) {\
|
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
|
// Writes to address 0 are writes to the video RAM. Store
|
||||||
// the value and return.
|
// the value and return.
|
||||||
if(!(address & 1)) {
|
if(!(address & 1)) {
|
||||||
@@ -625,7 +632,7 @@ void TMS9918::set_register(int address, uint8_t value) {
|
|||||||
|
|
||||||
uint8_t TMS9918::get_current_line() {
|
uint8_t TMS9918::get_current_line() {
|
||||||
// Determine the row to return.
|
// 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 =
|
int source_row =
|
||||||
(write_pointer_.column < row_change_position)
|
(write_pointer_.column < row_change_position)
|
||||||
? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines
|
? (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;
|
latched_column_ = write_pointer_.column;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t TMS9918::get_register(int address) {
|
uint8_t TMS9918::read(int address) {
|
||||||
write_phase_ = false;
|
write_phase_ = false;
|
||||||
|
|
||||||
// Reads from address 0 read video RAM, via the read-ahead buffer.
|
// 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;
|
int sprite_collision = 0;
|
||||||
memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[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};
|
constexpr 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 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.
|
// Draw all sprites into the sprite buffer.
|
||||||
const int shifter_target = sprites_16x16_ ? 32 : 16;
|
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. */
|
/*! Sets the scan target this TMS will post content to. */
|
||||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
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. */
|
/*! Sets the type of display the CRT will request. */
|
||||||
void set_display_type(Outputs::Display::DisplayType);
|
void set_display_type(Outputs::Display::DisplayType);
|
||||||
|
|
||||||
@@ -54,10 +57,10 @@ class TMS9918: public Base {
|
|||||||
void run_for(const HalfCycles cycles);
|
void run_for(const HalfCycles cycles);
|
||||||
|
|
||||||
/*! Sets a register value. */
|
/*! Sets a register value. */
|
||||||
void set_register(int address, uint8_t value);
|
void write(int address, uint8_t value);
|
||||||
|
|
||||||
/*! Gets a register 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. */
|
/*! Gets the current scan line; provided by the Master System only. */
|
||||||
uint8_t get_current_line();
|
uint8_t get_current_line();
|
||||||
@@ -69,8 +72,8 @@ class TMS9918: public Base {
|
|||||||
void latch_horizontal_counter();
|
void latch_horizontal_counter();
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
Returns the amount of time until get_interrupt_line would next return true if
|
Returns the amount of time until @c get_interrupt_line would next return true if
|
||||||
there are no interceding calls to set_register or get_register.
|
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
|
If get_interrupt_line is true now, returns zero. If get_interrupt_line would
|
||||||
never return true, returns -1.
|
never return true, returns -1.
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class Base {
|
|||||||
// (though, in practice, it won't happen until the next
|
// (though, in practice, it won't happen until the next
|
||||||
// external slot after this number of cycles after the
|
// external slot after this number of cycles after the
|
||||||
// device has requested the read or write).
|
// device has requested the read or write).
|
||||||
return 7;
|
return 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Holds the main status register.
|
// Holds the main status register.
|
||||||
|
|||||||
@@ -6,51 +6,66 @@
|
|||||||
// Copyright 2016 Thomas Harte. All rights reserved.
|
// Copyright 2016 Thomas Harte. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
#include "AY38910.hpp"
|
#include "AY38910.hpp"
|
||||||
|
|
||||||
#include <cmath>
|
//namespace GI {
|
||||||
|
//namespace AY38910 {
|
||||||
|
|
||||||
using namespace GI::AY38910;
|
using namespace GI::AY38910;
|
||||||
|
|
||||||
AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
|
template <bool is_stereo>
|
||||||
// set up envelope lookup tables
|
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 c = 0; c < 16; c++) {
|
||||||
for(int p = 0; p < 32; p++) {
|
for(int p = 0; p < 64; p++) {
|
||||||
switch(c) {
|
switch(c) {
|
||||||
case 0: case 1: case 2: case 3: case 9:
|
case 0: case 1: case 2: case 3: case 9:
|
||||||
envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0;
|
/* Envelope: \____ */
|
||||||
envelope_overflow_masks_[c] = 0x1f;
|
envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0;
|
||||||
|
envelope_overflow_masks_[c] = 0x3f;
|
||||||
break;
|
break;
|
||||||
case 4: case 5: case 6: case 7: case 15:
|
case 4: case 5: case 6: case 7: case 15:
|
||||||
envelope_shapes_[c][p] = (p < 16) ? p : 0;
|
/* Envelope: /____ */
|
||||||
envelope_overflow_masks_[c] = 0x1f;
|
envelope_shapes_[c][p] = (p < 32) ? p : 0;
|
||||||
|
envelope_overflow_masks_[c] = 0x3f;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 8:
|
case 8:
|
||||||
envelope_shapes_[c][p] = (p & 0xf) ^ 0xf;
|
/* Envelope: \\\\\\\\ */
|
||||||
|
envelope_shapes_[c][p] = (p & 0x1f) ^ 0x1f;
|
||||||
envelope_overflow_masks_[c] = 0x00;
|
envelope_overflow_masks_[c] = 0x00;
|
||||||
break;
|
break;
|
||||||
case 12:
|
case 12:
|
||||||
envelope_shapes_[c][p] = (p & 0xf);
|
/* Envelope: //////// */
|
||||||
|
envelope_shapes_[c][p] = (p & 0x1f);
|
||||||
envelope_overflow_masks_[c] = 0x00;
|
envelope_overflow_masks_[c] = 0x00;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 10:
|
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;
|
envelope_overflow_masks_[c] = 0x00;
|
||||||
break;
|
break;
|
||||||
case 14:
|
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;
|
envelope_overflow_masks_[c] = 0x00;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 11:
|
case 11:
|
||||||
envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0xf;
|
/* Envelope: \------ (if - is high) */
|
||||||
envelope_overflow_masks_[c] = 0x1f;
|
envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0x1f;
|
||||||
|
envelope_overflow_masks_[c] = 0x3f;
|
||||||
break;
|
break;
|
||||||
case 13:
|
case 13:
|
||||||
envelope_shapes_[c][p] = (p < 16) ? p : 0xf;
|
/* Envelope: /------- */
|
||||||
envelope_overflow_masks_[c] = 0x1f;
|
envelope_shapes_[c][p] = (p < 32) ? p : 0x1f;
|
||||||
|
envelope_overflow_masks_[c] = 0x3f;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,21 +74,50 @@ AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_
|
|||||||
set_sample_volume_range(0);
|
set_sample_volume_range(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AY38910::set_sample_volume_range(std::int16_t range) {
|
template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::int16_t range) {
|
||||||
// set up volume lookup table
|
// Set up volume lookup table; the function below is based on a combination of the graph
|
||||||
const float max_volume = static_cast<float>(range) / 3.0f; // As there are three channels.
|
// from the YM's datasheet, showing a clear power curve, and fitting that to observed
|
||||||
const float root_two = sqrtf(2.0f);
|
// values reported elsewhere.
|
||||||
for(int v = 0; v < 16; v++) {
|
const float max_volume = float(range) / 3.0f; // As there are three channels.
|
||||||
volumes_[v] = static_cast<int>(max_volume / powf(root_two, static_cast<float>(v ^ 0xf)));
|
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();
|
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;
|
std::size_t c = 0;
|
||||||
while((master_divider_&7) && c < number_of_samples) {
|
while((master_divider_&3) && c < number_of_samples) {
|
||||||
target[c] = output_volume_;
|
if constexpr (is_stereo) {
|
||||||
|
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||||
|
} else {
|
||||||
|
target[c] = int16_t(output_volume_);
|
||||||
|
}
|
||||||
master_divider_++;
|
master_divider_++;
|
||||||
c++;
|
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]--;\
|
if(tone_counters_[c]) tone_counters_[c]--;\
|
||||||
else {\
|
else {\
|
||||||
tone_outputs_[c] ^= 1;\
|
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(0);
|
||||||
step_channel(1);
|
step_channel(1);
|
||||||
step_channel(2);
|
step_channel(2);
|
||||||
|
|
||||||
#undef step_channel
|
#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.
|
// it into the official 17 upon divider underflow.
|
||||||
if(noise_counter_) noise_counter_--;
|
if(noise_counter_) noise_counter_--;
|
||||||
else {
|
else {
|
||||||
noise_counter_ = noise_period_;
|
noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes.
|
||||||
noise_output_ ^= noise_shift_register_&1;
|
noise_output_ ^= noise_shift_register_&1;
|
||||||
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
|
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
|
||||||
noise_shift_register_ >>= 1;
|
noise_shift_register_ >>= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... and the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
|
// Update 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.
|
// implementing non-repeating patterns by locking them to the final table position.
|
||||||
if(envelope_divider_) envelope_divider_--;
|
if(envelope_divider_) envelope_divider_--;
|
||||||
else {
|
else {
|
||||||
envelope_divider_ = envelope_period_;
|
envelope_divider_ = envelope_period_;
|
||||||
envelope_position_ ++;
|
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();
|
evaluate_output_volume();
|
||||||
|
|
||||||
for(int ic = 0; ic < 8 && c < number_of_samples; ic++) {
|
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
|
||||||
target[c] = output_volume_;
|
if constexpr (is_stereo) {
|
||||||
|
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||||
|
} else {
|
||||||
|
target[c] = int16_t(output_volume_);
|
||||||
|
}
|
||||||
c++;
|
c++;
|
||||||
master_divider_++;
|
master_divider_++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
master_divider_ &= 7;
|
master_divider_ &= 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AY38910::evaluate_output_volume() {
|
template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
|
||||||
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_];
|
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
|
||||||
|
|
||||||
// The output level for a channel is:
|
// The output level for a channel is:
|
||||||
// 1 if neither tone nor noise is enabled;
|
// 1 if neither tone nor noise is enabled;
|
||||||
@@ -142,9 +190,20 @@ void AY38910::evaluate_output_volume() {
|
|||||||
};
|
};
|
||||||
#undef level
|
#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) \
|
#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] = {
|
const int volumes[3] = {
|
||||||
channel_volume(8),
|
channel_volume(8),
|
||||||
@@ -153,34 +212,47 @@ void AY38910::evaluate_output_volume() {
|
|||||||
};
|
};
|
||||||
#undef channel_volume
|
#undef channel_volume
|
||||||
|
|
||||||
// Mix additively.
|
// Mix additively, weighting if in stereo.
|
||||||
output_volume_ = static_cast<int16_t>(
|
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[0]] * channel_levels[0] +
|
||||||
volumes_[volumes[1]] * channel_levels[1] +
|
volumes_[volumes[1]] * channel_levels[1] +
|
||||||
volumes_[volumes[2]] * channel_levels[2]
|
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.
|
// 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;
|
return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Register manipulation
|
// 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;
|
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.
|
// There are only 16 registers.
|
||||||
if(selected_register_ > 15) return;
|
if(selected_register_ > 15) return;
|
||||||
|
|
||||||
// If this is a register that affects audio output, enqueue a mutation onto the
|
// If this is a register that affects audio output, enqueue a mutation onto the
|
||||||
// audio generation thread.
|
// audio generation thread.
|
||||||
if(selected_register_ < 14) {
|
if(selected_register_ < 14) {
|
||||||
const int selected_register = selected_register_;
|
task_queue_.defer([this, selected_register = selected_register_, value] () {
|
||||||
task_queue_.defer([=] () {
|
|
||||||
// Perform any register-specific mutation to output generation.
|
// Perform any register-specific mutation to output generation.
|
||||||
uint8_t masked_value = value;
|
uint8_t masked_value = value;
|
||||||
switch(selected_register) {
|
switch(selected_register) {
|
||||||
@@ -189,7 +261,7 @@ void AY38910::set_register_value(uint8_t value) {
|
|||||||
int channel = selected_register >> 1;
|
int channel = selected_register >> 1;
|
||||||
|
|
||||||
if(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
|
else
|
||||||
tone_periods_[channel] = (tone_periods_[channel] & ~0xff) | value;
|
tone_periods_[channel] = (tone_periods_[channel] & ~0xff) | value;
|
||||||
}
|
}
|
||||||
@@ -204,7 +276,7 @@ void AY38910::set_register_value(uint8_t value) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 12:
|
case 12:
|
||||||
envelope_period_ = (envelope_period_ & 0xff) | static_cast<int>(value << 8);
|
envelope_period_ = (envelope_period_ & 0xff) | int(value << 8);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 13:
|
case 13:
|
||||||
@@ -242,7 +314,7 @@ void AY38910::set_register_value(uint8_t value) {
|
|||||||
if(update_port_a) set_port_output(false);
|
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
|
// This table ensures that bits that aren't defined within the AY are returned as 0s
|
||||||
// when read, conforming to CPC-sourced unit tests.
|
// when read, conforming to CPC-sourced unit tests.
|
||||||
const uint8_t register_masks[16] = {
|
const uint8_t register_masks[16] = {
|
||||||
@@ -256,24 +328,24 @@ uint8_t AY38910::get_register_value() {
|
|||||||
|
|
||||||
// MARK: - Port querying
|
// 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];
|
return registers_[port_b ? 15 : 14];
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Bus handling
|
// 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;
|
port_handler_ = handler;
|
||||||
set_port_output(true);
|
set_port_output(true);
|
||||||
set_port_output(false);
|
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;
|
data_input_ = r;
|
||||||
update_bus();
|
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,
|
// 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,
|
// 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.
|
// 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) {
|
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
|
// 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.
|
// 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_;
|
return data_output_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AY38910::set_control_lines(ControlLines control_lines) {
|
template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) {
|
||||||
switch(static_cast<int>(control_lines)) {
|
switch(int(control_lines)) {
|
||||||
default: control_state_ = Inactive; break;
|
default: control_state_ = Inactive; break;
|
||||||
|
|
||||||
case static_cast<int>(BDIR | BC2 | BC1):
|
case int(BDIR | BC2 | BC1):
|
||||||
case BDIR:
|
case BDIR:
|
||||||
case BC1: control_state_ = LatchAddress; break;
|
case BC1: control_state_ = LatchAddress; break;
|
||||||
|
|
||||||
case static_cast<int>(BC2 | BC1): control_state_ = Read; break;
|
case int(BC2 | BC1): control_state_ = Read; break;
|
||||||
case static_cast<int>(BDIR | BC2): control_state_ = Write; break;
|
case int(BDIR | BC2): control_state_ = Write; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
update_bus();
|
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.
|
// Assume no output, unless this turns out to be a read.
|
||||||
data_output_ = 0xff;
|
data_output_ = 0xff;
|
||||||
switch(control_state_) {
|
switch(control_state_) {
|
||||||
@@ -324,3 +396,7 @@ void AY38910::update_bus() {
|
|||||||
case Read: data_output_ = get_register_value(); break;
|
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>;
|
||||||
|
|||||||
@@ -35,9 +35,10 @@ class PortHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
Requests the current input on an AY port.
|
Sets the current output on an AY port.
|
||||||
|
|
||||||
@param port_b @c true if the input being queried is Port B. @c false if it is Port A.
|
@param port_b @c true if the output being posted is Port B. @c false if it is Port A.
|
||||||
|
@param value the value now being output.
|
||||||
*/
|
*/
|
||||||
virtual void set_port_output(bool port_b, uint8_t value) {}
|
virtual void set_port_output(bool port_b, uint8_t value) {}
|
||||||
};
|
};
|
||||||
@@ -51,15 +52,24 @@ enum ControlLines {
|
|||||||
BDIR = (1 << 2)
|
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
|
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
|
noise generator and a volume envelope generator, which also provides two bidirectional
|
||||||
interface ports.
|
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:
|
public:
|
||||||
/// Creates a new AY38910.
|
/// 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.
|
/// Sets the value the AY would read from its data lines if it were not outputting.
|
||||||
void set_data_input(uint8_t r);
|
void set_data_input(uint8_t r);
|
||||||
@@ -83,10 +93,23 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
|||||||
*/
|
*/
|
||||||
void set_port_handler(PortHandler *);
|
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.
|
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
|
||||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||||
bool is_zero_level();
|
bool is_zero_level();
|
||||||
void set_sample_volume_range(std::int16_t range);
|
void set_sample_volume_range(std::int16_t range);
|
||||||
|
static constexpr bool get_is_stereo() { return is_stereo; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||||
@@ -108,11 +131,11 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
|||||||
|
|
||||||
int envelope_period_ = 0;
|
int envelope_period_ = 0;
|
||||||
int envelope_divider_ = 0;
|
int envelope_divider_ = 0;
|
||||||
int envelope_position_ = 0;
|
int envelope_position_ = 0, envelope_position_mask_ = 0;
|
||||||
int envelope_shapes_[16][32];
|
int envelope_shapes_[16][64];
|
||||||
int envelope_overflow_masks_[16];
|
int envelope_overflow_masks_[16];
|
||||||
|
|
||||||
int volumes_[16];
|
int volumes_[32];
|
||||||
|
|
||||||
enum ControlState {
|
enum ControlState {
|
||||||
Inactive,
|
Inactive,
|
||||||
@@ -127,14 +150,21 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
|||||||
|
|
||||||
uint8_t data_input_, data_output_;
|
uint8_t data_input_, data_output_;
|
||||||
|
|
||||||
int16_t output_volume_;
|
uint32_t output_volume_;
|
||||||
void evaluate_output_volume();
|
|
||||||
|
|
||||||
void update_bus();
|
void update_bus();
|
||||||
PortHandler *port_handler_ = nullptr;
|
PortHandler *port_handler_ = nullptr;
|
||||||
void set_port_output(bool port_b);
|
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) {
|
void Toggle::set_output(bool enabled) {
|
||||||
if(is_enabled_ == enabled) return;
|
if(is_enabled_ == enabled) return;
|
||||||
is_enabled_ = enabled;
|
is_enabled_ = enabled;
|
||||||
audio_queue_.defer([=] {
|
audio_queue_.defer([this, enabled] {
|
||||||
level_ = enabled ? volume_ : 0;
|
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 get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||||
void set_sample_volume_range(std::int16_t range);
|
void set_sample_volume_range(std::int16_t range);
|
||||||
void skip_samples(const std::size_t number_of_samples);
|
void skip_samples(const std::size_t number_of_samples);
|
||||||
|
static constexpr bool get_is_stereo() { return false; }
|
||||||
|
|
||||||
void set_output(bool enabled);
|
void set_output(bool enabled);
|
||||||
bool get_output();
|
bool get_output();
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ void DiskII::select_drive(int drive) {
|
|||||||
void DiskII::run_for(const Cycles cycles) {
|
void DiskII::run_for(const Cycles cycles) {
|
||||||
if(preferred_clocking() == ClockingHint::Preference::None) return;
|
if(preferred_clocking() == ClockingHint::Preference::None) return;
|
||||||
|
|
||||||
int integer_cycles = cycles.as_int();
|
auto integer_cycles = cycles.as_integral();
|
||||||
while(integer_cycles--) {
|
while(integer_cycles--) {
|
||||||
const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6);
|
const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6);
|
||||||
if(flux_duration_) {
|
if(flux_duration_) {
|
||||||
@@ -124,7 +124,7 @@ void DiskII::run_for(const Cycles cycles) {
|
|||||||
// motor switch being flipped and the drive motor actually switching off.
|
// motor switch being flipped and the drive motor actually switching off.
|
||||||
// This models that, accepting overrun as a risk.
|
// This models that, accepting overrun as a risk.
|
||||||
if(motor_off_time_ >= 0) {
|
if(motor_off_time_ >= 0) {
|
||||||
motor_off_time_ -= cycles.as_int();
|
motor_off_time_ -= cycles.as_integral();
|
||||||
if(motor_off_time_ < 0) {
|
if(motor_off_time_ < 0) {
|
||||||
set_control(Control::Motor, false);
|
set_control(Control::Motor, false);
|
||||||
}
|
}
|
||||||
@@ -266,7 +266,7 @@ int DiskII::read_address(int address) {
|
|||||||
break;
|
break;
|
||||||
case 0xf:
|
case 0xf:
|
||||||
if(!(inputs_ & input_mode))
|
if(!(inputs_ & input_mode))
|
||||||
drives_[active_drive_].begin_writing(Storage::Time(1, clock_rate_), false);
|
drives_[active_drive_].begin_writing(Storage::Time(1, int(clock_rate_)), false);
|
||||||
inputs_ |= input_mode;
|
inputs_ |= input_mode;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class DiskII:
|
|||||||
The value returned by @c read_address if accessing that address
|
The value returned by @c read_address if accessing that address
|
||||||
didn't cause the disk II to place anything onto the bus.
|
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.
|
/// Advances the controller by @c cycles.
|
||||||
void run_for(const Cycles cycles);
|
void run_for(const Cycles cycles);
|
||||||
@@ -76,7 +76,7 @@ class DiskII:
|
|||||||
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
|
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
|
||||||
|
|
||||||
// As per Sleeper.
|
// As per Sleeper.
|
||||||
ClockingHint::Preference preferred_clocking() override;
|
ClockingHint::Preference preferred_clocking() final;
|
||||||
|
|
||||||
// The Disk II functions as a potential target for @c Activity::Sources.
|
// The Disk II functions as a potential target for @c Activity::Sources.
|
||||||
void set_activity_observer(Activity::Observer *observer);
|
void set_activity_observer(Activity::Observer *observer);
|
||||||
@@ -98,10 +98,10 @@ class DiskII:
|
|||||||
void select_drive(int drive);
|
void select_drive(int drive);
|
||||||
|
|
||||||
uint8_t trigger_address(int address, uint8_t value);
|
uint8_t trigger_address(int address, uint8_t value);
|
||||||
void process_event(const Storage::Disk::Drive::Event &event) override;
|
void process_event(const Storage::Disk::Drive::Event &event) final;
|
||||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) override;
|
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) final;
|
||||||
|
|
||||||
const int clock_rate_ = 0;
|
const Cycles::IntType clock_rate_ = 0;
|
||||||
|
|
||||||
uint8_t state_ = 0;
|
uint8_t state_ = 0;
|
||||||
uint8_t inputs_ = 0;
|
uint8_t inputs_ = 0;
|
||||||
@@ -109,7 +109,7 @@ class DiskII:
|
|||||||
|
|
||||||
int stepper_mask_ = 0;
|
int stepper_mask_ = 0;
|
||||||
int stepper_position_ = 0;
|
int stepper_position_ = 0;
|
||||||
int motor_off_time_ = -1;
|
Cycles::IntType motor_off_time_ = -1;
|
||||||
|
|
||||||
bool is_write_protected();
|
bool is_write_protected();
|
||||||
std::array<uint8_t, 256> state_machine_;
|
std::array<uint8_t, 256> state_machine_;
|
||||||
|
|||||||
@@ -13,15 +13,15 @@
|
|||||||
using namespace Apple;
|
using namespace Apple;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
const int CA0 = 1 << 0;
|
constexpr int CA0 = 1 << 0;
|
||||||
const int CA1 = 1 << 1;
|
constexpr int CA1 = 1 << 1;
|
||||||
const int CA2 = 1 << 2;
|
constexpr int CA2 = 1 << 2;
|
||||||
const int LSTRB = 1 << 3;
|
constexpr int LSTRB = 1 << 3;
|
||||||
const int ENABLE = 1 << 4;
|
constexpr int ENABLE = 1 << 4;
|
||||||
const int DRIVESEL = 1 << 5; /* This means drive select, like on the original Disk II. */
|
constexpr int DRIVESEL = 1 << 5; /* This means drive select, like on the original Disk II. */
|
||||||
const int Q6 = 1 << 6;
|
constexpr int Q6 = 1 << 6;
|
||||||
const int Q7 = 1 << 7;
|
constexpr 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 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) :
|
IWM::IWM(int clock_rate) :
|
||||||
@@ -233,25 +233,34 @@ void IWM::run_for(const Cycles cycles) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Activity otherwise depends on mode and motor state.
|
// Activity otherwise depends on mode and motor state.
|
||||||
int integer_cycles = cycles.as_int();
|
auto integer_cycles = cycles.as_integral();
|
||||||
switch(shift_mode_) {
|
switch(shift_mode_) {
|
||||||
case ShiftMode::Reading:
|
case ShiftMode::Reading: {
|
||||||
|
// Per the IWM patent, column 7, around line 35 onwards: "The expected time
|
||||||
|
// is widened by approximately one-half an interval before and after the
|
||||||
|
// expected time since the data is not precisely spaced when read due to
|
||||||
|
// variations in drive speed and other external factors". The error_margin
|
||||||
|
// here implements the 'after' part of that contract.
|
||||||
|
const auto error_margin = Cycles(bit_length_.as_integral() >> 1);
|
||||||
|
|
||||||
if(drive_is_rotating_[active_drive_]) {
|
if(drive_is_rotating_[active_drive_]) {
|
||||||
while(integer_cycles--) {
|
while(integer_cycles--) {
|
||||||
drives_[active_drive_]->run_for(Cycles(1));
|
drives_[active_drive_]->run_for(Cycles(1));
|
||||||
++cycles_since_shift_;
|
++cycles_since_shift_;
|
||||||
if(cycles_since_shift_ == bit_length_ + Cycles(2)) {
|
if(cycles_since_shift_ == bit_length_ + error_margin) {
|
||||||
propose_shift(0);
|
propose_shift(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
while(cycles_since_shift_ + integer_cycles >= bit_length_ + Cycles(2)) {
|
while(cycles_since_shift_ + integer_cycles >= bit_length_ + error_margin) {
|
||||||
|
const auto run_length = bit_length_ + error_margin - cycles_since_shift_;
|
||||||
|
integer_cycles -= run_length.as_integral();
|
||||||
|
cycles_since_shift_ += run_length;
|
||||||
propose_shift(0);
|
propose_shift(0);
|
||||||
integer_cycles -= bit_length_.as_int() + 2 - cycles_since_shift_.as_int();
|
|
||||||
}
|
}
|
||||||
cycles_since_shift_ += Cycles(integer_cycles);
|
cycles_since_shift_ += Cycles(integer_cycles);
|
||||||
}
|
}
|
||||||
break;
|
} break;
|
||||||
|
|
||||||
case ShiftMode::Writing:
|
case ShiftMode::Writing:
|
||||||
if(drives_[active_drive_]->is_writing()) {
|
if(drives_[active_drive_]->is_writing()) {
|
||||||
@@ -263,7 +272,7 @@ void IWM::run_for(const Cycles cycles) {
|
|||||||
drives_[active_drive_]->write_bit(shift_register_ & 0x80);
|
drives_[active_drive_]->write_bit(shift_register_ & 0x80);
|
||||||
shift_register_ <<= 1;
|
shift_register_ <<= 1;
|
||||||
|
|
||||||
integer_cycles -= cycles_until_write.as_int();
|
integer_cycles -= cycles_until_write.as_integral();
|
||||||
cycles_since_shift_ = Cycles(0);
|
cycles_since_shift_ = Cycles(0);
|
||||||
|
|
||||||
--output_bits_remaining_;
|
--output_bits_remaining_;
|
||||||
@@ -324,7 +333,7 @@ void IWM::select_shift_mode() {
|
|||||||
|
|
||||||
// If writing mode just began, set the drive into write mode and cue up the first output byte.
|
// If writing mode just began, set the drive into write mode and cue up the first output byte.
|
||||||
if(drives_[active_drive_] && old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) {
|
if(drives_[active_drive_] && old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) {
|
||||||
drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_int()), false);
|
drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_integral()), false);
|
||||||
shift_register_ = next_output_;
|
shift_register_ = next_output_;
|
||||||
write_handshake_ |= 0x80 | 0x40;
|
write_handshake_ |= 0x80 | 0x40;
|
||||||
output_bits_remaining_ = 8;
|
output_bits_remaining_ = 8;
|
||||||
@@ -351,12 +360,28 @@ void IWM::propose_shift(uint8_t bit) {
|
|||||||
// TODO: synchronous mode.
|
// TODO: synchronous mode.
|
||||||
|
|
||||||
// LOG("Shifting input");
|
// LOG("Shifting input");
|
||||||
|
|
||||||
|
// See above for text from the IWM patent, column 7, around line 35 onwards.
|
||||||
|
// The error_margin here implements the 'before' part of that contract.
|
||||||
|
//
|
||||||
|
// Basic effective logic: if at least 1 is fozund in the bit_length_ cycles centred
|
||||||
|
// on the current expected bit delivery time as implied by cycles_since_shift_,
|
||||||
|
// shift in a 1 and start a new window wherever the first found 1 was.
|
||||||
|
//
|
||||||
|
// If no 1s are found, shift in a 0 and don't alter expectations as to window placement.
|
||||||
|
const auto error_margin = Cycles(bit_length_.as_integral() >> 1);
|
||||||
|
if(bit && cycles_since_shift_ < error_margin) return;
|
||||||
|
|
||||||
shift_register_ = uint8_t((shift_register_ << 1) | bit);
|
shift_register_ = uint8_t((shift_register_ << 1) | bit);
|
||||||
if(shift_register_ & 0x80) {
|
if(shift_register_ & 0x80) {
|
||||||
data_register_ = shift_register_;
|
data_register_ = shift_register_;
|
||||||
shift_register_ = 0;
|
shift_register_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(bit)
|
||||||
cycles_since_shift_ = Cycles(0);
|
cycles_since_shift_ = Cycles(0);
|
||||||
|
else
|
||||||
|
cycles_since_shift_ -= bit_length_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IWM::set_drive(int slot, IWMDrive *drive) {
|
void IWM::set_drive(int slot, IWMDrive *drive) {
|
||||||
@@ -374,3 +399,8 @@ void IWM::set_component_prefers_clocking(ClockingHint::Source *component, Clocki
|
|||||||
drive_is_rotating_[1] = is_rotating;
|
drive_is_rotating_[1] = is_rotating;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IWM::set_activity_observer(Activity::Observer *observer) {
|
||||||
|
if(drives_[0]) drives_[0]->set_activity_observer(observer, "Internal Floppy", true);
|
||||||
|
if(drives_[1]) drives_[1]->set_activity_observer(observer, "External Floppy", true);
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
#ifndef IWM_hpp
|
#ifndef IWM_hpp
|
||||||
#define IWM_hpp
|
#define IWM_hpp
|
||||||
|
|
||||||
|
#include "../../Activity/Observer.hpp"
|
||||||
|
|
||||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||||
#include "../../ClockReceiver/ClockingHintSource.hpp"
|
#include "../../ClockReceiver/ClockingHintSource.hpp"
|
||||||
|
|
||||||
#include "../../Storage/Disk/Drive.hpp"
|
#include "../../Storage/Disk/Drive.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -67,9 +70,13 @@ class IWM:
|
|||||||
/// Connects a drive to the IWM.
|
/// Connects a drive to the IWM.
|
||||||
void set_drive(int slot, IWMDrive *drive);
|
void set_drive(int slot, IWMDrive *drive);
|
||||||
|
|
||||||
|
/// Registers the currently-connected drives as @c Activity::Sources ;
|
||||||
|
/// the first will be declared 'Internal', the second 'External'.
|
||||||
|
void set_activity_observer(Activity::Observer *observer);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Storage::Disk::Drive::EventDelegate.
|
// 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_;
|
const int clock_rate_;
|
||||||
|
|
||||||
@@ -84,7 +91,7 @@ class IWM:
|
|||||||
IWMDrive *drives_[2] = {nullptr, nullptr};
|
IWMDrive *drives_[2] = {nullptr, nullptr};
|
||||||
bool drive_is_rotating_[2] = {false, false};
|
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_;
|
Cycles cycles_until_disable_;
|
||||||
uint8_t write_handshake_ = 0x80;
|
uint8_t write_handshake_ = 0x80;
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ void DoubleDensityDrive::set_rotation_speed(float revolutions_per_minute) {
|
|||||||
|
|
||||||
// MARK: - Control input/output.
|
// MARK: - Control input/output.
|
||||||
|
|
||||||
void DoubleDensityDrive::set_enabled(bool) {
|
void DoubleDensityDrive::set_enabled(bool enabled) {
|
||||||
|
// Disabling a drive also stops its motor.
|
||||||
|
if(!enabled) set_motor_on(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DoubleDensityDrive::set_control_lines(int lines) {
|
void DoubleDensityDrive::set_control_lines(int lines) {
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ class DoubleDensityDrive: public IWMDrive {
|
|||||||
*/
|
*/
|
||||||
void set_rotation_speed(float revolutions_per_minute);
|
void set_rotation_speed(float revolutions_per_minute);
|
||||||
|
|
||||||
void set_enabled(bool) override;
|
|
||||||
void set_control_lines(int) override;
|
|
||||||
bool read() override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void set_enabled(bool) final;
|
||||||
|
void set_control_lines(int) final;
|
||||||
|
bool read() final;
|
||||||
|
|
||||||
// To receive the proper notifications from Storage::Disk::Drive.
|
// To receive the proper notifications from Storage::Disk::Drive.
|
||||||
void did_step(Storage::Disk::HeadPosition to_position) override;
|
void did_step(Storage::Disk::HeadPosition to_position) final;
|
||||||
void did_set_disk() override;
|
void did_set_disk() final;
|
||||||
|
|
||||||
const bool is_800k_;
|
const bool is_800k_;
|
||||||
bool has_new_disk_ = false;
|
bool has_new_disk_ = false;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ void SCC::write(uint16_t address, uint8_t value) {
|
|||||||
address &= 0xff;
|
address &= 0xff;
|
||||||
if(address < 0x80) ram_[address] = value;
|
if(address < 0x80) ram_[address] = value;
|
||||||
|
|
||||||
task_queue_.defer([=] {
|
task_queue_.defer([this, address, value] {
|
||||||
// Check for a write into waveform memory.
|
// Check for a write into waveform memory.
|
||||||
if(address < 0x80) {
|
if(address < 0x80) {
|
||||||
waves_[address >> 5].samples[address & 0x1f] = value;
|
waves_[address >> 5].samples[address & 0x1f] = value;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
|
|||||||
/// As per ::SampleSource; provides audio output.
|
/// As per ::SampleSource; provides audio output.
|
||||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||||
void set_sample_volume_range(std::int16_t range);
|
void set_sample_volume_range(std::int16_t range);
|
||||||
|
static constexpr bool get_is_stereo() { return false; }
|
||||||
|
|
||||||
/// Writes to the SCC.
|
/// Writes to the SCC.
|
||||||
void write(uint16_t address, uint8_t value);
|
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();
|
evaluate_output_volume();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SN76489::set_register(uint8_t value) {
|
void SN76489::write(uint8_t value) {
|
||||||
task_queue_.defer([value, this] () {
|
task_queue_.defer([value, this] () {
|
||||||
if(value & 0x80) {
|
if(value & 0x80) {
|
||||||
active_register_ = value;
|
active_register_ = value;
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ class SN76489: public Outputs::Speaker::SampleSource {
|
|||||||
SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1);
|
SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1);
|
||||||
|
|
||||||
/// Writes a new value to the SN76489.
|
/// Writes a new value to the SN76489.
|
||||||
void set_register(uint8_t value);
|
void write(uint8_t value);
|
||||||
|
|
||||||
// As per SampleSource.
|
// As per SampleSource.
|
||||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||||
bool is_zero_level();
|
bool is_zero_level();
|
||||||
void set_sample_volume_range(std::int16_t range);
|
void set_sample_volume_range(std::int16_t range);
|
||||||
|
static constexpr bool get_is_stereo() { return false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int master_divider_ = 0;
|
int master_divider_ = 0;
|
||||||
|
|||||||
140
Components/Serial/Line.cpp
Normal file
140
Components/Serial/Line.cpp
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
//
|
||||||
|
// SerialPort.cpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 12/10/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "Line.hpp"
|
||||||
|
|
||||||
|
using namespace Serial;
|
||||||
|
|
||||||
|
void Line::set_writer_clock_rate(HalfCycles clock_rate) {
|
||||||
|
clock_rate_ = clock_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Line::advance_writer(HalfCycles cycles) {
|
||||||
|
if(cycles == HalfCycles(0)) return;
|
||||||
|
|
||||||
|
const auto integral_cycles = cycles.as_integral();
|
||||||
|
remaining_delays_ = std::max(remaining_delays_ - integral_cycles, Cycles::IntType(0));
|
||||||
|
if(events_.empty()) {
|
||||||
|
write_cycles_since_delegate_call_ += integral_cycles;
|
||||||
|
if(transmission_extra_) {
|
||||||
|
transmission_extra_ -= integral_cycles;
|
||||||
|
if(transmission_extra_ <= 0) {
|
||||||
|
transmission_extra_ = 0;
|
||||||
|
update_delegate(level_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while(!events_.empty()) {
|
||||||
|
if(events_.front().delay <= integral_cycles) {
|
||||||
|
cycles -= events_.front().delay;
|
||||||
|
write_cycles_since_delegate_call_ += events_.front().delay;
|
||||||
|
const auto old_level = level_;
|
||||||
|
|
||||||
|
auto iterator = events_.begin() + 1;
|
||||||
|
while(iterator != events_.end() && iterator->type != Event::Delay) {
|
||||||
|
level_ = iterator->type == Event::SetHigh;
|
||||||
|
++iterator;
|
||||||
|
}
|
||||||
|
events_.erase(events_.begin(), iterator);
|
||||||
|
|
||||||
|
if(old_level != level_) {
|
||||||
|
update_delegate(old_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Book enough extra time for the read delegate to be posted
|
||||||
|
// the final bit if one is attached.
|
||||||
|
if(events_.empty()) {
|
||||||
|
transmission_extra_ = minimum_write_cycles_for_read_delegate_bit();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
events_.front().delay -= integral_cycles;
|
||||||
|
write_cycles_since_delegate_call_ += integral_cycles;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Line::write(bool level) {
|
||||||
|
if(!events_.empty()) {
|
||||||
|
events_.emplace_back();
|
||||||
|
events_.back().type = level ? Event::SetHigh : Event::SetLow;
|
||||||
|
} else {
|
||||||
|
level_ = level;
|
||||||
|
transmission_extra_ = minimum_write_cycles_for_read_delegate_bit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Line::write(HalfCycles cycles, int count, int levels) {
|
||||||
|
remaining_delays_ += count * cycles.as_integral();
|
||||||
|
|
||||||
|
auto event = events_.size();
|
||||||
|
events_.resize(events_.size() + size_t(count)*2);
|
||||||
|
while(count--) {
|
||||||
|
events_[event].type = Event::Delay;
|
||||||
|
events_[event].delay = int(cycles.as_integral());
|
||||||
|
events_[event+1].type = (levels&1) ? Event::SetHigh : Event::SetLow;
|
||||||
|
levels >>= 1;
|
||||||
|
event += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Line::reset_writing() {
|
||||||
|
remaining_delays_ = 0;
|
||||||
|
events_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Line::read() {
|
||||||
|
return level_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Line::set_read_delegate(ReadDelegate *delegate, Storage::Time bit_length) {
|
||||||
|
read_delegate_ = delegate;
|
||||||
|
read_delegate_bit_length_ = bit_length;
|
||||||
|
read_delegate_bit_length_.simplify();
|
||||||
|
write_cycles_since_delegate_call_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Line::update_delegate(bool level) {
|
||||||
|
// Exit early if there's no delegate, or if the delegate is waiting for
|
||||||
|
// zero and this isn't zero.
|
||||||
|
if(!read_delegate_) return;
|
||||||
|
|
||||||
|
const int cycles_to_forward = write_cycles_since_delegate_call_;
|
||||||
|
write_cycles_since_delegate_call_ = 0;
|
||||||
|
if(level && read_delegate_phase_ == ReadDelegatePhase::WaitingForZero) return;
|
||||||
|
|
||||||
|
// Deal with a transition out of waiting-for-zero mode by seeding time left
|
||||||
|
// in bit at half a bit.
|
||||||
|
if(read_delegate_phase_ == ReadDelegatePhase::WaitingForZero) {
|
||||||
|
time_left_in_bit_ = read_delegate_bit_length_;
|
||||||
|
time_left_in_bit_.clock_rate <<= 1;
|
||||||
|
read_delegate_phase_ = ReadDelegatePhase::Serialising;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward as many bits as occur.
|
||||||
|
Storage::Time time_left(cycles_to_forward, int(clock_rate_.as_integral()));
|
||||||
|
const int bit = level ? 1 : 0;
|
||||||
|
int bits = 0;
|
||||||
|
while(time_left >= time_left_in_bit_) {
|
||||||
|
++bits;
|
||||||
|
if(!read_delegate_->serial_line_did_produce_bit(this, bit)) {
|
||||||
|
read_delegate_phase_ = ReadDelegatePhase::WaitingForZero;
|
||||||
|
if(bit) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_left -= time_left_in_bit_;
|
||||||
|
time_left_in_bit_ = read_delegate_bit_length_;
|
||||||
|
}
|
||||||
|
time_left_in_bit_ -= time_left;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cycles::IntType Line::minimum_write_cycles_for_read_delegate_bit() {
|
||||||
|
if(!read_delegate_) return 0;
|
||||||
|
return 1 + (read_delegate_bit_length_ * static_cast<unsigned int>(clock_rate_.as_integral())).get<int>();
|
||||||
|
}
|
||||||
112
Components/Serial/Line.hpp
Normal file
112
Components/Serial/Line.hpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// SerialPort.hpp
|
||||||
|
// Clock Signal
|
||||||
|
//
|
||||||
|
// Created by Thomas Harte on 12/10/2019.
|
||||||
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SerialPort_hpp
|
||||||
|
#define SerialPort_hpp
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include "../../Storage/Storage.hpp"
|
||||||
|
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||||
|
#include "../../ClockReceiver/ForceInline.hpp"
|
||||||
|
|
||||||
|
namespace Serial {
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@c Line connects a single reader and a single writer, allowing timestamped events to be
|
||||||
|
published and consumed, potentially with a clock conversion in between. It allows line
|
||||||
|
levels to be written and read in larger collections.
|
||||||
|
|
||||||
|
It is assumed that the owner of the reader and writer will ensure that the reader will never
|
||||||
|
get ahead of the writer. If the writer posts events behind the reader they will simply be
|
||||||
|
given instanteous effect.
|
||||||
|
*/
|
||||||
|
class Line {
|
||||||
|
public:
|
||||||
|
void set_writer_clock_rate(HalfCycles clock_rate);
|
||||||
|
|
||||||
|
/// Advances the read position by @c cycles relative to the writer's
|
||||||
|
/// clock rate.
|
||||||
|
void advance_writer(HalfCycles cycles);
|
||||||
|
|
||||||
|
/// Sets the line to @c level.
|
||||||
|
void write(bool level);
|
||||||
|
|
||||||
|
/// Enqueues @c count level changes, the first occurring immediately
|
||||||
|
/// after the final event currently posted and each subsequent event
|
||||||
|
/// occurring @c cycles after the previous. An additional gap of @c cycles
|
||||||
|
/// is scheduled after the final output. The levels to output are
|
||||||
|
/// taken from @c levels which is read from lsb to msb. @c cycles is
|
||||||
|
/// relative to the writer's clock rate.
|
||||||
|
void write(HalfCycles cycles, int count, int levels);
|
||||||
|
|
||||||
|
/// @returns the number of cycles until currently enqueued write data is exhausted.
|
||||||
|
forceinline HalfCycles write_data_time_remaining() const {
|
||||||
|
return HalfCycles(remaining_delays_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @returns the number of cycles left until it is guaranteed that a passive reader
|
||||||
|
/// has received all currently-enqueued bits.
|
||||||
|
forceinline HalfCycles transmission_data_time_remaining() const {
|
||||||
|
return HalfCycles(remaining_delays_ + transmission_extra_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eliminates all future write states, leaving the output at whatever it is now.
|
||||||
|
void reset_writing();
|
||||||
|
|
||||||
|
/// @returns The instantaneous level of this line.
|
||||||
|
bool read();
|
||||||
|
|
||||||
|
struct ReadDelegate {
|
||||||
|
virtual bool serial_line_did_produce_bit(Line *line, int bit) = 0;
|
||||||
|
};
|
||||||
|
/*!
|
||||||
|
Sets a read delegate, which will receive samples of the output level every
|
||||||
|
@c bit_lengths of a second apart subject to a state machine:
|
||||||
|
|
||||||
|
* initially no bits will be delivered;
|
||||||
|
* when a zero level is first detected, the line will wait half a bit's length, then start
|
||||||
|
sampling at single-bit intervals, passing each bit to the delegate while it returns @c true;
|
||||||
|
* as soon as the delegate returns @c false, the line will return to the initial state.
|
||||||
|
*/
|
||||||
|
void set_read_delegate(ReadDelegate *delegate, Storage::Time bit_length);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Event {
|
||||||
|
enum Type {
|
||||||
|
Delay, SetHigh, SetLow
|
||||||
|
} type;
|
||||||
|
int delay;
|
||||||
|
};
|
||||||
|
std::vector<Event> events_;
|
||||||
|
HalfCycles::IntType remaining_delays_ = 0;
|
||||||
|
HalfCycles::IntType transmission_extra_ = 0;
|
||||||
|
bool level_ = true;
|
||||||
|
HalfCycles clock_rate_ = 0;
|
||||||
|
|
||||||
|
ReadDelegate *read_delegate_ = nullptr;
|
||||||
|
Storage::Time read_delegate_bit_length_, time_left_in_bit_;
|
||||||
|
int write_cycles_since_delegate_call_ = 0;
|
||||||
|
enum class ReadDelegatePhase {
|
||||||
|
WaitingForZero,
|
||||||
|
Serialising
|
||||||
|
} read_delegate_phase_ = ReadDelegatePhase::WaitingForZero;
|
||||||
|
|
||||||
|
void update_delegate(bool level);
|
||||||
|
HalfCycles::IntType minimum_write_cycles_for_read_delegate_bit();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Defines an RS-232-esque srial port.
|
||||||
|
*/
|
||||||
|
class Port {
|
||||||
|
public:
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* SerialPort_hpp */
|
||||||
@@ -18,7 +18,7 @@ AsyncTaskQueue::AsyncTaskQueue()
|
|||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL);
|
serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL);
|
||||||
#else
|
#else
|
||||||
thread_.reset(new std::thread([this]() {
|
thread_ = std::make_unique<std::thread>([this]() {
|
||||||
while(!should_destruct_) {
|
while(!should_destruct_) {
|
||||||
std::function<void(void)> next_function;
|
std::function<void(void)> next_function;
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ AsyncTaskQueue::AsyncTaskQueue()
|
|||||||
processing_condition_.wait(lock);
|
processing_condition_.wait(lock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +70,8 @@ void AsyncTaskQueue::flush() {
|
|||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
dispatch_sync(serial_dispatch_queue_, ^{});
|
dispatch_sync(serial_dispatch_queue_, ^{});
|
||||||
#else
|
#else
|
||||||
std::shared_ptr<std::mutex> flush_mutex(new std::mutex);
|
auto flush_mutex = std::make_shared<std::mutex>();
|
||||||
std::shared_ptr<std::condition_variable> flush_condition(new std::condition_variable);
|
auto flush_condition = std::make_shared<std::condition_variable>();
|
||||||
std::unique_lock<std::mutex> lock(*flush_mutex);
|
std::unique_lock<std::mutex> lock(*flush_mutex);
|
||||||
enqueue([=] () {
|
enqueue([=] () {
|
||||||
std::unique_lock<std::mutex> inner_lock(*flush_mutex);
|
std::unique_lock<std::mutex> inner_lock(*flush_mutex);
|
||||||
@@ -88,7 +88,7 @@ DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() {
|
|||||||
|
|
||||||
void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
|
void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
|
||||||
if(!deferred_tasks_) {
|
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);
|
deferred_tasks_->push_back(function);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,61 +12,89 @@
|
|||||||
|
|
||||||
using namespace Concurrency;
|
using namespace Concurrency;
|
||||||
|
|
||||||
BestEffortUpdater::BestEffortUpdater() {
|
BestEffortUpdater::BestEffortUpdater() :
|
||||||
// ATOMIC_FLAG_INIT isn't necessarily safe to use, so establish default state by other means.
|
update_thread_([this]() {
|
||||||
update_is_ongoing_.clear();
|
this->update_loop();
|
||||||
}
|
}) {}
|
||||||
|
|
||||||
BestEffortUpdater::~BestEffortUpdater() {
|
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();
|
flush();
|
||||||
|
|
||||||
|
// Wind up the update thread.
|
||||||
|
should_quit_ = true;
|
||||||
|
update();
|
||||||
|
update_thread_.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BestEffortUpdater::update() {
|
void BestEffortUpdater::update(int flags) {
|
||||||
// Perform an update only if one is not currently ongoing.
|
// Bump the requested target time and set the update requested flag.
|
||||||
if(!update_is_ongoing_.test_and_set()) {
|
{
|
||||||
async_task_queue_.enqueue([this]() {
|
std::lock_guard<decltype(update_mutex_)> lock(update_mutex_);
|
||||||
// Get time now using the highest-resolution clock provided by the implementation, and determine
|
has_skipped_ = update_requested_;
|
||||||
// the duration since the last time this section was entered.
|
update_requested_ = true;
|
||||||
const std::chrono::time_point<std::chrono::high_resolution_clock> now = std::chrono::high_resolution_clock::now();
|
flags_ |= flags;
|
||||||
const auto elapsed = now - previous_time_point_;
|
target_time_ = std::chrono::high_resolution_clock::now().time_since_epoch().count();
|
||||||
previous_time_point_ = now;
|
}
|
||||||
|
update_condition_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
if(has_previous_time_point_) {
|
void BestEffortUpdater::update_loop() {
|
||||||
// If the duration is valid, convert it to integer cycles, maintaining a rolling error and call the delegate
|
while(true) {
|
||||||
// if there is one. Proceed only if the number of cycles is positive, and cap it to the per-second maximum as
|
std::unique_lock<decltype(update_mutex_)> lock(update_mutex_);
|
||||||
// it's possible this is an adjustable clock so be ready to swallow unexpected adjustments.
|
is_updating_ = false;
|
||||||
const int64_t integer_duration = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
|
|
||||||
if(integer_duration > 0) {
|
// Wait to be signalled.
|
||||||
if(delegate_) {
|
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
|
// Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
|
||||||
// brief system interruption.
|
// brief system interruption.
|
||||||
const double duration = std::min(static_cast<double>(integer_duration) / 1e9, 0.2);
|
const double duration = std::min(double(integer_duration) / 1e9, 0.2);
|
||||||
delegate_->update(this, duration, has_skipped_);
|
const double elapsed_duration = delegate->update(this, duration, has_skipped_, flags);
|
||||||
}
|
|
||||||
|
previous_time_point_ += int64_t(elapsed_duration * 1e9);
|
||||||
has_skipped_ = false;
|
has_skipped_ = false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
has_previous_time_point_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow furthers updates to occur.
|
|
||||||
update_is_ongoing_.clear();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
async_task_queue_.enqueue([this]() {
|
|
||||||
has_skipped_ = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BestEffortUpdater::flush() {
|
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) {
|
void BestEffortUpdater::set_delegate(Delegate *const delegate) {
|
||||||
async_task_queue_.enqueue([this, delegate]() {
|
delegate_.store(delegate);
|
||||||
delegate_ = delegate;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "AsyncTaskQueue.hpp"
|
|
||||||
#include "../ClockReceiver/TimeTypes.hpp"
|
#include "../ClockReceiver/TimeTypes.hpp"
|
||||||
|
|
||||||
namespace Concurrency {
|
namespace Concurrency {
|
||||||
@@ -31,7 +33,13 @@ class BestEffortUpdater {
|
|||||||
|
|
||||||
/// A delegate receives timing cues.
|
/// A delegate receives timing cues.
|
||||||
struct Delegate {
|
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.
|
/// 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.
|
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.
|
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();
|
void flush();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::atomic_flag update_is_ongoing_;
|
std::atomic<bool> should_quit_;
|
||||||
AsyncTaskQueue async_task_queue_;
|
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_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,16 +14,16 @@ namespace {
|
|||||||
Appends a Boolean selection of @c selection for option @c name to @c selection_set.
|
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) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
Enquires for a Boolean selection for option @c name from @c selections_by_option, storing it to @c result if found.
|
Enquires for a Boolean selection for option @c name from @c selections_by_option, storing it to @c result if found.
|
||||||
*/
|
*/
|
||||||
bool get_bool(const Configurable::SelectionSet &selections_by_option, const std::string &name, bool &result) {
|
bool get_bool(const Configurable::SelectionSet &selections_by_option, const std::string &name, bool &result) {
|
||||||
auto quickload = Configurable::selection<Configurable::BooleanSelection>(selections_by_option, "quickload");
|
auto selection = Configurable::selection<Configurable::BooleanSelection>(selections_by_option, name);
|
||||||
if(!quickload) return false;
|
if(!selection) return false;
|
||||||
result = quickload->value;
|
result = selection->value;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@ std::vector<std::unique_ptr<Configurable::Option>> Configurable::standard_option
|
|||||||
options.emplace_back(new Configurable::ListOption("Display", "display", display_options));
|
options.emplace_back(new Configurable::ListOption("Display", "display", display_options));
|
||||||
}
|
}
|
||||||
if(mask & AutomaticTapeMotorControl) options.emplace_back(new Configurable::BooleanOption("Automatic Tape Motor Control", "autotapemotor"));
|
if(mask & AutomaticTapeMotorControl) options.emplace_back(new Configurable::BooleanOption("Automatic Tape Motor Control", "autotapemotor"));
|
||||||
|
if(mask & QuickBoot) options.emplace_back(new Configurable::BooleanOption("Boot Quickly", "quickboot"));
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +64,11 @@ void Configurable::append_display_selection(Configurable::SelectionSet &selectio
|
|||||||
case Display::CompositeMonochrome: string_selection = "composite-mono"; break;
|
case Display::CompositeMonochrome: string_selection = "composite-mono"; break;
|
||||||
case Display::CompositeColour: string_selection = "composite"; 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) {
|
||||||
|
append_bool(selection_set, "quickboot", selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Selection parsers
|
// MARK: - Selection parsers
|
||||||
@@ -97,3 +102,7 @@ bool Configurable::get_display(const Configurable::SelectionSet &selections_by_o
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Configurable::get_quick_boot(const Configurable::SelectionSet &selections_by_option, bool &result) {
|
||||||
|
return get_bool(selections_by_option, "quickboot", result);
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ enum StandardOptions {
|
|||||||
DisplayCompositeColour = (1 << 2),
|
DisplayCompositeColour = (1 << 2),
|
||||||
DisplayCompositeMonochrome = (1 << 3),
|
DisplayCompositeMonochrome = (1 << 3),
|
||||||
QuickLoadTape = (1 << 4),
|
QuickLoadTape = (1 << 4),
|
||||||
AutomaticTapeMotorControl = (1 << 5)
|
AutomaticTapeMotorControl = (1 << 5),
|
||||||
|
QuickBoot = (1 << 6),
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class Display {
|
enum class Display {
|
||||||
@@ -49,6 +50,11 @@ void append_automatic_tape_motor_control_selection(SelectionSet &selection_set,
|
|||||||
*/
|
*/
|
||||||
void append_display_selection(SelectionSet &selection_set, Display selection);
|
void append_display_selection(SelectionSet &selection_set, Display selection);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Appends to @c selection_set a selection of @c selection for QuickBoot.
|
||||||
|
*/
|
||||||
|
void append_quick_boot_selection(SelectionSet &selection_set, bool selection);
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
Attempts to discern a QuickLoadTape selection from @c selections_by_option.
|
Attempts to discern a QuickLoadTape selection from @c selections_by_option.
|
||||||
|
|
||||||
@@ -76,6 +82,15 @@ bool get_automatic_tape_motor_control_selection(const SelectionSet &selections_b
|
|||||||
*/
|
*/
|
||||||
bool get_display(const SelectionSet &selections_by_option, Display &result);
|
bool get_display(const SelectionSet &selections_by_option, Display &result);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Attempts to QuickBoot a QuickLoadTape selection from @c selections_by_option.
|
||||||
|
|
||||||
|
@param selections_by_option The user selections.
|
||||||
|
@param result The location to which the selection will be stored if found.
|
||||||
|
@returns @c true if a selection is found; @c false otherwise.
|
||||||
|
*/
|
||||||
|
bool get_quick_boot(const SelectionSet &selections_by_option, bool &result);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* StandardOptions_hpp */
|
#endif /* StandardOptions_hpp */
|
||||||
|
|||||||
@@ -170,11 +170,11 @@ class ConcreteJoystick: public Joystick {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Input> &get_inputs() override final {
|
std::vector<Input> &get_inputs() final {
|
||||||
return inputs_;
|
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 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) {
|
if(input.is_button() || stick_types_[input.info.control.index] == StickType::Digital) {
|
||||||
did_set_input(input, is_active);
|
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 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) {
|
if(!input.is_button() && stick_types_[input.info.control.index] == StickType::Analogue) {
|
||||||
did_set_input(input, value);
|
did_set_input(input, value);
|
||||||
|
|||||||
@@ -10,13 +10,14 @@
|
|||||||
|
|
||||||
using namespace Inputs;
|
using namespace Inputs;
|
||||||
|
|
||||||
Keyboard::Keyboard() {
|
Keyboard::Keyboard(const std::set<Key> &essential_modifiers) : essential_modifiers_(essential_modifiers) {
|
||||||
for(int k = 0; k < int(Key::Help); ++k) {
|
for(int k = 0; k < int(Key::Help); ++k) {
|
||||||
observed_keys_.insert(Key(k));
|
observed_keys_.insert(Key(k));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keyboard::Keyboard(const std::set<Key> &observed_keys) : observed_keys_(observed_keys), is_exclusive_(false) {}
|
Keyboard::Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers) :
|
||||||
|
observed_keys_(observed_keys), essential_modifiers_(essential_modifiers), is_exclusive_(false) {}
|
||||||
|
|
||||||
void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
||||||
std::size_t key_offset = static_cast<std::size_t>(key);
|
std::size_t key_offset = static_cast<std::size_t>(key);
|
||||||
@@ -28,6 +29,10 @@ void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
|||||||
if(delegate_) delegate_->keyboard_did_change_key(this, key, is_pressed);
|
if(delegate_) delegate_->keyboard_did_change_key(this, key, is_pressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::set<Inputs::Keyboard::Key> &Keyboard::get_essential_modifiers() {
|
||||||
|
return essential_modifiers_;
|
||||||
|
}
|
||||||
|
|
||||||
void Keyboard::reset_all_keys() {
|
void Keyboard::reset_all_keys() {
|
||||||
std::fill(key_states_.begin(), key_states_.end(), false);
|
std::fill(key_states_.begin(), key_states_.end(), false);
|
||||||
if(delegate_) delegate_->reset_all_keys(this);
|
if(delegate_) delegate_->reset_all_keys(this);
|
||||||
|
|||||||
@@ -23,26 +23,26 @@ class Keyboard {
|
|||||||
public:
|
public:
|
||||||
enum class Key {
|
enum class Key {
|
||||||
Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, PrintScreen, ScrollLock, Pause,
|
Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, PrintScreen, ScrollLock, Pause,
|
||||||
BackTick, k1, k2, k3, k4, k5, k6, k7, k8, k9, k0, Hyphen, Equals, BackSpace,
|
BackTick, k1, k2, k3, k4, k5, k6, k7, k8, k9, k0, Hyphen, Equals, Backspace,
|
||||||
Tab, Q, W, E, R, T, Y, U, I, O, P, OpenSquareBracket, CloseSquareBracket, BackSlash,
|
Tab, Q, W, E, R, T, Y, U, I, O, P, OpenSquareBracket, CloseSquareBracket, Backslash,
|
||||||
CapsLock, A, S, D, F, G, H, J, K, L, Semicolon, Quote, Hash, Enter,
|
CapsLock, A, S, D, F, G, H, J, K, L, Semicolon, Quote, Hash, Enter,
|
||||||
LeftShift, Z, X, C, V, B, N, M, Comma, FullStop, ForwardSlash, RightShift,
|
LeftShift, Z, X, C, V, B, N, M, Comma, FullStop, ForwardSlash, RightShift,
|
||||||
LeftControl, LeftOption, LeftMeta, Space, RightMeta, RightOption, RightControl,
|
LeftControl, LeftOption, LeftMeta, Space, RightMeta, RightOption, RightControl,
|
||||||
Left, Right, Up, Down,
|
Left, Right, Up, Down,
|
||||||
Insert, Home, PageUp, Delete, End, PageDown,
|
Insert, Home, PageUp, Delete, End, PageDown,
|
||||||
NumLock, KeyPadSlash, KeyPadAsterisk, KeyPadDelete,
|
NumLock, KeypadSlash, KeypadAsterisk, KeypadDelete,
|
||||||
KeyPad7, KeyPad8, KeyPad9, KeyPadPlus,
|
Keypad7, Keypad8, Keypad9, KeypadPlus,
|
||||||
KeyPad4, KeyPad5, KeyPad6, KeyPadMinus,
|
Keypad4, Keypad5, Keypad6, KeypadMinus,
|
||||||
KeyPad1, KeyPad2, KeyPad3, KeyPadEnter,
|
Keypad1, Keypad2, Keypad3, KeypadEnter,
|
||||||
KeyPad0, KeyPadDecimalPoint, KeyPadEquals,
|
Keypad0, KeypadDecimalPoint, KeypadEquals,
|
||||||
Help
|
Help
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Constructs a Keyboard that declares itself to observe all keys.
|
/// Constructs a Keyboard that declares itself to observe all keys.
|
||||||
Keyboard();
|
Keyboard(const std::set<Key> &essential_modifiers = {});
|
||||||
|
|
||||||
/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys.
|
/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys.
|
||||||
Keyboard(const std::set<Key> &observed_keys);
|
Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers);
|
||||||
|
|
||||||
// Host interface.
|
// Host interface.
|
||||||
virtual void set_key_pressed(Key key, char value, bool is_pressed);
|
virtual void set_key_pressed(Key key, char value, bool is_pressed);
|
||||||
@@ -51,10 +51,18 @@ class Keyboard {
|
|||||||
/// @returns a set of all Keys that this keyboard responds to.
|
/// @returns a set of all Keys that this keyboard responds to.
|
||||||
virtual const std::set<Key> &observed_keys();
|
virtual const std::set<Key> &observed_keys();
|
||||||
|
|
||||||
/*
|
/// @returns the list of modifiers that this keyboard considers 'essential' (i.e. both mapped and highly used).
|
||||||
|
virtual const std::set<Inputs::Keyboard::Key> &get_essential_modifiers();
|
||||||
|
|
||||||
|
/*!
|
||||||
@returns @c true if this keyboard, on its original machine, looked
|
@returns @c true if this keyboard, on its original machine, looked
|
||||||
like a complete keyboard — i.e. if a user would expect this keyboard
|
like a complete keyboard — i.e. if a user would expect this keyboard
|
||||||
to be the only thing a real keyboard maps to.
|
to be the only thing a real keyboard maps to.
|
||||||
|
|
||||||
|
So this would be true of something like the Amstrad CPC, which has a full
|
||||||
|
keyboard, but it would be false of something like the Sega Master System
|
||||||
|
which has some buttons that you'd expect an emulator to map to its host
|
||||||
|
keyboard but which does not offer a full keyboard.
|
||||||
*/
|
*/
|
||||||
virtual bool is_exclusive();
|
virtual bool is_exclusive();
|
||||||
|
|
||||||
@@ -68,6 +76,7 @@ class Keyboard {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::set<Key> observed_keys_;
|
std::set<Key> observed_keys_;
|
||||||
|
std::set<Key> essential_modifiers_;
|
||||||
std::vector<bool> key_states_;
|
std::vector<bool> key_states_;
|
||||||
Delegate *delegate_ = nullptr;
|
Delegate *delegate_ = nullptr;
|
||||||
bool is_exclusive_ = true;
|
bool is_exclusive_ = true;
|
||||||
|
|||||||
@@ -34,24 +34,24 @@ class QuadratureMouse: public Mouse {
|
|||||||
/*
|
/*
|
||||||
Inputs, to satisfy the Mouse interface.
|
Inputs, to satisfy the Mouse interface.
|
||||||
*/
|
*/
|
||||||
void move(int x, int y) override {
|
void move(int x, int y) final {
|
||||||
// Accumulate all provided motion.
|
// Accumulate all provided motion.
|
||||||
axes_[0] += x;
|
axes_[0] += x;
|
||||||
axes_[1] += y;
|
axes_[1] += y;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get_number_of_buttons() override {
|
int get_number_of_buttons() final {
|
||||||
return number_of_buttons_;
|
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)
|
if(is_pressed)
|
||||||
button_flags_ |= (1 << index);
|
button_flags_ |= (1 << index);
|
||||||
else
|
else
|
||||||
button_flags_ &= ~(1 << index);
|
button_flags_ &= ~(1 << index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset_all_buttons() override {
|
void reset_all_buttons() final {
|
||||||
button_flags_ = 0;
|
button_flags_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace AmstradCPC {
|
|||||||
|
|
||||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||||
return Configurable::standard_options(
|
return Configurable::standard_options(
|
||||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour)
|
Configurable::StandardOptions(Configurable::DisplayRGB | Configurable::DisplayCompositeColour)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +124,11 @@ class InterruptTimer {
|
|||||||
class AYDeferrer {
|
class AYDeferrer {
|
||||||
public:
|
public:
|
||||||
/// Constructs a new AY instance and sets its clock rate.
|
/// 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);
|
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() {
|
~AYDeferrer() {
|
||||||
@@ -153,14 +156,14 @@ class AYDeferrer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @returns the AY itself.
|
/// @returns the AY itself.
|
||||||
GI::AY38910::AY38910 &ay() {
|
GI::AY38910::AY38910<true> &ay() {
|
||||||
return ay_;
|
return ay_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||||
GI::AY38910::AY38910 ay_;
|
GI::AY38910::AY38910<true> ay_;
|
||||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
|
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910<true>> speaker_;
|
||||||
HalfCycles cycles_since_update_;
|
HalfCycles cycles_since_update_;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,7 +174,7 @@ class AYDeferrer {
|
|||||||
*/
|
*/
|
||||||
class CRTCBusHandler {
|
class CRTCBusHandler {
|
||||||
public:
|
public:
|
||||||
CRTCBusHandler(uint8_t *ram, InterruptTimer &interrupt_timer) :
|
CRTCBusHandler(const uint8_t *ram, InterruptTimer &interrupt_timer) :
|
||||||
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2),
|
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2),
|
||||||
ram_(ram),
|
ram_(ram),
|
||||||
interrupt_timer_(interrupt_timer) {
|
interrupt_timer_(interrupt_timer) {
|
||||||
@@ -249,44 +252,46 @@ class CRTCBusHandler {
|
|||||||
// the CPC shuffles output lines as:
|
// the CPC shuffles output lines as:
|
||||||
// MA13 MA12 RA2 RA1 RA0 MA9 MA8 MA7 MA6 MA5 MA4 MA3 MA2 MA1 MA0 CCLK
|
// MA13 MA12 RA2 RA1 RA0 MA9 MA8 MA7 MA6 MA5 MA4 MA3 MA2 MA1 MA0 CCLK
|
||||||
// ... so form the real access address.
|
// ... so form the real access address.
|
||||||
uint16_t address =
|
const uint16_t address =
|
||||||
static_cast<uint16_t>(
|
uint16_t(
|
||||||
((state.refresh_address & 0x3ff) << 1) |
|
((state.refresh_address & 0x3ff) << 1) |
|
||||||
((state.row_address & 0x7) << 11) |
|
((state.row_address & 0x7) << 11) |
|
||||||
((state.refresh_address & 0x3000) << 2)
|
((state.refresh_address & 0x3000) << 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
// fetch two bytes and translate into pixels
|
// Fetch two bytes and translate into pixels. Guaranteed: the mode can change only at
|
||||||
|
// hsync, so there's no risk of pixel_pointer_ overrunning 320 output pixels without
|
||||||
|
// exactly reaching 320 output pixels.
|
||||||
switch(mode_) {
|
switch(mode_) {
|
||||||
case 0:
|
case 0:
|
||||||
reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode0_output_[ram_[address]];
|
reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode0_output_[ram_[address]];
|
||||||
reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode0_output_[ram_[address+1]];
|
reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode0_output_[ram_[address+1]];
|
||||||
pixel_pointer_ += 4;
|
pixel_pointer_ += 2 * sizeof(uint16_t);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
reinterpret_cast<uint32_t *>(pixel_pointer_)[0] = mode1_output_[ram_[address]];
|
reinterpret_cast<uint32_t *>(pixel_pointer_)[0] = mode1_output_[ram_[address]];
|
||||||
reinterpret_cast<uint32_t *>(pixel_pointer_)[1] = mode1_output_[ram_[address+1]];
|
reinterpret_cast<uint32_t *>(pixel_pointer_)[1] = mode1_output_[ram_[address+1]];
|
||||||
pixel_pointer_ += 8;
|
pixel_pointer_ += 2 * sizeof(uint32_t);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
reinterpret_cast<uint64_t *>(pixel_pointer_)[0] = mode2_output_[ram_[address]];
|
reinterpret_cast<uint64_t *>(pixel_pointer_)[0] = mode2_output_[ram_[address]];
|
||||||
reinterpret_cast<uint64_t *>(pixel_pointer_)[1] = mode2_output_[ram_[address+1]];
|
reinterpret_cast<uint64_t *>(pixel_pointer_)[1] = mode2_output_[ram_[address+1]];
|
||||||
pixel_pointer_ += 16;
|
pixel_pointer_ += 2 * sizeof(uint64_t);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode3_output_[ram_[address]];
|
reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode3_output_[ram_[address]];
|
||||||
reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode3_output_[ram_[address+1]];
|
reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode3_output_[ram_[address+1]];
|
||||||
pixel_pointer_ += 4;
|
pixel_pointer_ += 2 * sizeof(uint16_t);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// flush the current buffer pixel if full; the CRTC allows many different display
|
// Flush the current buffer pixel if full; the CRTC allows many different display
|
||||||
// widths so it's not necessarily possible to predict the correct number in advance
|
// widths so it's not necessarily possible to predict the correct number in advance
|
||||||
// and using the upper bound could lead to inefficient behaviour
|
// and using the upper bound could lead to inefficient behaviour.
|
||||||
if(pixel_pointer_ == pixel_data_ + 320) {
|
if(pixel_pointer_ == pixel_data_ + 320) {
|
||||||
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
|
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
|
||||||
pixel_pointer_ = pixel_data_ = nullptr;
|
pixel_pointer_ = pixel_data_ = nullptr;
|
||||||
@@ -333,6 +338,11 @@ class CRTCBusHandler {
|
|||||||
crt_.set_scan_target(scan_target);
|
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.
|
/// Sets the type of display.
|
||||||
void set_display_type(Outputs::Display::DisplayType display_type) {
|
void set_display_type(Outputs::Display::DisplayType display_type) {
|
||||||
crt_.set_display_type(display_type);
|
crt_.set_display_type(display_type);
|
||||||
@@ -369,9 +379,17 @@ class CRTCBusHandler {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void output_border(int length) {
|
void output_border(int length) {
|
||||||
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_.begin_data(1));
|
assert(length >= 0);
|
||||||
|
|
||||||
|
// A black border can be output via crt_.output_blank for a minor performance
|
||||||
|
// win; otherwise paint whatever the border colour really is.
|
||||||
|
if(border_) {
|
||||||
|
uint8_t *const colour_pointer = static_cast<uint8_t *>(crt_.begin_data(1));
|
||||||
if(colour_pointer) *colour_pointer = border_;
|
if(colour_pointer) *colour_pointer = border_;
|
||||||
crt_.output_level(length * 16);
|
crt_.output_level(length * 16);
|
||||||
|
} else {
|
||||||
|
crt_.output_blank(length * 16);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#define Mode0Colour0(c) ((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2)
|
#define Mode0Colour0(c) ((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2)
|
||||||
@@ -387,16 +405,16 @@ class CRTCBusHandler {
|
|||||||
|
|
||||||
void establish_palette_hits() {
|
void establish_palette_hits() {
|
||||||
for(int c = 0; c < 256; c++) {
|
for(int c = 0; c < 256; c++) {
|
||||||
mode0_palette_hits_[Mode0Colour0(c)].push_back(static_cast<uint8_t>(c));
|
mode0_palette_hits_[Mode0Colour0(c)].push_back(uint8_t(c));
|
||||||
mode0_palette_hits_[Mode0Colour1(c)].push_back(static_cast<uint8_t>(c));
|
mode0_palette_hits_[Mode0Colour1(c)].push_back(uint8_t(c));
|
||||||
|
|
||||||
mode1_palette_hits_[Mode1Colour0(c)].push_back(static_cast<uint8_t>(c));
|
mode1_palette_hits_[Mode1Colour0(c)].push_back(uint8_t(c));
|
||||||
mode1_palette_hits_[Mode1Colour1(c)].push_back(static_cast<uint8_t>(c));
|
mode1_palette_hits_[Mode1Colour1(c)].push_back(uint8_t(c));
|
||||||
mode1_palette_hits_[Mode1Colour2(c)].push_back(static_cast<uint8_t>(c));
|
mode1_palette_hits_[Mode1Colour2(c)].push_back(uint8_t(c));
|
||||||
mode1_palette_hits_[Mode1Colour3(c)].push_back(static_cast<uint8_t>(c));
|
mode1_palette_hits_[Mode1Colour3(c)].push_back(uint8_t(c));
|
||||||
|
|
||||||
mode3_palette_hits_[Mode3Colour0(c)].push_back(static_cast<uint8_t>(c));
|
mode3_palette_hits_[Mode3Colour0(c)].push_back(uint8_t(c));
|
||||||
mode3_palette_hits_[Mode3Colour1(c)].push_back(static_cast<uint8_t>(c));
|
mode3_palette_hits_[Mode3Colour1(c)].push_back(uint8_t(c));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +424,7 @@ class CRTCBusHandler {
|
|||||||
// Mode 0: abcdefgh -> [gcea] [hdfb]
|
// Mode 0: abcdefgh -> [gcea] [hdfb]
|
||||||
for(int c = 0; c < 256; c++) {
|
for(int c = 0; c < 256; c++) {
|
||||||
// prepare mode 0
|
// prepare mode 0
|
||||||
uint8_t *mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]);
|
uint8_t *const mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]);
|
||||||
mode0_pixels[0] = palette_[Mode0Colour0(c)];
|
mode0_pixels[0] = palette_[Mode0Colour0(c)];
|
||||||
mode0_pixels[1] = palette_[Mode0Colour1(c)];
|
mode0_pixels[1] = palette_[Mode0Colour1(c)];
|
||||||
}
|
}
|
||||||
@@ -415,7 +433,7 @@ class CRTCBusHandler {
|
|||||||
case 1:
|
case 1:
|
||||||
for(int c = 0; c < 256; c++) {
|
for(int c = 0; c < 256; c++) {
|
||||||
// prepare mode 1
|
// prepare mode 1
|
||||||
uint8_t *mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]);
|
uint8_t *const mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]);
|
||||||
mode1_pixels[0] = palette_[Mode1Colour0(c)];
|
mode1_pixels[0] = palette_[Mode1Colour0(c)];
|
||||||
mode1_pixels[1] = palette_[Mode1Colour1(c)];
|
mode1_pixels[1] = palette_[Mode1Colour1(c)];
|
||||||
mode1_pixels[2] = palette_[Mode1Colour2(c)];
|
mode1_pixels[2] = palette_[Mode1Colour2(c)];
|
||||||
@@ -426,7 +444,7 @@ class CRTCBusHandler {
|
|||||||
case 2:
|
case 2:
|
||||||
for(int c = 0; c < 256; c++) {
|
for(int c = 0; c < 256; c++) {
|
||||||
// prepare mode 2
|
// prepare mode 2
|
||||||
uint8_t *mode2_pixels = reinterpret_cast<uint8_t *>(&mode2_output_[c]);
|
uint8_t *const mode2_pixels = reinterpret_cast<uint8_t *>(&mode2_output_[c]);
|
||||||
mode2_pixels[0] = palette_[((c & 0x80) >> 7)];
|
mode2_pixels[0] = palette_[((c & 0x80) >> 7)];
|
||||||
mode2_pixels[1] = palette_[((c & 0x40) >> 6)];
|
mode2_pixels[1] = palette_[((c & 0x40) >> 6)];
|
||||||
mode2_pixels[2] = palette_[((c & 0x20) >> 5)];
|
mode2_pixels[2] = palette_[((c & 0x20) >> 5)];
|
||||||
@@ -441,7 +459,7 @@ class CRTCBusHandler {
|
|||||||
case 3:
|
case 3:
|
||||||
for(int c = 0; c < 256; c++) {
|
for(int c = 0; c < 256; c++) {
|
||||||
// prepare mode 3
|
// prepare mode 3
|
||||||
uint8_t *mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]);
|
uint8_t *const mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]);
|
||||||
mode3_pixels[0] = palette_[Mode3Colour0(c)];
|
mode3_pixels[0] = palette_[Mode3Colour0(c)];
|
||||||
mode3_pixels[1] = palette_[Mode3Colour1(c)];
|
mode3_pixels[1] = palette_[Mode3Colour1(c)];
|
||||||
}
|
}
|
||||||
@@ -453,7 +471,7 @@ class CRTCBusHandler {
|
|||||||
switch(mode_) {
|
switch(mode_) {
|
||||||
case 0: {
|
case 0: {
|
||||||
for(uint8_t c : mode0_palette_hits_[pen]) {
|
for(uint8_t c : mode0_palette_hits_[pen]) {
|
||||||
uint8_t *mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]);
|
uint8_t *const mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]);
|
||||||
mode0_pixels[0] = palette_[Mode0Colour0(c)];
|
mode0_pixels[0] = palette_[Mode0Colour0(c)];
|
||||||
mode0_pixels[1] = palette_[Mode0Colour1(c)];
|
mode0_pixels[1] = palette_[Mode0Colour1(c)];
|
||||||
}
|
}
|
||||||
@@ -461,7 +479,7 @@ class CRTCBusHandler {
|
|||||||
case 1:
|
case 1:
|
||||||
if(pen > 3) return;
|
if(pen > 3) return;
|
||||||
for(uint8_t c : mode1_palette_hits_[pen]) {
|
for(uint8_t c : mode1_palette_hits_[pen]) {
|
||||||
uint8_t *mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]);
|
uint8_t *const mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]);
|
||||||
mode1_pixels[0] = palette_[Mode1Colour0(c)];
|
mode1_pixels[0] = palette_[Mode1Colour0(c)];
|
||||||
mode1_pixels[1] = palette_[Mode1Colour1(c)];
|
mode1_pixels[1] = palette_[Mode1Colour1(c)];
|
||||||
mode1_pixels[2] = palette_[Mode1Colour2(c)];
|
mode1_pixels[2] = palette_[Mode1Colour2(c)];
|
||||||
@@ -478,7 +496,7 @@ class CRTCBusHandler {
|
|||||||
if(pen > 3) return;
|
if(pen > 3) return;
|
||||||
// Same argument applies here as to case 1, as the unused bits aren't masked out.
|
// Same argument applies here as to case 1, as the unused bits aren't masked out.
|
||||||
for(uint8_t c : mode3_palette_hits_[pen]) {
|
for(uint8_t c : mode3_palette_hits_[pen]) {
|
||||||
uint8_t *mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]);
|
uint8_t *const mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]);
|
||||||
mode3_pixels[0] = palette_[Mode3Colour0(c)];
|
mode3_pixels[0] = palette_[Mode3Colour0(c)];
|
||||||
mode3_pixels[1] = palette_[Mode3Colour1(c)];
|
mode3_pixels[1] = palette_[Mode3Colour1(c)];
|
||||||
}
|
}
|
||||||
@@ -499,7 +517,7 @@ class CRTCBusHandler {
|
|||||||
|
|
||||||
uint8_t mapped_palette_value(uint8_t colour) {
|
uint8_t mapped_palette_value(uint8_t colour) {
|
||||||
#define COL(r, g, b) (r << 4) | (g << 2) | b
|
#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(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(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),
|
COL(2, 0, 1), COL(2, 2, 1), COL(2, 2, 0), COL(2, 2, 2),
|
||||||
@@ -528,7 +546,7 @@ class CRTCBusHandler {
|
|||||||
Outputs::CRT::CRT crt_;
|
Outputs::CRT::CRT crt_;
|
||||||
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
|
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
|
||||||
|
|
||||||
uint8_t *ram_ = nullptr;
|
const uint8_t *const ram_ = nullptr;
|
||||||
|
|
||||||
int next_mode_ = 2, mode_ = 2;
|
int next_mode_ = 2, mode_ = 2;
|
||||||
|
|
||||||
@@ -564,7 +582,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
|||||||
Sets the row currently being reported to the AY.
|
Sets the row currently being reported to the AY.
|
||||||
*/
|
*/
|
||||||
void set_row(int row) {
|
void set_row(int row) {
|
||||||
row_ = static_cast<size_t>(row);
|
row_ = size_t(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@@ -583,7 +601,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
|||||||
*/
|
*/
|
||||||
void set_is_pressed(bool is_pressed, int line, int key) {
|
void set_is_pressed(bool is_pressed, int line, int key) {
|
||||||
int mask = 1 << key;
|
int mask = 1 << key;
|
||||||
assert(static_cast<size_t>(line) < sizeof(rows_));
|
assert(size_t(line) < sizeof(rows_));
|
||||||
if(is_pressed) rows_[line] &= ~mask; else rows_[line] |= mask;
|
if(is_pressed) rows_[line] &= ~mask; else rows_[line] |= mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +612,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
|||||||
memset(rows_, 0xff, sizeof(rows_));
|
memset(rows_, 0xff, sizeof(rows_));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
|
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
|
||||||
return joysticks_;
|
return joysticks_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,7 +635,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
|||||||
}),
|
}),
|
||||||
state_(state) {}
|
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;
|
uint8_t mask = 0;
|
||||||
switch(input.type) {
|
switch(input.type) {
|
||||||
default: return;
|
default: return;
|
||||||
@@ -646,17 +664,15 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
|||||||
class FDC: public Intel::i8272::i8272 {
|
class FDC: public Intel::i8272::i8272 {
|
||||||
private:
|
private:
|
||||||
Intel::i8272::BusHandler bus_handler_;
|
Intel::i8272::BusHandler bus_handler_;
|
||||||
std::shared_ptr<Storage::Disk::Drive> drive_;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
FDC() :
|
FDC() : i8272(bus_handler_, Cycles(8000000)) {
|
||||||
i8272(bus_handler_, Cycles(8000000)),
|
emplace_drive(8000000, 300, 1);
|
||||||
drive_(new Storage::Disk::Drive(8000000, 300, 1)) {
|
set_drive(1);
|
||||||
set_drive(drive_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_motor_on(bool on) {
|
void set_motor_on(bool on) {
|
||||||
drive_->set_motor_on(on);
|
get_drive().set_motor_on(on);
|
||||||
}
|
}
|
||||||
|
|
||||||
void select_drive(int c) {
|
void select_drive(int c) {
|
||||||
@@ -664,11 +680,11 @@ class FDC: public Intel::i8272::i8272 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
|
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) {
|
void set_activity_observer(Activity::Observer *observer) {
|
||||||
drive_->set_activity_observer(observer, "Drive 1", true);
|
get_drive().set_activity_observer(observer, "Drive 1", true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -816,8 +832,8 @@ template <bool has_fdc> class ConcreteMachine:
|
|||||||
for(std::size_t index = 0; index < roms.size(); ++index) {
|
for(std::size_t index = 0; index < roms.size(); ++index) {
|
||||||
auto &data = roms[index];
|
auto &data = roms[index];
|
||||||
if(!data) throw ROMMachine::Error::MissingROMs;
|
if(!data) throw ROMMachine::Error::MissingROMs;
|
||||||
roms_[static_cast<int>(index)] = std::move(*data);
|
roms_[int(index)] = std::move(*data);
|
||||||
roms_[static_cast<int>(index)].resize(16384);
|
roms_[int(index)].resize(16384);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Establish default memory map
|
// Establish default memory map
|
||||||
@@ -862,13 +878,15 @@ template <bool has_fdc> class ConcreteMachine:
|
|||||||
|
|
||||||
// TODO (in the player, not here): adapt it to accept an input clock rate and
|
// TODO (in the player, not here): adapt it to accept an input clock rate and
|
||||||
// run_for as HalfCycles
|
// run_for as HalfCycles
|
||||||
if(!tape_player_is_sleeping_) tape_player_.run_for(cycle.length.as_int());
|
if(!tape_player_is_sleeping_) tape_player_.run_for(cycle.length.as_integral());
|
||||||
|
|
||||||
// Pump the AY
|
// Pump the AY
|
||||||
ay_.run_for(cycle.length);
|
ay_.run_for(cycle.length);
|
||||||
|
|
||||||
|
if constexpr (has_fdc) {
|
||||||
// Clock the FDC, if connected, using a lazy scale by two
|
// Clock the FDC, if connected, using a lazy scale by two
|
||||||
time_since_fdc_update_ += cycle.length;
|
time_since_fdc_update_ += cycle.length;
|
||||||
|
}
|
||||||
|
|
||||||
// Update typing activity
|
// Update typing activity
|
||||||
if(typer_) typer_->run_for(cycle.length);
|
if(typer_) typer_->run_for(cycle.length);
|
||||||
@@ -894,10 +912,12 @@ template <bool has_fdc> class ConcreteMachine:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for an upper ROM selection
|
// Check for an upper ROM selection
|
||||||
if(has_fdc && !(address&0x2000)) {
|
if constexpr (has_fdc) {
|
||||||
|
if(!(address&0x2000)) {
|
||||||
upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC;
|
upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC;
|
||||||
if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data();
|
if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for a CRTC access
|
// Check for a CRTC access
|
||||||
if(!(address & 0x4000)) {
|
if(!(address & 0x4000)) {
|
||||||
@@ -910,20 +930,22 @@ template <bool has_fdc> class ConcreteMachine:
|
|||||||
|
|
||||||
// Check for an 8255 PIO access
|
// Check for an 8255 PIO access
|
||||||
if(!(address & 0x800)) {
|
if(!(address & 0x800)) {
|
||||||
i8255_.set_register((address >> 8) & 3, *cycle.value);
|
i8255_.write((address >> 8) & 3, *cycle.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if constexpr (has_fdc) {
|
||||||
// Check for an FDC access
|
// Check for an FDC access
|
||||||
if(has_fdc && (address & 0x580) == 0x100) {
|
if((address & 0x580) == 0x100) {
|
||||||
flush_fdc();
|
flush_fdc();
|
||||||
fdc_.set_register(address & 1, *cycle.value);
|
fdc_.write(address & 1, *cycle.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a disk motor access
|
// Check for a disk motor access
|
||||||
if(has_fdc && !(address & 0x580)) {
|
if(!(address & 0x580)) {
|
||||||
flush_fdc();
|
flush_fdc();
|
||||||
fdc_.set_motor_on(!!(*cycle.value));
|
fdc_.set_motor_on(!!(*cycle.value));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case CPU::Z80::PartialMachineCycle::Input:
|
case CPU::Z80::PartialMachineCycle::Input:
|
||||||
// Default to nothing answering
|
// Default to nothing answering
|
||||||
@@ -931,13 +953,15 @@ template <bool has_fdc> class ConcreteMachine:
|
|||||||
|
|
||||||
// Check for a PIO access
|
// Check for a PIO access
|
||||||
if(!(address & 0x800)) {
|
if(!(address & 0x800)) {
|
||||||
*cycle.value &= i8255_.get_register((address >> 8) & 3);
|
*cycle.value &= i8255_.read((address >> 8) & 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for an FDC access
|
// Check for an FDC access
|
||||||
if(has_fdc && (address & 0x580) == 0x100) {
|
if constexpr (has_fdc) {
|
||||||
|
if((address & 0x580) == 0x100) {
|
||||||
flush_fdc();
|
flush_fdc();
|
||||||
*cycle.value &= fdc_.get_register(address & 1);
|
*cycle.value &= fdc_.read(address & 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a CRTC access; the below is not a typo, the CRTC can be selected
|
// Check for a CRTC access; the below is not a typo, the CRTC can be selected
|
||||||
@@ -984,26 +1008,31 @@ template <bool has_fdc> class ConcreteMachine:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A CRTMachine function; sets the destination for video.
|
/// 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);
|
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.
|
/// 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);
|
crtc_bus_handler_.set_display_type(display_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @returns the speaker in use.
|
/// @returns the speaker in use.
|
||||||
Outputs::Speaker::Speaker *get_speaker() override final {
|
Outputs::Speaker::Speaker *get_speaker() final {
|
||||||
return ay_.get_speaker();
|
return ay_.get_speaker();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wires virtual-dispatched CRTMachine run_for requests to the static Z80 method.
|
/// 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);
|
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 there are any tapes supplied, use the first of them.
|
||||||
if(!media.tapes.empty()) {
|
if(!media.tapes.empty()) {
|
||||||
tape_player_.set_tape(media.tapes.front());
|
tape_player_.set_tape(media.tapes.front());
|
||||||
@@ -1020,70 +1049,70 @@ template <bool has_fdc> class ConcreteMachine:
|
|||||||
return !media.tapes.empty() || (!media.disks.empty() && has_fdc);
|
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;
|
fdc_is_sleeping_ = fdc_.preferred_clocking() == ClockingHint::Preference::None;
|
||||||
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
|
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Keyboard
|
// 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());
|
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper());
|
||||||
Utility::TypeRecipient::add_typer(string, std::move(mapper));
|
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.
|
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.
|
return Cycles(160000); // Type one character per frame.
|
||||||
}
|
}
|
||||||
|
|
||||||
// See header; sets a key as either pressed or released.
|
// 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);
|
key_state_.set_is_pressed(isPressed, key >> 4, key & 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
// See header; sets all keys to released.
|
// See header; sets all keys to released.
|
||||||
void clear_all_keys() override final {
|
void clear_all_keys() final {
|
||||||
key_state_.clear_all_keys();
|
key_state_.clear_all_keys();
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardMapper *get_keyboard_mapper() override {
|
KeyboardMapper *get_keyboard_mapper() final {
|
||||||
return &keyboard_mapper_;
|
return &keyboard_mapper_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Activity Source
|
// MARK: - Activity Source
|
||||||
void set_activity_observer(Activity::Observer *observer) override {
|
void set_activity_observer(Activity::Observer *observer) final {
|
||||||
if(has_fdc) fdc_.set_activity_observer(observer);
|
if constexpr (has_fdc) fdc_.set_activity_observer(observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Configuration options.
|
// 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();
|
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;
|
Configurable::Display display;
|
||||||
if(Configurable::get_display(selections_by_option, display)) {
|
if(Configurable::get_display(selections_by_option, display)) {
|
||||||
set_video_signal_configurable(display);
|
set_video_signal_configurable(display);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Configurable::SelectionSet get_accurate_selections() override {
|
Configurable::SelectionSet get_accurate_selections() final {
|
||||||
Configurable::SelectionSet selection_set;
|
Configurable::SelectionSet selection_set;
|
||||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||||
return selection_set;
|
return selection_set;
|
||||||
}
|
}
|
||||||
|
|
||||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||||
Configurable::SelectionSet selection_set;
|
Configurable::SelectionSet selection_set;
|
||||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||||
return selection_set;
|
return selection_set;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Joysticks
|
// MARK: - Joysticks
|
||||||
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();
|
return key_state_.get_joysticks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1145,12 +1174,14 @@ template <bool has_fdc> class ConcreteMachine:
|
|||||||
FDC fdc_;
|
FDC fdc_;
|
||||||
HalfCycles time_since_fdc_update_;
|
HalfCycles time_since_fdc_update_;
|
||||||
void flush_fdc() {
|
void flush_fdc() {
|
||||||
|
if constexpr (has_fdc) {
|
||||||
// Clock the FDC, if connected, using a lazy scale by two
|
// Clock the FDC, if connected, using a lazy scale by two
|
||||||
if(has_fdc && !fdc_is_sleeping_) {
|
if(!fdc_is_sleeping_) {
|
||||||
fdc_.run_for(Cycles(time_since_fdc_update_.as_int()));
|
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_;
|
InterruptTimer interrupt_timer_;
|
||||||
Storage::Tape::BinaryTapePlayer tape_player_;
|
Storage::Tape::BinaryTapePlayer tape_player_;
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
|||||||
BIND(F11, KeyRightSquareBracket);
|
BIND(F11, KeyRightSquareBracket);
|
||||||
BIND(F12, KeyClear);
|
BIND(F12, KeyClear);
|
||||||
|
|
||||||
BIND(Hyphen, KeyMinus); BIND(Equals, KeyCaret); BIND(BackSpace, KeyDelete);
|
BIND(Hyphen, KeyMinus); BIND(Equals, KeyCaret); BIND(Backspace, KeyDelete);
|
||||||
BIND(Tab, KeyTab);
|
BIND(Tab, KeyTab);
|
||||||
|
|
||||||
BIND(OpenSquareBracket, KeyAt);
|
BIND(OpenSquareBracket, KeyAt);
|
||||||
BIND(CloseSquareBracket, KeyLeftSquareBracket);
|
BIND(CloseSquareBracket, KeyLeftSquareBracket);
|
||||||
BIND(BackSlash, KeyBackSlash);
|
BIND(Backslash, KeyBackSlash);
|
||||||
|
|
||||||
BIND(CapsLock, KeyCapsLock);
|
BIND(CapsLock, KeyCapsLock);
|
||||||
BIND(Semicolon, KeyColon);
|
BIND(Semicolon, KeyColon);
|
||||||
@@ -57,19 +57,19 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
|||||||
BIND(Left, KeyLeft); BIND(Right, KeyRight);
|
BIND(Left, KeyLeft); BIND(Right, KeyRight);
|
||||||
BIND(Up, KeyUp); BIND(Down, KeyDown);
|
BIND(Up, KeyUp); BIND(Down, KeyDown);
|
||||||
|
|
||||||
BIND(KeyPad0, KeyF0);
|
BIND(Keypad0, KeyF0);
|
||||||
BIND(KeyPad1, KeyF1); BIND(KeyPad2, KeyF2); BIND(KeyPad3, KeyF3);
|
BIND(Keypad1, KeyF1); BIND(Keypad2, KeyF2); BIND(Keypad3, KeyF3);
|
||||||
BIND(KeyPad4, KeyF4); BIND(KeyPad5, KeyF5); BIND(KeyPad6, KeyF6);
|
BIND(Keypad4, KeyF4); BIND(Keypad5, KeyF5); BIND(Keypad6, KeyF6);
|
||||||
BIND(KeyPad7, KeyF7); BIND(KeyPad8, KeyF8); BIND(KeyPad9, KeyF9);
|
BIND(Keypad7, KeyF7); BIND(Keypad8, KeyF8); BIND(Keypad9, KeyF9);
|
||||||
BIND(KeyPadPlus, KeySemicolon);
|
BIND(KeypadPlus, KeySemicolon);
|
||||||
BIND(KeyPadMinus, KeyMinus);
|
BIND(KeypadMinus, KeyMinus);
|
||||||
|
|
||||||
BIND(KeyPadEnter, KeyEnter);
|
BIND(KeypadEnter, KeyEnter);
|
||||||
BIND(KeyPadDecimalPoint, KeyFullStop);
|
BIND(KeypadDecimalPoint, KeyFullStop);
|
||||||
BIND(KeyPadEquals, KeyMinus);
|
BIND(KeypadEquals, KeyMinus);
|
||||||
BIND(KeyPadSlash, KeyForwardSlash);
|
BIND(KeypadSlash, KeyForwardSlash);
|
||||||
BIND(KeyPadAsterisk, KeyColon);
|
BIND(KeypadAsterisk, KeyColon);
|
||||||
BIND(KeyPadDelete, KeyDelete);
|
BIND(KeypadDelete, KeyDelete);
|
||||||
}
|
}
|
||||||
#undef BIND
|
#undef BIND
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,14 +79,16 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
void update_video() {
|
void update_video() {
|
||||||
video_.run_for(cycles_since_video_update_.flush<Cycles>());
|
video_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||||
}
|
}
|
||||||
static const int audio_divider = 8;
|
static constexpr int audio_divider = 8;
|
||||||
void update_audio() {
|
void update_audio() {
|
||||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
|
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
|
||||||
}
|
}
|
||||||
void update_just_in_time_cards() {
|
void update_just_in_time_cards() {
|
||||||
|
if(cycles_since_card_update_ > Cycles(0)) {
|
||||||
for(const auto &card : just_in_time_cards_) {
|
for(const auto &card : just_in_time_cards_) {
|
||||||
card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
|
card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
cycles_since_card_update_ = 0;
|
cycles_since_card_update_ = 0;
|
||||||
stretched_cycles_since_card_update_ = 0;
|
stretched_cycles_since_card_update_ = 0;
|
||||||
}
|
}
|
||||||
@@ -124,22 +126,28 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
pick_card_messaging_group(card);
|
pick_card_messaging_group(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_every_cycle_card(Apple::II::Card *card) {
|
bool is_every_cycle_card(const Apple::II::Card *card) {
|
||||||
return !card->get_select_constraints();
|
return !card->get_select_constraints();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool card_lists_are_dirty_ = true;
|
||||||
|
bool card_became_just_in_time_ = false;
|
||||||
void pick_card_messaging_group(Apple::II::Card *card) {
|
void pick_card_messaging_group(Apple::II::Card *card) {
|
||||||
|
// Simplify to a card being either just-in-time or realtime.
|
||||||
|
// Don't worry about exactly what it's watching,
|
||||||
const bool is_every_cycle = is_every_cycle_card(card);
|
const bool is_every_cycle = is_every_cycle_card(card);
|
||||||
std::vector<Apple::II::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_;
|
std::vector<Apple::II::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_;
|
||||||
std::vector<Apple::II::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_;
|
|
||||||
|
|
||||||
|
// If the card is already in the proper group, stop.
|
||||||
if(std::find(intended.begin(), intended.end(), card) != intended.end()) return;
|
if(std::find(intended.begin(), intended.end(), card) != intended.end()) return;
|
||||||
auto old_membership = std::find(undesired.begin(), undesired.end(), card);
|
|
||||||
if(old_membership != undesired.end()) undesired.erase(old_membership);
|
// Otherwise, mark the sets as dirty. It isn't safe to transition the card here,
|
||||||
intended.push_back(card);
|
// as the main loop may be part way through iterating the two lists.
|
||||||
|
card_lists_are_dirty_ = true;
|
||||||
|
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);
|
pick_card_messaging_group(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,12 +283,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
Input(Input::Fire, 2),
|
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))
|
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;
|
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) {
|
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
|
||||||
buttons[input.info.control.index] = value;
|
buttons[input.info.control.index] = value;
|
||||||
}
|
}
|
||||||
@@ -321,7 +329,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
audio_toggle_(audio_queue_),
|
audio_toggle_(audio_queue_),
|
||||||
speaker_(audio_toggle_) {
|
speaker_(audio_toggle_) {
|
||||||
// The system's master clock rate.
|
// 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
|
// 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
|
// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
|
||||||
@@ -409,16 +417,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
audio_queue_.flush();
|
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);
|
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.
|
/// 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);
|
video_.set_display_type(display_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
Outputs::Speaker::Speaker *get_speaker() override {
|
Outputs::Speaker::Speaker *get_speaker() final {
|
||||||
return &speaker_;
|
return &speaker_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,6 +765,31 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the card lists if any mutations are due.
|
||||||
|
if(card_lists_are_dirty_) {
|
||||||
|
card_lists_are_dirty_ = false;
|
||||||
|
|
||||||
|
// There's only one counter of time since update
|
||||||
|
// for just-in-time cards. If something new is
|
||||||
|
// transitioning, that needs to be zeroed.
|
||||||
|
if(card_became_just_in_time_) {
|
||||||
|
card_became_just_in_time_ = false;
|
||||||
|
update_just_in_time_cards();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the two lists and repopulate.
|
||||||
|
every_cycle_cards_.clear();
|
||||||
|
just_in_time_cards_.clear();
|
||||||
|
for(const auto &card: cards_) {
|
||||||
|
if(!card) continue;
|
||||||
|
if(is_every_cycle_card(card.get())) {
|
||||||
|
every_cycle_cards_.push_back(card.get());
|
||||||
|
} else {
|
||||||
|
just_in_time_cards_.push_back(card.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update analogue charge level.
|
// Update analogue charge level.
|
||||||
analogue_charge_ = std::min(analogue_charge_ + 1.0f / 2820.0f, 1.1f);
|
analogue_charge_ = std::min(analogue_charge_ + 1.0f / 2820.0f, 1.1f);
|
||||||
|
|
||||||
@@ -766,15 +803,15 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
audio_queue_.perform();
|
audio_queue_.perform();
|
||||||
}
|
}
|
||||||
|
|
||||||
void run_for(const Cycles cycles) override {
|
void run_for(const Cycles cycles) final {
|
||||||
m6502_.run_for(cycles);
|
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;
|
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) {
|
switch(key) {
|
||||||
default: break;
|
default: break;
|
||||||
case Key::F12:
|
case Key::F12:
|
||||||
@@ -795,7 +832,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
case Key::Right: value = 0x15; break;
|
case Key::Right: value = 0x15; break;
|
||||||
case Key::Down: value = 0x0a; break;
|
case Key::Down: value = 0x0a; break;
|
||||||
case Key::Up: value = 0x0b; break;
|
case Key::Up: value = 0x0b; break;
|
||||||
case Key::BackSpace: value = 0x7f; break;
|
case Key::Backspace: value = 0x7f; break;
|
||||||
default: return;
|
default: return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -813,38 +850,38 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Inputs::Keyboard &get_keyboard() override {
|
Inputs::Keyboard &get_keyboard() final {
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void type_string(const std::string &string) override {
|
void type_string(const std::string &string) final {
|
||||||
string_serialiser_.reset(new Utility::StringSerialiser(string, true));
|
string_serialiser_ = std::make_unique<Utility::StringSerialiser>(string, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK:: Configuration options.
|
// 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();
|
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;
|
Configurable::Display display;
|
||||||
if(Configurable::get_display(selections_by_option, display)) {
|
if(Configurable::get_display(selections_by_option, display)) {
|
||||||
set_video_signal_configurable(display);
|
set_video_signal_configurable(display);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Configurable::SelectionSet get_accurate_selections() override {
|
Configurable::SelectionSet get_accurate_selections() final {
|
||||||
Configurable::SelectionSet selection_set;
|
Configurable::SelectionSet selection_set;
|
||||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||||
return selection_set;
|
return selection_set;
|
||||||
}
|
}
|
||||||
|
|
||||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||||
return get_accurate_selections();
|
return get_accurate_selections();
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: MediaTarget
|
// MARK: MediaTarget
|
||||||
bool insert_media(const Analyser::Static::Media &media) override {
|
bool insert_media(const Analyser::Static::Media &media) final {
|
||||||
if(!media.disks.empty()) {
|
if(!media.disks.empty()) {
|
||||||
auto diskii = diskii_card();
|
auto diskii = diskii_card();
|
||||||
if(diskii) diskii->set_disk(media.disks[0], 0);
|
if(diskii) diskii->set_disk(media.disks[0], 0);
|
||||||
@@ -853,14 +890,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Activity::Source
|
// MARK: Activity::Source
|
||||||
void set_activity_observer(Activity::Observer *observer) override {
|
void set_activity_observer(Activity::Observer *observer) final {
|
||||||
for(const auto &card: cards_) {
|
for(const auto &card: cards_) {
|
||||||
if(card) card->set_activity_observer(observer);
|
if(card) card->set_activity_observer(observer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: JoystickMachine
|
// MARK: JoystickMachine
|
||||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||||
return joysticks_;
|
return joysticks_;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -83,10 +83,8 @@ class Card {
|
|||||||
will receive a perform_bus_operation every cycle. To reduce the number of
|
will receive a perform_bus_operation every cycle. To reduce the number of
|
||||||
virtual method calls, they **will not** receive run_for. run_for will propagate
|
virtual method calls, they **will not** receive run_for. run_for will propagate
|
||||||
only to cards that register for IO and/or Device accesses only.
|
only to cards that register for IO and/or Device accesses only.
|
||||||
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
int get_select_constraints() {
|
int get_select_constraints() const {
|
||||||
return select_constraints_;
|
return select_constraints_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ void DiskIICard::perform_bus_operation(Select select, bool is_read, uint16_t add
|
|||||||
|
|
||||||
void DiskIICard::run_for(Cycles cycles, int stretches) {
|
void DiskIICard::run_for(Cycles cycles, int stretches) {
|
||||||
if(diskii_clocking_preference_ == ClockingHint::Preference::None) return;
|
if(diskii_clocking_preference_ == ClockingHint::Preference::None) return;
|
||||||
diskii_.run_for(Cycles(cycles.as_int() * 2));
|
diskii_.run_for(Cycles(cycles.as_integral() * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DiskIICard::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive) {
|
void DiskIICard::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive) {
|
||||||
@@ -65,7 +65,7 @@ void DiskIICard::set_activity_observer(Activity::Observer *observer) {
|
|||||||
|
|
||||||
void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) {
|
void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) {
|
||||||
diskii_clocking_preference_ = preference;
|
diskii_clocking_preference_ = preference;
|
||||||
set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : 0);
|
set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage::Disk::Drive &DiskIICard::get_drive(int drive) {
|
Storage::Disk::Drive &DiskIICard::get_drive(int drive) {
|
||||||
|
|||||||
@@ -27,16 +27,16 @@ class DiskIICard: public Card, public ClockingHint::Observer {
|
|||||||
public:
|
public:
|
||||||
DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector);
|
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 perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final;
|
||||||
void run_for(Cycles cycles, int stretches) override;
|
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);
|
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
|
||||||
Storage::Disk::Drive &get_drive(int drive);
|
Storage::Disk::Drive &get_drive(int drive);
|
||||||
|
|
||||||
private:
|
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_;
|
std::vector<uint8_t> boot_;
|
||||||
Apple::DiskII diskii_;
|
Apple::DiskII diskii_;
|
||||||
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;
|
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);
|
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) {
|
void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||||
crt_.set_display_type(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) {
|
void VideoBase::set_alternative_character_set(bool alternative_character_set) {
|
||||||
set_alternative_character_set_ = 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;
|
alternative_character_set_ = alternative_character_set;
|
||||||
if(alternative_character_set) {
|
if(alternative_character_set) {
|
||||||
character_zones[1].address_mask = 0xff;
|
character_zones[1].address_mask = 0xff;
|
||||||
@@ -74,7 +78,7 @@ bool VideoBase::get_alternative_character_set() {
|
|||||||
|
|
||||||
void VideoBase::set_80_columns(bool columns_80) {
|
void VideoBase::set_80_columns(bool columns_80) {
|
||||||
set_columns_80_ = columns_80;
|
set_columns_80_ = columns_80;
|
||||||
deferrer_.defer(Cycles(2), [=] {
|
deferrer_.defer(Cycles(2), [this, columns_80] {
|
||||||
columns_80_ = columns_80;
|
columns_80_ = columns_80;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -101,7 +105,7 @@ bool VideoBase::get_page2() {
|
|||||||
|
|
||||||
void VideoBase::set_text(bool text) {
|
void VideoBase::set_text(bool text) {
|
||||||
set_text_ = text;
|
set_text_ = text;
|
||||||
deferrer_.defer(Cycles(2), [=] {
|
deferrer_.defer(Cycles(2), [this, text] {
|
||||||
text_ = text;
|
text_ = text;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -112,7 +116,7 @@ bool VideoBase::get_text() {
|
|||||||
|
|
||||||
void VideoBase::set_mixed(bool mixed) {
|
void VideoBase::set_mixed(bool mixed) {
|
||||||
set_mixed_ = mixed;
|
set_mixed_ = mixed;
|
||||||
deferrer_.defer(Cycles(2), [=] {
|
deferrer_.defer(Cycles(2), [this, mixed] {
|
||||||
mixed_ = mixed;
|
mixed_ = mixed;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -123,7 +127,7 @@ bool VideoBase::get_mixed() {
|
|||||||
|
|
||||||
void VideoBase::set_high_resolution(bool high_resolution) {
|
void VideoBase::set_high_resolution(bool high_resolution) {
|
||||||
set_high_resolution_ = high_resolution;
|
set_high_resolution_ = high_resolution;
|
||||||
deferrer_.defer(Cycles(2), [=] {
|
deferrer_.defer(Cycles(2), [this, high_resolution] {
|
||||||
high_resolution_ = high_resolution;
|
high_resolution_ = high_resolution;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -134,7 +138,7 @@ bool VideoBase::get_high_resolution() {
|
|||||||
|
|
||||||
void VideoBase::set_annunciator_3(bool annunciator_3) {
|
void VideoBase::set_annunciator_3(bool annunciator_3) {
|
||||||
set_annunciator_3_ = annunciator_3;
|
set_annunciator_3_ = annunciator_3;
|
||||||
deferrer_.defer(Cycles(2), [=] {
|
deferrer_.defer(Cycles(2), [this, annunciator_3] {
|
||||||
annunciator_3_ = annunciator_3;
|
annunciator_3_ = annunciator_3;
|
||||||
high_resolution_mask_ = annunciator_3_ ? 0x7f : 0xff;
|
high_resolution_mask_ = annunciator_3_ ? 0x7f : 0xff;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ class VideoBase {
|
|||||||
/// Sets the scan target.
|
/// Sets the scan target.
|
||||||
void set_scan_target(Outputs::Display::ScanTarget *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.
|
/// Sets the type of output.
|
||||||
void set_display_type(Outputs::Display::DisplayType);
|
void set_display_type(Outputs::Display::DisplayType);
|
||||||
|
|
||||||
@@ -203,7 +206,7 @@ class VideoBase {
|
|||||||
std::array<uint8_t, 40> auxiliary_stream_;
|
std::array<uint8_t, 40> auxiliary_stream_;
|
||||||
|
|
||||||
bool is_iie_ = false;
|
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
|
// Describes the current text mode mapping from in-memory character index
|
||||||
// to output character.
|
// 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;
|
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.
|
// Maintain a DeferredQueue for delayed mode switches.
|
||||||
DeferredQueue<Cycles> deferrer_;
|
DeferredQueuePerformer<Cycles> deferrer_;
|
||||||
};
|
};
|
||||||
|
|
||||||
template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||||
public:
|
public:
|
||||||
/// Constructs an instance of the video feed; a CRT is also created.
|
/// Constructs an instance of the video feed; a CRT is also created.
|
||||||
Video(BusHandler &bus_handler) :
|
Video(BusHandler &bus_handler) :
|
||||||
VideoBase(is_iie, [=] (Cycles cycles) { advance(cycles); }),
|
VideoBase(is_iie, [this] (Cycles cycles) { advance(cycles); }),
|
||||||
bus_handler_(bus_handler) {}
|
bus_handler_(bus_handler) {}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@@ -284,7 +287,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
|||||||
// Source: Have an Apple Split by Bob Bishop; http://rich12345.tripod.com/aiivideo/softalk.html
|
// Source: Have an Apple Split by Bob Bishop; http://rich12345.tripod.com/aiivideo/softalk.html
|
||||||
|
|
||||||
// Determine column at offset.
|
// Determine column at offset.
|
||||||
int mapped_column = column_ + offset.as_int();
|
int mapped_column = column_ + int(offset.as_integral());
|
||||||
|
|
||||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||||
// (so what was column 0 is now column 25).
|
// (so what was column 0 is now column 25).
|
||||||
@@ -315,7 +318,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
|||||||
bool get_is_vertical_blank(Cycles offset) {
|
bool get_is_vertical_blank(Cycles offset) {
|
||||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||||
// (so what was column 0 is now column 25).
|
// (so what was column 0 is now column 25).
|
||||||
int mapped_column = column_ + offset.as_int();
|
int mapped_column = column_ + int(offset.as_integral());
|
||||||
|
|
||||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||||
// (so what was column 0 is now column 25).
|
// (so what was column 0 is now column 25).
|
||||||
@@ -339,11 +342,11 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
|||||||
|
|
||||||
A frame is oriented around 65 cycles across, 262 lines down.
|
A frame is oriented around 65 cycles across, 262 lines down.
|
||||||
*/
|
*/
|
||||||
static const int first_sync_line = 220; // A complete guess. Information needed.
|
constexpr int first_sync_line = 220; // A complete guess. Information needed.
|
||||||
static const int first_sync_column = 49; // Also a guess.
|
constexpr int first_sync_column = 49; // Also a guess.
|
||||||
static const int sync_length = 4; // One of the two likely candidates.
|
constexpr int sync_length = 4; // One of the two likely candidates.
|
||||||
|
|
||||||
int int_cycles = cycles.as_int();
|
int int_cycles = int(cycles.as_integral());
|
||||||
while(int_cycles) {
|
while(int_cycles) {
|
||||||
const int cycles_this_line = std::min(65 - column_, int_cycles);
|
const int cycles_this_line = std::min(65 - column_, int_cycles);
|
||||||
const int ending_column = column_ + cycles_this_line;
|
const int ending_column = column_ + cycles_this_line;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ void Audio::set_volume(int volume) {
|
|||||||
posted_volume_ = volume;
|
posted_volume_ = volume;
|
||||||
|
|
||||||
// Post the volume change as a deferred event.
|
// Post the volume change as a deferred event.
|
||||||
task_queue_.defer([=] () {
|
task_queue_.defer([this, volume] () {
|
||||||
volume_ = volume;
|
volume_ = volume;
|
||||||
set_volume_multiplier();
|
set_volume_multiplier();
|
||||||
});
|
});
|
||||||
@@ -47,7 +47,7 @@ void Audio::set_enabled(bool on) {
|
|||||||
posted_enable_mask_ = int(on);
|
posted_enable_mask_ = int(on);
|
||||||
|
|
||||||
// Post the enabled mask change as a deferred event.
|
// Post the enabled mask change as a deferred event.
|
||||||
task_queue_.defer([=] () {
|
task_queue_.defer([this, on] () {
|
||||||
enabled_mask_ = int(on);
|
enabled_mask_ = int(on);
|
||||||
set_volume_multiplier();
|
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);
|
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||||
bool is_zero_level();
|
bool is_zero_level();
|
||||||
void set_sample_volume_range(std::int16_t range);
|
void set_sample_volume_range(std::int16_t range);
|
||||||
|
constexpr static bool get_is_stereo() { return false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
using namespace Apple::Macintosh;
|
using namespace Apple::Macintosh;
|
||||||
|
|
||||||
void DriveSpeedAccumulator::post_sample(uint8_t sample) {
|
void DriveSpeedAccumulator::post_sample(uint8_t sample) {
|
||||||
if(!number_of_drives_) return;
|
if(!delegate_) return;
|
||||||
|
|
||||||
// An Euler-esque approximation is used here: just collect all
|
// An Euler-esque approximation is used here: just collect all
|
||||||
// the samples until there is a certain small quantity of them,
|
// the samples until there is a certain small quantity of them,
|
||||||
@@ -50,14 +50,7 @@ void DriveSpeedAccumulator::post_sample(uint8_t sample) {
|
|||||||
const float normalised_sum = float(sum) / float(samples_.size());
|
const float normalised_sum = float(sum) / float(samples_.size());
|
||||||
const float rotation_speed = (normalised_sum * 27.08f) - 259.0f;
|
const float rotation_speed = (normalised_sum * 27.08f) - 259.0f;
|
||||||
|
|
||||||
for(int c = 0; c < number_of_drives_; ++c) {
|
delegate_->drive_speed_accumulator_set_drive_speed(this, rotation_speed);
|
||||||
drives_[c]->set_rotation_speed(rotation_speed);
|
|
||||||
}
|
|
||||||
// printf("RPM: %0.2f (%d sum)\n", rotation_speed, sum);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DriveSpeedAccumulator::add_drive(Apple::Macintosh::DoubleDensityDrive *drive) {
|
|
||||||
drives_[number_of_drives_] = drive;
|
|
||||||
++number_of_drives_;
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user