mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-03 01:16:26 +00:00
Compare commits
774 Commits
2019-08-01
...
2020-02-12
Author | SHA1 | Date | |
---|---|---|---|
|
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 {
|
||||
public:
|
||||
/*! @returns The computed probability, based on the history of events. */
|
||||
float get_confidence() override;
|
||||
float get_confidence() final;
|
||||
|
||||
/*! Records an event that implies this is the appropriate class: pushes probability up towards 1.0. */
|
||||
void add_hit();
|
||||
|
@@ -32,7 +32,7 @@ class ConfidenceSummary: public ConfidenceSource {
|
||||
const std::vector<float> &weights);
|
||||
|
||||
/*! @returns The weighted sum of all sources. */
|
||||
float get_confidence() override;
|
||||
float get_confidence() final;
|
||||
|
||||
private:
|
||||
std::vector<ConfidenceSource *> sources_;
|
||||
|
@@ -60,6 +60,13 @@ void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target)
|
||||
if(crt_machine) crt_machine->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus MultiCRTMachine::get_scan_status() const {
|
||||
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
|
||||
if(crt_machine) crt_machine->get_scan_status();
|
||||
|
||||
return Outputs::Display::ScanStatus();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() {
|
||||
return speaker_;
|
||||
}
|
||||
|
@@ -53,12 +53,13 @@ class MultiCRTMachine: public CRTMachine::Machine {
|
||||
}
|
||||
|
||||
// Below is the standard CRTMachine::Machine interface; see there for documentation.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override;
|
||||
Outputs::Speaker::Speaker *get_speaker() override;
|
||||
void run_for(Time::Seconds duration) override;
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final;
|
||||
Outputs::Display::ScanStatus get_scan_status() const final;
|
||||
Outputs::Speaker::Speaker *get_speaker() final;
|
||||
void run_for(Time::Seconds duration) final;
|
||||
|
||||
private:
|
||||
void run_for(const Cycles cycles) override {}
|
||||
void run_for(const Cycles cycles) final {}
|
||||
const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_;
|
||||
std::recursive_mutex &machines_mutex_;
|
||||
std::vector<Concurrency::AsyncTaskQueue> queues_;
|
||||
|
@@ -28,10 +28,10 @@ class MultiConfigurable: public Configurable::Device {
|
||||
MultiConfigurable(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
// Below is the standard Configurable::Device interface; see there for documentation.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override;
|
||||
void set_selections(const Configurable::SelectionSet &selection_by_option) override;
|
||||
Configurable::SelectionSet get_accurate_selections() override;
|
||||
Configurable::SelectionSet get_user_friendly_selections() override;
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final;
|
||||
void set_selections(const Configurable::SelectionSet &selection_by_option) final;
|
||||
Configurable::SelectionSet get_accurate_selections() final;
|
||||
Configurable::SelectionSet get_user_friendly_selections() final;
|
||||
|
||||
private:
|
||||
std::vector<Configurable::Device *> devices_;
|
||||
|
@@ -25,7 +25,7 @@ class MultiJoystick: public Inputs::Joystick {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Input> &get_inputs() override {
|
||||
std::vector<Input> &get_inputs() final {
|
||||
if(inputs.empty()) {
|
||||
for(const auto &joystick: joysticks_) {
|
||||
std::vector<Input> joystick_inputs = joystick->get_inputs();
|
||||
@@ -40,19 +40,19 @@ class MultiJoystick: public Inputs::Joystick {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
void set_input(const Input &digital_input, bool is_active) override {
|
||||
void set_input(const Input &digital_input, bool is_active) final {
|
||||
for(const auto &joystick: joysticks_) {
|
||||
joystick->set_input(digital_input, is_active);
|
||||
}
|
||||
}
|
||||
|
||||
void set_input(const Input &digital_input, float value) override {
|
||||
void set_input(const Input &digital_input, float value) final {
|
||||
for(const auto &joystick: joysticks_) {
|
||||
joystick->set_input(digital_input, value);
|
||||
}
|
||||
}
|
||||
|
||||
void reset_all_inputs() override {
|
||||
void reset_all_inputs() final {
|
||||
for(const auto &joystick: joysticks_) {
|
||||
joystick->reset_all_inputs();
|
||||
}
|
||||
@@ -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_;
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ class MultiJoystickMachine: public JoystickMachine::Machine {
|
||||
MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
// Below is the standard JoystickMachine::Machine interface; see there for documentation.
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override;
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
|
@@ -32,10 +32,10 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine {
|
||||
public:
|
||||
MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines);
|
||||
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) override;
|
||||
void reset_all_keys() override;
|
||||
const std::set<Key> &observed_keys() override;
|
||||
bool is_exclusive() override;
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) final;
|
||||
void reset_all_keys() final;
|
||||
const std::set<Key> &observed_keys() final;
|
||||
bool is_exclusive() final;
|
||||
|
||||
private:
|
||||
const std::vector<::KeyboardMachine::Machine *> &machines_;
|
||||
@@ -48,10 +48,10 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine {
|
||||
MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
// Below is the standard KeyboardMachine::Machine interface; see there for documentation.
|
||||
void clear_all_keys() override;
|
||||
void set_key_state(uint16_t key, bool is_pressed) override;
|
||||
void type_string(const std::string &) override;
|
||||
Inputs::Keyboard &get_keyboard() override;
|
||||
void clear_all_keys() final;
|
||||
void set_key_state(uint16_t key, bool is_pressed) final;
|
||||
void type_string(const std::string &) final;
|
||||
Inputs::Keyboard &get_keyboard() final;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ struct MultiMediaTarget: public MediaTarget::Machine {
|
||||
MultiMediaTarget(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
// Below is the standard MediaTarget::Machine interface; see there for documentation.
|
||||
bool insert_media(const Analyser::Static::Media &media) override;
|
||||
bool insert_media(const Analyser::Static::Media &media) final;
|
||||
|
||||
private:
|
||||
std::vector<MediaTarget::Machine *> targets_;
|
||||
|
@@ -37,9 +37,9 @@ float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum)
|
||||
return ideal / static_cast<float>(speakers_.size());
|
||||
}
|
||||
|
||||
void MultiSpeaker::set_output_rate(float cycles_per_second, int buffer_size) {
|
||||
void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size) {
|
||||
for(const auto &speaker: speakers_) {
|
||||
speaker->set_output_rate(cycles_per_second, buffer_size);
|
||||
speaker->set_computed_output_rate(cycles_per_second, buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vec
|
||||
std::lock_guard<std::mutex> lock_guard(front_speaker_mutex_);
|
||||
if(speaker != front_speaker_) return;
|
||||
}
|
||||
delegate_->speaker_did_complete_samples(this, buffer);
|
||||
did_complete_samples(this, buffer);
|
||||
}
|
||||
|
||||
void MultiSpeaker::speaker_did_change_input_clock(Speaker *speaker) {
|
||||
|
@@ -39,12 +39,12 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker:
|
||||
|
||||
// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation.
|
||||
float get_ideal_clock_rate_in_range(float minimum, float maximum) override;
|
||||
void set_output_rate(float cycles_per_second, int buffer_size) override;
|
||||
void set_computed_output_rate(float cycles_per_second, int buffer_size) override;
|
||||
void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override;
|
||||
|
||||
private:
|
||||
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) override;
|
||||
void speaker_did_change_input_clock(Speaker *speaker) override;
|
||||
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final;
|
||||
void speaker_did_change_input_clock(Speaker *speaker) final;
|
||||
MultiSpeaker(const std::vector<Outputs::Speaker::Speaker *> &speakers);
|
||||
|
||||
std::vector<Outputs::Speaker::Speaker *> speakers_;
|
||||
|
@@ -50,17 +50,17 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De
|
||||
static bool would_collapse(const std::vector<std::unique_ptr<DynamicMachine>> &machines);
|
||||
MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines);
|
||||
|
||||
Activity::Source *activity_source() override;
|
||||
Configurable::Device *configurable_device() override;
|
||||
CRTMachine::Machine *crt_machine() override;
|
||||
JoystickMachine::Machine *joystick_machine() override;
|
||||
MouseMachine::Machine *mouse_machine() override;
|
||||
KeyboardMachine::Machine *keyboard_machine() override;
|
||||
MediaTarget::Machine *media_target() override;
|
||||
void *raw_pointer() override;
|
||||
Activity::Source *activity_source() final;
|
||||
Configurable::Device *configurable_device() final;
|
||||
CRTMachine::Machine *crt_machine() final;
|
||||
JoystickMachine::Machine *joystick_machine() final;
|
||||
MouseMachine::Machine *mouse_machine() final;
|
||||
KeyboardMachine::Machine *keyboard_machine() final;
|
||||
MediaTarget::Machine *media_target() final;
|
||||
void *raw_pointer() final;
|
||||
|
||||
private:
|
||||
void multi_crt_did_run_machines() override;
|
||||
void multi_crt_did_run_machines() final;
|
||||
|
||||
std::vector<std::unique_ptr<DynamicMachine>> machines_;
|
||||
std::recursive_mutex machines_mutex_;
|
||||
|
@@ -15,6 +15,7 @@ enum class Machine {
|
||||
AmstradCPC,
|
||||
AppleII,
|
||||
Atari2600,
|
||||
AtariST,
|
||||
ColecoVision,
|
||||
Electron,
|
||||
Macintosh,
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
#include "../../../Storage/Disk/Controller/DiskController.hpp"
|
||||
#include "../../../Storage/Disk/Encodings/MFM/Parser.hpp"
|
||||
#include "../../../NumberTheory/CRC.hpp"
|
||||
#include "../../../Numeric/CRC.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -18,7 +18,7 @@ using namespace Analyser::Static::Acorn;
|
||||
|
||||
std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
// c.f. http://beebwiki.mdfs.net/Acorn_DFS_disc_format
|
||||
std::unique_ptr<Catalogue> catalogue(new Catalogue);
|
||||
auto catalogue = std::make_unique<Catalogue>();
|
||||
Storage::Encodings::MFM::Parser parser(false, disk);
|
||||
|
||||
Storage::Encodings::MFM::Sector *names = parser.get_sector(0, 0, 0);
|
||||
@@ -75,7 +75,7 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s
|
||||
return catalogue;
|
||||
}
|
||||
std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
std::unique_ptr<Catalogue> catalogue(new Catalogue);
|
||||
auto catalogue = std::make_unique<Catalogue>();
|
||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||
|
||||
Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.get_sector(0, 0, 1);
|
||||
|
@@ -58,7 +58,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
}
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::Electron;
|
||||
target->confidence = 0.5; // TODO: a proper estimation
|
||||
target->has_dfs = false;
|
||||
|
@@ -10,13 +10,13 @@
|
||||
|
||||
#include <deque>
|
||||
|
||||
#include "../../../NumberTheory/CRC.hpp"
|
||||
#include "../../../Numeric/CRC.hpp"
|
||||
#include "../../../Storage/Tape/Parsers/Acorn.hpp"
|
||||
|
||||
using namespace Analyser::Static::Acorn;
|
||||
|
||||
static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::Tape::Tape> &tape, Storage::Tape::Acorn::Parser &parser) {
|
||||
std::unique_ptr<File::Chunk> new_chunk(new File::Chunk);
|
||||
auto new_chunk = std::make_unique<File::Chunk>();
|
||||
int shift_register = 0;
|
||||
|
||||
// TODO: move this into the parser
|
||||
@@ -90,7 +90,7 @@ static std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &chunks) {
|
||||
if(!chunks.size()) return nullptr;
|
||||
|
||||
// accumulate chunks for as long as block number is sequential and the end-of-file bit isn't set
|
||||
std::unique_ptr<File> file(new File);
|
||||
auto file = std::make_unique<File>();
|
||||
|
||||
uint16_t block_number = 0;
|
||||
|
||||
|
@@ -181,7 +181,7 @@ static bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, co
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
TargetList destination;
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::AmstradCPC;
|
||||
target->confidence = 0.5;
|
||||
|
||||
|
@@ -10,7 +10,7 @@
|
||||
#include "Target.hpp"
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::AppleII::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
auto target = std::unique_ptr<Target>(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::AppleII;
|
||||
target->media = media;
|
||||
|
||||
|
@@ -12,9 +12,10 @@
|
||||
|
||||
#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
|
||||
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
|
||||
// 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
|
||||
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
|
||||
// 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?)
|
||||
@@ -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[0] == 0x78
|
||||
) {
|
||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::ActivisionStack;
|
||||
target.paging_model = Target::PagingModel::ActivisionStack;
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
|
||||
if(parker_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::ParkerBros;
|
||||
else if(tigervision_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::Tigervision;
|
||||
if(parker_access_count > atari_access_count) target.paging_model = Target::PagingModel::ParkerBros;
|
||||
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
|
||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari16k;
|
||||
target.paging_model = Target::PagingModel::Atari16k;
|
||||
|
||||
std::set<uint16_t> internal_accesses;
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
target.paging_model =
|
||||
(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) {
|
||||
DeterminePagingFor2kCartridge(target, segment);
|
||||
return;
|
||||
@@ -140,16 +141,16 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target,
|
||||
DeterminePagingFor8kCartridge(target, segment, disassembly);
|
||||
break;
|
||||
case 10495:
|
||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::Pitfall2;
|
||||
target.paging_model = Target::PagingModel::Pitfall2;
|
||||
break;
|
||||
case 12288:
|
||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::CBSRamPlus;
|
||||
target.paging_model = Target::PagingModel::CBSRamPlus;
|
||||
break;
|
||||
case 16384:
|
||||
DeterminePagingFor16kCartridge(target, segment, disassembly);
|
||||
break;
|
||||
case 32768:
|
||||
target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari32k;
|
||||
target.paging_model = Target::PagingModel::Atari32k;
|
||||
break;
|
||||
case 65536:
|
||||
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
|
||||
// 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.
|
||||
if( target.paging_model != Analyser::Static::Atari::Target::PagingModel::CBSRamPlus &&
|
||||
target.paging_model != Analyser::Static::Atari::Target::PagingModel::MNetwork) {
|
||||
if( target.paging_model != Target::PagingModel::CBSRamPlus &&
|
||||
target.paging_model != Target::PagingModel::MNetwork) {
|
||||
bool has_superchip = true;
|
||||
for(std::size_t address = 0; address < 128; address++) {
|
||||
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
|
||||
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();
|
||||
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?
|
||||
std::unique_ptr<Analyser::Static::Atari::Target> target(new Analyser::Static::Atari::Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::Atari2600;
|
||||
target->confidence = 0.5;
|
||||
target->media.cartridges = media.cartridges;
|
||||
target->paging_model = Analyser::Static::Atari::Target::PagingModel::None;
|
||||
target->paging_model = Target::PagingModel::None;
|
||||
target->uses_superchip = false;
|
||||
|
||||
// try to figure out the paging scheme
|
@@ -15,7 +15,7 @@
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Atari {
|
||||
namespace Atari2600 {
|
||||
|
||||
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.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_Atari_Target_h
|
||||
#define Analyser_Static_Atari_Target_h
|
||||
#ifndef Analyser_Static_Atari2600_Target_h
|
||||
#define Analyser_Static_Atari2600_Target_h
|
||||
|
||||
#include "../StaticAnalyser.hpp"
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Atari {
|
||||
namespace Atari2600 {
|
||||
|
||||
struct Target: public ::Analyser::Static::Target {
|
||||
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) {
|
||||
TargetList targets;
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::ColecoVision;
|
||||
target->confidence = 1.0f - 1.0f / 32768.0f;
|
||||
target->media.cartridges = ColecoCartridgesFrom(media.cartridges);
|
||||
|
@@ -19,12 +19,10 @@ using namespace Analyser::Static::Commodore;
|
||||
|
||||
class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||
public:
|
||||
std::shared_ptr<Storage::Disk::Drive> drive;
|
||||
|
||||
CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) {
|
||||
drive.reset(new Storage::Disk::Drive(4000000, 300, 2));
|
||||
set_drive(drive);
|
||||
drive->set_motor_on(true);
|
||||
emplace_drive(4000000, 300, 2);
|
||||
set_drive(1);
|
||||
get_drive().set_motor_on(true);
|
||||
}
|
||||
|
||||
struct Sector {
|
||||
@@ -61,6 +59,10 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||
return get_sector(sector);
|
||||
}
|
||||
|
||||
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
get_drive().set_disk(disk);
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned int shift_register_;
|
||||
int index_count_;
|
||||
@@ -125,7 +127,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||
}
|
||||
|
||||
std::shared_ptr<Sector> get_next_sector() {
|
||||
std::shared_ptr<Sector> sector(new Sector);
|
||||
auto sector = std::make_shared<Sector>();
|
||||
const int max_index_count = index_count_ + 2;
|
||||
|
||||
while(index_count_ < max_index_count) {
|
||||
@@ -170,7 +172,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||
std::vector<File> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
std::vector<File> files;
|
||||
CommodoreGCRParser parser;
|
||||
parser.drive->set_disk(disk);
|
||||
parser.set_disk(disk);
|
||||
|
||||
// find any sector whatsoever to establish the current track
|
||||
std::shared_ptr<CommodoreGCRParser::Sector> sector;
|
||||
|
@@ -16,6 +16,7 @@
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
using namespace Analyser::Static::Commodore;
|
||||
@@ -44,7 +45,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
TargetList destination;
|
||||
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::Vic20; // TODO: machine estimation
|
||||
target->confidence = 0.5; // TODO: a proper estimation
|
||||
|
||||
@@ -78,7 +79,7 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
||||
}
|
||||
|
||||
if(!files.empty()) {
|
||||
target->memory_model = Target::MemoryModel::Unexpanded;
|
||||
auto memory_model = Target::MemoryModel::Unexpanded;
|
||||
std::ostringstream string_stream;
|
||||
string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ",";
|
||||
if(files.front().is_basic()) {
|
||||
@@ -94,16 +95,18 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
||||
default:
|
||||
LOG("Unrecognised loading address for Commodore program: " << PADHEX(4) << files.front().starting_address);
|
||||
case 0x1001:
|
||||
target->memory_model = Target::MemoryModel::Unexpanded;
|
||||
memory_model = Target::MemoryModel::Unexpanded;
|
||||
break;
|
||||
case 0x1201:
|
||||
target->memory_model = Target::MemoryModel::ThirtyTwoKB;
|
||||
memory_model = Target::MemoryModel::ThirtyTwoKB;
|
||||
break;
|
||||
case 0x0401:
|
||||
target->memory_model = Target::MemoryModel::EightKB;
|
||||
memory_model = Target::MemoryModel::EightKB;
|
||||
break;
|
||||
}
|
||||
|
||||
target->set_memory_model(memory_model);
|
||||
|
||||
// General approach: increase memory size conservatively such that the largest file found will fit.
|
||||
// for(File &file : files) {
|
||||
// std::size_t file_size = file.data.size();
|
||||
@@ -145,13 +148,52 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
||||
}
|
||||
|
||||
if(!target->media.empty()) {
|
||||
// Inspect filename for a region hint.
|
||||
// Inspect filename for configuration hints.
|
||||
std::string lowercase_name = file_name;
|
||||
std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower);
|
||||
|
||||
// Hint 1: 'ntsc' anywhere in the name implies America.
|
||||
if(lowercase_name.find("ntsc") != std::string::npos) {
|
||||
target->region = Analyser::Static::Commodore::Target::Region::American;
|
||||
}
|
||||
|
||||
// Potential additional hints: check for TheC64 tags.
|
||||
auto final_underscore = lowercase_name.find_last_of('_');
|
||||
if(final_underscore != std::string::npos) {
|
||||
auto iterator = lowercase_name.begin() + ssize_t(final_underscore) + 1;
|
||||
|
||||
while(iterator != lowercase_name.end()) {
|
||||
// Grab the next tag.
|
||||
char next_tag[3] = {0, 0, 0};
|
||||
next_tag[0] = *iterator++;
|
||||
if(iterator == lowercase_name.end()) break;
|
||||
next_tag[1] = *iterator++;
|
||||
|
||||
// Exit early if attempting to read another tag has run over the file extension.
|
||||
if(next_tag[0] == '.' || next_tag[1] == '.') break;
|
||||
|
||||
// Check whether it's anything.
|
||||
target->enabled_ram.bank0 |= !strcmp(next_tag, "b0");
|
||||
target->enabled_ram.bank1 |= !strcmp(next_tag, "b1");
|
||||
target->enabled_ram.bank2 |= !strcmp(next_tag, "b2");
|
||||
target->enabled_ram.bank3 |= !strcmp(next_tag, "b3");
|
||||
target->enabled_ram.bank5 |= !strcmp(next_tag, "b5");
|
||||
if(!strcmp(next_tag, "tn")) { // i.e. NTSC.
|
||||
target->region = Analyser::Static::Commodore::Target::Region::American;
|
||||
}
|
||||
if(!strcmp(next_tag, "tp")) { // i.e. PAL.
|
||||
target->region = Analyser::Static::Commodore::Target::Region::European;
|
||||
}
|
||||
|
||||
// Unhandled:
|
||||
//
|
||||
// M6: this is a C64 file.
|
||||
// MV: this is a Vic-20 file.
|
||||
// J1/J2: this C64 file should have the primary joystick in slot 1/2.
|
||||
// RO: this disk image should be treated as read-only.
|
||||
}
|
||||
}
|
||||
|
||||
// Attach a 1540 if there are any disks here.
|
||||
target->has_c1540 = !target->media.disks.empty();
|
||||
|
||||
|
@@ -31,7 +31,26 @@ struct Target: public ::Analyser::Static::Target {
|
||||
Swedish
|
||||
};
|
||||
|
||||
MemoryModel memory_model = MemoryModel::Unexpanded;
|
||||
/// Maps from a named memory model to a bank enabled/disabled set.
|
||||
void set_memory_model(MemoryModel memory_model) {
|
||||
// This is correct for unexpanded and 32kb memory models.
|
||||
enabled_ram.bank0 = enabled_ram.bank1 =
|
||||
enabled_ram.bank2 = enabled_ram.bank3 =
|
||||
enabled_ram.bank5 = memory_model == MemoryModel::ThirtyTwoKB;
|
||||
|
||||
// Bank 0 will need to be enabled if this is an 8kb machine.
|
||||
enabled_ram.bank0 |= memory_model == MemoryModel::EightKB;
|
||||
}
|
||||
struct {
|
||||
bool bank0 = false;
|
||||
bool bank1 = false;
|
||||
bool bank2 = false;
|
||||
bool bank3 = false;
|
||||
bool bank5 = false;
|
||||
// Sic. There is no bank 4; this is because the area that logically would be
|
||||
// bank 4 is occupied by the character ROM, colour RAM, hardware registers, etc.
|
||||
} enabled_ram;
|
||||
|
||||
Region region = Region::European;
|
||||
bool has_c1540 = false;
|
||||
std::string loading_command;
|
||||
|
@@ -34,7 +34,7 @@ static std::unique_ptr<Analyser::Static::Target> CartridgeTarget(
|
||||
output_segments.emplace_back(start_address, segment.data);
|
||||
}
|
||||
|
||||
std::unique_ptr<Analyser::Static::MSX::Target> target(new Analyser::Static::MSX::Target);
|
||||
auto target = std::make_unique<Analyser::Static::MSX::Target>();
|
||||
target->machine = Analyser::Machine::MSX;
|
||||
target->confidence = confidence;
|
||||
|
||||
@@ -269,7 +269,7 @@ Analyser::Static::TargetList Analyser::Static::MSX::GetTargets(const Media &medi
|
||||
std::move(cartridge_targets.begin(), cartridge_targets.end(), std::back_inserter(destination));
|
||||
|
||||
// Consider building a target for disks and/or tapes.
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
|
||||
// Check tapes for loadable files.
|
||||
for(auto &tape : media.tapes) {
|
||||
|
@@ -10,10 +10,10 @@
|
||||
#include "Target.hpp"
|
||||
|
||||
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.
|
||||
if(media.disks.empty()) return {};
|
||||
// This analyser can comprehend disks and mass-storage devices only.
|
||||
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;
|
||||
|
||||
using Target = Analyser::Static::Macintosh::Target;
|
||||
|
@@ -21,7 +21,7 @@ struct Target: public ::Analyser::Static::Target {
|
||||
MacPlus
|
||||
};
|
||||
|
||||
Model model = Model::Mac512ke;
|
||||
Model model = Model::MacPlus;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -20,7 +20,9 @@
|
||||
|
||||
using namespace Analyser::Static::Oric;
|
||||
|
||||
static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) {
|
||||
namespace {
|
||||
|
||||
int score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) {
|
||||
int score = 0;
|
||||
|
||||
for(const auto address : disassembly.outward_calls) score += (rom_functions.find(address) != rom_functions.end()) ? 1 : -1;
|
||||
@@ -30,7 +32,7 @@ static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, cons
|
||||
return score;
|
||||
}
|
||||
|
||||
static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||
int basic10_score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||
const std::set<uint16_t> rom_functions = {
|
||||
0x0228, 0x022b,
|
||||
0xc3ca, 0xc3f8, 0xc448, 0xc47c, 0xc4b5, 0xc4e3, 0xc4e0, 0xc524, 0xc56f, 0xc5a2, 0xc5f8, 0xc60a, 0xc6a5, 0xc6de, 0xc719, 0xc738,
|
||||
@@ -51,10 +53,10 @@ static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembl
|
||||
0x0228, 0x0229, 0x022a, 0x022b, 0x022c, 0x022d, 0x0230
|
||||
};
|
||||
|
||||
return Score(disassembly, rom_functions, variable_locations);
|
||||
return score(disassembly, rom_functions, variable_locations);
|
||||
}
|
||||
|
||||
static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||
int basic11_score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
|
||||
const std::set<uint16_t> rom_functions = {
|
||||
0x0238, 0x023b, 0x023e, 0x0241, 0x0244, 0x0247,
|
||||
0xc3c6, 0xc3f4, 0xc444, 0xc47c, 0xc4a8, 0xc4d3, 0xc4e0, 0xc524, 0xc55f, 0xc592, 0xc5e8, 0xc5fa, 0xc692, 0xc6b3, 0xc6ee, 0xc70d,
|
||||
@@ -76,10 +78,10 @@ static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembl
|
||||
0x0244, 0x0245, 0x0246, 0x0247, 0x0248, 0x0249, 0x024a, 0x024b, 0x024c
|
||||
};
|
||||
|
||||
return Score(disassembly, rom_functions, variable_locations);
|
||||
return score(disassembly, rom_functions, variable_locations);
|
||||
}
|
||||
|
||||
static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) {
|
||||
bool is_microdisc(Storage::Encodings::MFM::Parser &parser) {
|
||||
/*
|
||||
The Microdisc boot sector is sector 2 of track 0 and contains a 23-byte signature.
|
||||
*/
|
||||
@@ -100,8 +102,51 @@ static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) {
|
||||
return !std::memcmp(signature, first_sample.data(), sizeof(signature));
|
||||
}
|
||||
|
||||
bool is_400_loader(Storage::Encodings::MFM::Parser &parser, uint16_t range_start, uint16_t range_end) {
|
||||
/*
|
||||
Both the Jasmin and BD-DOS boot sectors are sector 1 of track 0 and are loaded at $400;
|
||||
use disassembly to test for likely matches.
|
||||
*/
|
||||
|
||||
Storage::Encodings::MFM::Sector *sector = parser.get_sector(0, 0, 1);
|
||||
if(!sector) return false;
|
||||
if(sector->samples.empty()) return false;
|
||||
|
||||
// Take a copy of the first sampling, and keep only the final 256 bytes (assuming at least that many were found).
|
||||
std::vector<uint8_t> first_sample = sector->samples[0];
|
||||
if(first_sample.size() < 256) return false;
|
||||
if(first_sample.size() > 256) {
|
||||
first_sample.erase(first_sample.end() - 256, first_sample.end());
|
||||
}
|
||||
|
||||
// Grab a disassembly.
|
||||
const auto disassembly =
|
||||
Analyser::Static::MOS6502::Disassemble(first_sample, Analyser::Static::Disassembler::OffsetMapper(0x400), {0x400});
|
||||
|
||||
// Check for references to the Jasmin registers.
|
||||
int register_hits = 0;
|
||||
for(auto list : {disassembly.external_stores, disassembly.external_loads, disassembly.external_modifies}) {
|
||||
for(auto address : list) {
|
||||
register_hits += (address >= range_start && address <= range_end);
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary, sure, but as long as at least two accesses to the requested register range are found, accept this.
|
||||
return register_hits >= 2;
|
||||
}
|
||||
|
||||
bool is_jasmin(Storage::Encodings::MFM::Parser &parser) {
|
||||
return is_400_loader(parser, 0x3f4, 0x3ff);
|
||||
}
|
||||
|
||||
bool is_bd500(Storage::Encodings::MFM::Parser &parser) {
|
||||
return is_400_loader(parser, 0x310, 0x323);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
target->machine = Machine::Oric;
|
||||
target->confidence = 0.5;
|
||||
|
||||
@@ -115,12 +160,10 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
|
||||
for(const auto &file : tape_files) {
|
||||
if(file.data_type == File::MachineCode) {
|
||||
std::vector<uint16_t> entry_points = {file.starting_address};
|
||||
Analyser::Static::MOS6502::Disassembly disassembly =
|
||||
const Analyser::Static::MOS6502::Disassembly disassembly =
|
||||
Analyser::Static::MOS6502::Disassemble(file.data, Analyser::Static::Disassembler::OffsetMapper(file.starting_address), entry_points);
|
||||
|
||||
int basic10_score = Basic10Score(disassembly);
|
||||
int basic11_score = Basic11Score(disassembly);
|
||||
if(basic10_score > basic11_score) basic10_votes++; else basic11_votes++;
|
||||
if(basic10_score(disassembly) > basic11_score(disassembly)) ++basic10_votes; else ++basic11_votes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,12 +173,22 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
|
||||
}
|
||||
|
||||
if(!media.disks.empty()) {
|
||||
// Only the Microdisc is emulated right now, so accept only disks that it can boot from.
|
||||
// 8-DOS is recognised by a dedicated Disk II analyser, so check only for Microdisc,
|
||||
// Jasmin and BD-DOS formats here.
|
||||
for(auto &disk: media.disks) {
|
||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||
if(IsMicrodisc(parser)) {
|
||||
|
||||
if(is_microdisc(parser)) {
|
||||
target->disk_interface = Target::DiskInterface::Microdisc;
|
||||
target->media.disks.push_back(disk);
|
||||
} else if(is_jasmin(parser)) {
|
||||
target->disk_interface = Target::DiskInterface::Jasmin;
|
||||
target->should_start_jasmin = true;
|
||||
target->media.disks.push_back(disk);
|
||||
} else if(is_bd500(parser)) {
|
||||
target->disk_interface = Target::DiskInterface::BD500;
|
||||
target->media.disks.push_back(disk);
|
||||
target->rom = Target::ROM::BASIC10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -26,12 +26,15 @@ struct Target: public ::Analyser::Static::Target {
|
||||
enum class DiskInterface {
|
||||
Microdisc,
|
||||
Pravetz,
|
||||
Jasmin,
|
||||
BD500,
|
||||
None
|
||||
};
|
||||
|
||||
ROM rom = ROM::BASIC11;
|
||||
DiskInterface disk_interface = DiskInterface::None;
|
||||
std::string loading_command;
|
||||
bool should_start_jasmin = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &med
|
||||
return {};
|
||||
|
||||
TargetList targets;
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
auto target = std::make_unique<Target>();
|
||||
|
||||
target->machine = Machine::MasterSystem;
|
||||
|
||||
|
@@ -17,7 +17,8 @@
|
||||
#include "Acorn/StaticAnalyser.hpp"
|
||||
#include "AmstradCPC/StaticAnalyser.hpp"
|
||||
#include "AppleII/StaticAnalyser.hpp"
|
||||
#include "Atari/StaticAnalyser.hpp"
|
||||
#include "Atari2600/StaticAnalyser.hpp"
|
||||
#include "AtariST/StaticAnalyser.hpp"
|
||||
#include "Coleco/StaticAnalyser.hpp"
|
||||
#include "Commodore/StaticAnalyser.hpp"
|
||||
#include "DiskII/StaticAnalyser.hpp"
|
||||
@@ -40,12 +41,18 @@
|
||||
#include "../../Storage/Disk/DiskImage/Formats/G64.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/DMK.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/NIB.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/SSD.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/ST.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/STX.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp"
|
||||
|
||||
// Mass Storage Devices (i.e. usually, hard disks)
|
||||
#include "../../Storage/MassStorage/Formats/HFV.hpp"
|
||||
|
||||
// Tapes
|
||||
#include "../../Storage/Tape/Formats/CAS.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("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::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::OricMFMDSK>, TargetPlatform::Oric) // DSK (Oric)
|
||||
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)
|
||||
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("msa", result.disks, Disk::DiskImageHolder<Storage::Disk::MSA>, TargetPlatform::AtariST) // MSA
|
||||
Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII) // NIB
|
||||
Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O
|
||||
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("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS
|
||||
Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // SSD
|
||||
Format("st", result.disks, Disk::DiskImageHolder<Storage::Disk::ST>, TargetPlatform::AtariST) // ST
|
||||
Format("stx", result.disks, Disk::DiskImageHolder<Storage::Disk::STX>, TargetPlatform::AtariST) // STX
|
||||
Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore)
|
||||
Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric)
|
||||
Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX
|
||||
@@ -160,7 +171,7 @@ Media Analyser::Static::GetMedia(const std::string &file_name) {
|
||||
TargetList Analyser::Static::GetTargets(const std::string &file_name) {
|
||||
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.
|
||||
TargetPlatform::IntType potential_platforms = 0;
|
||||
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::AmstradCPC) Append(AmstradCPC);
|
||||
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::Commodore) Append(Commodore);
|
||||
if(potential_platforms & TargetPlatform::DiskII) Append(DiskII);
|
||||
|
@@ -11,9 +11,10 @@
|
||||
|
||||
#include "../Machines.hpp"
|
||||
|
||||
#include "../../Storage/Tape/Tape.hpp"
|
||||
#include "../../Storage/Disk/Disk.hpp"
|
||||
#include "../../Storage/Cartridge/Cartridge.hpp"
|
||||
#include "../../Storage/Disk/Disk.hpp"
|
||||
#include "../../Storage/MassStorage/MassStorageDevice.hpp"
|
||||
#include "../../Storage/Tape/Tape.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -29,9 +30,10 @@ struct Media {
|
||||
std::vector<std::shared_ptr<Storage::Disk::Disk>> disks;
|
||||
std::vector<std::shared_ptr<Storage::Tape::Tape>> tapes;
|
||||
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges;
|
||||
std::vector<std::shared_ptr<Storage::MassStorage::MassStorageDevice>> mass_storage_devices;
|
||||
|
||||
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
|
||||
|
||||
#include "ForceInline.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
/*
|
||||
Informal pattern for all classes that run from a clock cycle:
|
||||
@@ -54,7 +55,9 @@
|
||||
*/
|
||||
template <class T> class WrappedInt {
|
||||
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 T &operator =(const T &rhs) {
|
||||
@@ -133,16 +136,20 @@ template <class T> class WrappedInt {
|
||||
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
|
||||
|
||||
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
|
||||
the value of @c this modulo @c divisor and @c divided by @c divisor is returned.
|
||||
*/
|
||||
forceinline T divide(const T &divisor) {
|
||||
T result(length_ / divisor.length_);
|
||||
length_ %= divisor.length_;
|
||||
return result;
|
||||
template <typename Result = T> forceinline Result divide(const T &divisor) {
|
||||
Result r;
|
||||
static_cast<T *>(this)->fill(r, divisor);
|
||||
return r;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -161,13 +168,13 @@ template <class T> class WrappedInt {
|
||||
// classes that use this template.
|
||||
|
||||
protected:
|
||||
int length_;
|
||||
IntType length_;
|
||||
};
|
||||
|
||||
/// Describes an integer number of whole cycles: pairs of clock signal transitions.
|
||||
class Cycles: public WrappedInt<Cycles> {
|
||||
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(const Cycles &cycles) noexcept : WrappedInt<Cycles>(cycles.length_) {}
|
||||
|
||||
@@ -177,15 +184,20 @@ class Cycles: public WrappedInt<Cycles> {
|
||||
result.length_ = length_;
|
||||
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.
|
||||
class HalfCycles: public WrappedInt<HalfCycles> {
|
||||
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(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_) {}
|
||||
|
||||
/// @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_;
|
||||
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
|
||||
|
@@ -13,62 +13,68 @@
|
||||
#include <vector>
|
||||
|
||||
/*!
|
||||
A DeferredQueue maintains a list of ordered actions and the times at which
|
||||
they should happen, and divides a total execution period up into the portions
|
||||
that occur between those actions, triggering each action when it is reached.
|
||||
Provides the logic to insert into and traverse a list of future scheduled items.
|
||||
*/
|
||||
template <typename TimeUnit> class DeferredQueue {
|
||||
public:
|
||||
/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
|
||||
DeferredQueue(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
|
||||
|
||||
/*!
|
||||
Schedules @c action to occur in @c delay units of time.
|
||||
|
||||
Actions must be scheduled in the order they will occur. It is undefined behaviour
|
||||
to schedule them out of order.
|
||||
*/
|
||||
void defer(TimeUnit delay, const std::function<void(void)> &action) {
|
||||
pending_actions_.emplace_back(delay, action);
|
||||
}
|
||||
|
||||
/*!
|
||||
Runs for @c length units of time.
|
||||
|
||||
The constructor-supplied target will be called with one or more periods that add up to @c length;
|
||||
any scheduled actions will be called between periods.
|
||||
*/
|
||||
void run_for(TimeUnit length) {
|
||||
// If there are no pending actions, just run for the entire length.
|
||||
// This should be the normal branch.
|
||||
if(pending_actions_.empty()) {
|
||||
target_(length);
|
||||
// Apply immediately if there's no delay (or a negative delay).
|
||||
if(delay <= TimeUnit(0)) {
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
// Divide the time to run according to the pending actions.
|
||||
while(length > TimeUnit(0)) {
|
||||
TimeUnit next_period = pending_actions_.empty() ? length : std::min(length, pending_actions_[0].delay);
|
||||
target_(next_period);
|
||||
length -= next_period;
|
||||
if(!pending_actions_.empty()) {
|
||||
// Otherwise enqueue, having subtracted the delay for any preceding events,
|
||||
// and subtracting from the subsequent, if any.
|
||||
auto insertion_point = pending_actions_.begin();
|
||||
while(insertion_point != pending_actions_.end() && insertion_point->delay < delay) {
|
||||
delay -= insertion_point->delay;
|
||||
++insertion_point;
|
||||
}
|
||||
if(insertion_point != pending_actions_.end()) {
|
||||
insertion_point->delay -= delay;
|
||||
}
|
||||
|
||||
off_t performances = 0;
|
||||
for(auto &action: pending_actions_) {
|
||||
action.delay -= next_period;
|
||||
if(!action.delay) {
|
||||
action.action();
|
||||
++performances;
|
||||
}
|
||||
}
|
||||
if(performances) {
|
||||
pending_actions_.erase(pending_actions_.begin(), pending_actions_.begin() + performances);
|
||||
pending_actions_.emplace(insertion_point, delay, action);
|
||||
} else {
|
||||
pending_actions_.emplace_back(delay, action);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The amount of time until the next enqueued action will occur,
|
||||
or TimeUnit(-1) if the queue is empty.
|
||||
*/
|
||||
TimeUnit time_until_next_action() {
|
||||
if(pending_actions_.empty()) return TimeUnit(-1);
|
||||
return pending_actions_.front().delay;
|
||||
}
|
||||
|
||||
/*!
|
||||
Advances the queue the specified amount of time, performing any actions it reaches.
|
||||
*/
|
||||
void advance(TimeUnit time) {
|
||||
auto erase_iterator = pending_actions_.begin();
|
||||
while(erase_iterator != pending_actions_.end()) {
|
||||
erase_iterator->delay -= time;
|
||||
if(erase_iterator->delay <= TimeUnit(0)) {
|
||||
time = -erase_iterator->delay;
|
||||
erase_iterator->action();
|
||||
++erase_iterator;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(erase_iterator != pending_actions_.begin()) {
|
||||
pending_actions_.erase(pending_actions_.begin(), erase_iterator);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void(TimeUnit)> target_;
|
||||
|
||||
// The list of deferred actions.
|
||||
struct DeferredAction {
|
||||
TimeUnit delay;
|
||||
@@ -79,4 +85,40 @@ template <typename TimeUnit> class DeferredQueue {
|
||||
std::vector<DeferredAction> pending_actions_;
|
||||
};
|
||||
|
||||
/*!
|
||||
A DeferredQueue maintains a list of ordered actions and the times at which
|
||||
they should happen, and divides a total execution period up into the portions
|
||||
that occur between those actions, triggering each action when it is reached.
|
||||
|
||||
This list is efficient only for short queues.
|
||||
*/
|
||||
template <typename TimeUnit> class DeferredQueuePerformer: public DeferredQueue<TimeUnit> {
|
||||
public:
|
||||
/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
|
||||
DeferredQueuePerformer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
|
||||
|
||||
/*!
|
||||
Runs for @c length units of time.
|
||||
|
||||
The constructor-supplied target will be called with one or more periods that add up to @c length;
|
||||
any scheduled actions will be called between periods.
|
||||
*/
|
||||
void run_for(TimeUnit length) {
|
||||
auto time_to_next = DeferredQueue<TimeUnit>::time_until_next_action();
|
||||
while(time_to_next != TimeUnit(-1) && time_to_next <= length) {
|
||||
target_(time_to_next);
|
||||
length -= time_to_next;
|
||||
DeferredQueue<TimeUnit>::advance(time_to_next);
|
||||
}
|
||||
|
||||
DeferredQueue<TimeUnit>::advance(length);
|
||||
target_(length);
|
||||
|
||||
// TODO: optimise this to avoid the multiple std::vector deletes. Find a neat way to expose that solution, maybe?
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void(TimeUnit)> target_;
|
||||
};
|
||||
|
||||
#endif /* DeferredQueue_h */
|
||||
|
@@ -9,9 +9,9 @@
|
||||
#ifndef ForceInline_hpp
|
||||
#define ForceInline_hpp
|
||||
|
||||
#ifdef DEBUG
|
||||
#ifndef NDEBUG
|
||||
|
||||
#define forceinline
|
||||
#define forceinline inline
|
||||
|
||||
#else
|
||||
|
||||
|
@@ -10,6 +10,7 @@
|
||||
#define JustInTime_h
|
||||
|
||||
#include "../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "ForceInline.hpp"
|
||||
|
||||
/*!
|
||||
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
|
||||
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:
|
||||
/// Constructs a new JustInTimeActor using the same construction arguments as the included object.
|
||||
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
|
||||
/// Adds time to the actor.
|
||||
inline void operator += (const LocalTimeScale &rhs) {
|
||||
time_since_update_ += rhs;
|
||||
forceinline void operator += (const LocalTimeScale &rhs) {
|
||||
if constexpr (multiplier != 1) {
|
||||
time_since_update_ += rhs * multiplier;
|
||||
} else {
|
||||
time_since_update_ += rhs;
|
||||
}
|
||||
is_flushed_ = false;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time and returns a pointer to the included object.
|
||||
inline T *operator->() {
|
||||
forceinline T *operator->() {
|
||||
flush();
|
||||
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.
|
||||
inline T *last_valid() {
|
||||
forceinline T *last_valid() {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time.
|
||||
inline void flush() {
|
||||
if(!is_flushed_) object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
||||
is_flushed_ = true;
|
||||
forceinline void flush() {
|
||||
if(!is_flushed_) {
|
||||
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:
|
||||
@@ -55,12 +76,54 @@ template <class T, class LocalTimeScale, class TargetTimeScale = LocalTimeScale>
|
||||
bool is_flushed_ = true;
|
||||
};
|
||||
|
||||
/*!
|
||||
A RealTimeActor presents the same interface as a JustInTimeActor but doesn't defer work.
|
||||
Time added will be performed immediately.
|
||||
|
||||
Its primary purpose is to allow consumers to remain flexible in their scheduling.
|
||||
*/
|
||||
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class RealTimeActor {
|
||||
public:
|
||||
template<typename... Args> RealTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
|
||||
forceinline void operator += (const LocalTimeScale &rhs) {
|
||||
if constexpr (multiplier == 1 && divider == 1) {
|
||||
object_.run_for(TargetTimeScale(rhs));
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (multiplier == 1) {
|
||||
accumulated_time_ += rhs;
|
||||
} else {
|
||||
accumulated_time_ += rhs * multiplier;
|
||||
}
|
||||
|
||||
if constexpr (divider == 1) {
|
||||
const auto duration = accumulated_time_.template flush<TargetTimeScale>();
|
||||
object_.run_for(duration);
|
||||
} else {
|
||||
const auto duration = accumulated_time_.template divide<TargetTimeScale>(LocalTimeScale(divider));
|
||||
if(duration > TargetTimeScale(0))
|
||||
object_.run_for(duration);
|
||||
}
|
||||
}
|
||||
|
||||
forceinline T *operator->() { return &object_; }
|
||||
forceinline const T *operator->() const { return &object_; }
|
||||
forceinline T *last_valid() { return &object_; }
|
||||
forceinline void flush() {}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale accumulated_time_;
|
||||
};
|
||||
|
||||
/*!
|
||||
A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
|
||||
Any time the amount of accumulated time crosses a threshold provided at construction time,
|
||||
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:
|
||||
/// Constructs a new AsyncJustInTimeActor using the same construction arguments as the included object.
|
||||
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
|
||||
#define TimeTypes_h
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace Time {
|
||||
|
||||
typedef double Seconds;
|
||||
typedef int64_t Nanos;
|
||||
|
||||
inline Nanos nanos_now() {
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,8 @@
|
||||
#include "1770.hpp"
|
||||
|
||||
#include "../../Storage/Disk/Encodings/MFM/Constants.hpp"
|
||||
|
||||
#define LOG_PREFIX "[WD FDC] "
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
using namespace WD;
|
||||
@@ -16,19 +18,19 @@ using namespace WD;
|
||||
WD1770::WD1770(Personality p) :
|
||||
Storage::Disk::MFMController(8000000),
|
||||
personality_(p),
|
||||
interesting_event_mask_(static_cast<int>(Event1770::Command)) {
|
||||
interesting_event_mask_(int(Event1770::Command)) {
|
||||
set_is_double_density(false);
|
||||
posit_event(static_cast<int>(Event1770::Command));
|
||||
posit_event(int(Event1770::Command));
|
||||
}
|
||||
|
||||
void WD1770::set_register(int address, uint8_t value) {
|
||||
void WD1770::write(int address, uint8_t value) {
|
||||
switch(address&3) {
|
||||
case 0: {
|
||||
if((value&0xf0) == 0xd0) {
|
||||
if(value == 0xd0) {
|
||||
// Force interrupt **immediately**.
|
||||
LOG("Force interrupt immediately");
|
||||
posit_event(static_cast<int>(Event1770::ForceInterrupt));
|
||||
posit_event(int(Event1770::ForceInterrupt));
|
||||
} else {
|
||||
ERROR("!!!TODO: force interrupt!!!");
|
||||
update_status([] (Status &status) {
|
||||
@@ -37,7 +39,7 @@ void WD1770::set_register(int address, uint8_t value) {
|
||||
}
|
||||
} else {
|
||||
command_ = value;
|
||||
posit_event(static_cast<int>(Event1770::Command));
|
||||
posit_event(int(Event1770::Command));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -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) {
|
||||
default: {
|
||||
update_status([] (Status &status) {
|
||||
status.interrupt_request = false;
|
||||
});
|
||||
uint8_t status =
|
||||
(status_.write_protect ? Flag::WriteProtect : 0) |
|
||||
(status_.crc_error ? Flag::CRCError : 0) |
|
||||
(status_.busy ? Flag::Busy : 0);
|
||||
(status_.crc_error ? Flag::CRCError : 0) |
|
||||
(status_.busy ? Flag::Busy : 0);
|
||||
|
||||
// Per Jean Louis-Guérin's documentation:
|
||||
//
|
||||
// * the write-protect bit is locked into place by a type 2 or type 3 command, but is
|
||||
// read live after a type 1.
|
||||
// * the track 0 bit is captured during a type 1 instruction and lost upon any other type,
|
||||
// it is not live sampled.
|
||||
switch(status_.type) {
|
||||
case Status::One:
|
||||
status |=
|
||||
(get_drive().get_is_track_zero() ? Flag::TrackZero : 0) |
|
||||
(status_.seek_error ? Flag::SeekError : 0);
|
||||
// TODO: index hole
|
||||
(status_.track_zero ? Flag::TrackZero : 0) |
|
||||
(status_.seek_error ? Flag::SeekError : 0) |
|
||||
(get_drive().get_is_read_only() ? Flag::WriteProtect : 0) |
|
||||
(get_drive().get_index_pulse() ? Flag::Index : 0);
|
||||
break;
|
||||
|
||||
case Status::Two:
|
||||
case Status::Three:
|
||||
status |=
|
||||
(status_.write_protect ? Flag::WriteProtect : 0) |
|
||||
(status_.record_type ? Flag::RecordType : 0) |
|
||||
(status_.lost_data ? Flag::LostData : 0) |
|
||||
(status_.data_request ? Flag::DataRequest : 0) |
|
||||
@@ -89,10 +99,15 @@ uint8_t WD1770::get_register(int address) {
|
||||
if(status_.type == Status::One)
|
||||
status |= (status_.spin_up ? Flag::SpinUp : 0);
|
||||
}
|
||||
// LOG("Returned status " << PADHEX(2) << int(status) << " of type " << 1+int(status_.type));
|
||||
return status;
|
||||
}
|
||||
case 1: return track_;
|
||||
case 2: return sector_;
|
||||
case 1:
|
||||
LOG("Returned track " << int(track_));
|
||||
return track_;
|
||||
case 2:
|
||||
LOG("Returned sector " << int(sector_));
|
||||
return sector_;
|
||||
case 3:
|
||||
update_status([] (Status &status) {
|
||||
status.data_request = false;
|
||||
@@ -105,28 +120,30 @@ void WD1770::run_for(const Cycles cycles) {
|
||||
Storage::Disk::Controller::run_for(cycles);
|
||||
|
||||
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) {
|
||||
delay_time_ = 0;
|
||||
posit_event(static_cast<int>(Event1770::Timer));
|
||||
posit_event(int(Event1770::Timer));
|
||||
} else {
|
||||
delay_time_ -= number_of_cycles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(mask); return; case __LINE__:
|
||||
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = int(mask); return; case __LINE__:
|
||||
#define WAIT_FOR_TIME(ms) resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer);
|
||||
#define WAIT_FOR_BYTES(count) resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = static_cast<int>(Event::Token); return; }
|
||||
#define WAIT_FOR_BYTES(count) resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = int(Event::Token); return; }
|
||||
#define BEGIN_SECTION() switch(resume_point_) { default:
|
||||
#define END_SECTION() (void)0; }
|
||||
|
||||
#define READ_ID() \
|
||||
if(new_event_type == static_cast<int>(Event::Token)) { \
|
||||
if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; } \
|
||||
else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { \
|
||||
if(new_event_type == int(Event::Token)) { \
|
||||
if(!distance_into_section_ && get_latest_token().type == Token::ID) {\
|
||||
set_data_mode(DataMode::Reading); \
|
||||
++distance_into_section_; \
|
||||
} else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { \
|
||||
header_[distance_into_section_ - 1] = get_latest_token().byte_value; \
|
||||
distance_into_section_++; \
|
||||
++distance_into_section_; \
|
||||
} \
|
||||
}
|
||||
|
||||
@@ -159,10 +176,10 @@ void WD1770::run_for(const Cycles cycles) {
|
||||
// +--------+----------+-------------------------+
|
||||
|
||||
void WD1770::posit_event(int new_event_type) {
|
||||
if(new_event_type == static_cast<int>(Event::IndexHole)) {
|
||||
if(new_event_type == int(Event::IndexHole)) {
|
||||
index_hole_count_++;
|
||||
if(index_hole_count_target_ == index_hole_count_) {
|
||||
posit_event(static_cast<int>(Event1770::IndexHoleTarget));
|
||||
posit_event(int(Event1770::IndexHoleTarget));
|
||||
index_hole_count_target_ = -1;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
resume_point_ = 0;
|
||||
update_status([] (Status &status) {
|
||||
status.type = Status::One;
|
||||
status.data_request = false;
|
||||
status.spin_up = false;
|
||||
});
|
||||
} else {
|
||||
if(!(interesting_event_mask_ & static_cast<int>(new_event_type))) return;
|
||||
if(!(interesting_event_mask_ & int(new_event_type))) return;
|
||||
interesting_event_mask_ &= ~new_event_type;
|
||||
}
|
||||
|
||||
@@ -208,6 +226,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
update_status([] (Status &status) {
|
||||
status.busy = true;
|
||||
status.interrupt_request = false;
|
||||
status.track_zero = false; // Always reset by a non-type 1; so reset regardless and set properly later.
|
||||
});
|
||||
|
||||
LOG("Starting " << PADHEX(2) << int(command_));
|
||||
@@ -240,6 +259,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
status.data_request = false;
|
||||
});
|
||||
|
||||
LOG("Step/Seek/Restore with track " << int(track_) << " data " << int(data_));
|
||||
if(!has_motor_on_line() && !has_head_load_line()) goto test_type1_type;
|
||||
|
||||
if(has_motor_on_line()) goto begin_type1_spin_up;
|
||||
@@ -272,19 +292,19 @@ void WD1770::posit_event(int new_event_type) {
|
||||
}
|
||||
|
||||
perform_seek_or_restore_command:
|
||||
if(track_ == data_) goto verify;
|
||||
if(track_ == data_) goto verify_seek;
|
||||
step_direction_ = (data_ > track_);
|
||||
|
||||
adjust_track:
|
||||
if(step_direction_) track_++; else track_--;
|
||||
if(step_direction_) ++track_; else --track_;
|
||||
|
||||
perform_step:
|
||||
if(!step_direction_ && get_drive().get_is_track_zero()) {
|
||||
track_ = 0;
|
||||
goto verify;
|
||||
goto verify_seek;
|
||||
}
|
||||
get_drive().step(Storage::Disk::HeadPosition(step_direction_ ? 1 : -1));
|
||||
unsigned int time_to_wait;
|
||||
Cycles::IntType time_to_wait;
|
||||
switch(command_ & 3) {
|
||||
default:
|
||||
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;
|
||||
}
|
||||
WAIT_FOR_TIME(time_to_wait);
|
||||
if(command_ >> 5) goto verify;
|
||||
if(command_ >> 5) goto verify_seek;
|
||||
goto perform_seek_or_restore_command;
|
||||
|
||||
perform_step_command:
|
||||
if(command_ & 0x10) goto adjust_track;
|
||||
goto perform_step;
|
||||
|
||||
verify:
|
||||
verify_seek:
|
||||
update_status([this] (Status &status) {
|
||||
status.track_zero = get_drive().get_is_track_zero();
|
||||
});
|
||||
if(!(command_ & 0x04)) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
@@ -309,17 +332,20 @@ void WD1770::posit_event(int new_event_type) {
|
||||
distance_into_section_ = 0;
|
||||
|
||||
verify_read_data:
|
||||
WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token));
|
||||
WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token));
|
||||
READ_ID();
|
||||
|
||||
if(index_hole_count_ == 6) {
|
||||
LOG("Nothing found to verify");
|
||||
update_status([] (Status &status) {
|
||||
status.seek_error = true;
|
||||
});
|
||||
goto wait_for_command;
|
||||
}
|
||||
if(distance_into_section_ == 7) {
|
||||
distance_into_section_ = 0;
|
||||
set_data_mode(DataMode::Scanning);
|
||||
|
||||
if(get_crc_generator().get_value()) {
|
||||
update_status([] (Status &status) {
|
||||
status.crc_error = true;
|
||||
@@ -334,8 +360,6 @@ void WD1770::posit_event(int new_event_type) {
|
||||
});
|
||||
goto wait_for_command;
|
||||
}
|
||||
|
||||
distance_into_section_ = 0;
|
||||
}
|
||||
goto verify_read_data;
|
||||
|
||||
@@ -392,8 +416,11 @@ void WD1770::posit_event(int new_event_type) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
|
||||
distance_into_section_ = 0;
|
||||
set_data_mode(DataMode::Scanning);
|
||||
|
||||
type2_get_header:
|
||||
WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token));
|
||||
WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token));
|
||||
READ_ID();
|
||||
|
||||
if(index_hole_count_ == 5) {
|
||||
@@ -404,8 +431,10 @@ void WD1770::posit_event(int new_event_type) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
if(distance_into_section_ == 7) {
|
||||
LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||
distance_into_section_ = 0;
|
||||
set_data_mode(DataMode::Scanning);
|
||||
|
||||
LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||
if( header_[0] == track_ && header_[2] == sector_ &&
|
||||
(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) {
|
||||
LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||
@@ -422,7 +451,6 @@ void WD1770::posit_event(int new_event_type) {
|
||||
});
|
||||
goto type2_read_or_write_data;
|
||||
}
|
||||
distance_into_section_ = 0;
|
||||
}
|
||||
goto type2_get_header;
|
||||
|
||||
@@ -453,7 +481,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
status.data_request = true;
|
||||
});
|
||||
distance_into_section_++;
|
||||
if(distance_into_section_ == 128 << header_[3]) {
|
||||
if(distance_into_section_ == 128 << (header_[3]&3)) {
|
||||
distance_into_section_ = 0;
|
||||
goto type2_check_crc;
|
||||
}
|
||||
@@ -465,6 +493,9 @@ void WD1770::posit_event(int new_event_type) {
|
||||
header_[distance_into_section_] = get_latest_token().byte_value;
|
||||
distance_into_section_++;
|
||||
if(distance_into_section_ == 2) {
|
||||
distance_into_section_ = 0;
|
||||
set_data_mode(DataMode::Scanning);
|
||||
|
||||
if(get_crc_generator().get_value()) {
|
||||
LOG("CRC error; terminating");
|
||||
update_status([this] (Status &status) {
|
||||
@@ -473,11 +504,13 @@ void WD1770::posit_event(int new_event_type) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
|
||||
LOG("Finished reading sector " << std::dec << int(sector_));
|
||||
|
||||
if(command_ & 0x10) {
|
||||
sector_++;
|
||||
LOG("Advancing to search for sector " << std::dec << int(sector_));
|
||||
goto test_type2_write_protection;
|
||||
}
|
||||
LOG("Finished reading sector " << std::dec << int(sector_));
|
||||
goto wait_for_command;
|
||||
}
|
||||
goto type2_check_crc;
|
||||
@@ -531,7 +564,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
*/
|
||||
write_byte(data_);
|
||||
distance_into_section_++;
|
||||
if(distance_into_section_ == 128 << header_[3]) {
|
||||
if(distance_into_section_ == 128 << (header_[3]&3)) {
|
||||
goto type2_write_crc;
|
||||
}
|
||||
|
||||
@@ -610,8 +643,8 @@ void WD1770::posit_event(int new_event_type) {
|
||||
distance_into_section_ = 0;
|
||||
|
||||
read_address_get_header:
|
||||
WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token));
|
||||
if(new_event_type == static_cast<int>(Event::Token)) {
|
||||
WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token));
|
||||
if(new_event_type == int(Event::Token)) {
|
||||
if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; }
|
||||
else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {
|
||||
if(status_.data_request) {
|
||||
@@ -625,9 +658,11 @@ void WD1770::posit_event(int new_event_type) {
|
||||
update_status([] (Status &status) {
|
||||
status.data_request = true;
|
||||
});
|
||||
distance_into_section_++;
|
||||
++distance_into_section_;
|
||||
|
||||
if(distance_into_section_ == 7) {
|
||||
distance_into_section_ = 0;
|
||||
|
||||
if(get_crc_generator().get_value()) {
|
||||
update_status([] (Status &status) {
|
||||
status.crc_error = true;
|
||||
@@ -651,7 +686,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
index_hole_count_ = 0;
|
||||
|
||||
read_track_read_byte:
|
||||
WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole));
|
||||
WAIT_FOR_EVENT(int(Event::Token) | int(Event::IndexHole));
|
||||
if(index_hole_count_) {
|
||||
goto wait_for_command;
|
||||
}
|
||||
@@ -718,7 +753,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
case 0xfd: case 0xfe:
|
||||
// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022
|
||||
write_raw_short(
|
||||
static_cast<uint16_t>(
|
||||
uint16_t(
|
||||
0xa022 |
|
||||
((data_ & 0x80) << 7) |
|
||||
((data_ & 0x40) << 6) |
|
||||
@@ -767,15 +802,18 @@ void WD1770::posit_event(int new_event_type) {
|
||||
}
|
||||
|
||||
void WD1770::update_status(std::function<void(Status &)> updater) {
|
||||
const Status old_status = status_;
|
||||
|
||||
if(delegate_) {
|
||||
Status old_status = status_;
|
||||
updater(status_);
|
||||
bool did_change =
|
||||
const bool did_change =
|
||||
(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);
|
||||
}
|
||||
else updater(status_);
|
||||
} else updater(status_);
|
||||
|
||||
if(status_.busy != old_status.busy) update_clocking_observer();
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
|
||||
/// Writes @c value to the register at @c address. Only the low two bits of the address are decoded.
|
||||
void set_register(int address, uint8_t value);
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/// Fetches the value of the register @c address. Only the low two bits of the address are decoded.
|
||||
uint8_t get_register(int address);
|
||||
uint8_t read(int address);
|
||||
|
||||
/// Runs the controller for @c number_of_cycles cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
enum Flag: uint8_t {
|
||||
NotReady = 0x80,
|
||||
NotReady = 0x80, // 0x80
|
||||
MotorOn = 0x80,
|
||||
WriteProtect = 0x40,
|
||||
RecordType = 0x20,
|
||||
WriteProtect = 0x40, // 0x40
|
||||
RecordType = 0x20, // 0x20
|
||||
SpinUp = 0x20,
|
||||
HeadLoaded = 0x20,
|
||||
RecordNotFound = 0x10,
|
||||
RecordNotFound = 0x10, // 0x10
|
||||
SeekError = 0x10,
|
||||
CRCError = 0x08,
|
||||
LostData = 0x04,
|
||||
CRCError = 0x08, // 0x08
|
||||
LostData = 0x04, // 0x04
|
||||
TrackZero = 0x04,
|
||||
DataRequest = 0x02,
|
||||
DataRequest = 0x02, // 0x02
|
||||
Index = 0x02,
|
||||
Busy = 0x01
|
||||
Busy = 0x01 // 0x01
|
||||
};
|
||||
|
||||
/// @returns The current value of the IRQ line output.
|
||||
@@ -73,11 +73,16 @@ class WD1770: public Storage::Disk::MFMController {
|
||||
};
|
||||
inline void set_delegate(Delegate *delegate) { delegate_ = delegate; }
|
||||
|
||||
ClockingHint::Preference preferred_clocking() final;
|
||||
|
||||
protected:
|
||||
virtual void set_head_load_request(bool head_load);
|
||||
virtual void set_motor_on(bool motor_on);
|
||||
void set_head_loaded(bool head_loaded);
|
||||
|
||||
/// @returns The last value posted to @c set_head_loaded.
|
||||
bool get_head_loaded();
|
||||
|
||||
private:
|
||||
Personality personality_;
|
||||
inline bool has_motor_on_line() { return (personality_ != P1793 ) && (personality_ != P1773); }
|
||||
@@ -94,6 +99,7 @@ class WD1770: public Storage::Disk::MFMController {
|
||||
bool data_request = false;
|
||||
bool interrupt_request = false;
|
||||
bool busy = false;
|
||||
bool track_zero = false;
|
||||
enum {
|
||||
One, Two, Three
|
||||
} type = One;
|
||||
@@ -121,7 +127,7 @@ class WD1770: public Storage::Disk::MFMController {
|
||||
void posit_event(int type);
|
||||
int interesting_event_mask_;
|
||||
int resume_point_ = 0;
|
||||
unsigned int delay_time_ = 0;
|
||||
Cycles::IntType delay_time_ = 0;
|
||||
|
||||
// ID buffer
|
||||
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;
|
||||
|
||||
/*! Sets a register value. */
|
||||
void set_register(int address, uint8_t value);
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*! Gets a register value. */
|
||||
uint8_t get_register(int address);
|
||||
uint8_t read(int address);
|
||||
|
||||
/*! @returns the bus handler. */
|
||||
T &bus_handler();
|
||||
|
@@ -30,7 +30,7 @@ template <typename T> void MOS6522<T>::access(int address) {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) {
|
||||
template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
access(address);
|
||||
switch(address) {
|
||||
@@ -155,7 +155,7 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value)
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> uint8_t MOS6522<T>::get_register(int address) {
|
||||
template <typename T> uint8_t MOS6522<T>::read(int address) {
|
||||
address &= 0xf;
|
||||
access(address);
|
||||
switch(address) {
|
||||
@@ -346,7 +346,7 @@ template <typename T> void MOS6522<T>::do_phase1() {
|
||||
|
||||
/*! Runs for a specified number of 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(is_phase2_) {
|
||||
@@ -375,7 +375,7 @@ template <typename T> void MOS6522<T>::flush() {
|
||||
|
||||
/*! Runs for a specified number of 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--) {
|
||||
do_phase1();
|
||||
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 uint8_t get_ram(uint16_t address) { return ram_[address & 0x7f]; }
|
||||
|
||||
inline void set_register(int address, uint8_t value) {
|
||||
inline void write(int address, uint8_t value) {
|
||||
const uint8_t decodedAddress = address & 0x07;
|
||||
switch(decodedAddress) {
|
||||
// Port output
|
||||
@@ -63,7 +63,7 @@ template <class T> class MOS6532 {
|
||||
}
|
||||
}
|
||||
|
||||
inline uint8_t get_register(int address) {
|
||||
inline uint8_t read(int address) {
|
||||
const uint8_t decodedAddress = address & 0x7;
|
||||
switch(decodedAddress) {
|
||||
// Port input
|
||||
@@ -107,7 +107,7 @@ template <class T> class MOS6532 {
|
||||
}
|
||||
|
||||
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
|
||||
if(timer_.value >= number_of_cycles) {
|
||||
|
@@ -58,7 +58,7 @@ enum class OutputMode {
|
||||
To run the VIC for a cycle, the caller should call @c get_address, make the requested bus access
|
||||
and call @c set_graphics_value with the result.
|
||||
|
||||
@c set_register and @c get_register provide register access.
|
||||
@c write and @c read provide register access.
|
||||
*/
|
||||
template <class BusHandler> class MOS6560 {
|
||||
public:
|
||||
@@ -83,8 +83,9 @@ template <class BusHandler> class MOS6560 {
|
||||
speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0));
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const { return crt_.get_scaled_scan_status() / 4.0f; }
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
|
||||
Outputs::Speaker::Speaker *get_speaker() { return &speaker_; }
|
||||
|
||||
void set_high_frequency_cutoff(float cutoff) {
|
||||
@@ -170,7 +171,7 @@ template <class BusHandler> class MOS6560 {
|
||||
// keep track of the amount of time since the speaker was updated; lazy updates are applied
|
||||
cycles_since_speaker_update_ += cycles;
|
||||
|
||||
int number_of_cycles = cycles.as_int();
|
||||
auto number_of_cycles = cycles.as_integral();
|
||||
while(number_of_cycles--) {
|
||||
// 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_;
|
||||
@@ -353,7 +354,7 @@ template <class BusHandler> class MOS6560 {
|
||||
/*!
|
||||
Writes to a 6560 register.
|
||||
*/
|
||||
void set_register(int address, uint8_t value) {
|
||||
void write(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
registers_.direct_values[address] = value;
|
||||
switch(address) {
|
||||
@@ -417,7 +418,7 @@ template <class BusHandler> class MOS6560 {
|
||||
/*
|
||||
Reads from a 6560 register.
|
||||
*/
|
||||
uint8_t get_register(int address) {
|
||||
uint8_t read(int address) {
|
||||
address &= 0xf;
|
||||
switch(address) {
|
||||
default: return registers_.direct_values[address];
|
||||
|
@@ -111,7 +111,7 @@ template <class T> class CRTC6845 {
|
||||
}
|
||||
|
||||
void run_for(Cycles cycles) {
|
||||
int cyles_remaining = cycles.as_int();
|
||||
auto cyles_remaining = cycles.as_integral();
|
||||
while(cyles_remaining--) {
|
||||
// check for end of visible characters
|
||||
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
|
||||
then the PortHandler will be informed.
|
||||
*/
|
||||
void set_register(int address, uint8_t value) {
|
||||
void write(int address, uint8_t value) {
|
||||
switch(address & 3) {
|
||||
case 0:
|
||||
if(!(control_ & 0x10)) {
|
||||
@@ -60,7 +60,7 @@ template <class T> class i8255 {
|
||||
Obtains the current value for the register at @c address. If this provides a reading
|
||||
of input then the PortHandler will be queried.
|
||||
*/
|
||||
uint8_t get_register(int address) {
|
||||
uint8_t read(int address) {
|
||||
switch(address & 3) {
|
||||
case 0: return (control_ & 0x10) ? port_handler_.get_value(0) : outputs_[0];
|
||||
case 1: return (control_ & 0x02) ? port_handler_.get_value(1) : outputs_[1];
|
||||
|
@@ -95,11 +95,11 @@ void i8272::run_for(Cycles cycles) {
|
||||
|
||||
// check for an expired timer
|
||||
if(delay_time_ > 0) {
|
||||
if(cycles.as_int() >= delay_time_) {
|
||||
if(cycles.as_integral() >= delay_time_) {
|
||||
delay_time_ = 0;
|
||||
posit_event(static_cast<int>(Event8272::Timer));
|
||||
} 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_;
|
||||
for(int c = 0; c < 4; c++) {
|
||||
if(drives_[c].phase == Drive::Seeking) {
|
||||
drives_[c].step_rate_counter += cycles.as_int();
|
||||
int steps = drives_[c].step_rate_counter / (8000 * step_rate_time_);
|
||||
drives_[c].step_rate_counter += cycles.as_integral();
|
||||
auto steps = drives_[c].step_rate_counter / (8000 * step_rate_time_);
|
||||
drives_[c].step_rate_counter %= (8000 * step_rate_time_);
|
||||
while(steps--) {
|
||||
// Perform a step.
|
||||
@@ -141,12 +141,12 @@ void i8272::run_for(Cycles cycles) {
|
||||
int head = c&1;
|
||||
|
||||
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_is_loaded[head] = false;
|
||||
head_timers_running_--;
|
||||
} else {
|
||||
drives_[drive].head_unload_delay[head] -= cycles.as_int();
|
||||
drives_[drive].head_unload_delay[head] -= cycles.as_integral();
|
||||
}
|
||||
timers_left--;
|
||||
if(!timers_left) break;
|
||||
@@ -163,7 +163,7 @@ void i8272::run_for(Cycles cycles) {
|
||||
if(is_sleeping_) update_clocking_observer();
|
||||
}
|
||||
|
||||
void i8272::set_register(int address, uint8_t value) {
|
||||
void i8272::write(int address, uint8_t value) {
|
||||
// don't consider attempted sets to the status register
|
||||
if(!address) return;
|
||||
|
||||
@@ -181,7 +181,7 @@ void i8272::set_register(int address, uint8_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t i8272::get_register(int address) {
|
||||
uint8_t i8272::read(int address) {
|
||||
if(address) {
|
||||
if(result_stack_.empty()) return 0xff;
|
||||
uint8_t result = result_stack_.back();
|
||||
@@ -292,7 +292,7 @@ void i8272::posit_event(int event_type) {
|
||||
WAIT_FOR_EVENT(Event8272::CommandByte)
|
||||
SetBusy();
|
||||
|
||||
static const std::size_t required_lengths[32] = {
|
||||
static constexpr std::size_t required_lengths[32] = {
|
||||
0, 0, 9, 3, 2, 9, 9, 2,
|
||||
1, 9, 2, 0, 9, 6, 0, 3,
|
||||
0, 9, 0, 0, 0, 0, 0, 0,
|
||||
@@ -865,7 +865,7 @@ void i8272::posit_event(int event_type) {
|
||||
SetDataRequest();
|
||||
SetDataDirectionToProcessor();
|
||||
|
||||
// The actual stuff of unwinding result_stack_ is handled by ::get_register; wait
|
||||
// The actual stuff of unwinding result_stack_ is handled by ::read; wait
|
||||
// until the processor has read all result bytes.
|
||||
WAIT_FOR_EVENT(Event8272::ResultEmpty);
|
||||
|
||||
|
@@ -24,7 +24,7 @@ class BusHandler {
|
||||
virtual void set_interrupt(bool irq) {}
|
||||
};
|
||||
|
||||
class i8272: public Storage::Disk::MFMController {
|
||||
class i8272 : public Storage::Disk::MFMController {
|
||||
public:
|
||||
i8272(BusHandler &bus_handler, Cycles clock_rate);
|
||||
|
||||
@@ -33,13 +33,13 @@ class i8272: public Storage::Disk::MFMController {
|
||||
void set_data_input(uint8_t value);
|
||||
uint8_t get_data_output();
|
||||
|
||||
void set_register(int address, uint8_t value);
|
||||
uint8_t get_register(int address);
|
||||
void write(int address, uint8_t value);
|
||||
uint8_t read(int address);
|
||||
|
||||
void set_dma_acknowledge(bool dack);
|
||||
void set_terminal_count(bool tc);
|
||||
|
||||
ClockingHint::Preference preferred_clocking() override;
|
||||
ClockingHint::Preference preferred_clocking() final;
|
||||
|
||||
protected:
|
||||
virtual void select_drive(int number) = 0;
|
||||
@@ -67,13 +67,13 @@ class i8272: public Storage::Disk::MFMController {
|
||||
ResultEmpty = (1 << 5),
|
||||
NoLongerReady = (1 << 6)
|
||||
};
|
||||
void posit_event(int type) override;
|
||||
void posit_event(int type) final;
|
||||
int interesting_event_mask_ = static_cast<int>(Event8272::CommandByte);
|
||||
int resume_point_ = 0;
|
||||
bool is_access_command_ = false;
|
||||
|
||||
// The counter used for ::Timer events.
|
||||
int delay_time_ = 0;
|
||||
Cycles::IntType delay_time_ = 0;
|
||||
|
||||
// The connected drives.
|
||||
struct Drive {
|
||||
@@ -89,12 +89,12 @@ class i8272: public Storage::Disk::MFMController {
|
||||
bool seek_failed = false;
|
||||
|
||||
// Seeking: transient state.
|
||||
int step_rate_counter = 0;
|
||||
Cycles::IntType step_rate_counter = 0;
|
||||
int steps_taken = 0;
|
||||
int target_head_position = 0; // either an actual number, or -1 to indicate to step until track zero
|
||||
|
||||
// Head state.
|
||||
int head_unload_delay[2] = {0, 0};
|
||||
Cycles::IntType head_unload_delay[2] = {0, 0};
|
||||
bool head_is_loaded[2] = {false, false};
|
||||
|
||||
} drives_[4];
|
||||
|
@@ -17,16 +17,16 @@ using namespace TI::TMS;
|
||||
|
||||
namespace {
|
||||
|
||||
const uint8_t StatusInterrupt = 0x80;
|
||||
const uint8_t StatusSpriteOverflow = 0x40;
|
||||
constexpr uint8_t StatusInterrupt = 0x80;
|
||||
constexpr uint8_t StatusSpriteOverflow = 0x40;
|
||||
|
||||
const int StatusSpriteCollisionShift = 5;
|
||||
const uint8_t StatusSpriteCollision = 0x20;
|
||||
constexpr int StatusSpriteCollisionShift = 5;
|
||||
constexpr uint8_t StatusSpriteCollision = 0x20;
|
||||
|
||||
// 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole
|
||||
// line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368.
|
||||
const unsigned int CRTCyclesPerLine = 1365;
|
||||
const unsigned int CRTCyclesDivider = 4;
|
||||
constexpr unsigned int CRTCyclesPerLine = 1365;
|
||||
constexpr unsigned int CRTCyclesDivider = 4;
|
||||
|
||||
struct ReverseTable {
|
||||
std::uint8_t map[256];
|
||||
@@ -117,6 +117,14 @@ void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus TMS9918::get_scaled_scan_status() const {
|
||||
// The input was scaled by 3/4 to convert half cycles to internal ticks,
|
||||
// so undo that and also allow for: (i) the multiply by 4 that it takes
|
||||
// to reach the CRT; and (ii) the fact that the half-cycles value was scaled,
|
||||
// and this should really reply in whole cycles.
|
||||
return crt_.get_scaled_scan_status() * (4.0f / (3.0f * 8.0f));
|
||||
}
|
||||
|
||||
void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
@@ -166,7 +174,7 @@ void TMS9918::run_for(const HalfCycles cycles) {
|
||||
// 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
|
||||
// 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;
|
||||
int_cycles >>= 2;
|
||||
if(!int_cycles) return;
|
||||
@@ -352,8 +360,7 @@ void TMS9918::run_for(const HalfCycles cycles) {
|
||||
// Output video stream.
|
||||
// --------------------
|
||||
|
||||
#define intersect(left, right, code) \
|
||||
{ \
|
||||
#define intersect(left, right, code) { \
|
||||
const int start = std::max(read_pointer_.column, left); \
|
||||
const int end = std::min(end_column, right); \
|
||||
if(end > start) {\
|
||||
@@ -493,7 +500,7 @@ void Base::output_border(int cycles, uint32_t cram_dot) {
|
||||
}
|
||||
}
|
||||
|
||||
void TMS9918::set_register(int address, uint8_t value) {
|
||||
void TMS9918::write(int address, uint8_t value) {
|
||||
// Writes to address 0 are writes to the video RAM. Store
|
||||
// the value and return.
|
||||
if(!(address & 1)) {
|
||||
@@ -625,7 +632,7 @@ void TMS9918::set_register(int address, uint8_t value) {
|
||||
|
||||
uint8_t TMS9918::get_current_line() {
|
||||
// Determine the row to return.
|
||||
static const int row_change_position = 63; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality.
|
||||
constexpr int row_change_position = 63; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality.
|
||||
int source_row =
|
||||
(write_pointer_.column < row_change_position)
|
||||
? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines
|
||||
@@ -671,7 +678,7 @@ void TMS9918::latch_horizontal_counter() {
|
||||
latched_column_ = write_pointer_.column;
|
||||
}
|
||||
|
||||
uint8_t TMS9918::get_register(int address) {
|
||||
uint8_t TMS9918::read(int address) {
|
||||
write_phase_ = false;
|
||||
|
||||
// Reads from address 0 read video RAM, via the read-ahead buffer.
|
||||
@@ -830,8 +837,8 @@ void Base::draw_tms_character(int start, int end) {
|
||||
int sprite_collision = 0;
|
||||
memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0]));
|
||||
|
||||
static const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff};
|
||||
static const int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
|
||||
constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff};
|
||||
constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
|
||||
|
||||
// Draw all sprites into the sprite buffer.
|
||||
const int shifter_target = sprites_16x16_ ? 32 : 16;
|
||||
|
@@ -44,6 +44,9 @@ class TMS9918: public Base {
|
||||
/*! Sets the scan target this TMS will post content to. */
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*! Sets the type of display the CRT will request. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
@@ -54,10 +57,10 @@ class TMS9918: public Base {
|
||||
void run_for(const HalfCycles cycles);
|
||||
|
||||
/*! Sets a register value. */
|
||||
void set_register(int address, uint8_t value);
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*! Gets a register value. */
|
||||
uint8_t get_register(int address);
|
||||
uint8_t read(int address);
|
||||
|
||||
/*! Gets the current scan line; provided by the Master System only. */
|
||||
uint8_t get_current_line();
|
||||
@@ -69,8 +72,8 @@ class TMS9918: public Base {
|
||||
void latch_horizontal_counter();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until get_interrupt_line would next return true if
|
||||
there are no interceding calls to set_register or get_register.
|
||||
Returns the amount of time until @c get_interrupt_line would next return true if
|
||||
there are no interceding calls to @c write or to @c read.
|
||||
|
||||
If get_interrupt_line is true now, returns zero. If get_interrupt_line would
|
||||
never return true, returns -1.
|
||||
|
@@ -100,7 +100,7 @@ class Base {
|
||||
// (though, in practice, it won't happen until the next
|
||||
// external slot after this number of cycles after the
|
||||
// device has requested the read or write).
|
||||
return 7;
|
||||
return 6;
|
||||
}
|
||||
|
||||
// Holds the main status register.
|
||||
|
@@ -12,45 +12,56 @@
|
||||
|
||||
using namespace GI::AY38910;
|
||||
|
||||
AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
|
||||
// set up envelope lookup tables
|
||||
AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
|
||||
// Don't use the low bit of the envelope position if this is an AY.
|
||||
envelope_position_mask_ |= personality == Personality::AY38910;
|
||||
|
||||
// Set up envelope lookup tables.
|
||||
for(int c = 0; c < 16; c++) {
|
||||
for(int p = 0; p < 32; p++) {
|
||||
for(int p = 0; p < 64; p++) {
|
||||
switch(c) {
|
||||
case 0: case 1: case 2: case 3: case 9:
|
||||
envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0;
|
||||
envelope_overflow_masks_[c] = 0x1f;
|
||||
/* Envelope: \____ */
|
||||
envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0;
|
||||
envelope_overflow_masks_[c] = 0x3f;
|
||||
break;
|
||||
case 4: case 5: case 6: case 7: case 15:
|
||||
envelope_shapes_[c][p] = (p < 16) ? p : 0;
|
||||
envelope_overflow_masks_[c] = 0x1f;
|
||||
/* Envelope: /____ */
|
||||
envelope_shapes_[c][p] = (p < 32) ? p : 0;
|
||||
envelope_overflow_masks_[c] = 0x3f;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
envelope_shapes_[c][p] = (p & 0xf) ^ 0xf;
|
||||
/* Envelope: \\\\\\\\ */
|
||||
envelope_shapes_[c][p] = (p & 0x1f) ^ 0x1f;
|
||||
envelope_overflow_masks_[c] = 0x00;
|
||||
break;
|
||||
case 12:
|
||||
envelope_shapes_[c][p] = (p & 0xf);
|
||||
/* Envelope: //////// */
|
||||
envelope_shapes_[c][p] = (p & 0x1f);
|
||||
envelope_overflow_masks_[c] = 0x00;
|
||||
break;
|
||||
|
||||
case 10:
|
||||
envelope_shapes_[c][p] = (p & 0xf) ^ ((p < 16) ? 0xf : 0x0);
|
||||
/* Envelope: \/\/\/\/ */
|
||||
envelope_shapes_[c][p] = (p & 0x1f) ^ ((p < 32) ? 0x1f : 0x0);
|
||||
envelope_overflow_masks_[c] = 0x00;
|
||||
break;
|
||||
case 14:
|
||||
envelope_shapes_[c][p] = (p & 0xf) ^ ((p < 16) ? 0x0 : 0xf);
|
||||
/* Envelope: /\/\/\/\ */
|
||||
envelope_shapes_[c][p] = (p & 0x1f) ^ ((p < 32) ? 0x0 : 0x1f);
|
||||
envelope_overflow_masks_[c] = 0x00;
|
||||
break;
|
||||
|
||||
case 11:
|
||||
envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0xf;
|
||||
envelope_overflow_masks_[c] = 0x1f;
|
||||
/* Envelope: \------ (if - is high) */
|
||||
envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0x1f;
|
||||
envelope_overflow_masks_[c] = 0x3f;
|
||||
break;
|
||||
case 13:
|
||||
envelope_shapes_[c][p] = (p < 16) ? p : 0xf;
|
||||
envelope_overflow_masks_[c] = 0x1f;
|
||||
/* Envelope: /------- */
|
||||
envelope_shapes_[c][p] = (p < 32) ? p : 0x1f;
|
||||
envelope_overflow_masks_[c] = 0x3f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -61,18 +72,27 @@ AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_
|
||||
|
||||
void AY38910::set_sample_volume_range(std::int16_t range) {
|
||||
// set up volume lookup table
|
||||
const float max_volume = static_cast<float>(range) / 3.0f; // As there are three channels.
|
||||
const float root_two = sqrtf(2.0f);
|
||||
for(int v = 0; v < 16; v++) {
|
||||
volumes_[v] = static_cast<int>(max_volume / powf(root_two, static_cast<float>(v ^ 0xf)));
|
||||
const float max_volume = float(range) / 3.0f; // As there are three channels.
|
||||
constexpr float root_two = 1.414213562373095f;
|
||||
for(int v = 0; v < 32; v++) {
|
||||
volumes_[v] = int(max_volume / powf(root_two, float(v ^ 0x1f) / 2.0f));
|
||||
}
|
||||
volumes_[0] = 0;
|
||||
volumes_[0] = 0; // Tie level 0 to silence.
|
||||
evaluate_output_volume();
|
||||
}
|
||||
|
||||
void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
// Note on structure below: the real AY has a built-in divider of 8
|
||||
// prior to applying its tone and noise dividers. But the YM fills the
|
||||
// same total periods for noise and tone with double-precision envelopes.
|
||||
// Therefore this class implements a divider of 4 and doubles the tone
|
||||
// and noise periods. The envelope ticks along at the divide-by-four rate,
|
||||
// but if this is an AY rather than a YM then its lowest bit is forced to 1,
|
||||
// matching the YM datasheet's depiction of envelope level 31 as equal to
|
||||
// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc.
|
||||
|
||||
std::size_t c = 0;
|
||||
while((master_divider_&7) && c < number_of_samples) {
|
||||
while((master_divider_&3) && c < number_of_samples) {
|
||||
target[c] = output_volume_;
|
||||
master_divider_++;
|
||||
c++;
|
||||
@@ -83,49 +103,49 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
if(tone_counters_[c]) tone_counters_[c]--;\
|
||||
else {\
|
||||
tone_outputs_[c] ^= 1;\
|
||||
tone_counters_[c] = tone_periods_[c];\
|
||||
tone_counters_[c] = tone_periods_[c] << 1;\
|
||||
}
|
||||
|
||||
// update the tone channels
|
||||
// Update the tone channels.
|
||||
step_channel(0);
|
||||
step_channel(1);
|
||||
step_channel(2);
|
||||
|
||||
#undef step_channel
|
||||
|
||||
// ... the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
|
||||
// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
|
||||
// it into the official 17 upon divider underflow.
|
||||
if(noise_counter_) noise_counter_--;
|
||||
else {
|
||||
noise_counter_ = noise_period_;
|
||||
noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes.
|
||||
noise_output_ ^= noise_shift_register_&1;
|
||||
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
|
||||
noise_shift_register_ >>= 1;
|
||||
}
|
||||
|
||||
// ... and the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
|
||||
// implementing non-repeating patterns by locking them to table position 0x1f.
|
||||
// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
|
||||
// implementing non-repeating patterns by locking them to the final table position.
|
||||
if(envelope_divider_) envelope_divider_--;
|
||||
else {
|
||||
envelope_divider_ = envelope_period_;
|
||||
envelope_position_ ++;
|
||||
if(envelope_position_ == 32) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
|
||||
if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
|
||||
}
|
||||
|
||||
evaluate_output_volume();
|
||||
|
||||
for(int ic = 0; ic < 8 && c < number_of_samples; ic++) {
|
||||
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
|
||||
target[c] = output_volume_;
|
||||
c++;
|
||||
master_divider_++;
|
||||
}
|
||||
}
|
||||
|
||||
master_divider_ &= 7;
|
||||
master_divider_ &= 3;
|
||||
}
|
||||
|
||||
void AY38910::evaluate_output_volume() {
|
||||
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_];
|
||||
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
|
||||
|
||||
// The output level for a channel is:
|
||||
// 1 if neither tone nor noise is enabled;
|
||||
@@ -142,9 +162,20 @@ void AY38910::evaluate_output_volume() {
|
||||
};
|
||||
#undef level
|
||||
|
||||
// Channel volume is a simple selection: if the bit at 0x10 is set, use the envelope volume; otherwise use the lower four bits
|
||||
// This remapping table seeks to map 'channel volumes', i.e. the levels produced from the
|
||||
// 16-step progammatic volumes set per channel to 'envelope volumes', i.e. the 32-step
|
||||
// volumes that are produced by the envelope generators (on a YM at least). My reading of
|
||||
// the data sheet is that '0' is still off, but 15 should be as loud as peak envelope. So
|
||||
// I've thrown in the discontinuity at the low end, where it'll be very quiet.
|
||||
const int channel_volumes[] = {
|
||||
0, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31
|
||||
};
|
||||
static_assert(sizeof(channel_volumes) == 16*sizeof(int));
|
||||
|
||||
// Channel volume is a simple selection: if the bit at 0x10 is set, use the envelope volume; otherwise use the lower four bits,
|
||||
// mapped to the range 1–31 in case this is a YM.
|
||||
#define channel_volume(c) \
|
||||
((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * (output_registers_[c]&0xf)
|
||||
((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * channel_volumes[output_registers_[c]&0xf]
|
||||
|
||||
const int volumes[3] = {
|
||||
channel_volume(8),
|
||||
|
@@ -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) {}
|
||||
};
|
||||
@@ -51,6 +52,13 @@ enum ControlLines {
|
||||
BDIR = (1 << 2)
|
||||
};
|
||||
|
||||
enum class Personality {
|
||||
/// Provides 16 volume levels to envelopes.
|
||||
AY38910,
|
||||
/// Provides 32 volume levels to envelopes.
|
||||
YM2149F
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides emulation of an AY-3-8910 / YM2149, which is a three-channel sound chip with a
|
||||
noise generator and a volume envelope generator, which also provides two bidirectional
|
||||
@@ -59,7 +67,7 @@ enum ControlLines {
|
||||
class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
/// Creates a new AY38910.
|
||||
AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue);
|
||||
AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &);
|
||||
|
||||
/// Sets the value the AY would read from its data lines if it were not outputting.
|
||||
void set_data_input(uint8_t r);
|
||||
@@ -108,11 +116,11 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
|
||||
int envelope_period_ = 0;
|
||||
int envelope_divider_ = 0;
|
||||
int envelope_position_ = 0;
|
||||
int envelope_shapes_[16][32];
|
||||
int envelope_position_ = 0, envelope_position_mask_ = 0;
|
||||
int envelope_shapes_[16][64];
|
||||
int envelope_overflow_masks_[16];
|
||||
|
||||
int volumes_[16];
|
||||
int volumes_[32];
|
||||
|
||||
enum ControlState {
|
||||
Inactive,
|
||||
|
@@ -78,7 +78,7 @@ void DiskII::select_drive(int drive) {
|
||||
void DiskII::run_for(const Cycles cycles) {
|
||||
if(preferred_clocking() == ClockingHint::Preference::None) return;
|
||||
|
||||
int integer_cycles = cycles.as_int();
|
||||
auto integer_cycles = cycles.as_integral();
|
||||
while(integer_cycles--) {
|
||||
const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6);
|
||||
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.
|
||||
// This models that, accepting overrun as a risk.
|
||||
if(motor_off_time_ >= 0) {
|
||||
motor_off_time_ -= cycles.as_int();
|
||||
motor_off_time_ -= cycles.as_integral();
|
||||
if(motor_off_time_ < 0) {
|
||||
set_control(Control::Motor, false);
|
||||
}
|
||||
@@ -266,7 +266,7 @@ int DiskII::read_address(int address) {
|
||||
break;
|
||||
case 0xf:
|
||||
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;
|
||||
break;
|
||||
}
|
||||
|
@@ -26,7 +26,7 @@ namespace Apple {
|
||||
/*!
|
||||
Provides an emulation of the Apple Disk II.
|
||||
*/
|
||||
class DiskII:
|
||||
class DiskII :
|
||||
public Storage::Disk::Drive::EventDelegate,
|
||||
public ClockingHint::Source,
|
||||
public ClockingHint::Observer {
|
||||
@@ -48,7 +48,7 @@ class DiskII:
|
||||
The value returned by @c read_address if accessing that address
|
||||
didn't cause the disk II to place anything onto the bus.
|
||||
*/
|
||||
const int DidNotLoad = -1;
|
||||
static constexpr int DidNotLoad = -1;
|
||||
|
||||
/// Advances the controller by @c cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
@@ -76,7 +76,7 @@ class DiskII:
|
||||
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
|
||||
|
||||
// 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.
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
@@ -98,10 +98,10 @@ class DiskII:
|
||||
void select_drive(int drive);
|
||||
|
||||
uint8_t trigger_address(int address, uint8_t value);
|
||||
void process_event(const Storage::Disk::Drive::Event &event) override;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) override;
|
||||
void process_event(const Storage::Disk::Drive::Event &event) final;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) final;
|
||||
|
||||
const int clock_rate_ = 0;
|
||||
const Cycles::IntType clock_rate_ = 0;
|
||||
|
||||
uint8_t state_ = 0;
|
||||
uint8_t inputs_ = 0;
|
||||
@@ -109,7 +109,7 @@ class DiskII:
|
||||
|
||||
int stepper_mask_ = 0;
|
||||
int stepper_position_ = 0;
|
||||
int motor_off_time_ = -1;
|
||||
Cycles::IntType motor_off_time_ = -1;
|
||||
|
||||
bool is_write_protected();
|
||||
std::array<uint8_t, 256> state_machine_;
|
||||
|
@@ -13,15 +13,15 @@
|
||||
using namespace Apple;
|
||||
|
||||
namespace {
|
||||
const int CA0 = 1 << 0;
|
||||
const int CA1 = 1 << 1;
|
||||
const int CA2 = 1 << 2;
|
||||
const int LSTRB = 1 << 3;
|
||||
const int ENABLE = 1 << 4;
|
||||
const int DRIVESEL = 1 << 5; /* This means drive select, like on the original Disk II. */
|
||||
const int Q6 = 1 << 6;
|
||||
const int Q7 = 1 << 7;
|
||||
const int SEL = 1 << 8; /* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */
|
||||
constexpr int CA0 = 1 << 0;
|
||||
constexpr int CA1 = 1 << 1;
|
||||
constexpr int CA2 = 1 << 2;
|
||||
constexpr int LSTRB = 1 << 3;
|
||||
constexpr int ENABLE = 1 << 4;
|
||||
constexpr int DRIVESEL = 1 << 5; /* This means drive select, like on the original Disk II. */
|
||||
constexpr int Q6 = 1 << 6;
|
||||
constexpr int Q7 = 1 << 7;
|
||||
constexpr int SEL = 1 << 8; /* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */
|
||||
}
|
||||
|
||||
IWM::IWM(int clock_rate) :
|
||||
@@ -233,25 +233,34 @@ void IWM::run_for(const Cycles cycles) {
|
||||
}
|
||||
|
||||
// Activity otherwise depends on mode and motor state.
|
||||
int integer_cycles = cycles.as_int();
|
||||
auto integer_cycles = cycles.as_integral();
|
||||
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_]) {
|
||||
while(integer_cycles--) {
|
||||
drives_[active_drive_]->run_for(Cycles(1));
|
||||
++cycles_since_shift_;
|
||||
if(cycles_since_shift_ == bit_length_ + Cycles(2)) {
|
||||
if(cycles_since_shift_ == bit_length_ + error_margin) {
|
||||
propose_shift(0);
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
integer_cycles -= bit_length_.as_int() + 2 - cycles_since_shift_.as_int();
|
||||
}
|
||||
cycles_since_shift_ += Cycles(integer_cycles);
|
||||
}
|
||||
break;
|
||||
} break;
|
||||
|
||||
case ShiftMode::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);
|
||||
shift_register_ <<= 1;
|
||||
|
||||
integer_cycles -= cycles_until_write.as_int();
|
||||
integer_cycles -= cycles_until_write.as_integral();
|
||||
cycles_since_shift_ = Cycles(0);
|
||||
|
||||
--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(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_;
|
||||
write_handshake_ |= 0x80 | 0x40;
|
||||
output_bits_remaining_ = 8;
|
||||
@@ -351,12 +360,28 @@ void IWM::propose_shift(uint8_t bit) {
|
||||
// TODO: synchronous mode.
|
||||
|
||||
// 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);
|
||||
if(shift_register_ & 0x80) {
|
||||
data_register_ = shift_register_;
|
||||
shift_register_ = 0;
|
||||
}
|
||||
cycles_since_shift_ = Cycles(0);
|
||||
|
||||
if(bit)
|
||||
cycles_since_shift_ = Cycles(0);
|
||||
else
|
||||
cycles_since_shift_ -= bit_length_;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
#define IWM_hpp
|
||||
|
||||
#include "../../Activity/Observer.hpp"
|
||||
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../ClockReceiver/ClockingHintSource.hpp"
|
||||
|
||||
#include "../../Storage/Disk/Drive.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -67,9 +70,13 @@ class IWM:
|
||||
/// Connects a drive to the IWM.
|
||||
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:
|
||||
// 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_;
|
||||
|
||||
@@ -84,7 +91,7 @@ class IWM:
|
||||
IWMDrive *drives_[2] = {nullptr, nullptr};
|
||||
bool drive_is_rotating_[2] = {false, false};
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final;
|
||||
|
||||
Cycles cycles_until_disable_;
|
||||
uint8_t write_handshake_ = 0x80;
|
||||
|
@@ -71,7 +71,9 @@ void DoubleDensityDrive::set_rotation_speed(float revolutions_per_minute) {
|
||||
|
||||
// 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) {
|
||||
|
@@ -32,14 +32,14 @@ class DoubleDensityDrive: public IWMDrive {
|
||||
*/
|
||||
void set_rotation_speed(float revolutions_per_minute);
|
||||
|
||||
void set_enabled(bool) override;
|
||||
void set_control_lines(int) override;
|
||||
bool read() override;
|
||||
|
||||
private:
|
||||
void set_enabled(bool) final;
|
||||
void set_control_lines(int) final;
|
||||
bool read() final;
|
||||
|
||||
// To receive the proper notifications from Storage::Disk::Drive.
|
||||
void did_step(Storage::Disk::HeadPosition to_position) override;
|
||||
void did_set_disk() override;
|
||||
void did_step(Storage::Disk::HeadPosition to_position) final;
|
||||
void did_set_disk() final;
|
||||
|
||||
const bool is_800k_;
|
||||
bool has_new_disk_ = false;
|
||||
|
@@ -48,7 +48,7 @@ void SN76489::set_sample_volume_range(std::int16_t range) {
|
||||
evaluate_output_volume();
|
||||
}
|
||||
|
||||
void SN76489::set_register(uint8_t value) {
|
||||
void SN76489::write(uint8_t value) {
|
||||
task_queue_.defer([value, this] () {
|
||||
if(value & 0x80) {
|
||||
active_register_ = value;
|
||||
|
@@ -26,7 +26,7 @@ class SN76489: public Outputs::Speaker::SampleSource {
|
||||
SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1);
|
||||
|
||||
/// Writes a new value to the SN76489.
|
||||
void set_register(uint8_t value);
|
||||
void write(uint8_t value);
|
||||
|
||||
// As per SampleSource.
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
|
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__
|
||||
serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL);
|
||||
#else
|
||||
thread_.reset(new std::thread([this]() {
|
||||
thread_ = std::make_unique<std::thread>([this]() {
|
||||
while(!should_destruct_) {
|
||||
std::function<void(void)> next_function;
|
||||
|
||||
@@ -39,7 +39,7 @@ AsyncTaskQueue::AsyncTaskQueue()
|
||||
processing_condition_.wait(lock);
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ void AsyncTaskQueue::flush() {
|
||||
#ifdef __APPLE__
|
||||
dispatch_sync(serial_dispatch_queue_, ^{});
|
||||
#else
|
||||
std::shared_ptr<std::mutex> flush_mutex(new std::mutex);
|
||||
std::shared_ptr<std::condition_variable> flush_condition(new std::condition_variable);
|
||||
auto flush_mutex = std::make_shared<std::mutex>();
|
||||
auto flush_condition = std::make_shared<std::condition_variable>();
|
||||
std::unique_lock<std::mutex> lock(*flush_mutex);
|
||||
enqueue([=] () {
|
||||
std::unique_lock<std::mutex> inner_lock(*flush_mutex);
|
||||
@@ -88,7 +88,7 @@ DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() {
|
||||
|
||||
void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
|
||||
if(!deferred_tasks_) {
|
||||
deferred_tasks_.reset(new std::list<std::function<void(void)>>);
|
||||
deferred_tasks_ = std::make_shared<std::list<std::function<void(void)>>>();
|
||||
}
|
||||
deferred_tasks_->push_back(function);
|
||||
}
|
||||
|
@@ -12,61 +12,89 @@
|
||||
|
||||
using namespace Concurrency;
|
||||
|
||||
BestEffortUpdater::BestEffortUpdater() {
|
||||
// ATOMIC_FLAG_INIT isn't necessarily safe to use, so establish default state by other means.
|
||||
update_is_ongoing_.clear();
|
||||
}
|
||||
BestEffortUpdater::BestEffortUpdater() :
|
||||
update_thread_([this]() {
|
||||
this->update_loop();
|
||||
}) {}
|
||||
|
||||
BestEffortUpdater::~BestEffortUpdater() {
|
||||
// Don't allow further deconstruction until the task queue is stopped.
|
||||
// Sever the delegate now, as soon as possible, then wait for any
|
||||
// pending tasks to finish.
|
||||
set_delegate(nullptr);
|
||||
flush();
|
||||
|
||||
// Wind up the update thread.
|
||||
should_quit_ = true;
|
||||
update();
|
||||
update_thread_.join();
|
||||
}
|
||||
|
||||
void BestEffortUpdater::update() {
|
||||
// Perform an update only if one is not currently ongoing.
|
||||
if(!update_is_ongoing_.test_and_set()) {
|
||||
async_task_queue_.enqueue([this]() {
|
||||
// Get time now using the highest-resolution clock provided by the implementation, and determine
|
||||
// the duration since the last time this section was entered.
|
||||
const std::chrono::time_point<std::chrono::high_resolution_clock> now = std::chrono::high_resolution_clock::now();
|
||||
const auto elapsed = now - previous_time_point_;
|
||||
previous_time_point_ = now;
|
||||
void BestEffortUpdater::update(int flags) {
|
||||
// Bump the requested target time and set the update requested flag.
|
||||
{
|
||||
std::lock_guard<decltype(update_mutex_)> lock(update_mutex_);
|
||||
has_skipped_ = update_requested_;
|
||||
update_requested_ = true;
|
||||
flags_ |= flags;
|
||||
target_time_ = std::chrono::high_resolution_clock::now().time_since_epoch().count();
|
||||
}
|
||||
update_condition_.notify_one();
|
||||
}
|
||||
|
||||
if(has_previous_time_point_) {
|
||||
// If the duration is valid, convert it to integer cycles, maintaining a rolling error and call the delegate
|
||||
// if there is one. Proceed only if the number of cycles is positive, and cap it to the per-second maximum as
|
||||
// it's possible this is an adjustable clock so be ready to swallow unexpected adjustments.
|
||||
const int64_t integer_duration = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
|
||||
if(integer_duration > 0) {
|
||||
if(delegate_) {
|
||||
// Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
|
||||
// brief system interruption.
|
||||
const double duration = std::min(static_cast<double>(integer_duration) / 1e9, 0.2);
|
||||
delegate_->update(this, duration, has_skipped_);
|
||||
}
|
||||
has_skipped_ = false;
|
||||
}
|
||||
} else {
|
||||
has_previous_time_point_ = true;
|
||||
}
|
||||
void BestEffortUpdater::update_loop() {
|
||||
while(true) {
|
||||
std::unique_lock<decltype(update_mutex_)> lock(update_mutex_);
|
||||
is_updating_ = false;
|
||||
|
||||
// Allow furthers updates to occur.
|
||||
update_is_ongoing_.clear();
|
||||
});
|
||||
} else {
|
||||
async_task_queue_.enqueue([this]() {
|
||||
has_skipped_ = true;
|
||||
// Wait to be signalled.
|
||||
update_condition_.wait(lock, [this]() -> bool {
|
||||
return update_requested_;
|
||||
});
|
||||
|
||||
// Possibly this signalling really means 'quit'.
|
||||
if(should_quit_) return;
|
||||
|
||||
// Note update started, crib the target time.
|
||||
auto target_time = target_time_;
|
||||
update_requested_ = false;
|
||||
|
||||
// If this was actually the first update request, silently swallow it.
|
||||
if(!has_previous_time_point_) {
|
||||
has_previous_time_point_ = true;
|
||||
previous_time_point_ = target_time;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Release the lock on requesting new updates.
|
||||
is_updating_ = true;
|
||||
const int flags = flags_;
|
||||
flags_ = 0;
|
||||
lock.unlock();
|
||||
|
||||
// Invoke the delegate, if supplied, in order to run.
|
||||
const int64_t integer_duration = std::max(target_time - previous_time_point_, int64_t(0));
|
||||
const auto delegate = delegate_.load();
|
||||
if(delegate) {
|
||||
// Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
|
||||
// brief system interruption.
|
||||
const double duration = std::min(double(integer_duration) / 1e9, 0.2);
|
||||
const double elapsed_duration = delegate->update(this, duration, has_skipped_, flags);
|
||||
|
||||
previous_time_point_ += int64_t(elapsed_duration * 1e9);
|
||||
has_skipped_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BestEffortUpdater::flush() {
|
||||
async_task_queue_.flush();
|
||||
// Spin lock; this is allowed to be slow.
|
||||
while(true) {
|
||||
std::lock_guard<decltype(update_mutex_)> lock(update_mutex_);
|
||||
if(!is_updating_) return;
|
||||
}
|
||||
}
|
||||
|
||||
void BestEffortUpdater::set_delegate(Delegate *const delegate) {
|
||||
async_task_queue_.enqueue([this, delegate]() {
|
||||
delegate_ = delegate;
|
||||
});
|
||||
delegate_.store(delegate);
|
||||
}
|
||||
|
||||
|
@@ -11,8 +11,10 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "AsyncTaskQueue.hpp"
|
||||
#include "../ClockReceiver/TimeTypes.hpp"
|
||||
|
||||
namespace Concurrency {
|
||||
@@ -31,7 +33,13 @@ class BestEffortUpdater {
|
||||
|
||||
/// A delegate receives timing cues.
|
||||
struct Delegate {
|
||||
virtual void update(BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update) = 0;
|
||||
/*!
|
||||
Instructs the delegate to run for at least @c duration, providing hints as to whether multiple updates were requested before the previous had completed
|
||||
(as @c did_skip_previous_update) and providing the union of any flags supplied to @c update.
|
||||
|
||||
@returns The amount of time actually run for.
|
||||
*/
|
||||
virtual Time::Seconds update(BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update, int flags) = 0;
|
||||
};
|
||||
|
||||
/// Sets the current delegate.
|
||||
@@ -41,20 +49,32 @@ class BestEffortUpdater {
|
||||
If the delegate is not currently in the process of an `update` call, calls it now to catch up to the current time.
|
||||
The call is asynchronous; this method will return immediately.
|
||||
*/
|
||||
void update();
|
||||
void update(int flags = 0);
|
||||
|
||||
/// Blocks until any ongoing update is complete.
|
||||
/// Blocks until any ongoing update is complete; may spin.
|
||||
void flush();
|
||||
|
||||
private:
|
||||
std::atomic_flag update_is_ongoing_;
|
||||
AsyncTaskQueue async_task_queue_;
|
||||
std::atomic<bool> should_quit_;
|
||||
std::atomic<bool> is_updating_;
|
||||
|
||||
std::chrono::time_point<std::chrono::high_resolution_clock> previous_time_point_;
|
||||
int64_t target_time_;
|
||||
int flags_ = 0;
|
||||
bool update_requested_;
|
||||
std::mutex update_mutex_;
|
||||
std::condition_variable update_condition_;
|
||||
|
||||
decltype(target_time_) previous_time_point_;
|
||||
bool has_previous_time_point_ = false;
|
||||
bool has_skipped_ = false;
|
||||
std::atomic<bool> has_skipped_ = false;
|
||||
|
||||
Delegate *delegate_ = nullptr;
|
||||
std::atomic<Delegate *>delegate_ = nullptr;
|
||||
|
||||
void update_loop();
|
||||
|
||||
// This is deliberately at the bottom, to ensure it constructs after the various
|
||||
// mutexs, conditions, etc, that it'll depend upon.
|
||||
std::thread update_thread_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -14,16 +14,16 @@ namespace {
|
||||
Appends a Boolean selection of @c selection for option @c name to @c selection_set.
|
||||
*/
|
||||
void append_bool(Configurable::SelectionSet &selection_set, const std::string &name, bool selection) {
|
||||
selection_set[name] = std::unique_ptr<Configurable::Selection>(new Configurable::BooleanSelection(selection));
|
||||
selection_set[name] = std::make_unique<Configurable::BooleanSelection>(selection);
|
||||
}
|
||||
|
||||
/*!
|
||||
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) {
|
||||
auto quickload = Configurable::selection<Configurable::BooleanSelection>(selections_by_option, "quickload");
|
||||
if(!quickload) return false;
|
||||
result = quickload->value;
|
||||
auto selection = Configurable::selection<Configurable::BooleanSelection>(selections_by_option, name);
|
||||
if(!selection) return false;
|
||||
result = selection->value;
|
||||
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));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -63,7 +64,11 @@ void Configurable::append_display_selection(Configurable::SelectionSet &selectio
|
||||
case Display::CompositeMonochrome: string_selection = "composite-mono"; break;
|
||||
case Display::CompositeColour: string_selection = "composite"; break;
|
||||
}
|
||||
selection_set["display"] = std::unique_ptr<Configurable::Selection>(new Configurable::ListSelection(string_selection));
|
||||
selection_set["display"] = std::make_unique<Configurable::ListSelection>(string_selection);
|
||||
}
|
||||
|
||||
void Configurable::append_quick_boot_selection(Configurable::SelectionSet &selection_set, bool selection) {
|
||||
append_bool(selection_set, "quickboot", selection);
|
||||
}
|
||||
|
||||
// MARK: - Selection parsers
|
||||
@@ -97,3 +102,7 @@ bool Configurable::get_display(const Configurable::SelectionSet &selections_by_o
|
||||
}
|
||||
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),
|
||||
DisplayCompositeMonochrome = (1 << 3),
|
||||
QuickLoadTape = (1 << 4),
|
||||
AutomaticTapeMotorControl = (1 << 5)
|
||||
AutomaticTapeMotorControl = (1 << 5),
|
||||
QuickBoot = (1 << 6),
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
/*!
|
||||
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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
/*!
|
||||
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 */
|
||||
|
@@ -170,11 +170,11 @@ class ConcreteJoystick: public Joystick {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Input> &get_inputs() override final {
|
||||
std::vector<Input> &get_inputs() final {
|
||||
return inputs_;
|
||||
}
|
||||
|
||||
void set_input(const Input &input, bool is_active) override final {
|
||||
void set_input(const Input &input, bool is_active) final {
|
||||
// If this is a digital setting to a digital property, just pass it along.
|
||||
if(input.is_button() || stick_types_[input.info.control.index] == StickType::Digital) {
|
||||
did_set_input(input, is_active);
|
||||
@@ -193,7 +193,7 @@ class ConcreteJoystick: public Joystick {
|
||||
}
|
||||
}
|
||||
|
||||
void set_input(const Input &input, float value) override final {
|
||||
void set_input(const Input &input, float value) final {
|
||||
// If this is an analogue setting to an analogue property, just pass it along.
|
||||
if(!input.is_button() && stick_types_[input.info.control.index] == StickType::Analogue) {
|
||||
did_set_input(input, value);
|
||||
|
@@ -10,13 +10,14 @@
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
const std::set<Inputs::Keyboard::Key> &Keyboard::get_essential_modifiers() {
|
||||
return essential_modifiers_;
|
||||
}
|
||||
|
||||
void Keyboard::reset_all_keys() {
|
||||
std::fill(key_states_.begin(), key_states_.end(), false);
|
||||
if(delegate_) delegate_->reset_all_keys(this);
|
||||
|
@@ -23,26 +23,26 @@ class Keyboard {
|
||||
public:
|
||||
enum class Key {
|
||||
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,
|
||||
Tab, Q, W, E, R, T, Y, U, I, O, P, OpenSquareBracket, CloseSquareBracket, BackSlash,
|
||||
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,
|
||||
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,
|
||||
LeftControl, LeftOption, LeftMeta, Space, RightMeta, RightOption, RightControl,
|
||||
Left, Right, Up, Down,
|
||||
Insert, Home, PageUp, Delete, End, PageDown,
|
||||
NumLock, KeyPadSlash, KeyPadAsterisk, KeyPadDelete,
|
||||
KeyPad7, KeyPad8, KeyPad9, KeyPadPlus,
|
||||
KeyPad4, KeyPad5, KeyPad6, KeyPadMinus,
|
||||
KeyPad1, KeyPad2, KeyPad3, KeyPadEnter,
|
||||
KeyPad0, KeyPadDecimalPoint, KeyPadEquals,
|
||||
NumLock, KeypadSlash, KeypadAsterisk, KeypadDelete,
|
||||
Keypad7, Keypad8, Keypad9, KeypadPlus,
|
||||
Keypad4, Keypad5, Keypad6, KeypadMinus,
|
||||
Keypad1, Keypad2, Keypad3, KeypadEnter,
|
||||
Keypad0, KeypadDecimalPoint, KeypadEquals,
|
||||
Help
|
||||
};
|
||||
|
||||
/// 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.
|
||||
Keyboard(const std::set<Key> &observed_keys);
|
||||
Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers);
|
||||
|
||||
// Host interface.
|
||||
virtual void set_key_pressed(Key key, char value, bool is_pressed);
|
||||
@@ -51,10 +51,18 @@ class Keyboard {
|
||||
/// @returns a set of all Keys that this keyboard responds to.
|
||||
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
|
||||
like a complete keyboard — i.e. if a user would expect this keyboard
|
||||
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();
|
||||
|
||||
@@ -68,6 +76,7 @@ class Keyboard {
|
||||
|
||||
private:
|
||||
std::set<Key> observed_keys_;
|
||||
std::set<Key> essential_modifiers_;
|
||||
std::vector<bool> key_states_;
|
||||
Delegate *delegate_ = nullptr;
|
||||
bool is_exclusive_ = true;
|
||||
|
@@ -34,24 +34,24 @@ class QuadratureMouse: public Mouse {
|
||||
/*
|
||||
Inputs, to satisfy the Mouse interface.
|
||||
*/
|
||||
void move(int x, int y) override {
|
||||
void move(int x, int y) final {
|
||||
// Accumulate all provided motion.
|
||||
axes_[0] += x;
|
||||
axes_[1] += y;
|
||||
}
|
||||
|
||||
int get_number_of_buttons() override {
|
||||
int get_number_of_buttons() final {
|
||||
return number_of_buttons_;
|
||||
}
|
||||
|
||||
void set_button_pressed(int index, bool is_pressed) override {
|
||||
void set_button_pressed(int index, bool is_pressed) final {
|
||||
if(is_pressed)
|
||||
button_flags_ |= (1 << index);
|
||||
else
|
||||
button_flags_ &= ~(1 << index);
|
||||
}
|
||||
|
||||
void reset_all_buttons() override {
|
||||
void reset_all_buttons() final {
|
||||
button_flags_ = 0;
|
||||
}
|
||||
|
||||
|
@@ -41,7 +41,7 @@ namespace AmstradCPC {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour)
|
||||
Configurable::StandardOptions(Configurable::DisplayRGB | Configurable::DisplayCompositeColour)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ class InterruptTimer {
|
||||
class AYDeferrer {
|
||||
public:
|
||||
/// Constructs a new AY instance and sets its clock rate.
|
||||
AYDeferrer() : ay_(audio_queue_), speaker_(ay_) {
|
||||
AYDeferrer() : ay_(GI::AY38910::Personality::AY38910, audio_queue_), speaker_(ay_) {
|
||||
speaker_.set_input_rate(1000000);
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ class AYDeferrer {
|
||||
*/
|
||||
class CRTCBusHandler {
|
||||
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),
|
||||
ram_(ram),
|
||||
interrupt_timer_(interrupt_timer) {
|
||||
@@ -222,9 +222,9 @@ class CRTCBusHandler {
|
||||
if(cycles_) {
|
||||
switch(previous_output_mode_) {
|
||||
default:
|
||||
case OutputMode::Blank: crt_.output_blank(cycles_ * 16); break;
|
||||
case OutputMode::Blank: crt_.output_blank(cycles_ * 16); break;
|
||||
case OutputMode::Sync: crt_.output_sync(cycles_ * 16); break;
|
||||
case OutputMode::Border: output_border(cycles_); break;
|
||||
case OutputMode::Border: output_border(cycles_); break;
|
||||
case OutputMode::ColourBurst: crt_.output_default_colour_burst(cycles_ * 16); break;
|
||||
case OutputMode::Pixels:
|
||||
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
|
||||
@@ -249,44 +249,46 @@ class CRTCBusHandler {
|
||||
// the CPC shuffles output lines as:
|
||||
// MA13 MA12 RA2 RA1 RA0 MA9 MA8 MA7 MA6 MA5 MA4 MA3 MA2 MA1 MA0 CCLK
|
||||
// ... so form the real access address.
|
||||
uint16_t address =
|
||||
static_cast<uint16_t>(
|
||||
const uint16_t address =
|
||||
uint16_t(
|
||||
((state.refresh_address & 0x3ff) << 1) |
|
||||
((state.row_address & 0x7) << 11) |
|
||||
((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_) {
|
||||
case 0:
|
||||
reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode0_output_[ram_[address]];
|
||||
reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode0_output_[ram_[address+1]];
|
||||
pixel_pointer_ += 4;
|
||||
pixel_pointer_ += 2 * sizeof(uint16_t);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
reinterpret_cast<uint32_t *>(pixel_pointer_)[0] = mode1_output_[ram_[address]];
|
||||
reinterpret_cast<uint32_t *>(pixel_pointer_)[1] = mode1_output_[ram_[address+1]];
|
||||
pixel_pointer_ += 8;
|
||||
pixel_pointer_ += 2 * sizeof(uint32_t);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
reinterpret_cast<uint64_t *>(pixel_pointer_)[0] = mode2_output_[ram_[address]];
|
||||
reinterpret_cast<uint64_t *>(pixel_pointer_)[1] = mode2_output_[ram_[address+1]];
|
||||
pixel_pointer_ += 16;
|
||||
pixel_pointer_ += 2 * sizeof(uint64_t);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode3_output_[ram_[address]];
|
||||
reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode3_output_[ram_[address+1]];
|
||||
pixel_pointer_ += 4;
|
||||
pixel_pointer_ += 2 * sizeof(uint16_t);
|
||||
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
|
||||
// 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) {
|
||||
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
|
||||
pixel_pointer_ = pixel_data_ = nullptr;
|
||||
@@ -333,6 +335,11 @@ class CRTCBusHandler {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// @returns The current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 4.0f;
|
||||
}
|
||||
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
@@ -369,9 +376,17 @@ class CRTCBusHandler {
|
||||
|
||||
private:
|
||||
void output_border(int length) {
|
||||
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_.begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = border_;
|
||||
crt_.output_level(length * 16);
|
||||
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_;
|
||||
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)
|
||||
@@ -387,16 +402,16 @@ class CRTCBusHandler {
|
||||
|
||||
void establish_palette_hits() {
|
||||
for(int c = 0; c < 256; c++) {
|
||||
mode0_palette_hits_[Mode0Colour0(c)].push_back(static_cast<uint8_t>(c));
|
||||
mode0_palette_hits_[Mode0Colour1(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(uint8_t(c));
|
||||
|
||||
mode1_palette_hits_[Mode1Colour0(c)].push_back(static_cast<uint8_t>(c));
|
||||
mode1_palette_hits_[Mode1Colour1(c)].push_back(static_cast<uint8_t>(c));
|
||||
mode1_palette_hits_[Mode1Colour2(c)].push_back(static_cast<uint8_t>(c));
|
||||
mode1_palette_hits_[Mode1Colour3(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(uint8_t(c));
|
||||
mode1_palette_hits_[Mode1Colour2(c)].push_back(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_[Mode3Colour1(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(uint8_t(c));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +421,7 @@ class CRTCBusHandler {
|
||||
// Mode 0: abcdefgh -> [gcea] [hdfb]
|
||||
for(int c = 0; c < 256; c++) {
|
||||
// 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[1] = palette_[Mode0Colour1(c)];
|
||||
}
|
||||
@@ -415,7 +430,7 @@ class CRTCBusHandler {
|
||||
case 1:
|
||||
for(int c = 0; c < 256; c++) {
|
||||
// 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[1] = palette_[Mode1Colour1(c)];
|
||||
mode1_pixels[2] = palette_[Mode1Colour2(c)];
|
||||
@@ -426,7 +441,7 @@ class CRTCBusHandler {
|
||||
case 2:
|
||||
for(int c = 0; c < 256; c++) {
|
||||
// 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[1] = palette_[((c & 0x40) >> 6)];
|
||||
mode2_pixels[2] = palette_[((c & 0x20) >> 5)];
|
||||
@@ -441,7 +456,7 @@ class CRTCBusHandler {
|
||||
case 3:
|
||||
for(int c = 0; c < 256; c++) {
|
||||
// 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[1] = palette_[Mode3Colour1(c)];
|
||||
}
|
||||
@@ -453,7 +468,7 @@ class CRTCBusHandler {
|
||||
switch(mode_) {
|
||||
case 0: {
|
||||
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[1] = palette_[Mode0Colour1(c)];
|
||||
}
|
||||
@@ -461,7 +476,7 @@ class CRTCBusHandler {
|
||||
case 1:
|
||||
if(pen > 3) return;
|
||||
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[1] = palette_[Mode1Colour1(c)];
|
||||
mode1_pixels[2] = palette_[Mode1Colour2(c)];
|
||||
@@ -478,7 +493,7 @@ class CRTCBusHandler {
|
||||
if(pen > 3) return;
|
||||
// Same argument applies here as to case 1, as the unused bits aren't masked out.
|
||||
for(uint8_t c : mode3_palette_hits_[pen]) {
|
||||
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[1] = palette_[Mode3Colour1(c)];
|
||||
}
|
||||
@@ -499,7 +514,7 @@ class CRTCBusHandler {
|
||||
|
||||
uint8_t mapped_palette_value(uint8_t colour) {
|
||||
#define COL(r, g, b) (r << 4) | (g << 2) | b
|
||||
static const uint8_t mapping[32] = {
|
||||
constexpr uint8_t mapping[32] = {
|
||||
COL(1, 1, 1), COL(1, 1, 1), COL(0, 2, 1), COL(2, 2, 1),
|
||||
COL(0, 0, 1), COL(2, 0, 1), COL(0, 1, 1), COL(2, 1, 1),
|
||||
COL(2, 0, 1), COL(2, 2, 1), COL(2, 2, 0), COL(2, 2, 2),
|
||||
@@ -528,7 +543,7 @@ class CRTCBusHandler {
|
||||
Outputs::CRT::CRT crt_;
|
||||
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
|
||||
|
||||
uint8_t *ram_ = nullptr;
|
||||
const uint8_t *const ram_ = nullptr;
|
||||
|
||||
int next_mode_ = 2, mode_ = 2;
|
||||
|
||||
@@ -564,7 +579,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
||||
Sets the row currently being reported to the AY.
|
||||
*/
|
||||
void set_row(int row) {
|
||||
row_ = static_cast<size_t>(row);
|
||||
row_ = size_t(row);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -583,7 +598,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
||||
*/
|
||||
void set_is_pressed(bool is_pressed, int line, int 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;
|
||||
}
|
||||
|
||||
@@ -594,7 +609,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
||||
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_;
|
||||
}
|
||||
|
||||
@@ -617,7 +632,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
||||
}),
|
||||
state_(state) {}
|
||||
|
||||
void did_set_input(const Input &input, bool is_active) override {
|
||||
void did_set_input(const Input &input, bool is_active) final {
|
||||
uint8_t mask = 0;
|
||||
switch(input.type) {
|
||||
default: return;
|
||||
@@ -646,17 +661,15 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
||||
class FDC: public Intel::i8272::i8272 {
|
||||
private:
|
||||
Intel::i8272::BusHandler bus_handler_;
|
||||
std::shared_ptr<Storage::Disk::Drive> drive_;
|
||||
|
||||
public:
|
||||
FDC() :
|
||||
i8272(bus_handler_, Cycles(8000000)),
|
||||
drive_(new Storage::Disk::Drive(8000000, 300, 1)) {
|
||||
set_drive(drive_);
|
||||
FDC() : i8272(bus_handler_, Cycles(8000000)) {
|
||||
emplace_drive(8000000, 300, 1);
|
||||
set_drive(1);
|
||||
}
|
||||
|
||||
void set_motor_on(bool on) {
|
||||
drive_->set_motor_on(on);
|
||||
get_drive().set_motor_on(on);
|
||||
}
|
||||
|
||||
void select_drive(int c) {
|
||||
@@ -664,11 +677,11 @@ class FDC: public Intel::i8272::i8272 {
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
|
||||
drive_->set_disk(disk);
|
||||
get_drive().set_disk(disk);
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) {
|
||||
drive_->set_activity_observer(observer, "Drive 1", true);
|
||||
get_drive().set_activity_observer(observer, "Drive 1", true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -816,8 +829,8 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
for(std::size_t index = 0; index < roms.size(); ++index) {
|
||||
auto &data = roms[index];
|
||||
if(!data) throw ROMMachine::Error::MissingROMs;
|
||||
roms_[static_cast<int>(index)] = std::move(*data);
|
||||
roms_[static_cast<int>(index)].resize(16384);
|
||||
roms_[int(index)] = std::move(*data);
|
||||
roms_[int(index)].resize(16384);
|
||||
}
|
||||
|
||||
// Establish default memory map
|
||||
@@ -862,13 +875,15 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
|
||||
// TODO (in the player, not here): adapt it to accept an input clock rate and
|
||||
// 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
|
||||
ay_.run_for(cycle.length);
|
||||
|
||||
// Clock the FDC, if connected, using a lazy scale by two
|
||||
time_since_fdc_update_ += cycle.length;
|
||||
if constexpr (has_fdc) {
|
||||
// Clock the FDC, if connected, using a lazy scale by two
|
||||
time_since_fdc_update_ += cycle.length;
|
||||
}
|
||||
|
||||
// Update typing activity
|
||||
if(typer_) typer_->run_for(cycle.length);
|
||||
@@ -894,9 +909,11 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Check for an upper ROM selection
|
||||
if(has_fdc && !(address&0x2000)) {
|
||||
upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC;
|
||||
if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data();
|
||||
if constexpr (has_fdc) {
|
||||
if(!(address&0x2000)) {
|
||||
upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC;
|
||||
if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a CRTC access
|
||||
@@ -910,19 +927,21 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
|
||||
// Check for an 8255 PIO access
|
||||
if(!(address & 0x800)) {
|
||||
i8255_.set_register((address >> 8) & 3, *cycle.value);
|
||||
i8255_.write((address >> 8) & 3, *cycle.value);
|
||||
}
|
||||
|
||||
// Check for an FDC access
|
||||
if(has_fdc && (address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
fdc_.set_register(address & 1, *cycle.value);
|
||||
}
|
||||
if constexpr (has_fdc) {
|
||||
// Check for an FDC access
|
||||
if((address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
fdc_.write(address & 1, *cycle.value);
|
||||
}
|
||||
|
||||
// Check for a disk motor access
|
||||
if(has_fdc && !(address & 0x580)) {
|
||||
flush_fdc();
|
||||
fdc_.set_motor_on(!!(*cycle.value));
|
||||
// Check for a disk motor access
|
||||
if(!(address & 0x580)) {
|
||||
flush_fdc();
|
||||
fdc_.set_motor_on(!!(*cycle.value));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
@@ -931,13 +950,15 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
|
||||
// Check for a PIO access
|
||||
if(!(address & 0x800)) {
|
||||
*cycle.value &= i8255_.get_register((address >> 8) & 3);
|
||||
*cycle.value &= i8255_.read((address >> 8) & 3);
|
||||
}
|
||||
|
||||
// Check for an FDC access
|
||||
if(has_fdc && (address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
*cycle.value &= fdc_.get_register(address & 1);
|
||||
if constexpr (has_fdc) {
|
||||
if((address & 0x580) == 0x100) {
|
||||
flush_fdc();
|
||||
*cycle.value &= fdc_.read(address & 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a CRTC access; the below is not a typo, the CRTC can be selected
|
||||
@@ -984,26 +1005,31 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
}
|
||||
|
||||
/// A CRTMachine function; sets the destination for video.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
crtc_bus_handler_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// A CRTMachine function; returns the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return crtc_bus_handler_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
/// A CRTMachine function; sets the output display type.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override final {
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
crtc_bus_handler_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/// @returns the speaker in use.
|
||||
Outputs::Speaker::Speaker *get_speaker() override final {
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return ay_.get_speaker();
|
||||
}
|
||||
|
||||
/// Wires virtual-dispatched CRTMachine run_for requests to the static Z80 method.
|
||||
void run_for(const Cycles cycles) override final {
|
||||
void run_for(const Cycles cycles) final {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) override final {
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
// If there are any tapes supplied, use the first of them.
|
||||
if(!media.tapes.empty()) {
|
||||
tape_player_.set_tape(media.tapes.front());
|
||||
@@ -1020,70 +1046,70 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
return !media.tapes.empty() || (!media.disks.empty() && has_fdc);
|
||||
}
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override final {
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
|
||||
fdc_is_sleeping_ = fdc_.preferred_clocking() == ClockingHint::Preference::None;
|
||||
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
|
||||
}
|
||||
|
||||
// MARK: - Keyboard
|
||||
void type_string(const std::string &string) override final {
|
||||
void type_string(const std::string &string) final {
|
||||
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper());
|
||||
Utility::TypeRecipient::add_typer(string, std::move(mapper));
|
||||
}
|
||||
|
||||
HalfCycles get_typer_delay() override final {
|
||||
HalfCycles get_typer_delay() final {
|
||||
return Cycles(4000000); // Wait 1 second before typing.
|
||||
}
|
||||
|
||||
HalfCycles get_typer_frequency() override final {
|
||||
HalfCycles get_typer_frequency() final {
|
||||
return Cycles(160000); // Type one character per frame.
|
||||
}
|
||||
|
||||
// See header; sets a key as either pressed or released.
|
||||
void set_key_state(uint16_t key, bool isPressed) override final {
|
||||
void set_key_state(uint16_t key, bool isPressed) final {
|
||||
key_state_.set_is_pressed(isPressed, key >> 4, key & 7);
|
||||
}
|
||||
|
||||
// See header; sets all keys to released.
|
||||
void clear_all_keys() override final {
|
||||
void clear_all_keys() final {
|
||||
key_state_.clear_all_keys();
|
||||
}
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
if(has_fdc) fdc_.set_activity_observer(observer);
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
if constexpr (has_fdc) fdc_.set_activity_observer(observer);
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return AmstradCPC::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
Configurable::Display display;
|
||||
if(Configurable::get_display(selections_by_option, display)) {
|
||||
set_video_signal_configurable(display);
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
// MARK: - Joysticks
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1145,11 +1171,13 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
FDC fdc_;
|
||||
HalfCycles time_since_fdc_update_;
|
||||
void flush_fdc() {
|
||||
// Clock the FDC, if connected, using a lazy scale by two
|
||||
if(has_fdc && !fdc_is_sleeping_) {
|
||||
fdc_.run_for(Cycles(time_since_fdc_update_.as_int()));
|
||||
if constexpr (has_fdc) {
|
||||
// Clock the FDC, if connected, using a lazy scale by two
|
||||
if(!fdc_is_sleeping_) {
|
||||
fdc_.run_for(Cycles(time_since_fdc_update_.as_integral()));
|
||||
}
|
||||
time_since_fdc_update_ = HalfCycles(0);
|
||||
}
|
||||
time_since_fdc_update_ = HalfCycles(0);
|
||||
}
|
||||
|
||||
InterruptTimer interrupt_timer_;
|
||||
|
@@ -31,12 +31,12 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
BIND(F11, KeyRightSquareBracket);
|
||||
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(OpenSquareBracket, KeyAt);
|
||||
BIND(CloseSquareBracket, KeyLeftSquareBracket);
|
||||
BIND(BackSlash, KeyBackSlash);
|
||||
BIND(Backslash, KeyBackSlash);
|
||||
|
||||
BIND(CapsLock, KeyCapsLock);
|
||||
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(Up, KeyUp); BIND(Down, KeyDown);
|
||||
|
||||
BIND(KeyPad0, KeyF0);
|
||||
BIND(KeyPad1, KeyF1); BIND(KeyPad2, KeyF2); BIND(KeyPad3, KeyF3);
|
||||
BIND(KeyPad4, KeyF4); BIND(KeyPad5, KeyF5); BIND(KeyPad6, KeyF6);
|
||||
BIND(KeyPad7, KeyF7); BIND(KeyPad8, KeyF8); BIND(KeyPad9, KeyF9);
|
||||
BIND(KeyPadPlus, KeySemicolon);
|
||||
BIND(KeyPadMinus, KeyMinus);
|
||||
BIND(Keypad0, KeyF0);
|
||||
BIND(Keypad1, KeyF1); BIND(Keypad2, KeyF2); BIND(Keypad3, KeyF3);
|
||||
BIND(Keypad4, KeyF4); BIND(Keypad5, KeyF5); BIND(Keypad6, KeyF6);
|
||||
BIND(Keypad7, KeyF7); BIND(Keypad8, KeyF8); BIND(Keypad9, KeyF9);
|
||||
BIND(KeypadPlus, KeySemicolon);
|
||||
BIND(KeypadMinus, KeyMinus);
|
||||
|
||||
BIND(KeyPadEnter, KeyEnter);
|
||||
BIND(KeyPadDecimalPoint, KeyFullStop);
|
||||
BIND(KeyPadEquals, KeyMinus);
|
||||
BIND(KeyPadSlash, KeyForwardSlash);
|
||||
BIND(KeyPadAsterisk, KeyColon);
|
||||
BIND(KeyPadDelete, KeyDelete);
|
||||
BIND(KeypadEnter, KeyEnter);
|
||||
BIND(KeypadDecimalPoint, KeyFullStop);
|
||||
BIND(KeypadEquals, KeyMinus);
|
||||
BIND(KeypadSlash, KeyForwardSlash);
|
||||
BIND(KeypadAsterisk, KeyColon);
|
||||
BIND(KeypadDelete, KeyDelete);
|
||||
}
|
||||
#undef BIND
|
||||
}
|
||||
|
@@ -79,13 +79,15 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
void update_video() {
|
||||
video_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
static const int audio_divider = 8;
|
||||
static constexpr int audio_divider = 8;
|
||||
void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
|
||||
}
|
||||
void update_just_in_time_cards() {
|
||||
for(const auto &card : just_in_time_cards_) {
|
||||
card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
|
||||
if(cycles_since_card_update_ > Cycles(0)) {
|
||||
for(const auto &card : just_in_time_cards_) {
|
||||
card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
bool is_every_cycle_card(Apple::II::Card *card) {
|
||||
bool is_every_cycle_card(const Apple::II::Card *card) {
|
||||
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) {
|
||||
// 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);
|
||||
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;
|
||||
auto old_membership = std::find(undesired.begin(), undesired.end(), card);
|
||||
if(old_membership != undesired.end()) undesired.erase(old_membership);
|
||||
intended.push_back(card);
|
||||
|
||||
// Otherwise, mark the sets as dirty. It isn't safe to transition the card here,
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -275,12 +283,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
Input(Input::Fire, 2),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, float value) override {
|
||||
void did_set_input(const Input &input, float value) final {
|
||||
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
|
||||
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
|
||||
}
|
||||
|
||||
void did_set_input(const Input &input, bool value) override {
|
||||
void did_set_input(const Input &input, bool value) final {
|
||||
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
|
||||
buttons[input.info.control.index] = value;
|
||||
}
|
||||
@@ -321,7 +329,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
audio_toggle_(audio_queue_),
|
||||
speaker_(audio_toggle_) {
|
||||
// The system's master clock rate.
|
||||
const float master_clock = 14318180.0;
|
||||
constexpr float master_clock = 14318180.0;
|
||||
|
||||
// This is where things get slightly convoluted: establish the machine as having a clock rate
|
||||
// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
|
||||
@@ -409,16 +417,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
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();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
void run_for(const Cycles cycles) final {
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void reset_all_keys() override {
|
||||
void reset_all_keys() final {
|
||||
open_apple_is_pressed_ = closed_apple_is_pressed_ = key_is_down_ = false;
|
||||
}
|
||||
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) override {
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) final {
|
||||
switch(key) {
|
||||
default: break;
|
||||
case Key::F12:
|
||||
@@ -795,7 +832,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case Key::Right: value = 0x15; break;
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::BackSpace: value = 0x7f; break;
|
||||
case Key::Backspace: value = 0x7f; break;
|
||||
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;
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) override {
|
||||
string_serialiser_.reset(new Utility::StringSerialiser(string, true));
|
||||
void type_string(const std::string &string) final {
|
||||
string_serialiser_ = std::make_unique<Utility::StringSerialiser>(string, true);
|
||||
}
|
||||
|
||||
// MARK:: Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return Apple::II::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
Configurable::Display display;
|
||||
if(Configurable::get_display(selections_by_option, display)) {
|
||||
set_video_signal_configurable(display);
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
return get_accurate_selections();
|
||||
}
|
||||
|
||||
// MARK: MediaTarget
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if(!media.disks.empty()) {
|
||||
auto diskii = diskii_card();
|
||||
if(diskii) diskii->set_disk(media.disks[0], 0);
|
||||
@@ -853,14 +890,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: Activity::Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
for(const auto &card: cards_) {
|
||||
if(card) card->set_activity_observer(observer);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: JoystickMachine
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
}
|
||||
};
|
||||
|
@@ -83,10 +83,8 @@ class Card {
|
||||
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
|
||||
only to cards that register for IO and/or Device accesses only.
|
||||
|
||||
|
||||
*/
|
||||
int get_select_constraints() {
|
||||
int get_select_constraints() const {
|
||||
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) {
|
||||
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) {
|
||||
@@ -65,7 +65,7 @@ void DiskIICard::set_activity_observer(Activity::Observer *observer) {
|
||||
|
||||
void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::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) {
|
||||
|
@@ -27,16 +27,16 @@ class DiskIICard: public Card, public ClockingHint::Observer {
|
||||
public:
|
||||
DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector);
|
||||
|
||||
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) override;
|
||||
void run_for(Cycles cycles, int stretches) override;
|
||||
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final;
|
||||
void run_for(Cycles cycles, int stretches) final;
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) override;
|
||||
void set_activity_observer(Activity::Observer *observer) final;
|
||||
|
||||
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
|
||||
Storage::Disk::Drive &get_drive(int drive);
|
||||
|
||||
private:
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final;
|
||||
std::vector<uint8_t> boot_;
|
||||
Apple::DiskII diskii_;
|
||||
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
|
@@ -47,6 +47,10 @@ void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus VideoBase::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 14.0f;
|
||||
}
|
||||
|
||||
void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
@@ -40,6 +40,9 @@ class VideoBase {
|
||||
/// Sets the scan target.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
@@ -203,7 +206,7 @@ class VideoBase {
|
||||
std::array<uint8_t, 40> auxiliary_stream_;
|
||||
|
||||
bool is_iie_ = false;
|
||||
static const int flash_length = 8406;
|
||||
static constexpr int flash_length = 8406;
|
||||
|
||||
// Describes the current text mode mapping from in-memory character index
|
||||
// to output character.
|
||||
@@ -252,7 +255,7 @@ class VideoBase {
|
||||
void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
|
||||
|
||||
// Maintain a DeferredQueue for delayed mode switches.
|
||||
DeferredQueue<Cycles> deferrer_;
|
||||
DeferredQueuePerformer<Cycles> deferrer_;
|
||||
};
|
||||
|
||||
template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
@@ -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
|
||||
|
||||
// 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
|
||||
// (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) {
|
||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||
// (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
|
||||
// (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.
|
||||
*/
|
||||
static const int first_sync_line = 220; // A complete guess. Information needed.
|
||||
static const int first_sync_column = 49; // Also a guess.
|
||||
static const int sync_length = 4; // One of the two likely candidates.
|
||||
constexpr int first_sync_line = 220; // A complete guess. Information needed.
|
||||
constexpr int first_sync_column = 49; // Also a guess.
|
||||
constexpr int sync_length = 4; // One of the two likely candidates.
|
||||
|
||||
int int_cycles = cycles.as_int();
|
||||
int int_cycles = int(cycles.as_integral());
|
||||
while(int_cycles) {
|
||||
const int cycles_this_line = std::min(65 - column_, int_cycles);
|
||||
const int ending_column = column_ + cycles_this_line;
|
||||
|
@@ -11,7 +11,7 @@
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
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
|
||||
// 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 rotation_speed = (normalised_sum * 27.08f) - 259.0f;
|
||||
|
||||
for(int c = 0; c < number_of_drives_; ++c) {
|
||||
drives_[c]->set_rotation_speed(rotation_speed);
|
||||
}
|
||||
// printf("RPM: %0.2f (%d sum)\n", rotation_speed, sum);
|
||||
delegate_->drive_speed_accumulator_set_drive_speed(this, rotation_speed);
|
||||
}
|
||||
}
|
||||
|
||||
void DriveSpeedAccumulator::add_drive(Apple::Macintosh::DoubleDensityDrive *drive) {
|
||||
drives_[number_of_drives_] = drive;
|
||||
++number_of_drives_;
|
||||
}
|
||||
|
@@ -13,8 +13,6 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
@@ -25,18 +23,20 @@ class DriveSpeedAccumulator {
|
||||
*/
|
||||
void post_sample(uint8_t sample);
|
||||
|
||||
struct Delegate {
|
||||
virtual void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) = 0;
|
||||
};
|
||||
/*!
|
||||
Adds a connected drive. Up to two of these
|
||||
can be supplied. Only Macintosh DoubleDensityDrives
|
||||
are supported.
|
||||
Sets the delegate to receive drive speed changes.
|
||||
*/
|
||||
void add_drive(Apple::Macintosh::DoubleDensityDrive *drive);
|
||||
void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<uint8_t, 20> samples_;
|
||||
std::size_t sample_pointer_ = 0;
|
||||
Apple::Macintosh::DoubleDensityDrive *drives_[2] = {nullptr, nullptr};
|
||||
int number_of_drives_ = 0;
|
||||
Delegate *delegate_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
88
Machines/Apple/Macintosh/Keyboard.cpp
Normal file
88
Machines/Apple/Macintosh/Keyboard.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// Keyboard.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 02/08/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Keyboard.hpp"
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
using Key = Inputs::Keyboard::Key;
|
||||
using MacKey = Apple::Macintosh::Key;
|
||||
switch(key) {
|
||||
default: return KeyboardMachine::MappedMachine::KeyNotMapped;
|
||||
|
||||
#define Bind(x, y) case Key::x: return uint16_t(y)
|
||||
|
||||
Bind(BackTick, MacKey::BackTick);
|
||||
Bind(k1, MacKey::k1); Bind(k2, MacKey::k2); Bind(k3, MacKey::k3);
|
||||
Bind(k4, MacKey::k4); Bind(k5, MacKey::k5); Bind(k6, MacKey::k6);
|
||||
Bind(k7, MacKey::k7); Bind(k8, MacKey::k8); Bind(k9, MacKey::k9);
|
||||
Bind(k0, MacKey::k0);
|
||||
Bind(Hyphen, MacKey::Hyphen);
|
||||
Bind(Equals, MacKey::Equals);
|
||||
Bind(Backspace, MacKey::Backspace);
|
||||
|
||||
Bind(Tab, MacKey::Tab);
|
||||
Bind(Q, MacKey::Q); Bind(W, MacKey::W); Bind(E, MacKey::E); Bind(R, MacKey::R);
|
||||
Bind(T, MacKey::T); Bind(Y, MacKey::Y); Bind(U, MacKey::U); Bind(I, MacKey::I);
|
||||
Bind(O, MacKey::O); Bind(P, MacKey::P);
|
||||
Bind(OpenSquareBracket, MacKey::OpenSquareBracket);
|
||||
Bind(CloseSquareBracket, MacKey::CloseSquareBracket);
|
||||
|
||||
Bind(CapsLock, MacKey::CapsLock);
|
||||
Bind(A, MacKey::A); Bind(S, MacKey::S); Bind(D, MacKey::D); Bind(F, MacKey::F);
|
||||
Bind(G, MacKey::G); Bind(H, MacKey::H); Bind(J, MacKey::J); Bind(K, MacKey::K);
|
||||
Bind(L, MacKey::L);
|
||||
Bind(Semicolon, MacKey::Semicolon);
|
||||
Bind(Quote, MacKey::Quote);
|
||||
Bind(Enter, MacKey::Return);
|
||||
|
||||
Bind(LeftShift, MacKey::Shift);
|
||||
Bind(Z, MacKey::Z); Bind(X, MacKey::X); Bind(C, MacKey::C); Bind(V, MacKey::V);
|
||||
Bind(B, MacKey::B); Bind(N, MacKey::N); Bind(M, MacKey::M);
|
||||
Bind(Comma, MacKey::Comma);
|
||||
Bind(FullStop, MacKey::FullStop);
|
||||
Bind(ForwardSlash, MacKey::ForwardSlash);
|
||||
Bind(RightShift, MacKey::Shift);
|
||||
|
||||
Bind(Left, MacKey::Left);
|
||||
Bind(Right, MacKey::Right);
|
||||
Bind(Up, MacKey::Up);
|
||||
Bind(Down, MacKey::Down);
|
||||
|
||||
Bind(LeftOption, MacKey::Option);
|
||||
Bind(RightOption, MacKey::Option);
|
||||
Bind(LeftMeta, MacKey::Command);
|
||||
Bind(RightMeta, MacKey::Command);
|
||||
|
||||
Bind(Space, MacKey::Space);
|
||||
Bind(Backslash, MacKey::Backslash);
|
||||
|
||||
Bind(KeypadDelete, MacKey::KeypadDelete);
|
||||
Bind(KeypadEquals, MacKey::KeypadEquals);
|
||||
Bind(KeypadSlash, MacKey::KeypadSlash);
|
||||
Bind(KeypadAsterisk, MacKey::KeypadAsterisk);
|
||||
Bind(KeypadMinus, MacKey::KeypadMinus);
|
||||
Bind(KeypadPlus, MacKey::KeypadPlus);
|
||||
Bind(KeypadEnter, MacKey::KeypadEnter);
|
||||
Bind(KeypadDecimalPoint, MacKey::KeypadDecimalPoint);
|
||||
|
||||
Bind(Keypad9, MacKey::Keypad9);
|
||||
Bind(Keypad8, MacKey::Keypad8);
|
||||
Bind(Keypad7, MacKey::Keypad7);
|
||||
Bind(Keypad6, MacKey::Keypad6);
|
||||
Bind(Keypad5, MacKey::Keypad5);
|
||||
Bind(Keypad4, MacKey::Keypad4);
|
||||
Bind(Keypad3, MacKey::Keypad3);
|
||||
Bind(Keypad2, MacKey::Keypad2);
|
||||
Bind(Keypad1, MacKey::Keypad1);
|
||||
Bind(Keypad0, MacKey::Keypad0);
|
||||
|
||||
#undef Bind
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@
|
||||
#define Apple_Macintosh_Keyboard_hpp
|
||||
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
@@ -17,6 +18,72 @@
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
constexpr uint16_t KeypadMask = 0x100;
|
||||
|
||||
/*!
|
||||
Defines the keycodes that could be passed directly to a Macintosh via set_key_pressed.
|
||||
*/
|
||||
enum class Key: uint16_t {
|
||||
/*
|
||||
See p284 of the Apple Guide to the Macintosh Family Hardware
|
||||
for documentation of the mapping below.
|
||||
*/
|
||||
BackTick = 0x65,
|
||||
k1 = 0x25, k2 = 0x27, k3 = 0x29, k4 = 0x2b, k5 = 0x2f,
|
||||
k6 = 0x2d, k7 = 0x35, k8 = 0x39, k9 = 0x33, k0 = 0x3b,
|
||||
|
||||
Hyphen = 0x37,
|
||||
Equals = 0x31,
|
||||
Backspace = 0x67,
|
||||
Tab = 0x61,
|
||||
|
||||
Q = 0x19, W = 0x1b, E = 0x1d, R = 0x1f, T = 0x23, Y = 0x21, U = 0x41, I = 0x45, O = 0x3f, P = 0x47,
|
||||
A = 0x01, S = 0x03, D = 0x05, F = 0x07, G = 0x0b, H = 0x09, J = 0x4d, K = 0x51, L = 0x4b,
|
||||
Z = 0x0d, X = 0x0f, C = 0x11, V = 0x13, B = 0x17, N = 0x5b, M = 0x5d,
|
||||
|
||||
OpenSquareBracket = 0x43,
|
||||
CloseSquareBracket = 0x3d,
|
||||
Semicolon = 0x53,
|
||||
Quote = 0x4f,
|
||||
Comma = 0x57,
|
||||
FullStop = 0x5f,
|
||||
ForwardSlash = 0x59,
|
||||
|
||||
CapsLock = 0x73,
|
||||
Shift = 0x71,
|
||||
Option = 0x75,
|
||||
Command = 0x6f,
|
||||
|
||||
Space = 0x63,
|
||||
Backslash = 0x55,
|
||||
Return = 0x49,
|
||||
|
||||
Left = KeypadMask | 0x0d,
|
||||
Right = KeypadMask | 0x05,
|
||||
Up = KeypadMask | 0x1b,
|
||||
Down = KeypadMask | 0x11,
|
||||
|
||||
KeypadDelete = KeypadMask | 0x0f,
|
||||
KeypadEquals = KeypadMask | 0x11,
|
||||
KeypadSlash = KeypadMask | 0x1b,
|
||||
KeypadAsterisk = KeypadMask | 0x05,
|
||||
KeypadMinus = KeypadMask | 0x1d,
|
||||
KeypadPlus = KeypadMask | 0x0d,
|
||||
KeypadEnter = KeypadMask | 0x19,
|
||||
KeypadDecimalPoint = KeypadMask | 0x03,
|
||||
|
||||
Keypad9 = KeypadMask | 0x39,
|
||||
Keypad8 = KeypadMask | 0x37,
|
||||
Keypad7 = KeypadMask | 0x33,
|
||||
Keypad6 = KeypadMask | 0x31,
|
||||
Keypad5 = KeypadMask | 0x2f,
|
||||
Keypad4 = KeypadMask | 0x2d,
|
||||
Keypad3 = KeypadMask | 0x2b,
|
||||
Keypad2 = KeypadMask | 0x29,
|
||||
Keypad1 = KeypadMask | 0x27,
|
||||
Keypad0 = KeypadMask | 0x25
|
||||
};
|
||||
|
||||
class Keyboard {
|
||||
public:
|
||||
void set_input(bool data) {
|
||||
@@ -147,14 +214,16 @@ class Keyboard {
|
||||
|
||||
// Keys on the keypad are preceded by a $79 keycode; in the internal naming scheme
|
||||
// they are indicated by having bit 8 set. So add the $79 prefix if required.
|
||||
if(key & 0x100) {
|
||||
if(key & KeypadMask) {
|
||||
key_queue_.insert(key_queue_.begin(), 0x79);
|
||||
}
|
||||
key_queue_.insert(key_queue_.begin(), (is_pressed ? 0x00 : 0x80) | uint8_t(key));
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
/// Performs the pre-ADB Apple keyboard protocol command @c command, returning
|
||||
/// the proper result if the command were to terminate now. So, it treats inquiry
|
||||
/// and instant as the same command.
|
||||
int perform_command(int command) {
|
||||
switch(command) {
|
||||
case 0x10: // Inquiry.
|
||||
@@ -180,22 +249,41 @@ class Keyboard {
|
||||
return 0x7b; // No key transition.
|
||||
}
|
||||
|
||||
/// Maintains the current operating mode — a record of what the
|
||||
/// keyboard is doing now.
|
||||
enum class Mode {
|
||||
/// The keyboard is waiting to begin a transaction.
|
||||
Waiting,
|
||||
/// The keyboard is currently clocking in a new command.
|
||||
AcceptingCommand,
|
||||
/// The keyboard is waiting for the computer to indicate that it is ready for a response.
|
||||
AwaitingEndOfCommand,
|
||||
/// The keyboard is in the process of performing the command it most-recently received.
|
||||
/// If the command was an 'inquiry', this state may persist for a non-neglibible period of time.
|
||||
PerformingCommand,
|
||||
/// The keyboard is currently shifting a response back to the computer.
|
||||
SendingResponse,
|
||||
PerformingCommand
|
||||
} mode_ = Mode::Waiting;
|
||||
|
||||
/// Holds a count of progress through the current @c Mode. Exact meaning depends on mode.
|
||||
int phase_ = 0;
|
||||
/// Holds the most-recently-received command; the command is shifted into here as it is received
|
||||
/// so this may not be valid prior to Mode::PerformingCommand.
|
||||
int command_ = 0;
|
||||
/// Populated during PerformingCommand as the response to the most-recently-received command, this
|
||||
/// is then shifted out to teh host computer. So it is guaranteed valid at the beginning of Mode::SendingResponse,
|
||||
/// but not afterwards.
|
||||
int response_ = 0;
|
||||
|
||||
/// The current state of the serial connection's data input.
|
||||
bool data_input_ = false;
|
||||
/// The current clock output from this keyboard.
|
||||
bool clock_output_ = false;
|
||||
|
||||
// TODO: improve this very, very simple implementation.
|
||||
/// Guards multithread access to key_queue_.
|
||||
std::mutex key_queue_mutex_;
|
||||
/// A FIFO queue for key events, in the form they'd be communicated to the Macintosh,
|
||||
/// with the newest events towards the front.
|
||||
std::vector<uint8_t> key_queue_;
|
||||
};
|
||||
|
||||
@@ -203,89 +291,7 @@ class Keyboard {
|
||||
Provides a mapping from idiomatic PC keys to Macintosh keys.
|
||||
*/
|
||||
class KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) override {
|
||||
using Key = Inputs::Keyboard::Key;
|
||||
switch(key) {
|
||||
default: return KeyboardMachine::MappedMachine::KeyNotMapped;
|
||||
|
||||
/*
|
||||
See p284 of the Apple Guide to the Macintosh Family Hardware
|
||||
for documentation of the mapping below.
|
||||
*/
|
||||
|
||||
case Key::BackTick: return 0x65;
|
||||
case Key::k1: return 0x25;
|
||||
case Key::k2: return 0x27;
|
||||
case Key::k3: return 0x29;
|
||||
case Key::k4: return 0x2b;
|
||||
case Key::k5: return 0x2f;
|
||||
case Key::k6: return 0x2d;
|
||||
case Key::k7: return 0x35;
|
||||
case Key::k8: return 0x39;
|
||||
case Key::k9: return 0x33;
|
||||
case Key::k0: return 0x3b;
|
||||
case Key::Hyphen: return 0x37;
|
||||
case Key::Equals: return 0x31;
|
||||
case Key::BackSpace: return 0x67;
|
||||
|
||||
case Key::Tab: return 0x61;
|
||||
case Key::Q: return 0x19;
|
||||
case Key::W: return 0x1b;
|
||||
case Key::E: return 0x1d;
|
||||
case Key::R: return 0x1f;
|
||||
case Key::T: return 0x23;
|
||||
case Key::Y: return 0x21;
|
||||
case Key::U: return 0x41;
|
||||
case Key::I: return 0x45;
|
||||
case Key::O: return 0x3f;
|
||||
case Key::P: return 0x47;
|
||||
case Key::OpenSquareBracket: return 0x43;
|
||||
case Key::CloseSquareBracket: return 0x3d;
|
||||
|
||||
case Key::CapsLock: return 0x73;
|
||||
case Key::A: return 0x01;
|
||||
case Key::S: return 0x03;
|
||||
case Key::D: return 0x05;
|
||||
case Key::F: return 0x07;
|
||||
case Key::G: return 0x0b;
|
||||
case Key::H: return 0x09;
|
||||
case Key::J: return 0x4d;
|
||||
case Key::K: return 0x51;
|
||||
case Key::L: return 0x4b;
|
||||
case Key::Semicolon: return 0x53;
|
||||
case Key::Quote: return 0x4f;
|
||||
case Key::Enter: return 0x49;
|
||||
|
||||
case Key::LeftShift: return 0x71;
|
||||
case Key::Z: return 0x0d;
|
||||
case Key::X: return 0x0f;
|
||||
case Key::C: return 0x11;
|
||||
case Key::V: return 0x13;
|
||||
case Key::B: return 0x17;
|
||||
case Key::N: return 0x5b;
|
||||
case Key::M: return 0x5d;
|
||||
case Key::Comma: return 0x57;
|
||||
case Key::FullStop: return 0x5f;
|
||||
case Key::ForwardSlash: return 0x59;
|
||||
case Key::RightShift: return 0x71;
|
||||
|
||||
case Key::Left: return 0x100 | 0x0d;
|
||||
case Key::Right: return 0x100 | 0x05;
|
||||
case Key::Up: return 0x100 | 0x1b;
|
||||
case Key::Down: return 0x100 | 0x11;
|
||||
|
||||
case Key::LeftOption:
|
||||
case Key::RightOption: return 0x75;
|
||||
case Key::LeftMeta:
|
||||
case Key::RightMeta: return 0x6f;
|
||||
|
||||
case Key::Space: return 0x63;
|
||||
case Key::BackSlash: return 0x55;
|
||||
|
||||
/* TODO: the numeric keypad. */
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) final;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@
|
||||
#include "RealTimeClock.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#include "../../../Activity/Source.hpp"
|
||||
#include "../../CRTMachine.hpp"
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
#include "../../MediaTarget.hpp"
|
||||
@@ -25,15 +26,22 @@
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include "../../../ClockReceiver/JustInTime.hpp"
|
||||
#include "../../../ClockReceiver/ClockingHintSource.hpp"
|
||||
#include "../../../Configurable/StandardOptions.hpp"
|
||||
|
||||
//#define LOG_TRACE
|
||||
|
||||
#include "../../../Components/5380/ncr5380.hpp"
|
||||
#include "../../../Components/6522/6522.hpp"
|
||||
#include "../../../Components/8530/z8530.hpp"
|
||||
#include "../../../Components/DiskII/IWM.hpp"
|
||||
#include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp"
|
||||
#include "../../../Processors/68000/68000.hpp"
|
||||
|
||||
#include "../../../Storage/MassStorage/SCSI/SCSI.hpp"
|
||||
#include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp"
|
||||
#include "../../../Storage/MassStorage/Encodings/MacintoshVolume.hpp"
|
||||
|
||||
#include "../../../Analyser/Static/Macintosh/Target.hpp"
|
||||
|
||||
#include "../../Utility/MemoryPacker.hpp"
|
||||
@@ -41,13 +49,19 @@
|
||||
|
||||
namespace {
|
||||
|
||||
const int CLOCK_RATE = 7833600;
|
||||
constexpr int CLOCK_RATE = 7833600;
|
||||
|
||||
}
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::QuickBoot)
|
||||
);
|
||||
}
|
||||
|
||||
template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachine:
|
||||
public Machine,
|
||||
public CRTMachine::Machine,
|
||||
@@ -55,16 +69,28 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
public MouseMachine::Machine,
|
||||
public CPU::MC68000::BusHandler,
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public Zilog::SCC::z8530::Delegate {
|
||||
public Zilog::SCC::z8530::Delegate,
|
||||
public Activity::Source,
|
||||
public Configurable::Device,
|
||||
public DriveSpeedAccumulator::Delegate,
|
||||
public ClockingHint::Observer {
|
||||
public:
|
||||
using Target = Analyser::Static::Macintosh::Target;
|
||||
|
||||
ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
KeyboardMachine::MappedMachine({
|
||||
Inputs::Keyboard::Key::LeftShift, Inputs::Keyboard::Key::RightShift,
|
||||
Inputs::Keyboard::Key::LeftOption, Inputs::Keyboard::Key::RightOption,
|
||||
Inputs::Keyboard::Key::LeftMeta, Inputs::Keyboard::Key::RightMeta,
|
||||
}),
|
||||
mc68000_(*this),
|
||||
iwm_(CLOCK_RATE),
|
||||
video_(ram_, audio_, drive_speed_accumulator_),
|
||||
video_(audio_, drive_speed_accumulator_),
|
||||
via_(via_port_handler_),
|
||||
via_port_handler_(*this, clock_, keyboard_, video_, audio_, iwm_, mouse_),
|
||||
via_port_handler_(*this, clock_, keyboard_, audio_, iwm_, mouse_),
|
||||
scsi_bus_(CLOCK_RATE * 2),
|
||||
scsi_(scsi_bus_, CLOCK_RATE * 2),
|
||||
hard_drive_(scsi_bus_, 6 /* SCSI ID */),
|
||||
drives_{
|
||||
{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke},
|
||||
{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke}
|
||||
@@ -91,7 +117,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
break;
|
||||
case Model::Mac512ke:
|
||||
case Model::MacPlus: {
|
||||
ram_size = 512*1024;
|
||||
ram_size = ((model == Model::MacPlus) ? 4096 : 512)*1024;
|
||||
rom_size = 128*1024;
|
||||
const std::initializer_list<uint32_t> crc32s = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e };
|
||||
rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s);
|
||||
@@ -99,7 +125,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
}
|
||||
ram_mask_ = (ram_size >> 1) - 1;
|
||||
rom_mask_ = (rom_size >> 1) - 1;
|
||||
video_.set_ram_mask(ram_mask_);
|
||||
ram_.resize(ram_size >> 1);
|
||||
video_.set_ram(ram_.data(), ram_mask_);
|
||||
|
||||
// Grab a copy of the ROM and convert it into big-endian data.
|
||||
const auto roms = rom_fetcher(rom_descriptions);
|
||||
@@ -110,62 +137,462 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
Memory::PackBigEndian16(*roms[0], rom_);
|
||||
|
||||
// Randomise memory contents.
|
||||
Memory::Fuzz(ram_, sizeof(ram_) / sizeof(*ram_));
|
||||
Memory::Fuzz(ram_);
|
||||
|
||||
// Attach the drives to the IWM.
|
||||
iwm_.iwm.set_drive(0, &drives_[0]);
|
||||
iwm_.iwm.set_drive(1, &drives_[1]);
|
||||
iwm_->set_drive(0, &drives_[0]);
|
||||
iwm_->set_drive(1, &drives_[1]);
|
||||
|
||||
// If they are 400kb drives, also attach them to the drive-speed accumulator.
|
||||
if(!drives_[0].is_800k()) drive_speed_accumulator_.add_drive(&drives_[0]);
|
||||
if(!drives_[1].is_800k()) drive_speed_accumulator_.add_drive(&drives_[1]);
|
||||
if(!drives_[0].is_800k() || !drives_[1].is_800k()) {
|
||||
drive_speed_accumulator_.set_delegate(this);
|
||||
}
|
||||
|
||||
// Make sure interrupt changes from the SCC are observed.
|
||||
scc_.set_delegate(this);
|
||||
|
||||
// Also watch for changes in clocking requirement from the SCSI chip.
|
||||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
scsi_bus_.set_clocking_hint_observer(this);
|
||||
}
|
||||
|
||||
// The Mac runs at 7.8336mHz.
|
||||
set_clock_rate(double(CLOCK_RATE));
|
||||
audio_.speaker.set_input_rate(float(CLOCK_RATE) / 2.0f);
|
||||
|
||||
// Insert any supplied media.
|
||||
insert_media(target.media);
|
||||
|
||||
// Set the immutables of the memory map.
|
||||
setup_memory_map();
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_.queue.flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &audio_.speaker;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
void run_for(const Cycles cycles) final {
|
||||
mc68000_.run_for(cycles);
|
||||
}
|
||||
|
||||
using Microcycle = CPU::MC68000::Microcycle;
|
||||
|
||||
HalfCycles perform_bus_operation(const Microcycle &cycle, int is_supervisor) {
|
||||
// TODO: pick a delay if this is a video-clashing memory fetch.
|
||||
HalfCycles delay(0);
|
||||
forceinline HalfCycles perform_bus_operation(const Microcycle &cycle, int is_supervisor) {
|
||||
// Advance time.
|
||||
advance_time(cycle.length);
|
||||
|
||||
time_since_video_update_ += cycle.length;
|
||||
iwm_.time_since_update += cycle.length;
|
||||
// A null cycle leaves nothing else to do.
|
||||
if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return HalfCycles(0);
|
||||
|
||||
// Grab the value on the address bus, at word precision.
|
||||
uint32_t word_address = cycle.active_operation_word_address();
|
||||
|
||||
// Everything above E0 0000 is signalled as being on the peripheral bus.
|
||||
mc68000_.set_is_peripheral_address(word_address >= 0x700000);
|
||||
|
||||
// All code below deals only with reads and writes — cycles in which a
|
||||
// data select is active. So quit now if this is not the active part of
|
||||
// a read or write.
|
||||
//
|
||||
// The 68000 uses 6800-style autovectored interrupts, so the mere act of
|
||||
// having set VPA above deals with those given that the generated address
|
||||
// for interrupt acknowledge cycles always has all bits set except the
|
||||
// lowest explicit address lines.
|
||||
if(!cycle.data_select_active() || (cycle.operation & Microcycle::InterruptAcknowledge)) return HalfCycles(0);
|
||||
|
||||
// Grab the word-precision address being accessed.
|
||||
uint16_t *memory_base = nullptr;
|
||||
HalfCycles delay;
|
||||
switch(memory_map_[word_address >> 16]) {
|
||||
default: assert(false);
|
||||
|
||||
case BusDevice::Unassigned:
|
||||
fill_unmapped(cycle);
|
||||
return delay;
|
||||
|
||||
case BusDevice::VIA: {
|
||||
if(*cycle.address & 1) {
|
||||
fill_unmapped(cycle);
|
||||
} else {
|
||||
const int register_address = word_address >> 8;
|
||||
|
||||
// VIA accesses are via address 0xefe1fe + register*512,
|
||||
// which at word precision is 0x77f0ff + register*256.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = via_.read(register_address);
|
||||
} else {
|
||||
via_.write(register_address, cycle.value->halves.low);
|
||||
}
|
||||
|
||||
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
|
||||
}
|
||||
} return delay;
|
||||
|
||||
case BusDevice::PhaseRead: {
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = phase_ & 7;
|
||||
}
|
||||
|
||||
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
|
||||
} return delay;
|
||||
|
||||
case BusDevice::IWM: {
|
||||
if(*cycle.address & 1) {
|
||||
const int register_address = word_address >> 8;
|
||||
|
||||
// The IWM; this is a purely polled device, so can be run on demand.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = iwm_->read(register_address);
|
||||
} else {
|
||||
iwm_->write(register_address, cycle.value->halves.low);
|
||||
}
|
||||
|
||||
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
|
||||
} else {
|
||||
fill_unmapped(cycle);
|
||||
}
|
||||
} return delay;
|
||||
|
||||
case BusDevice::SCSI: {
|
||||
const int register_address = word_address >> 3;
|
||||
const bool dma_acknowledge = word_address & 0x100;
|
||||
|
||||
// Even accesses = read; odd = write.
|
||||
if(*cycle.address & 1) {
|
||||
// Odd access => this is a write. Data will be in the upper byte.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
scsi_.write(register_address, 0xff, dma_acknowledge);
|
||||
} else {
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
scsi_.write(register_address, cycle.value->halves.high, dma_acknowledge);
|
||||
} else {
|
||||
scsi_.write(register_address, cycle.value->halves.low, dma_acknowledge);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Even access => this is a read.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
const auto result = scsi_.read(register_address, dma_acknowledge);
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
// Data is loaded on the top part of the bus only.
|
||||
cycle.value->full = uint16_t((result << 8) | 0xff);
|
||||
} else {
|
||||
cycle.value->halves.low = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
} return delay;
|
||||
|
||||
case BusDevice::SCCReadResetPhase: {
|
||||
// Any word access here adjusts phase.
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
adjust_phase();
|
||||
} else {
|
||||
// A0 = 1 => reset; A0 = 0 => read.
|
||||
if(*cycle.address & 1) {
|
||||
scc_.reset();
|
||||
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = 0xff;
|
||||
}
|
||||
} else {
|
||||
const auto read = scc_.read(int(word_address));
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = read;
|
||||
}
|
||||
}
|
||||
}
|
||||
} return delay;
|
||||
|
||||
case BusDevice::SCCWrite: {
|
||||
// Any word access here adjusts phase.
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
adjust_phase();
|
||||
} else {
|
||||
if(*cycle.address & 1) {
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
scc_.write(int(word_address), 0xff);
|
||||
cycle.value->halves.low = 0xff;
|
||||
} else {
|
||||
scc_.write(int(word_address), cycle.value->halves.low);
|
||||
}
|
||||
} else {
|
||||
fill_unmapped(cycle);
|
||||
}
|
||||
}
|
||||
} return delay;
|
||||
|
||||
case BusDevice::RAM: {
|
||||
// This is coupled with the Macintosh implementation of video; the magic
|
||||
// constant should probably be factored into the Video class.
|
||||
// It embodies knowledge of the fact that video (and audio) will always
|
||||
// be fetched from the final $d900 bytes (i.e. $6c80 words) of memory.
|
||||
// (And that ram_mask_ = ram size - 1).
|
||||
if(word_address > ram_mask_ - 0x6c80)
|
||||
update_video();
|
||||
|
||||
memory_base = ram_.data();
|
||||
word_address &= ram_mask_;
|
||||
|
||||
// Apply a delay due to video contention if applicable; scheme applied:
|
||||
// only every other access slot is available during the period of video
|
||||
// output. I believe this to be correct for the 128k, 512k and Plus.
|
||||
// More research to do on other models.
|
||||
if(video_is_outputting() && ram_subcycle_ < 8) {
|
||||
delay = HalfCycles(8 - ram_subcycle_);
|
||||
advance_time(delay);
|
||||
}
|
||||
} break;
|
||||
|
||||
case BusDevice::ROM: {
|
||||
if(!(cycle.operation & Microcycle::Read)) return delay;
|
||||
memory_base = rom_;
|
||||
word_address &= rom_mask_;
|
||||
} break;
|
||||
}
|
||||
|
||||
// If control has fallen through to here, the access is either a read from ROM, or a read or write to RAM.
|
||||
switch(cycle.operation & (Microcycle::SelectWord | Microcycle::SelectByte | Microcycle::Read)) {
|
||||
default:
|
||||
break;
|
||||
|
||||
case Microcycle::SelectWord | Microcycle::Read:
|
||||
cycle.value->full = memory_base[word_address];
|
||||
break;
|
||||
case Microcycle::SelectByte | Microcycle::Read:
|
||||
cycle.value->halves.low = uint8_t(memory_base[word_address] >> cycle.byte_shift());
|
||||
break;
|
||||
case Microcycle::SelectWord:
|
||||
memory_base[word_address] = cycle.value->full;
|
||||
break;
|
||||
case Microcycle::SelectByte:
|
||||
memory_base[word_address] = uint16_t(
|
||||
(cycle.value->halves.low << cycle.byte_shift()) |
|
||||
(memory_base[word_address] & cycle.untouched_byte_mask())
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return delay;
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// Flush the video before the audio queue; in a Mac the
|
||||
// video is responsible for providing part of the
|
||||
// audio signal, so the two aren't as distinct as in
|
||||
// most machines.
|
||||
update_video();
|
||||
|
||||
// As above: flush audio after video.
|
||||
via_.flush();
|
||||
audio_.queue.perform();
|
||||
|
||||
// This avoids deferring IWM costs indefinitely, until
|
||||
// they become artbitrarily large.
|
||||
iwm_.flush();
|
||||
}
|
||||
|
||||
void set_rom_is_overlay(bool rom_is_overlay) {
|
||||
ROM_is_overlay_ = rom_is_overlay;
|
||||
|
||||
using Model = Analyser::Static::Macintosh::Target::Model;
|
||||
switch(model) {
|
||||
case Model::Mac128k:
|
||||
case Model::Mac512k:
|
||||
case Model::Mac512ke:
|
||||
populate_memory_map(0, [rom_is_overlay] (std::function<void(int target, BusDevice device)> map_to) {
|
||||
// Addresses up to $80 0000 aren't affected by this bit.
|
||||
if(rom_is_overlay) {
|
||||
// Up to $60 0000 mirrors of the ROM alternate with unassigned areas every $10 0000 byes.
|
||||
for(int c = 0; c < 0x600000; c += 0x100000) {
|
||||
map_to(c + 0x100000, (c & 0x100000) ? BusDevice::Unassigned : BusDevice::ROM);
|
||||
}
|
||||
map_to(0x800000, BusDevice::RAM);
|
||||
} else {
|
||||
map_to(0x400000, BusDevice::RAM);
|
||||
map_to(0x500000, BusDevice::ROM);
|
||||
map_to(0x800000, BusDevice::Unassigned);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case Model::MacPlus:
|
||||
populate_memory_map(0, [rom_is_overlay] (std::function<void(int target, BusDevice device)> map_to) {
|
||||
// Addresses up to $80 0000 aren't affected by this bit.
|
||||
if(rom_is_overlay) {
|
||||
for(int c = 0; c < 0x580000; c += 0x20000) {
|
||||
map_to(c + 0x20000, ((c & 0x100000) || (c & 0x20000)) ? BusDevice::Unassigned : BusDevice::ROM);
|
||||
}
|
||||
map_to(0x600000, BusDevice::SCSI);
|
||||
map_to(0x800000, BusDevice::RAM);
|
||||
} else {
|
||||
map_to(0x400000, BusDevice::RAM);
|
||||
for(int c = 0x400000; c < 0x580000; c += 0x20000) {
|
||||
map_to(c + 0x20000, ((c & 0x100000) || (c & 0x20000)) ? BusDevice::Unassigned : BusDevice::ROM);
|
||||
}
|
||||
map_to(0x600000, BusDevice::SCSI);
|
||||
map_to(0x800000, BusDevice::Unassigned);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool video_is_outputting() {
|
||||
return video_.is_outputting(time_since_video_update_);
|
||||
}
|
||||
|
||||
void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) {
|
||||
update_video();
|
||||
video_.set_use_alternate_buffers(use_alternate_screen_buffer, use_alternate_audio_buffer);
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if(media.disks.empty() && media.mass_storage_devices.empty())
|
||||
return false;
|
||||
|
||||
// TODO: shouldn't allow disks to be replaced like this, as the Mac
|
||||
// uses software eject. Will need to expand messaging ability of
|
||||
// insert_media.
|
||||
if(!media.disks.empty()) {
|
||||
if(drives_[0].has_disk())
|
||||
drives_[1].set_disk(media.disks[0]);
|
||||
else
|
||||
drives_[0].set_disk(media.disks[0]);
|
||||
}
|
||||
|
||||
// TODO: allow this only at machine startup.
|
||||
if(!media.mass_storage_devices.empty()) {
|
||||
const auto volume = dynamic_cast<Storage::MassStorage::Encodings::Macintosh::Volume *>(media.mass_storage_devices.front().get());
|
||||
if(volume) {
|
||||
volume->set_drive_type(Storage::MassStorage::Encodings::Macintosh::DriveType::SCSI);
|
||||
}
|
||||
hard_drive_->set_storage(media.mass_storage_devices.front());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: Keyboard input.
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||||
keyboard_.enqueue_key_state(key, is_pressed);
|
||||
}
|
||||
|
||||
// TODO: clear all keys.
|
||||
|
||||
// MARK: Interrupt updates.
|
||||
|
||||
void did_change_interrupt_status(Zilog::SCC::z8530 *sender, bool new_status) final {
|
||||
update_interrupt_input();
|
||||
}
|
||||
|
||||
void update_interrupt_input() {
|
||||
// Update interrupt input.
|
||||
// TODO: does this really cascade like this?
|
||||
if(scc_.get_interrupt_line()) {
|
||||
mc68000_.set_interrupt_level(2);
|
||||
} else if(via_.get_interrupt_line()) {
|
||||
mc68000_.set_interrupt_level(1);
|
||||
} else {
|
||||
mc68000_.set_interrupt_level(0);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
iwm_->set_activity_observer(observer);
|
||||
|
||||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
scsi_bus_.set_activity_observer(observer);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
|
||||
return Apple::Macintosh::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
|
||||
bool quick_boot;
|
||||
if(Configurable::get_quick_boot(selections_by_option, quick_boot)) {
|
||||
if(quick_boot) {
|
||||
// Cf. Big Mess o' Wires' disassembly of the Mac Plus ROM, and the
|
||||
// test at $E00. TODO: adapt as(/if?) necessary for other Macs.
|
||||
ram_[0x02ae >> 1] = 0x40;
|
||||
ram_[0x02b0 >> 1] = 0x00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_boot_selection(selection_set, false);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() final {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_boot_selection(selection_set, true);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
private:
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
|
||||
scsi_bus_is_clocked_ = scsi_bus_.preferred_clocking() != ClockingHint::Preference::None;
|
||||
}
|
||||
|
||||
void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) final {
|
||||
iwm_.flush();
|
||||
drives_[0].set_rotation_speed(speed);
|
||||
drives_[1].set_rotation_speed(speed);
|
||||
}
|
||||
|
||||
forceinline void adjust_phase() {
|
||||
++phase_;
|
||||
}
|
||||
|
||||
forceinline void fill_unmapped(const Microcycle &cycle) {
|
||||
if(!(cycle.operation & Microcycle::Read)) return;
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
cycle.value->full = 0xffff;
|
||||
} else {
|
||||
cycle.value->halves.low = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances all non-CPU components by @c duration half cycles.
|
||||
forceinline void advance_time(HalfCycles duration) {
|
||||
time_since_video_update_ += duration;
|
||||
iwm_ += duration;
|
||||
ram_subcycle_ = (ram_subcycle_ + duration.as_integral()) & 15;
|
||||
|
||||
// The VIA runs at one-tenth of the 68000's clock speed, in sync with the E clock.
|
||||
// See: Guide to the Macintosh Hardware Family p149 (PDF p188). Some extra division
|
||||
// may occur here in order to provide VSYNC at a proper moment.
|
||||
// Possibly route vsync.
|
||||
if(time_since_video_update_ < time_until_video_event_) {
|
||||
via_clock_ += cycle.length;
|
||||
via_clock_ += duration;
|
||||
via_.run_for(via_clock_.divide(HalfCycles(10)));
|
||||
} else {
|
||||
auto via_time_base = time_since_video_update_ - cycle.length;
|
||||
auto via_cycles_outstanding = cycle.length;
|
||||
auto via_time_base = time_since_video_update_ - duration;
|
||||
auto via_cycles_outstanding = duration;
|
||||
while(time_until_video_event_ < time_since_video_update_) {
|
||||
const auto via_cycles = time_until_video_event_ - via_time_base;
|
||||
via_time_base = HalfCycles(0);
|
||||
@@ -187,7 +614,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
// The keyboard also has a clock, albeit a very slow one — 100,000 cycles/second.
|
||||
// Its clock and data lines are connected to the VIA.
|
||||
keyboard_clock_ += cycle.length;
|
||||
keyboard_clock_ += duration;
|
||||
const auto keyboard_ticks = keyboard_clock_.divide(HalfCycles(CLOCK_RATE / 100000));
|
||||
if(keyboard_ticks > HalfCycles(0)) {
|
||||
keyboard_.run_for(keyboard_ticks);
|
||||
@@ -197,7 +624,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
// Feed mouse inputs within at most 1250 cycles of each other.
|
||||
if(mouse_.has_steps()) {
|
||||
time_since_mouse_update_ += cycle.length;
|
||||
time_since_mouse_update_ += duration;
|
||||
const auto mouse_ticks = time_since_mouse_update_.divide(HalfCycles(2500));
|
||||
if(mouse_ticks > HalfCycles(0)) {
|
||||
mouse_.prepare_step();
|
||||
@@ -210,8 +637,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
// anything connected.
|
||||
|
||||
// Consider updating the real-time clock.
|
||||
real_time_clock_ += cycle.length;
|
||||
auto ticks = real_time_clock_.divide_cycles(Cycles(CLOCK_RATE)).as_int();
|
||||
real_time_clock_ += duration;
|
||||
auto ticks = real_time_clock_.divide_cycles(Cycles(CLOCK_RATE)).as_integral();
|
||||
while(ticks--) {
|
||||
clock_.update();
|
||||
// TODO: leave a delay between toggling the input rather than using this coupled hack.
|
||||
@@ -219,272 +646,27 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two, false);
|
||||
}
|
||||
|
||||
// A null cycle leaves nothing else to do.
|
||||
if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return delay;
|
||||
|
||||
auto word_address = cycle.active_operation_word_address();
|
||||
|
||||
// Everything above E0 0000 is signalled as being on the peripheral bus.
|
||||
mc68000_.set_is_peripheral_address(word_address >= 0x700000);
|
||||
|
||||
// All code below deals only with reads and writes — cycles in which a
|
||||
// data select is active. So quit now if this is not the active part of
|
||||
// a read or write.
|
||||
if(!cycle.data_select_active()) return delay;
|
||||
|
||||
// Check whether this access maps into the IO area; if so then
|
||||
// apply more complicated decoding logic.
|
||||
if(word_address >= 0x400000) {
|
||||
const int register_address = word_address >> 8;
|
||||
|
||||
switch(word_address & 0x78f000) {
|
||||
case 0x70f000:
|
||||
// VIA accesses are via address 0xefe1fe + register*512,
|
||||
// which at word precision is 0x77f0ff + register*256.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = via_.get_register(register_address);
|
||||
} else {
|
||||
via_.set_register(register_address, cycle.value->halves.low);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x68f000:
|
||||
// The IWM; this is a purely polled device, so can be run on demand.
|
||||
iwm_.flush();
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = iwm_.iwm.read(register_address);
|
||||
} else {
|
||||
iwm_.iwm.write(register_address, cycle.value->halves.low);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x780000:
|
||||
// Phase read.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = phase_ & 7;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x480000: case 0x48f000:
|
||||
case 0x580000: case 0x58f000:
|
||||
// Any word access here adjusts phase.
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
++phase_;
|
||||
} else {
|
||||
if(word_address < 0x500000) {
|
||||
// A0 = 1 => reset; A0 = 0 => read.
|
||||
if(*cycle.address & 1) {
|
||||
scc_.reset();
|
||||
} else {
|
||||
const auto read = scc_.read(int(word_address));
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = read;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(*cycle.address & 1) {
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
scc_.write(int(word_address), 0xff);
|
||||
} else {
|
||||
scc_.write(int(word_address), cycle.value->halves.low);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
LOG("Unrecognised read " << PADHEX(6) << (*cycle.address & 0xffffff));
|
||||
cycle.value->halves.low = 0x00;
|
||||
} else {
|
||||
LOG("Unrecognised write %06x" << PADHEX(6) << (*cycle.address & 0xffffff));
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
|
||||
|
||||
return delay;
|
||||
}
|
||||
|
||||
// Having reached here, this is a RAM or ROM access.
|
||||
|
||||
// When ROM overlay is enabled, the ROM begins at both $000000 and $400000,
|
||||
// and RAM is available at $600000.
|
||||
//
|
||||
// Otherwise RAM is mapped at $000000 and ROM from $400000.
|
||||
uint16_t *memory_base;
|
||||
if(
|
||||
(!ROM_is_overlay_ && word_address < 0x200000) ||
|
||||
(ROM_is_overlay_ && word_address >= 0x300000)
|
||||
) {
|
||||
memory_base = ram_;
|
||||
word_address &= ram_mask_;
|
||||
|
||||
// This is coupled with the Macintosh implementation of video; the magic
|
||||
// constant should probably be factored into the Video class.
|
||||
// It embodies knowledge of the fact that video (and audio) will always
|
||||
// be fetched from the final $d900 bytes (i.e. $6c80 words) of memory.
|
||||
// (And that ram_mask_ = ram size - 1).
|
||||
// if(word_address > ram_mask_ - 0x6c80)
|
||||
update_video();
|
||||
} else {
|
||||
memory_base = rom_;
|
||||
word_address &= rom_mask_;
|
||||
|
||||
// Writes to ROM have no effect, and it doesn't mirror above 0x60000.
|
||||
if(!(cycle.operation & Microcycle::Read)) return delay;
|
||||
if(word_address >= 0x300000) {
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
cycle.value->full = 0xffff;
|
||||
} else {
|
||||
cycle.value->halves.low = 0xff;
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
|
||||
switch(cycle.operation & (Microcycle::SelectWord | Microcycle::SelectByte | Microcycle::Read | Microcycle::InterruptAcknowledge)) {
|
||||
default:
|
||||
break;
|
||||
|
||||
// Catches the deliberation set of operation to 0 above.
|
||||
case 0: break;
|
||||
|
||||
case Microcycle::InterruptAcknowledge | Microcycle::SelectByte:
|
||||
// The Macintosh uses autovectored interrupts.
|
||||
mc68000_.set_is_peripheral_address(true);
|
||||
break;
|
||||
|
||||
case Microcycle::SelectWord | Microcycle::Read:
|
||||
cycle.value->full = memory_base[word_address];
|
||||
break;
|
||||
case Microcycle::SelectByte | Microcycle::Read:
|
||||
cycle.value->halves.low = uint8_t(memory_base[word_address] >> cycle.byte_shift());
|
||||
break;
|
||||
case Microcycle::SelectWord:
|
||||
memory_base[word_address] = cycle.value->full;
|
||||
break;
|
||||
case Microcycle::SelectByte:
|
||||
memory_base[word_address] = uint16_t(
|
||||
(cycle.value->halves.low << cycle.byte_shift()) |
|
||||
(memory_base[word_address] & cycle.untouched_byte_mask())
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
Normal memory map:
|
||||
|
||||
000000: RAM
|
||||
400000: ROM
|
||||
9FFFF8+: SCC read operations
|
||||
BFFFF8+: SCC write operations
|
||||
DFE1FF+: IWM
|
||||
EFE1FE+: VIA
|
||||
*/
|
||||
|
||||
return delay;
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// Flush the video before the audio queue; in a Mac the
|
||||
// video is responsible for providing part of the
|
||||
// audio signal, so the two aren't as distinct as in
|
||||
// most machines.
|
||||
update_video();
|
||||
|
||||
// As above: flush audio after video.
|
||||
via_.flush();
|
||||
audio_.queue.perform();
|
||||
|
||||
// Experimental?
|
||||
iwm_.flush();
|
||||
}
|
||||
|
||||
void set_rom_is_overlay(bool rom_is_overlay) {
|
||||
ROM_is_overlay_ = rom_is_overlay;
|
||||
}
|
||||
|
||||
bool video_is_outputting() {
|
||||
return video_.is_outputting(time_since_video_update_);
|
||||
}
|
||||
|
||||
void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) {
|
||||
update_video();
|
||||
video_.set_use_alternate_buffers(use_alternate_screen_buffer, use_alternate_audio_buffer);
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
if(media.disks.empty())
|
||||
return false;
|
||||
|
||||
// TODO: shouldn't allow disks to be replaced like this, as the Mac
|
||||
// uses software eject. Will need to expand messaging ability of
|
||||
// insert_media.
|
||||
if(drives_[0].has_disk())
|
||||
drives_[1].set_disk(media.disks[0]);
|
||||
else
|
||||
drives_[0].set_disk(media.disks[0]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: Keyboard input.
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool is_pressed) override {
|
||||
keyboard_.enqueue_key_state(key, is_pressed);
|
||||
}
|
||||
|
||||
// TODO: clear all keys.
|
||||
|
||||
// MARK: Interrupt updates.
|
||||
|
||||
void did_change_interrupt_status(Zilog::SCC::z8530 *sender, bool new_status) override {
|
||||
update_interrupt_input();
|
||||
}
|
||||
|
||||
void update_interrupt_input() {
|
||||
// Update interrupt input.
|
||||
// TODO: does this really cascade like this?
|
||||
if(scc_.get_interrupt_line()) {
|
||||
mc68000_.set_interrupt_level(2);
|
||||
} else if(via_.get_interrupt_line()) {
|
||||
mc68000_.set_interrupt_level(1);
|
||||
} else {
|
||||
mc68000_.set_interrupt_level(0);
|
||||
// Update the SCSI if currently active.
|
||||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||||
if(scsi_bus_is_clocked_) scsi_bus_.run_for(duration);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void update_video() {
|
||||
forceinline void update_video() {
|
||||
video_.run_for(time_since_video_update_.flush<HalfCycles>());
|
||||
time_until_video_event_ = video_.get_next_sequence_point();
|
||||
}
|
||||
|
||||
Inputs::Mouse &get_mouse() override {
|
||||
Inputs::Mouse &get_mouse() final {
|
||||
return mouse_;
|
||||
}
|
||||
|
||||
struct IWM {
|
||||
IWM(int clock_rate) : iwm(clock_rate) {}
|
||||
|
||||
HalfCycles time_since_update;
|
||||
Apple::IWM iwm;
|
||||
|
||||
void flush() {
|
||||
iwm.run_for(time_since_update.flush<Cycles>());
|
||||
}
|
||||
};
|
||||
using IWMActor = JustInTimeActor<IWM, 1, 1, HalfCycles, Cycles>;
|
||||
|
||||
class VIAPortHandler: public MOS::MOS6522::PortHandler {
|
||||
public:
|
||||
VIAPortHandler(ConcreteMachine &machine, RealTimeClock &clock, Keyboard &keyboard, Video &video, DeferredAudio &audio, IWM &iwm, Inputs::QuadratureMouse &mouse) :
|
||||
machine_(machine), clock_(clock), keyboard_(keyboard), video_(video), audio_(audio), iwm_(iwm), mouse_(mouse) {}
|
||||
VIAPortHandler(ConcreteMachine &machine, RealTimeClock &clock, Keyboard &keyboard, DeferredAudio &audio, IWMActor &iwm, Inputs::QuadratureMouse &mouse) :
|
||||
machine_(machine), clock_(clock), keyboard_(keyboard), audio_(audio), iwm_(iwm), mouse_(mouse) {}
|
||||
|
||||
using Port = MOS::MOS6522::Port;
|
||||
using Line = MOS::MOS6522::Line;
|
||||
@@ -505,8 +687,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
b3: 0 = use alternate sound buffer, 1 = use ordinary sound buffer
|
||||
b2–b0: audio output volume
|
||||
*/
|
||||
iwm_.flush();
|
||||
iwm_.iwm.set_select(!!(value & 0x20));
|
||||
iwm_->set_select(!!(value & 0x20));
|
||||
|
||||
machine_.set_use_alternate_buffers(!(value & 0x40), !(value&0x08));
|
||||
machine_.set_rom_is_overlay(!!(value & 0x10));
|
||||
@@ -574,7 +755,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
void run_for(HalfCycles duration) {
|
||||
// The 6522 enjoys a divide-by-ten, so multiply back up here to make the
|
||||
// divided-by-two clock the audio works on.
|
||||
audio_.time_since_update += HalfCycles(duration.as_int() * 5);
|
||||
audio_.time_since_update += HalfCycles(duration.as_integral() * 5);
|
||||
}
|
||||
|
||||
void flush() {
|
||||
@@ -589,16 +770,15 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
ConcreteMachine &machine_;
|
||||
RealTimeClock &clock_;
|
||||
Keyboard &keyboard_;
|
||||
Video &video_;
|
||||
DeferredAudio &audio_;
|
||||
IWM &iwm_;
|
||||
IWMActor &iwm_;
|
||||
Inputs::QuadratureMouse &mouse_;
|
||||
};
|
||||
|
||||
CPU::MC68000::Processor<ConcreteMachine, true> mc68000_;
|
||||
|
||||
DriveSpeedAccumulator drive_speed_accumulator_;
|
||||
IWM iwm_;
|
||||
IWMActor iwm_;
|
||||
|
||||
DeferredAudio audio_;
|
||||
Video video_;
|
||||
@@ -610,6 +790,10 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
VIAPortHandler via_port_handler_;
|
||||
|
||||
Zilog::SCC::z8530 scc_;
|
||||
SCSI::Bus scsi_bus_;
|
||||
NCR::NCR5380::NCR5380 scsi_;
|
||||
SCSI::Target::Target<SCSI::DirectAccessDevice> hard_drive_;
|
||||
bool scsi_bus_is_clocked_ = false;
|
||||
|
||||
HalfCycles via_clock_;
|
||||
HalfCycles real_time_clock_;
|
||||
@@ -620,16 +804,61 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
bool ROM_is_overlay_ = true;
|
||||
int phase_ = 1;
|
||||
int ram_subcycle_ = 0;
|
||||
|
||||
DoubleDensityDrive drives_[2];
|
||||
Inputs::QuadratureMouse mouse_;
|
||||
|
||||
Apple::Macintosh::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
enum class BusDevice {
|
||||
RAM, ROM, VIA, IWM, SCCWrite, SCCReadResetPhase, SCSI, PhaseRead, Unassigned
|
||||
};
|
||||
|
||||
/// Divides the 24-bit address space up into $20000 (i.e. 128kb) segments, recording
|
||||
/// which device is current mapped in each area. Keeping it in a table is a bit faster
|
||||
/// than the multi-level address inspection that is otherwise required, as well as
|
||||
/// simplifying slightly the handling of different models.
|
||||
///
|
||||
/// So: index with the top 7 bits of the 24-bit address.
|
||||
BusDevice memory_map_[128];
|
||||
|
||||
void setup_memory_map() {
|
||||
// Apply the power-up memory map, i.e. assume that ROM_is_overlay_ = true;
|
||||
// start by calling into set_rom_is_overlay to seed everything up to $800000.
|
||||
set_rom_is_overlay(true);
|
||||
|
||||
populate_memory_map(0x800000, [] (std::function<void(int target, BusDevice device)> map_to) {
|
||||
map_to(0x900000, BusDevice::Unassigned);
|
||||
map_to(0xa00000, BusDevice::SCCReadResetPhase);
|
||||
map_to(0xb00000, BusDevice::Unassigned);
|
||||
map_to(0xc00000, BusDevice::SCCWrite);
|
||||
map_to(0xd00000, BusDevice::Unassigned);
|
||||
map_to(0xe00000, BusDevice::IWM);
|
||||
map_to(0xe80000, BusDevice::Unassigned);
|
||||
map_to(0xf00000, BusDevice::VIA);
|
||||
map_to(0xf80000, BusDevice::PhaseRead);
|
||||
map_to(0x1000000, BusDevice::Unassigned);
|
||||
});
|
||||
}
|
||||
|
||||
void populate_memory_map(int start_address, std::function<void(std::function<void(int, BusDevice)>)> populator) {
|
||||
// Define semantics for below; map_to will write from the current cursor position
|
||||
// to the supplied 24-bit address, setting a particular mapped device.
|
||||
int segment = start_address >> 17;
|
||||
auto map_to = [&segment, this](int address, BusDevice device) {
|
||||
for(; segment < address >> 17; ++segment) {
|
||||
this->memory_map_[segment] = device;
|
||||
}
|
||||
};
|
||||
|
||||
populator(map_to);
|
||||
}
|
||||
|
||||
uint32_t ram_mask_ = 0;
|
||||
uint32_t rom_mask_ = 0;
|
||||
uint16_t rom_[64*1024];
|
||||
uint16_t ram_[256*1024];
|
||||
uint16_t rom_[64*1024]; // i.e. up to 128kb in size.
|
||||
std::vector<uint16_t> ram_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -9,12 +9,15 @@
|
||||
#ifndef Macintosh_hpp
|
||||
#define Macintosh_hpp
|
||||
|
||||
#include "../../../Configurable/Configurable.hpp"
|
||||
#include "../../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options();
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
|
@@ -25,8 +25,15 @@ class RealTimeClock {
|
||||
public:
|
||||
RealTimeClock() {
|
||||
// TODO: this should persist, if possible, rather than
|
||||
// being randomly initialised.
|
||||
Memory::Fuzz(data_, sizeof(data_));
|
||||
// being default initialised.
|
||||
const uint8_t default_data[] = {
|
||||
0xa8, 0x00, 0x00, 0x00,
|
||||
0xcc, 0x0a, 0xcc, 0x0a,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x02, 0x63, 0x00,
|
||||
0x03, 0x88, 0x00, 0x4c
|
||||
};
|
||||
memcpy(data_, default_data, sizeof(data_));
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@@ -12,16 +12,6 @@
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
namespace {
|
||||
|
||||
const HalfCycles line_length(704);
|
||||
const int number_of_lines = 370;
|
||||
const HalfCycles frame_length(line_length * HalfCycles(number_of_lines));
|
||||
const int sync_start = 36;
|
||||
const int sync_end = 38;
|
||||
|
||||
}
|
||||
|
||||
// Re: CRT timings, see the Apple Guide to the Macintosh Hardware Family,
|
||||
// bottom of page 400:
|
||||
//
|
||||
@@ -33,11 +23,10 @@ const int sync_end = 38;
|
||||
// "The visible portion of a full-screen display consists of 342 horizontal scan lines...
|
||||
// During the vertical blanking interval, the turned-off beam ... traces out an additional 28 scan lines,"
|
||||
//
|
||||
Video::Video(uint16_t *ram, DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) :
|
||||
Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) :
|
||||
audio_(audio),
|
||||
drive_speed_accumulator_(drive_speed_accumulator),
|
||||
crt_(704, 1, 370, Outputs::Display::ColourSpace::YIQ, 1, 1, 6, false, Outputs::Display::InputDataType::Luminance1),
|
||||
ram_(ram) {
|
||||
crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) {
|
||||
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
|
||||
@@ -48,6 +37,10 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 2.0f;
|
||||
}
|
||||
|
||||
void Video::run_for(HalfCycles duration) {
|
||||
// Determine the current video and audio bases. These values don't appear to be latched, they apply immediately.
|
||||
const size_t video_base = (use_alternate_screen_buffer_ ? (0xffff2700 >> 1) : (0xffffa700 >> 1)) & ram_mask_;
|
||||
@@ -58,7 +51,7 @@ void Video::run_for(HalfCycles duration) {
|
||||
// the number of fetches.
|
||||
while(duration > HalfCycles(0)) {
|
||||
const auto pixel_start = frame_position_ % line_length;
|
||||
const int line = (frame_position_ / line_length).as_int();
|
||||
const int line = int((frame_position_ / line_length).as_integral());
|
||||
|
||||
const auto cycles_left_in_line = std::min(line_length - pixel_start, duration);
|
||||
|
||||
@@ -73,8 +66,8 @@ void Video::run_for(HalfCycles duration) {
|
||||
//
|
||||
// Then 12 lines of border, 3 of sync, 11 more of border.
|
||||
|
||||
const int first_word = pixel_start.as_int() >> 4;
|
||||
const int final_word = (pixel_start + cycles_left_in_line).as_int() >> 4;
|
||||
const int first_word = int(pixel_start.as_integral()) >> 4;
|
||||
const int final_word = int((pixel_start + cycles_left_in_line).as_integral()) >> 4;
|
||||
|
||||
if(first_word != final_word) {
|
||||
if(line < 342) {
|
||||
@@ -164,12 +157,12 @@ void Video::run_for(HalfCycles duration) {
|
||||
}
|
||||
|
||||
bool Video::vsync() {
|
||||
const int line = (frame_position_ / line_length).as_int();
|
||||
const auto line = (frame_position_ / line_length).as_integral();
|
||||
return line >= 353 && line < 356;
|
||||
}
|
||||
|
||||
HalfCycles Video::get_next_sequence_point() {
|
||||
const int line = (frame_position_ / line_length).as_int();
|
||||
const auto line = (frame_position_ / line_length).as_integral();
|
||||
if(line >= 353 && line < 356) {
|
||||
// Currently in vsync, so get time until start of line 357,
|
||||
// when vsync will end.
|
||||
@@ -184,18 +177,12 @@ HalfCycles Video::get_next_sequence_point() {
|
||||
}
|
||||
}
|
||||
|
||||
bool Video::is_outputting(HalfCycles offset) {
|
||||
const auto offset_position = frame_position_ + offset % frame_length;
|
||||
const int column = (offset_position % line_length).as_int() >> 4;
|
||||
const int line = (offset_position / line_length).as_int();
|
||||
return line < 342 && column < 32;
|
||||
}
|
||||
|
||||
void Video::set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) {
|
||||
use_alternate_screen_buffer_ = use_alternate_screen_buffer;
|
||||
use_alternate_audio_buffer_ = use_alternate_audio_buffer;
|
||||
}
|
||||
|
||||
void Video::set_ram_mask(uint32_t mask) {
|
||||
void Video::set_ram(uint16_t *ram, uint32_t mask) {
|
||||
ram_ = ram;
|
||||
ram_mask_ = mask;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user