mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
1800 Commits
2018-08-05
...
2020-01-15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bb294a023 | ||
|
|
1972ca00a4 | ||
|
|
6a185a574a | ||
|
|
c606931c93 | ||
|
|
93cecf0882 | ||
|
|
aac3d27c10 | ||
|
|
99122efbbc | ||
|
|
30e856b9e4 | ||
|
|
91fae86e73 | ||
|
|
f5c194386c | ||
|
|
98f7662185 | ||
|
|
62c3720c97 | ||
|
|
6b08239199 | ||
|
|
f258fc2971 | ||
|
|
6b84ae3095 | ||
|
|
5dd8c677f1 | ||
|
|
1cbcd5355f | ||
|
|
9799250f2c | ||
|
|
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 | ||
|
|
0c8e313fd5 | ||
|
|
f64ec11668 | ||
|
|
9bbccd89d3 | ||
|
|
1ae3799aee | ||
|
|
260843e5b1 | ||
|
|
e3f22e5787 | ||
|
|
2aa308efdd | ||
|
|
74c18d7861 | ||
|
|
c41cccd9a6 | ||
|
|
bba34b28b8 | ||
|
|
d8a41575c8 | ||
|
|
0521de668a | ||
|
|
12441bddab | ||
|
|
bc25c52683 | ||
|
|
eb3fb70ea1 | ||
|
|
2f90ed1f32 | ||
|
|
f3dd4b028d | ||
|
|
7dcad516bd | ||
|
|
9859f99513 | ||
|
|
51b7f2777d | ||
|
|
2f2478d2d3 | ||
|
|
a43ada82b2 | ||
|
|
5149f290d0 | ||
|
|
0dc6f08deb | ||
|
|
b1f04ed96d | ||
|
|
cd49b3c89b | ||
|
|
f894d43111 | ||
|
|
4033c0c754 | ||
|
|
786b26d23e | ||
|
|
d08d8ed22c | ||
|
|
b7b62aa3f6 | ||
|
|
39d7e3c62c | ||
|
|
81b57ecf7c | ||
|
|
572e8b52e1 | ||
|
|
9b634472c6 | ||
|
|
d8bc20b1ab | ||
|
|
d2bfd59953 | ||
|
|
3d20ae47ea | ||
|
|
85cf8d89bc | ||
|
|
50e954223a | ||
|
|
109d5d16bd | ||
|
|
1672dc5946 | ||
|
|
5769944918 | ||
|
|
9ef1211d53 | ||
|
|
f2ae04597f | ||
|
|
1327de1c82 | ||
|
|
827c4e172a | ||
|
|
c300bd17fa | ||
|
|
0187fd8eae | ||
|
|
0469f0240b | ||
|
|
4aca6c5ef8 | ||
|
|
d69aee4972 | ||
|
|
3da47318b1 | ||
|
|
ef036df2bc | ||
|
|
579f68cf11 | ||
|
|
90f6ca4635 | ||
|
|
374cac0107 | ||
|
|
4d361b1952 | ||
|
|
fcee7779b0 | ||
|
|
b4191b6225 | ||
|
|
dbee37ab34 | ||
|
|
a3ad0ab09b | ||
|
|
ed0c4c117b | ||
|
|
2432151bf8 | ||
|
|
2129bfc570 | ||
|
|
8de6cd3f44 | ||
|
|
9b9831f28b | ||
|
|
8a2cac0d0c | ||
|
|
e17b105574 | ||
|
|
67c5f6b7cb | ||
|
|
d452d070a1 | ||
|
|
a846c3245d | ||
|
|
4ffa3c1b49 | ||
|
|
b2a6682798 | ||
|
|
f3aac603f8 | ||
|
|
712cb473f7 | ||
|
|
3c68a5ca65 | ||
|
|
20670bab2f | ||
|
|
86d709ae01 | ||
|
|
0aba95cc9d | ||
|
|
de3c8373fd | ||
|
|
75ecd4e72d | ||
|
|
56555a4d99 | ||
|
|
cfad20bb33 | ||
|
|
fa226bb1b9 | ||
|
|
77333ff9f7 | ||
|
|
b9a34bee51 | ||
|
|
22ee51c12c | ||
|
|
ee8d853fcb | ||
|
|
19198ea665 | ||
|
|
bcbda4d855 | ||
|
|
79a624e696 | ||
|
|
c123ca1054 | ||
|
|
9f0f35033d | ||
|
|
3633285aaa | ||
|
|
cb16790330 | ||
|
|
67055d8b56 | ||
|
|
ca37fd8f4c | ||
|
|
46b98dab70 | ||
|
|
0568996264 | ||
|
|
7baad61746 | ||
|
|
1d1e0d74f8 | ||
|
|
d53d1c616f | ||
|
|
5b05a9bc61 | ||
|
|
2c39229b13 | ||
|
|
59b5dfddec | ||
|
|
b730ac5d5a | ||
|
|
4860d8a7df | ||
|
|
9f0cde3d69 | ||
|
|
c8917e677b | ||
|
|
6c2cc206a6 | ||
|
|
5a9f3cfc1e | ||
|
|
8f28b33342 | ||
|
|
cac97a9663 | ||
|
|
2ccb564a7b | ||
|
|
d1d0430fce | ||
|
|
be251d6b03 | ||
|
|
6cfaf920ee | ||
|
|
1657f8768c | ||
|
|
c4ab0bb867 | ||
|
|
886946cc8c | ||
|
|
ed4ddcfda8 | ||
|
|
7886cd63bd | ||
|
|
69b94719a1 | ||
|
|
b4a3f66773 | ||
|
|
ab14433151 | ||
|
|
5078f6fb5c | ||
|
|
fc6d62aefb | ||
|
|
f73bccfec8 | ||
|
|
96be1a3f62 | ||
|
|
52e96e3d2a | ||
|
|
33e2721eb2 | ||
|
|
4bc44666e5 | ||
|
|
3d8e4f96c8 | ||
|
|
94457d81b6 | ||
|
|
c212bf27db | ||
|
|
59b5ee65d4 | ||
|
|
60cedca97b | ||
|
|
1a9aa60bf7 | ||
|
|
6438a5ca1f | ||
|
|
3f303511bd | ||
|
|
fb352a8d40 | ||
|
|
ea7899f47d | ||
|
|
fb6da1de4a | ||
|
|
2651b15db1 | ||
|
|
6e7a733c3c | ||
|
|
245e27c893 | ||
|
|
793c2df7ee | ||
|
|
28de629c08 | ||
|
|
210bcaa56d | ||
|
|
d7329c1bdd | ||
|
|
a5f0761a43 | ||
|
|
dd963d6161 | ||
|
|
96c0253ee2 | ||
|
|
191a7a9386 | ||
|
|
387be4a0a6 | ||
|
|
b9c2c42bc0 | ||
|
|
fffe6ed2df | ||
|
|
c4cbe9476c | ||
|
|
0a67cc3dab | ||
|
|
726e07ed5b | ||
|
|
ebb6313eef | ||
|
|
11d8f765b2 | ||
|
|
514e57b3e9 | ||
|
|
d8fb6fb951 | ||
|
|
255f0d4b2a | ||
|
|
d30e7504c2 | ||
|
|
8d0cd356fd | ||
|
|
aff40bf00a | ||
|
|
eedf7358b4 | ||
|
|
26aebcc167 | ||
|
|
9d420c727e | ||
|
|
60fe84ad16 | ||
|
|
6a44c682ad | ||
|
|
60df44f0ca | ||
|
|
ac926f5070 | ||
|
|
6e9a4a48f7 | ||
|
|
a8894b308a | ||
|
|
7cc91e1bc5 | ||
|
|
9eb51f164c | ||
|
|
a1c00e9318 | ||
|
|
17666bc059 | ||
|
|
241d29ff7c | ||
|
|
c5039a4719 | ||
|
|
fd604048db | ||
|
|
6a77ed1e07 | ||
|
|
9e38815ec4 | ||
|
|
86c325c4ec | ||
|
|
bfcc6cf12c | ||
|
|
8ba8cf7c23 | ||
|
|
6c588a1510 | ||
|
|
651fd9c4a5 | ||
|
|
5d0db2198c | ||
|
|
d81053ea38 | ||
|
|
8d39c3bc98 | ||
|
|
da351a3e32 | ||
|
|
c0591090f5 | ||
|
|
538aecb46e | ||
|
|
dbdbea85c2 | ||
|
|
ba2224dd06 | ||
|
|
44e2aa9183 | ||
|
|
202bff70fe | ||
|
|
26c0cd7f7c | ||
|
|
cb76301fbe | ||
|
|
8bfa12edf1 | ||
|
|
7daa969a5a | ||
|
|
4aeb60100d | ||
|
|
e2c7aaac5a | ||
|
|
6ff661c30d | ||
|
|
79066f8628 | ||
|
|
2c813a2692 | ||
|
|
d2cb595b83 | ||
|
|
cc4abcb00a | ||
|
|
c1ca85987f | ||
|
|
ecb5a0b8cc | ||
|
|
e12e8fc616 | ||
|
|
1fbbf32cd2 | ||
|
|
31edb15369 | ||
|
|
d7883d18d4 | ||
|
|
40100773d3 | ||
|
|
4048ed3a33 | ||
|
|
11f2d3cea7 | ||
|
|
aa656a39b8 | ||
|
|
e830d23533 | ||
|
|
9a666fb8cc | ||
|
|
0e208ed432 | ||
|
|
c8b769de8a | ||
|
|
c447655047 | ||
|
|
3ec9a1d869 | ||
|
|
d326886852 | ||
|
|
faef917cbd | ||
|
|
d27ba90c07 | ||
|
|
db4ca746e3 | ||
|
|
d50fbfb506 | ||
|
|
5d283a9f1f | ||
|
|
86fdc75feb | ||
|
|
b63231523a | ||
|
|
70e296674d | ||
|
|
5089fcd2f6 | ||
|
|
df2ce8ca6f | ||
|
|
7e209353bb | ||
|
|
c2806a94e2 | ||
|
|
d428120776 | ||
|
|
8c8493bc9d | ||
|
|
6b996ae57d | ||
|
|
ccfe1b13cb | ||
|
|
0c1c10bc66 | ||
|
|
fafd1801fe | ||
|
|
bcf6f665b8 | ||
|
|
bd069490b5 | ||
|
|
79d8d27b4c | ||
|
|
624b0b6372 | ||
|
|
7976cf5b3c | ||
|
|
440f52c943 | ||
|
|
47b1218a68 | ||
|
|
91ced056d2 | ||
|
|
8dace34e63 | ||
|
|
8182b0363f | ||
|
|
c5b036fedf | ||
|
|
e26ddd0ed5 | ||
|
|
ca83431e54 | ||
|
|
68a3e5a739 | ||
|
|
b98f10cb45 | ||
|
|
9730800b6a | ||
|
|
506276a2bd | ||
|
|
00c32e4b59 | ||
|
|
df56e6fe53 | ||
|
|
756641e837 | ||
|
|
05c2854dbc | ||
|
|
5c8aacdc17 | ||
|
|
745a5ab749 | ||
|
|
fe0dc4df88 | ||
|
|
33f2664fe9 | ||
|
|
a17e47fa43 | ||
|
|
877b46d2c1 | ||
|
|
cc7226ae9f | ||
|
|
bde975a3b9 | ||
|
|
f6f9024631 | ||
|
|
39aae34323 | ||
|
|
5630141ad7 | ||
|
|
535747e3f2 | ||
|
|
59a94943aa | ||
|
|
bf4889f238 | ||
|
|
7cc5afd798 | ||
|
|
11ab021672 | ||
|
|
feafd4bdae | ||
|
|
d6150645c0 | ||
|
|
ccd2cb44a2 | ||
|
|
ec5701459c | ||
|
|
ad8b68c998 | ||
|
|
c8066b01b6 | ||
|
|
ebd59f4dd3 | ||
|
|
109953ef49 | ||
|
|
124c7bcbb0 | ||
|
|
a0321aa6ff | ||
|
|
567feaac10 | ||
|
|
15c38e2f15 | ||
|
|
3c075e9542 | ||
|
|
9230969f43 | ||
|
|
0e16c67805 | ||
|
|
697e094a4e | ||
|
|
50d37798a2 | ||
|
|
e9d0676e75 | ||
|
|
7591906777 | ||
|
|
08671ed69c | ||
|
|
511d292e73 | ||
|
|
a413ae11cb | ||
|
|
833258f3d7 | ||
|
|
b8a1553368 | ||
|
|
058fe3e986 | ||
|
|
51ee83a427 | ||
|
|
5b21da7874 | ||
|
|
bd7f00bd9c | ||
|
|
517cca251f | ||
|
|
1033abd9fe | ||
|
|
113d022741 | ||
|
|
299a7b99ae | ||
|
|
66540ff86f | ||
|
|
8557558bd8 | ||
|
|
376cf08c71 | ||
|
|
83e5e650d2 | ||
|
|
b860ba2ee3 | ||
|
|
661fe1e649 | ||
|
|
5b8375f0a0 | ||
|
|
abe55fe950 | ||
|
|
4d4ddded6d | ||
|
|
1328708a70 | ||
|
|
85298319fa | ||
|
|
881feb1bd3 | ||
|
|
3e9fa63799 | ||
|
|
da2b190288 | ||
|
|
48d837c636 | ||
|
|
983407896c | ||
|
|
5c08bb810e | ||
|
|
17635da812 | ||
|
|
6d985866ee | ||
|
|
723137c0d4 | ||
|
|
938928865d | ||
|
|
d80b0cbf90 | ||
|
|
e88ef30ce6 | ||
|
|
4197c6f149 | ||
|
|
035f07877c | ||
|
|
4632be4fe5 | ||
|
|
b3d2b4cd37 | ||
|
|
c86fe9ada9 | ||
|
|
ecf93b7822 | ||
|
|
541b75ee6e | ||
|
|
77b08febdb | ||
|
|
fcda376f33 | ||
|
|
0848fc7e03 | ||
|
|
3bb8d6717f | ||
|
|
5e2496d59c | ||
|
|
c52da9d802 | ||
|
|
1d3dde32f2 | ||
|
|
0b999ce0e4 | ||
|
|
b04bd7069d | ||
|
|
249b0fbb32 | ||
|
|
41740fb45e | ||
|
|
0ad88508f7 | ||
|
|
8293b18278 | ||
|
|
2ba0364850 | ||
|
|
8b72043f33 | ||
|
|
2e7bc0b98a | ||
|
|
f0f9722ca6 | ||
|
|
b5ef88902b | ||
|
|
8278809383 | ||
|
|
4367459cf2 | ||
|
|
254132b83d | ||
|
|
7b466e6d0a | ||
|
|
7e6d4f5a3e | ||
|
|
ce099a297a | ||
|
|
949c848815 | ||
|
|
9bf9b9ea8c | ||
|
|
d8ed8b66f3 | ||
|
|
a131d39451 | ||
|
|
b540f58457 | ||
|
|
4f5a38b5c5 | ||
|
|
cefc3af08b | ||
|
|
e6ed50383c | ||
|
|
96facc103a | ||
|
|
407bbfb379 | ||
|
|
a99ebda513 | ||
|
|
537b604fc9 | ||
|
|
98bc570bf7 | ||
|
|
181b77c490 | ||
|
|
bc9eb82e6f | ||
|
|
29fc024ecd | ||
|
|
c1695d0910 | ||
|
|
6d6a4e79c9 | ||
|
|
417a3e1540 | ||
|
|
fa8c804d47 | ||
|
|
68392ce6f5 | ||
|
|
6873f62ad8 | ||
|
|
5f385e15f6 | ||
|
|
8c5d37b6ee | ||
|
|
9c3c2192dd | ||
|
|
4f9f73ca81 | ||
|
|
2c9a1f7b16 | ||
|
|
0ea4c1ac80 | ||
|
|
a873ec97eb | ||
|
|
cc8a65780e | ||
|
|
c117deb43b | ||
|
|
ae31d45c88 | ||
|
|
a0eb20ff1f | ||
|
|
34fe9981e4 | ||
|
|
291e91375f | ||
|
|
857f74b320 | ||
|
|
1d9608efc7 | ||
|
|
93616a4903 | ||
|
|
bb07206c55 | ||
|
|
2e5c0811e7 | ||
|
|
f6ac407e4d | ||
|
|
078c3135df | ||
|
|
92568c90c8 | ||
|
|
f1879c5fbc | ||
|
|
31bb770fdd | ||
|
|
e430f2658f | ||
|
|
3060175ff5 | ||
|
|
eb4233e2fd | ||
|
|
6b4c656849 | ||
|
|
1b8fada6aa | ||
|
|
7332c64964 | ||
|
|
977f9ee831 | ||
|
|
16fb3b49a5 | ||
|
|
3da1b3bf9b | ||
|
|
bc00856c05 | ||
|
|
52e3dece81 | ||
|
|
2c1d8fa18a | ||
|
|
3e34ae67f6 | ||
|
|
d6e16d0042 | ||
|
|
8e02d29ae6 | ||
|
|
ceebecec8d | ||
|
|
05d1eda422 | ||
|
|
31f318ad43 | ||
|
|
270f46e147 | ||
|
|
c0e9c37cc7 | ||
|
|
8564945713 | ||
|
|
7bd7f3fb73 | ||
|
|
5b5bfc8445 | ||
|
|
c466b6f9e7 | ||
|
|
407643c575 | ||
|
|
d9071ee9f1 | ||
|
|
97e118abfa | ||
|
|
412f091d76 | ||
|
|
d9278e9827 | ||
|
|
ca1f669e64 | ||
|
|
0298b1b3b7 | ||
|
|
4b1324de77 | ||
|
|
8e8dce9bec | ||
|
|
f4350522bf | ||
|
|
e2abb66a11 | ||
|
|
ab5fcab9bf | ||
|
|
cf547ef569 | ||
|
|
e75b386f7d | ||
|
|
796203859f | ||
|
|
40f68b70c1 | ||
|
|
40b2fe7339 | ||
|
|
a3b6d2d16e | ||
|
|
3983f8303f | ||
|
|
7cbd5e0ef6 | ||
|
|
dab9bb6575 | ||
|
|
7df85ea695 | ||
|
|
c132bda01c | ||
|
|
4e25bcfcdc | ||
|
|
ea463549c7 | ||
|
|
723acb31b3 | ||
|
|
5725db9234 | ||
|
|
8557e563bc | ||
|
|
d2491633ce | ||
|
|
002796e5f5 | ||
|
|
fa0accf251 | ||
|
|
dcb8176d90 | ||
|
|
be32b1a198 | ||
|
|
582e4acc11 | ||
|
|
10f75acf71 | ||
|
|
b9933f512f | ||
|
|
75a7f7ab22 | ||
|
|
757be2906e | ||
|
|
e214584c76 | ||
|
|
0bb6b498ce | ||
|
|
958d44a20d | ||
|
|
bb9424d944 | ||
|
|
11bf706aa2 | ||
|
|
033b8e6b36 | ||
|
|
7c3ea7b2ea | ||
|
|
a08043ae88 | ||
|
|
7c132a3ed5 | ||
|
|
20e774be1e | ||
|
|
6d6046757d | ||
|
|
55073b0a52 | ||
|
|
44eb4e51ed | ||
|
|
3cb042a49d | ||
|
|
b78ea7d24c | ||
|
|
c66728dce2 | ||
|
|
0be9a0cb88 | ||
|
|
a90f12dab7 | ||
|
|
ef33b004f9 | ||
|
|
2cac4b0d74 | ||
|
|
a49f516265 | ||
|
|
71ac26944d | ||
|
|
2d97fc1f59 | ||
|
|
9ef7743205 | ||
|
|
ee7ae11e90 | ||
|
|
f67d7f1db5 | ||
|
|
99981751a2 | ||
|
|
ffdf02c5df | ||
|
|
27c7d00a05 | ||
|
|
64c4137e5b | ||
|
|
8c26d0c6e6 | ||
|
|
81dcfd9f85 | ||
|
|
9334557fbf | ||
|
|
b09de8efce | ||
|
|
5a50eb56dd | ||
|
|
e49b257e94 | ||
|
|
b8a0f4e831 | ||
|
|
c265ea9847 | ||
|
|
29f8dcfb40 | ||
|
|
0c05983617 | ||
|
|
0bd653708c | ||
|
|
41d800cb63 | ||
|
|
cadc0bd509 | ||
|
|
b64da2710a | ||
|
|
82b08d0e3a | ||
|
|
8f77d1831b | ||
|
|
be722143e1 | ||
|
|
d8d974e2d7 | ||
|
|
9b7ca6f271 | ||
|
|
8ce018dbab | ||
|
|
180062c58c | ||
|
|
6076b8df69 | ||
|
|
5e65ee79b1 | ||
|
|
c0861c7362 | ||
|
|
37656f14d8 | ||
|
|
dec5535e54 | ||
|
|
1f0e3b157a | ||
|
|
d802e83f49 | ||
|
|
ebcae25762 | ||
|
|
5330267d16 | ||
|
|
892476973b | ||
|
|
84f4a25bc9 | ||
|
|
1460a88bb3 | ||
|
|
62e4c23961 | ||
|
|
d25ab35d58 | ||
|
|
a223cd90a1 | ||
|
|
aef92ba29c | ||
|
|
328d297490 | ||
|
|
3d240f3f18 | ||
|
|
45f35236a7 | ||
|
|
fba210f7ce | ||
|
|
8a09e5fc16 | ||
|
|
52e33e861c | ||
|
|
75d8824e6b | ||
|
|
325af677d3 | ||
|
|
1003e70b5e | ||
|
|
d70229201d | ||
|
|
823f91605b | ||
|
|
53f75034fc | ||
|
|
78649a5b54 | ||
|
|
f48db625a0 | ||
|
|
2ba66c4457 | ||
|
|
2c78ea1a4e | ||
|
|
73f50ac44e | ||
|
|
9ce48953c1 | ||
|
|
1098cd0c6b | ||
|
|
652ebd143c | ||
|
|
8e9d7c0f40 | ||
|
|
a64948a2ba | ||
|
|
43f619a081 | ||
|
|
a07de97df4 | ||
|
|
85d25068a8 | ||
|
|
7a0319cfe5 | ||
|
|
f750671f33 | ||
|
|
7886fe677a | ||
|
|
73c027f8e3 | ||
|
|
eda88cc462 | ||
|
|
652f4ebfed | ||
|
|
06a2f59bd0 | ||
|
|
0af57806da | ||
|
|
03f365e696 | ||
|
|
49a22674ba | ||
|
|
ec494511ec | ||
|
|
af02ce9c6e | ||
|
|
56e42859ab | ||
|
|
2d153359f8 | ||
|
|
068ce23716 | ||
|
|
03be2e3652 | ||
|
|
4ef2c0bed8 | ||
|
|
bfd405613c | ||
|
|
73e1c8c780 | ||
|
|
689ba1d4a2 | ||
|
|
39b9d00550 | ||
|
|
64f99d83a4 | ||
|
|
8f1faefa1c | ||
|
|
2c5ff9ada0 | ||
|
|
a9ceef5c37 | ||
|
|
c6f977ed4b | ||
|
|
cb240cd32a | ||
|
|
bc6349f823 | ||
|
|
a93a1ae40f | ||
|
|
25254255fe | ||
|
|
b0b2798f39 | ||
|
|
7f5c637aeb | ||
|
|
42634b500c | ||
|
|
6f0eb5eccd | ||
|
|
3d83891eb0 | ||
|
|
69a2a133d5 | ||
|
|
be4b38c76a | ||
|
|
7163b1132c | ||
|
|
3ccec1c996 | ||
|
|
47359dc8f1 | ||
|
|
43532c8455 | ||
|
|
d7c3d4ce52 | ||
|
|
ed7060a105 | ||
|
|
db0da4b741 | ||
|
|
c9c16968bb | ||
|
|
87420881c8 | ||
|
|
fdc598f2e1 | ||
|
|
f679145bd1 | ||
|
|
eeb161ec51 | ||
|
|
21cb7307d0 | ||
|
|
412a1eb7ee | ||
|
|
1d801acf72 | ||
|
|
0d7bbdad54 | ||
|
|
53b3d9cf9d | ||
|
|
c3ebbfb10e | ||
|
|
58f035e31a | ||
|
|
a8f1d98d40 | ||
|
|
cf6fa98433 | ||
|
|
937b3ca81d | ||
|
|
d0c5cf0d2d | ||
|
|
4cbf2bef82 | ||
|
|
388d808536 | ||
|
|
720aba3f2d | ||
|
|
f9101de956 | ||
|
|
bb04981280 | ||
|
|
57898ed6dd | ||
|
|
33b53e7605 | ||
|
|
9e8928aad9 | ||
|
|
89c71f9119 | ||
|
|
a4f6db6719 | ||
|
|
2d8e65ea32 | ||
|
|
48d1d27067 | ||
|
|
98aa597510 | ||
|
|
de56d48b2f | ||
|
|
4aeb9a7c56 | ||
|
|
b9b52b7c8b | ||
|
|
dc464a0b7b | ||
|
|
13b6079826 | ||
|
|
6f7dd10d95 | ||
|
|
24fb95291a | ||
|
|
48430bee60 | ||
|
|
42997dcb80 | ||
|
|
0ace189e38 | ||
|
|
d03a7911b5 | ||
|
|
84422676cb | ||
|
|
7441e3f4c5 | ||
|
|
f18132d674 | ||
|
|
5660007221 | ||
|
|
cfebf1dc4a | ||
|
|
5b0111b4c8 | ||
|
|
62a1d69cee | ||
|
|
86a6b04d4a | ||
|
|
8915950c7d | ||
|
|
641e349f33 | ||
|
|
72b4bf9c98 | ||
|
|
ccdeb3fbc8 | ||
|
|
34047fa60a | ||
|
|
05d483bc5b | ||
|
|
113efd9b16 | ||
|
|
c11a1f9679 | ||
|
|
2beeaa513b | ||
|
|
5b56ad0d78 | ||
|
|
bee0d09877 | ||
|
|
42d8d187b3 | ||
|
|
d97348dd38 | ||
|
|
e1ebb7ce9c | ||
|
|
47dd8ad069 | ||
|
|
6a55d75b3d | ||
|
|
d5b4ddd9e5 | ||
|
|
9c8a2265b5 | ||
|
|
84d7157dfb | ||
|
|
ddce4fb46b | ||
|
|
1ccee036c4 | ||
|
|
ef085e3f93 | ||
|
|
3862a93ff9 | ||
|
|
fc21fbd1f1 | ||
|
|
903f9b5240 | ||
|
|
816ad0a94c | ||
|
|
0536697d8f | ||
|
|
0dbd8a667d | ||
|
|
56e691f256 | ||
|
|
b0503efa3d | ||
|
|
b81e59fd8f | ||
|
|
d2da55aa03 | ||
|
|
28e69152d8 | ||
|
|
d122535a65 | ||
|
|
c8c24f81c8 | ||
|
|
db078c7363 | ||
|
|
6b4f6971de | ||
|
|
79707a3c66 | ||
|
|
694783efe9 | ||
|
|
68c5474e36 | ||
|
|
cd055a0298 | ||
|
|
8f2abab0d9 | ||
|
|
4c5dee866b | ||
|
|
7030abca97 | ||
|
|
c7c21a7e2c | ||
|
|
b23e10e261 | ||
|
|
16731661e8 | ||
|
|
7bb90c78d9 | ||
|
|
a6e61ef83b | ||
|
|
d4134cd0d8 | ||
|
|
c775a6c0f8 | ||
|
|
2f9e825728 | ||
|
|
2f491b5be1 | ||
|
|
de7ebead23 | ||
|
|
c0c4704419 | ||
|
|
ec14750ff1 | ||
|
|
e43de5f1ba | ||
|
|
080f949f89 | ||
|
|
9f6956bd87 | ||
|
|
ddf5e1632d | ||
|
|
40bfde41cb | ||
|
|
e0751af56d | ||
|
|
3979faf43b | ||
|
|
878b480a44 | ||
|
|
b35b6b2ba8 | ||
|
|
d0b967ce53 | ||
|
|
e5addb27ec | ||
|
|
ac8d43cc4a | ||
|
|
40ee215b1b | ||
|
|
6c1d94beaa | ||
|
|
6b2e1fe62b | ||
|
|
8ecf885629 | ||
|
|
6d76b7cd94 | ||
|
|
7bd721f334 | ||
|
|
7939897622 | ||
|
|
77bebd4a65 | ||
|
|
5d68a5bdd0 | ||
|
|
3e0b5433b9 | ||
|
|
ec8f1157c8 | ||
|
|
037cbd534e | ||
|
|
208ef70e31 | ||
|
|
2fa4c59523 | ||
|
|
cda0a2de79 | ||
|
|
008f50832c | ||
|
|
c94acb1ca2 | ||
|
|
d341f98b09 | ||
|
|
e35a3ab566 | ||
|
|
b3b4b7cf0c | ||
|
|
1cd6d58f17 | ||
|
|
eecd4417e7 | ||
|
|
21908dfcef | ||
|
|
75987f64ec | ||
|
|
798cc58f76 | ||
|
|
6ba1194d74 | ||
|
|
e5f75b5df2 | ||
|
|
b75ad3def2 | ||
|
|
10c98f0a15 | ||
|
|
caf72afcb4 | ||
|
|
687e0b376e | ||
|
|
122857e5b5 | ||
|
|
5002290428 | ||
|
|
d09ac3384f | ||
|
|
b6a4a7e0a5 | ||
|
|
c87994336c | ||
|
|
85ad490089 | ||
|
|
73e32a9c76 | ||
|
|
a321ff3037 | ||
|
|
68d6feaa03 | ||
|
|
74e1a9a621 | ||
|
|
097bc7055e | ||
|
|
6a43fc5df0 | ||
|
|
312f38906b | ||
|
|
f0ec9fa5d2 | ||
|
|
20b4896940 | ||
|
|
6a93d2d006 | ||
|
|
ae0bc7e7aa | ||
|
|
a8acadbe13 | ||
|
|
727f2e2ba0 | ||
|
|
a6683cb9b8 | ||
|
|
5ceb711bd3 | ||
|
|
4748b09721 | ||
|
|
d593796dae | ||
|
|
ef0dbc2a41 | ||
|
|
6c49953115 | ||
|
|
55290f4dad | ||
|
|
f373a3fbb1 | ||
|
|
bb03d2f2ad | ||
|
|
82922aa2c7 | ||
|
|
7aec5be61a | ||
|
|
2ef6d4327c | ||
|
|
cc95e587db | ||
|
|
e89e55a9bb | ||
|
|
7c2c243985 | ||
|
|
25a1f23fc0 | ||
|
|
27541196cc | ||
|
|
5d9521fcb9 | ||
|
|
ccb52fb625 | ||
|
|
028e530232 | ||
|
|
906a2ff6eb | ||
|
|
248a8efd2f | ||
|
|
c392c819c1 | ||
|
|
e9d9ff0da0 | ||
|
|
46d756d298 | ||
|
|
fd0ffc7085 | ||
|
|
601961deeb | ||
|
|
557a2a0ddf | ||
|
|
b723740f64 | ||
|
|
6be46ae921 | ||
|
|
a25470ee41 | ||
|
|
fd579a019b | ||
|
|
e39ecf59ef | ||
|
|
5f90941e4e | ||
|
|
64465f97b6 | ||
|
|
aa22af6f05 | ||
|
|
a6383247fc | ||
|
|
d45c2a1f28 | ||
|
|
61a63a673c | ||
|
|
5618288459 | ||
|
|
b69ac4ec2f | ||
|
|
f3174069fa | ||
|
|
cd1e796093 | ||
|
|
dd4af4f0df | ||
|
|
76656fab23 | ||
|
|
cf49603a9e | ||
|
|
6c92853461 | ||
|
|
6a62cf9146 | ||
|
|
4fa6bc0ad1 | ||
|
|
95685749ad | ||
|
|
d7c0f0c804 | ||
|
|
6b42b92930 | ||
|
|
f4764ea680 | ||
|
|
538c57664f | ||
|
|
a66a20f7fe | ||
|
|
d4ac79b0af | ||
|
|
a5a3769a0f | ||
|
|
dc4b5cc37d | ||
|
|
ee89be6730 | ||
|
|
770d7e90e9 | ||
|
|
b9aca39eb0 | ||
|
|
c0454ff101 | ||
|
|
a697a2e4f6 | ||
|
|
396cf72029 | ||
|
|
bfe9704829 | ||
|
|
43ee540233 | ||
|
|
817aa186c2 | ||
|
|
38ffc4fdb3 | ||
|
|
f12d734957 | ||
|
|
a70991d50e | ||
|
|
4c00456166 | ||
|
|
26219213d7 | ||
|
|
97c5ee6c0a | ||
|
|
75bc0e451d | ||
|
|
6496b6313c | ||
|
|
c5d9bf2c12 | ||
|
|
8f05560dd7 | ||
|
|
06c0c64c1a | ||
|
|
c173777d12 | ||
|
|
16dfeb3fc8 | ||
|
|
5a31891048 | ||
|
|
8b37496447 | ||
|
|
8f6664f0d7 | ||
|
|
15b1176841 | ||
|
|
3eab1f8f7c | ||
|
|
9dff13cbbf | ||
|
|
a47de9a884 | ||
|
|
8a699b6072 | ||
|
|
87df8b9e85 | ||
|
|
91b19c5c70 | ||
|
|
0487580a1a | ||
|
|
3dca836571 | ||
|
|
6ba02c44d0 | ||
|
|
bf3ab4e260 | ||
|
|
02f9cada43 | ||
|
|
654a19ea15 | ||
|
|
ecb5504bd1 | ||
|
|
2adf3d353e | ||
|
|
3045e85004 | ||
|
|
e9d1afd515 | ||
|
|
833ab7945b | ||
|
|
0af1d668a6 | ||
|
|
0ac62e3805 | ||
|
|
938d09f34a | ||
|
|
dce52d740d | ||
|
|
3ae333fa84 | ||
|
|
d5af1f3948 | ||
|
|
0ba3ae53ab | ||
|
|
be12d78c83 | ||
|
|
b70227ac1b | ||
|
|
6d277fecd5 | ||
|
|
491817d85c | ||
|
|
20faf4e477 | ||
|
|
4fe5c7c24e | ||
|
|
36bf640c6f | ||
|
|
7881e40e0b | ||
|
|
55da1e9c0f | ||
|
|
9799aa0975 | ||
|
|
1effb97b74 | ||
|
|
eb28095041 | ||
|
|
014da41471 | ||
|
|
0446e350d3 | ||
|
|
05fb7db147 | ||
|
|
f6562de325 | ||
|
|
b40211d2c0 | ||
|
|
da4d883321 | ||
|
|
373820f080 | ||
|
|
6e517983b9 | ||
|
|
b9a752fda1 | ||
|
|
f65d80b7d1 | ||
|
|
4701aa149a | ||
|
|
0d051502e2 | ||
|
|
2e28a8e51c | ||
|
|
4af0b74a42 | ||
|
|
d1fc39d6e5 | ||
|
|
4f0d324a6b | ||
|
|
8652d8b23d | ||
|
|
e02aa885d8 | ||
|
|
1fc9356796 | ||
|
|
bb09762029 | ||
|
|
05a5c7120e | ||
|
|
521d603902 | ||
|
|
916710353a | ||
|
|
53b00dea3f | ||
|
|
0587b9f257 | ||
|
|
9621ba59ae | ||
|
|
5accd8cf08 | ||
|
|
38c130df2b | ||
|
|
8730ffb4e2 | ||
|
|
a8645f80bf | ||
|
|
278585fd94 | ||
|
|
d61c3a9442 | ||
|
|
2cdeaa2575 | ||
|
|
286783e880 | ||
|
|
f7b0f1af70 | ||
|
|
f69cb28933 | ||
|
|
e3fd63b2d7 | ||
|
|
6cb956d1d6 | ||
|
|
00e7958a97 | ||
|
|
cba8e6814f | ||
|
|
2f995eb622 | ||
|
|
90fbad0f1c | ||
|
|
2cbd28478d | ||
|
|
7eeefd2602 | ||
|
|
1331457314 | ||
|
|
7855145ebd | ||
|
|
6ab30e9cac | ||
|
|
027e9c7816 | ||
|
|
7c65cfd932 | ||
|
|
883680731a | ||
|
|
fb3171f366 | ||
|
|
c07f9fed99 | ||
|
|
616777517d | ||
|
|
b3f1677da5 | ||
|
|
16f08eb654 | ||
|
|
a38974ef2e | ||
|
|
725b364bbc | ||
|
|
30b99f0049 | ||
|
|
b61de65b43 | ||
|
|
0822c96ce0 | ||
|
|
3b164e5ffe | ||
|
|
d5f1e76707 | ||
|
|
f49718e94b | ||
|
|
b18db78cce | ||
|
|
c39fd17e54 | ||
|
|
78fff5bdd9 | ||
|
|
6fff514901 | ||
|
|
f9a6c00493 | ||
|
|
fa77d81813 | ||
|
|
f0b6c406ff | ||
|
|
c365cca38a | ||
|
|
4cd65eab5c | ||
|
|
2ee360e6ba | ||
|
|
9bc09046c0 | ||
|
|
10d9cbdeb1 | ||
|
|
57f03e660c | ||
|
|
512f085891 | ||
|
|
6a2db52adb | ||
|
|
34e13d0d4d | ||
|
|
da00c832f5 | ||
|
|
8ff265c3a1 | ||
|
|
0278d5b61c | ||
|
|
1fc88c4eff | ||
|
|
58ca74c68a | ||
|
|
b4f871a2ef | ||
|
|
0f7bf6d6c6 | ||
|
|
5dfe7d8596 | ||
|
|
231009b901 | ||
|
|
1c5f939aea | ||
|
|
c1e6406fc9 | ||
|
|
d66979c68f | ||
|
|
6c09abc6cb | ||
|
|
9e52ead09a | ||
|
|
9ab0c54426 | ||
|
|
f6af6778ab | ||
|
|
6a94dda60d | ||
|
|
82b7944599 | ||
|
|
52e02db5c8 | ||
|
|
9a933993f5 | ||
|
|
062b2ae8d3 | ||
|
|
9f69dbf31a | ||
|
|
63fb3f03d1 | ||
|
|
2e379b0834 | ||
|
|
f00f6c8c23 | ||
|
|
50e23f4a2e | ||
|
|
acdc84e08c | ||
|
|
c128ddb549 | ||
|
|
dccf17e770 | ||
|
|
2d8ab72e22 | ||
|
|
748366c70e | ||
|
|
7a74fe2ff7 | ||
|
|
e410302237 | ||
|
|
bca2161a05 | ||
|
|
5f789092be | ||
|
|
6975ed22c0 | ||
|
|
24644f1dd1 | ||
|
|
3bead07043 | ||
|
|
ee20e42372 | ||
|
|
df411b4ede | ||
|
|
bfb9d8ccb6 | ||
|
|
338aec2930 | ||
|
|
e6510dc87b | ||
|
|
76f3b9f6ba | ||
|
|
7830cda912 | ||
|
|
aac97a8983 | ||
|
|
ca26dfcd61 | ||
|
|
858721a7a5 | ||
|
|
89db1d6a6a | ||
|
|
de4e5c40aa | ||
|
|
05248ab990 | ||
|
|
252f47a425 | ||
|
|
be52b31b5c | ||
|
|
23c3fa6993 | ||
|
|
499fc62187 | ||
|
|
1dd5272190 | ||
|
|
5361120353 | ||
|
|
60bab8fdf1 | ||
|
|
cc99b0f532 | ||
|
|
91aa8f9295 | ||
|
|
e9328d819e | ||
|
|
48ece623e7 | ||
|
|
23191efc05 | ||
|
|
0d8af010b6 | ||
|
|
7b9bb772ca | ||
|
|
f7e211c245 | ||
|
|
43bcb6415b | ||
|
|
15ec4aaa43 | ||
|
|
364859467f | ||
|
|
35c2e74af8 | ||
|
|
19482a563f | ||
|
|
2e4c4c3e91 | ||
|
|
7515fa8a98 | ||
|
|
5b9e7213dd | ||
|
|
2253341904 | ||
|
|
e155dc8d6e | ||
|
|
00b2db4fb9 | ||
|
|
f59386f523 | ||
|
|
9683c8f664 | ||
|
|
38a1fde3bf | ||
|
|
40c7a63fb5 | ||
|
|
d9e65cd758 | ||
|
|
e511261b04 | ||
|
|
0d01346ad4 | ||
|
|
e7f4babf41 | ||
|
|
a29a8e292b | ||
|
|
a8b116e217 | ||
|
|
e582b4c8ca | ||
|
|
3b70dbfebe | ||
|
|
868cd5cb09 | ||
|
|
dec18d9acc | ||
|
|
a7508bc2ae | ||
|
|
a38639d099 | ||
|
|
c6e94bc2a6 | ||
|
|
12e9478a81 | ||
|
|
09dafb1a79 | ||
|
|
3358c07107 | ||
|
|
36ff2105fb | ||
|
|
1739d18433 | ||
|
|
61272cfe20 | ||
|
|
31b048f966 | ||
|
|
7e29d49451 | ||
|
|
5445081c96 | ||
|
|
d791facc87 | ||
|
|
21a9bd927a | ||
|
|
d73d3b4480 | ||
|
|
7b9c1bb69c | ||
|
|
224b3163f2 | ||
|
|
fc84ae611e | ||
|
|
22a52bdca2 | ||
|
|
6e9cd5cb21 | ||
|
|
c73445199c | ||
|
|
ab02f82470 | ||
|
|
1e3318816c | ||
|
|
4c8781c762 | ||
|
|
3a3dec92c7 | ||
|
|
5a5fc1ae1a | ||
|
|
8d79a1e381 | ||
|
|
d70f5da94e | ||
|
|
05d4274019 | ||
|
|
afeec09902 | ||
|
|
0526ac2ee2 | ||
|
|
6725ee2190 | ||
|
|
8b661fb90f | ||
|
|
dab7d3db1b | ||
|
|
1cba3d48d9 | ||
|
|
d53b38ec7e | ||
|
|
5d0f47eda2 | ||
|
|
2e04c4442c | ||
|
|
f639cdc8ad | ||
|
|
71ec7624ca | ||
|
|
0599d9602e | ||
|
|
234bef2a88 | ||
|
|
adb574e1cd | ||
|
|
1f491e764e | ||
|
|
114a43a662 | ||
|
|
5547c39c91 | ||
|
|
97a89aaf4d | ||
|
|
61e46399dc | ||
|
|
e802f6ecc2 | ||
|
|
4209f0e044 | ||
|
|
33576aa2c4 | ||
|
|
17bf1a64bf | ||
|
|
f8d46f8f3d | ||
|
|
8787d85e64 | ||
|
|
7f0f17f435 | ||
|
|
0e7f54f375 | ||
|
|
b3bdfa9f46 | ||
|
|
592ec69d36 | ||
|
|
60e00ddd02 | ||
|
|
6806193dc2 | ||
|
|
c35dca783f | ||
|
|
901e0d65b9 | ||
|
|
ddf45a0010 | ||
|
|
1eca4463b3 | ||
|
|
be01203cc1 | ||
|
|
4d1d19a464 | ||
|
|
760817eb3b | ||
|
|
cb47575860 | ||
|
|
434d184503 | ||
|
|
7374c665e8 | ||
|
|
10c930a59d | ||
|
|
60ab6f0c2a | ||
|
|
a13eb351da | ||
|
|
4b91910fab | ||
|
|
f46d52364c | ||
|
|
878c63dcd2 | ||
|
|
261fb3d4f8 | ||
|
|
b63e0cff72 | ||
|
|
5d6e479338 | ||
|
|
90094529a5 | ||
|
|
aed4c0539e | ||
|
|
8b50ab2593 | ||
|
|
95164b79c9 | ||
|
|
6f838fe190 | ||
|
|
bb680b40d8 | ||
|
|
e3f6da6994 | ||
|
|
e46bde35f5 | ||
|
|
32338bea4d | ||
|
|
5c881bd19d | ||
|
|
1a44ef0469 | ||
|
|
ebce9a2e51 | ||
|
|
633af4d404 | ||
|
|
76a73c835c | ||
|
|
c1d1c451ef | ||
|
|
3be30d8c71 | ||
|
|
d4c1244485 | ||
|
|
c61b9dca17 | ||
|
|
39bf682016 | ||
|
|
60ac9b49ea | ||
|
|
a8bb18e2cf | ||
|
|
1852786609 | ||
|
|
31df8c7e91 | ||
|
|
832939f5b7 | ||
|
|
bcd0479074 | ||
|
|
d72dd8c4ff | ||
|
|
c939a274be |
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
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
using namespace Analyser::Dynamic;
|
||||
|
||||
MultiCRTMachine::MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::mutex &machines_mutex) :
|
||||
MultiCRTMachine::MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::recursive_mutex &machines_mutex) :
|
||||
machines_(machines), machines_mutex_(machines_mutex), queues_(machines.size()) {
|
||||
speaker_ = MultiSpeaker::create(machines);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Ma
|
||||
std::condition_variable condition;
|
||||
std::mutex mutex;
|
||||
{
|
||||
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
|
||||
std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_);
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
outstanding_machines = machines_.size();
|
||||
|
||||
@@ -46,29 +46,18 @@ void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Ma
|
||||
}
|
||||
|
||||
void MultiCRTMachine::perform_serial(const std::function<void (::CRTMachine::Machine *)> &function) {
|
||||
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
|
||||
std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_);
|
||||
for(const auto &machine: machines_) {
|
||||
CRTMachine::Machine *crt_machine = machine->crt_machine();
|
||||
CRTMachine::Machine *const crt_machine = machine->crt_machine();
|
||||
if(crt_machine) function(crt_machine);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiCRTMachine::setup_output(float aspect_ratio) {
|
||||
perform_serial([=](::CRTMachine::Machine *machine) {
|
||||
machine->setup_output(aspect_ratio);
|
||||
});
|
||||
}
|
||||
void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
scan_target_ = scan_target;
|
||||
|
||||
void MultiCRTMachine::close_output() {
|
||||
perform_serial([=](::CRTMachine::Machine *machine) {
|
||||
machine->close_output();
|
||||
});
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *MultiCRTMachine::get_crt() {
|
||||
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
|
||||
CRTMachine::Machine *crt_machine = machines_.front()->crt_machine();
|
||||
return crt_machine ? crt_machine->get_crt() : nullptr;
|
||||
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
|
||||
if(crt_machine) crt_machine->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() {
|
||||
@@ -84,6 +73,14 @@ void MultiCRTMachine::run_for(Time::Seconds duration) {
|
||||
}
|
||||
|
||||
void MultiCRTMachine::did_change_machine_order() {
|
||||
if(scan_target_) scan_target_->will_change_owner();
|
||||
|
||||
perform_serial([=](::CRTMachine::Machine *machine) {
|
||||
machine->set_scan_target(nullptr);
|
||||
});
|
||||
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
|
||||
if(crt_machine) crt_machine->set_scan_target(scan_target_);
|
||||
|
||||
if(speaker_) {
|
||||
speaker_->set_new_front_machine(machines_.front().get());
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Dynamic {
|
||||
*/
|
||||
class MultiCRTMachine: public CRTMachine::Machine {
|
||||
public:
|
||||
MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::mutex &machines_mutex);
|
||||
MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::recursive_mutex &machines_mutex);
|
||||
|
||||
/*!
|
||||
Informs the MultiCRTMachine that the order of machines has changed; the MultiCRTMachine
|
||||
@@ -53,19 +53,18 @@ class MultiCRTMachine: public CRTMachine::Machine {
|
||||
}
|
||||
|
||||
// Below is the standard CRTMachine::Machine interface; see there for documentation.
|
||||
void setup_output(float aspect_ratio) override;
|
||||
void close_output() override;
|
||||
Outputs::CRT::CRT *get_crt() override;
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override;
|
||||
Outputs::Speaker::Speaker *get_speaker() override;
|
||||
void run_for(Time::Seconds duration) override;
|
||||
|
||||
private:
|
||||
void run_for(const Cycles cycles) override {}
|
||||
const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_;
|
||||
std::mutex &machines_mutex_;
|
||||
std::recursive_mutex &machines_mutex_;
|
||||
std::vector<Concurrency::AsyncTaskQueue> queues_;
|
||||
MultiSpeaker *speaker_ = nullptr;
|
||||
Delegate *delegate_ = nullptr;
|
||||
Outputs::Display::ScanTarget *scan_target_ = nullptr;
|
||||
|
||||
/*!
|
||||
Performs a parallel for operation across all machines, performing the supplied
|
||||
|
||||
@@ -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() override;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
using namespace Analyser::Dynamic;
|
||||
|
||||
MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) {
|
||||
MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) :
|
||||
keyboard_(machines_) {
|
||||
for(const auto &machine: machines) {
|
||||
KeyboardMachine::Machine *keyboard_machine = machine->keyboard_machine();
|
||||
if(keyboard_machine) machines_.push_back(keyboard_machine);
|
||||
@@ -35,9 +36,34 @@ void MultiKeyboardMachine::type_string(const std::string &string) {
|
||||
}
|
||||
}
|
||||
|
||||
void MultiKeyboardMachine::keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) {
|
||||
Inputs::Keyboard &MultiKeyboardMachine::get_keyboard() {
|
||||
return keyboard_;
|
||||
}
|
||||
|
||||
MultiKeyboardMachine::MultiKeyboard::MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines)
|
||||
: machines_(machines) {
|
||||
for(const auto &machine: machines_) {
|
||||
uint16_t mapped_key = machine->get_keyboard_mapper()->mapped_key_for_key(key);
|
||||
if(mapped_key != KeyNotMapped) machine->set_key_state(mapped_key, is_pressed);
|
||||
observed_keys_.insert(machine->get_keyboard().observed_keys().begin(), machine->get_keyboard().observed_keys().end());
|
||||
is_exclusive_ |= machine->get_keyboard().is_exclusive();
|
||||
}
|
||||
}
|
||||
|
||||
void MultiKeyboardMachine::MultiKeyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
||||
for(const auto &machine: machines_) {
|
||||
machine->get_keyboard().set_key_pressed(key, value, is_pressed);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiKeyboardMachine::MultiKeyboard::reset_all_keys() {
|
||||
for(const auto &machine: machines_) {
|
||||
machine->get_keyboard().reset_all_keys();
|
||||
}
|
||||
}
|
||||
|
||||
const std::set<Inputs::Keyboard::Key> &MultiKeyboardMachine::MultiKeyboard::observed_keys() {
|
||||
return observed_keys_;
|
||||
}
|
||||
|
||||
bool MultiKeyboardMachine::MultiKeyboard::is_exclusive() {
|
||||
return is_exclusive_;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,25 @@ namespace Dynamic {
|
||||
order of delivered messages.
|
||||
*/
|
||||
class MultiKeyboardMachine: public KeyboardMachine::Machine {
|
||||
private:
|
||||
std::vector<::KeyboardMachine::Machine *> machines_;
|
||||
|
||||
class MultiKeyboard: public Inputs::Keyboard {
|
||||
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;
|
||||
|
||||
private:
|
||||
const std::vector<::KeyboardMachine::Machine *> &machines_;
|
||||
std::set<Key> observed_keys_;
|
||||
bool is_exclusive_ = false;
|
||||
};
|
||||
MultiKeyboard keyboard_;
|
||||
|
||||
public:
|
||||
MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
@@ -32,10 +51,7 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine {
|
||||
void clear_all_keys() override;
|
||||
void set_key_state(uint16_t key, bool is_pressed) override;
|
||||
void type_string(const std::string &) override;
|
||||
void keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) override;
|
||||
|
||||
private:
|
||||
std::vector<::KeyboardMachine::Machine *> machines_;
|
||||
Inputs::Keyboard &get_keyboard() override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#include "MultiMachine.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -58,6 +59,11 @@ KeyboardMachine::Machine *MultiMachine::keyboard_machine() {
|
||||
}
|
||||
}
|
||||
|
||||
MouseMachine::Machine *MultiMachine::mouse_machine() {
|
||||
// TODO.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Configurable::Device *MultiMachine::configurable_device() {
|
||||
if(has_picked_) {
|
||||
return machines_.front()->configurable_device();
|
||||
@@ -73,15 +79,13 @@ bool MultiMachine::would_collapse(const std::vector<std::unique_ptr<DynamicMachi
|
||||
}
|
||||
|
||||
void MultiMachine::multi_crt_did_run_machines() {
|
||||
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
|
||||
#ifdef DEBUG
|
||||
std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_);
|
||||
#ifndef NDEBUG
|
||||
for(const auto &machine: machines_) {
|
||||
CRTMachine::Machine *crt = machine->crt_machine();
|
||||
printf("%0.2f ", crt->get_confidence());
|
||||
crt->print_type();
|
||||
printf("; ");
|
||||
LOGNBR(PADHEX(2) << crt->get_confidence() << " " << crt->debug_type() << "; ");
|
||||
}
|
||||
printf("\n");
|
||||
LOGNBR(std::endl);
|
||||
#endif
|
||||
|
||||
DynamicMachine *front = machines_.front().get();
|
||||
|
||||
@@ -54,6 +54,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De
|
||||
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;
|
||||
@@ -62,7 +63,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De
|
||||
void multi_crt_did_run_machines() override;
|
||||
|
||||
std::vector<std::unique_ptr<DynamicMachine>> machines_;
|
||||
std::mutex machines_mutex_;
|
||||
std::recursive_mutex machines_mutex_;
|
||||
|
||||
MultiConfigurable configurable_;
|
||||
MultiCRTMachine crt_machine_;
|
||||
|
||||
@@ -15,8 +15,11 @@ enum class Machine {
|
||||
AmstradCPC,
|
||||
AppleII,
|
||||
Atari2600,
|
||||
AtariST,
|
||||
ColecoVision,
|
||||
Electron,
|
||||
Macintosh,
|
||||
MasterSystem,
|
||||
MSX,
|
||||
Oric,
|
||||
Vic20,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
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;
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ struct Target: public ::Analyser::Static::Target {
|
||||
enum class Model {
|
||||
II,
|
||||
IIplus,
|
||||
IIe
|
||||
IIe,
|
||||
EnhancedIIe
|
||||
};
|
||||
enum class DiskController {
|
||||
None,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -23,5 +23,4 @@ TargetList GetTargets(const Media &media, const std::string &file_name, TargetPl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif /* StaticAnalyser_hpp */
|
||||
|
||||
@@ -22,7 +22,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
|
||||
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));
|
||||
drive = std::make_shared<Storage::Disk::Drive>(4000000, 300, 2);
|
||||
set_drive(drive);
|
||||
drive->set_motor_on(true);
|
||||
}
|
||||
@@ -125,7 +125,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) {
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
#include "Tape.hpp"
|
||||
#include "Target.hpp"
|
||||
#include "../../../Storage/Cartridge/Encodings/CommodoreROM.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
using namespace Analyser::Static::Commodore;
|
||||
@@ -43,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
|
||||
|
||||
@@ -77,10 +79,10 @@ 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()) {
|
||||
if(files.front().is_basic()) {
|
||||
string_stream << "0";
|
||||
} else {
|
||||
string_stream << "1";
|
||||
@@ -91,18 +93,20 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
||||
// make a first guess based on loading address
|
||||
switch(files.front().starting_address) {
|
||||
default:
|
||||
printf("Starting address %04x?\n", files.front().starting_address);
|
||||
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();
|
||||
@@ -144,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;
|
||||
|
||||
@@ -63,9 +63,9 @@ class Accessor {
|
||||
#define z(v) (v & 7)
|
||||
|
||||
Instruction::Condition condition_table[] = {
|
||||
Instruction::Condition::NZ, Instruction::Condition::Z,
|
||||
Instruction::Condition::NC, Instruction::Condition::C,
|
||||
Instruction::Condition::PO, Instruction::Condition::PE,
|
||||
Instruction::Condition::NZ, Instruction::Condition::Z,
|
||||
Instruction::Condition::NC, Instruction::Condition::C,
|
||||
Instruction::Condition::PO, Instruction::Condition::PE,
|
||||
Instruction::Condition::P, Instruction::Condition::M
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -285,7 +285,12 @@ Analyser::Static::TargetList Analyser::Static::MSX::GetTargets(const Media &medi
|
||||
}
|
||||
}
|
||||
|
||||
// Region selection: for now, this as simple as:
|
||||
// "If a tape is involved, be European. Otherwise be American (i.e. English, but 60Hz)".
|
||||
target->region = target->media.tapes.empty() ? Target::Region::USA : Target::Region::Europe;
|
||||
|
||||
// Blindly accept disks for now.
|
||||
// TODO: how to spot an MSX disk?
|
||||
target->media.disks = media.disks;
|
||||
target->has_disk_drive = !media.disks.empty();
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ namespace MSX {
|
||||
struct Target: public ::Analyser::Static::Target {
|
||||
bool has_disk_drive = false;
|
||||
std::string loading_command;
|
||||
|
||||
enum class Region {
|
||||
Japan,
|
||||
USA,
|
||||
Europe
|
||||
} region = Region::USA;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
26
Analyser/Static/Macintosh/StaticAnalyser.cpp
Normal file
26
Analyser/Static/Macintosh/StaticAnalyser.cpp
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// StaticAnalyser.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 02/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "StaticAnalyser.hpp"
|
||||
#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 and mass-storage devices only.
|
||||
if(media.disks.empty() && media.mass_storage_devices.empty()) return {};
|
||||
|
||||
// As there is at least one usable media image, wave it through.
|
||||
Analyser::Static::TargetList targets;
|
||||
|
||||
using Target = Analyser::Static::Macintosh::Target;
|
||||
auto *target = new Target;
|
||||
target->machine = Analyser::Machine::Macintosh;
|
||||
target->media = media;
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
|
||||
|
||||
return targets;
|
||||
}
|
||||
27
Analyser/Static/Macintosh/StaticAnalyser.hpp
Normal file
27
Analyser/Static/Macintosh/StaticAnalyser.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// StaticAnalyser.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 02/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_Macintosh_StaticAnalyser_hpp
|
||||
#define Analyser_Static_Macintosh_StaticAnalyser_hpp
|
||||
|
||||
#include "../StaticAnalyser.hpp"
|
||||
#include "../../../Storage/TargetPlatforms.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Macintosh {
|
||||
|
||||
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif /* Analyser_Static_Macintosh_StaticAnalyser_hpp */
|
||||
31
Analyser/Static/Macintosh/Target.hpp
Normal file
31
Analyser/Static/Macintosh/Target.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Target.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_Macintosh_Target_h
|
||||
#define Analyser_Static_Macintosh_Target_h
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Macintosh {
|
||||
|
||||
struct Target: public ::Analyser::Static::Target {
|
||||
enum class Model {
|
||||
Mac128k,
|
||||
Mac512k,
|
||||
Mac512ke,
|
||||
MacPlus
|
||||
};
|
||||
|
||||
Model model = Model::MacPlus;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Analyser_Static_Macintosh_Target_h */
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
82
Analyser/Static/Sega/StaticAnalyser.cpp
Normal file
82
Analyser/Static/Sega/StaticAnalyser.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// StaticAnalyser.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/09/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "StaticAnalyser.hpp"
|
||||
|
||||
#include "Target.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
if(media.cartridges.empty())
|
||||
return {};
|
||||
|
||||
TargetList targets;
|
||||
auto target = std::make_unique<Target>();
|
||||
|
||||
target->machine = Machine::MasterSystem;
|
||||
|
||||
// Files named .sg are treated as for the SG1000; otherwise assume a Master System.
|
||||
if(file_name.size() >= 2 && *(file_name.end() - 2) == 's' && *(file_name.end() - 1) == 'g') {
|
||||
target->model = Target::Model::SG1000;
|
||||
} else {
|
||||
target->model = Target::Model::MasterSystem;
|
||||
}
|
||||
|
||||
// If this is a Master System title, look for a ROM header.
|
||||
if(target->model == Target::Model::MasterSystem) {
|
||||
const auto &data = media.cartridges.front()->get_segments()[0].data;
|
||||
|
||||
// First try to locate a header.
|
||||
size_t header_offset = 0;
|
||||
size_t potential_offsets[] = {0x1ff0, 0x3ff0, 0x7ff0};
|
||||
for(auto potential_offset: potential_offsets) {
|
||||
if(!memcmp(&data[potential_offset], "TMR SEGA", 8)) {
|
||||
header_offset = potential_offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If a header was found, use it to crib region.
|
||||
if(header_offset) {
|
||||
// Treat export titles as European by default; decline to
|
||||
// do so only if (US) or (NTSC) is in the file name.
|
||||
const uint8_t region = data[header_offset + 0x0f] >> 4;
|
||||
switch(region) {
|
||||
default: break;
|
||||
case 4: {
|
||||
std::string lowercase_name = file_name;
|
||||
std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower);
|
||||
if(lowercase_name.find("(jp)") == std::string::npos) {
|
||||
target->region =
|
||||
(lowercase_name.find("(us)") == std::string::npos &&
|
||||
lowercase_name.find("(ntsc)") == std::string::npos) ? Target::Region::Europe : Target::Region::USA;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
// Also check for a Codemasters header.
|
||||
// If one is found, set the paging scheme appropriately.
|
||||
const uint16_t inverse_checksum = uint16_t(0x10000 - (data[0x7fe6] | (data[0x7fe7] << 8)));
|
||||
if(
|
||||
data[0x7fe3] >= 0x87 && data[0x7fe3] < 0x96 && // i.e. game is dated between 1987 and 1996
|
||||
(inverse_checksum&0xff) == data[0x7fe8] &&
|
||||
(inverse_checksum >> 8) == data[0x7fe9] && // i.e. the standard checksum appears to be present
|
||||
!data[0x7fea] && !data[0x7feb] && !data[0x7fec] && !data[0x7fed] && !data[0x7fee] && !data[0x7fef]
|
||||
) {
|
||||
target->paging_scheme = Target::PagingScheme::Codemasters;
|
||||
target->model = Target::Model::MasterSystem2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target->media.cartridges = media.cartridges;
|
||||
targets.push_back(std::move(target));
|
||||
return targets;
|
||||
}
|
||||
26
Analyser/Static/Sega/StaticAnalyser.hpp
Normal file
26
Analyser/Static/Sega/StaticAnalyser.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// StaticAnalyser.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/09/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef StaticAnalyser_Sega_StaticAnalyser_hpp
|
||||
#define StaticAnalyser_Sega_StaticAnalyser_hpp
|
||||
|
||||
#include "../StaticAnalyser.hpp"
|
||||
#include "../../../Storage/TargetPlatforms.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Sega {
|
||||
|
||||
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* StaticAnalyser_hpp */
|
||||
46
Analyser/Static/Sega/Target.hpp
Normal file
46
Analyser/Static/Sega/Target.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// Target.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 23/09/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_Sega_Target_h
|
||||
#define Analyser_Static_Sega_Target_h
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Sega {
|
||||
|
||||
struct Target: public ::Analyser::Static::Target {
|
||||
enum class Model {
|
||||
SG1000,
|
||||
MasterSystem,
|
||||
MasterSystem2,
|
||||
};
|
||||
|
||||
enum class Region {
|
||||
Japan,
|
||||
USA,
|
||||
Europe,
|
||||
Brazil
|
||||
};
|
||||
|
||||
enum class PagingScheme {
|
||||
Sega,
|
||||
Codemasters
|
||||
};
|
||||
|
||||
Model model = Model::MasterSystem;
|
||||
Region region = Region::Japan;
|
||||
PagingScheme paging_scheme = PagingScheme::Sega;
|
||||
};
|
||||
|
||||
#define is_master_system(v) v >= Analyser::Static::Sega::Target::Model::MasterSystem
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Analyser_Static_Sega_Target_h */
|
||||
@@ -17,12 +17,15 @@
|
||||
#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"
|
||||
#include "Macintosh/StaticAnalyser.hpp"
|
||||
#include "MSX/StaticAnalyser.hpp"
|
||||
#include "Oric/StaticAnalyser.hpp"
|
||||
#include "Sega/StaticAnalyser.hpp"
|
||||
#include "ZX8081/StaticAnalyser.hpp"
|
||||
|
||||
// Cartridges
|
||||
@@ -34,15 +37,22 @@
|
||||
#include "../../Storage/Disk/DiskImage/Formats/AppleDSK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/D64.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp"
|
||||
#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"
|
||||
@@ -84,34 +94,39 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
TryInsert(list, class, platforms) \
|
||||
}
|
||||
|
||||
Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 80
|
||||
Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 81
|
||||
Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // A26
|
||||
Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn) // ADF
|
||||
Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge) // BIN
|
||||
Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX) // CAS
|
||||
Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT
|
||||
Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision) // COL
|
||||
Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape) // CSW
|
||||
Format("d64", result.disks, Disk::DiskImageHolder<Storage::Disk::D64>, TargetPlatform::Commodore) // D64
|
||||
Format("dmk", result.disks, Disk::DiskImageHolder<Storage::Disk::DMK>, TargetPlatform::MSX) // DMK
|
||||
Format("do", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DO
|
||||
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)
|
||||
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
|
||||
Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 80
|
||||
Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 81
|
||||
Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // A26
|
||||
Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn) // ADF
|
||||
Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge) // BIN (cartridge dump)
|
||||
Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX) // CAS
|
||||
Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT
|
||||
Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision) // COL
|
||||
Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape) // CSW
|
||||
Format("d64", result.disks, Disk::DiskImageHolder<Storage::Disk::D64>, TargetPlatform::Commodore) // D64
|
||||
Format("dmk", result.disks, Disk::DiskImageHolder<Storage::Disk::DMK>, TargetPlatform::MSX) // DMK
|
||||
Format("do", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DO
|
||||
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, 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
|
||||
Format( "hfe",
|
||||
result.disks,
|
||||
Disk::DiskImageHolder<Storage::Disk::HFE>,
|
||||
TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric)
|
||||
// HFE (TODO: switch to AllDisk once the MSX stops being so greedy)
|
||||
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
|
||||
Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // PO
|
||||
Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P81
|
||||
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
|
||||
Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // PO
|
||||
Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P81
|
||||
|
||||
// PRG
|
||||
if(extension == "prg") {
|
||||
@@ -128,14 +143,18 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
Format( "rom",
|
||||
result.cartridges,
|
||||
Cartridge::BinaryDump,
|
||||
TargetPlatform::AcornElectron | TargetPlatform::ColecoVision | TargetPlatform::MSX) // ROM
|
||||
Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // SSD
|
||||
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
|
||||
Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081) // TZX
|
||||
Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn) // UEF (tape)
|
||||
Format("woz", result.disks, Disk::DiskImageHolder<Storage::Disk::WOZ>, TargetPlatform::DiskII) // WOZ
|
||||
TargetPlatform::AcornElectron | TargetPlatform::ColecoVision | TargetPlatform::MSX) // ROM
|
||||
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
|
||||
Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081) // TZX
|
||||
Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn) // UEF (tape)
|
||||
Format("woz", result.disks, Disk::DiskImageHolder<Storage::Disk::WOZ>, TargetPlatform::DiskII) // WOZ
|
||||
|
||||
#undef Format
|
||||
#undef Insert
|
||||
@@ -152,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);
|
||||
@@ -166,12 +185,15 @@ 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);
|
||||
if(potential_platforms & TargetPlatform::Macintosh) Append(Macintosh);
|
||||
if(potential_platforms & TargetPlatform::MSX) Append(MSX);
|
||||
if(potential_platforms & TargetPlatform::Oric) Append(Oric);
|
||||
if(potential_platforms & TargetPlatform::Sega) Append(Sega);
|
||||
if(potential_platforms & TargetPlatform::ZX8081) Append(ZX8081);
|
||||
#undef Append
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#ifndef ClockReceiver_hpp
|
||||
#define ClockReceiver_hpp
|
||||
|
||||
#include "ForceInline.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
/*
|
||||
Informal pattern for all classes that run from a clock cycle:
|
||||
|
||||
@@ -52,149 +55,193 @@
|
||||
*/
|
||||
template <class T> class WrappedInt {
|
||||
public:
|
||||
constexpr WrappedInt(int l) : length_(l) {}
|
||||
constexpr WrappedInt() : length_(0) {}
|
||||
using IntType = int64_t;
|
||||
|
||||
T &operator =(const T &rhs) {
|
||||
forceinline constexpr WrappedInt(IntType l) noexcept : length_(l) {}
|
||||
forceinline constexpr WrappedInt() noexcept : length_(0) {}
|
||||
|
||||
forceinline T &operator =(const T &rhs) {
|
||||
length_ = rhs.length_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
T &operator +=(const T &rhs) {
|
||||
forceinline T &operator +=(const T &rhs) {
|
||||
length_ += rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator -=(const T &rhs) {
|
||||
forceinline T &operator -=(const T &rhs) {
|
||||
length_ -= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator ++() {
|
||||
forceinline T &operator ++() {
|
||||
++ length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator ++(int) {
|
||||
forceinline T &operator ++(int) {
|
||||
length_ ++;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator --() {
|
||||
forceinline T &operator --() {
|
||||
-- length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator --(int) {
|
||||
forceinline T &operator --(int) {
|
||||
length_ --;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator %=(const T &rhs) {
|
||||
forceinline T &operator *=(const T &rhs) {
|
||||
length_ *= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
forceinline T &operator /=(const T &rhs) {
|
||||
length_ /= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
forceinline T &operator %=(const T &rhs) {
|
||||
length_ %= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator &=(const T &rhs) {
|
||||
forceinline T &operator &=(const T &rhs) {
|
||||
length_ &= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
constexpr T operator +(const T &rhs) const { return T(length_ + rhs.length_); }
|
||||
constexpr T operator -(const T &rhs) const { return T(length_ - rhs.length_); }
|
||||
forceinline constexpr T operator +(const T &rhs) const { return T(length_ + rhs.length_); }
|
||||
forceinline constexpr T operator -(const T &rhs) const { return T(length_ - rhs.length_); }
|
||||
|
||||
constexpr T operator %(const T &rhs) const { return T(length_ % rhs.length_); }
|
||||
constexpr T operator &(const T &rhs) const { return T(length_ & rhs.length_); }
|
||||
forceinline constexpr T operator *(const T &rhs) const { return T(length_ * rhs.length_); }
|
||||
forceinline constexpr T operator /(const T &rhs) const { return T(length_ / rhs.length_); }
|
||||
|
||||
constexpr T operator -() const { return T(- length_); }
|
||||
forceinline constexpr T operator %(const T &rhs) const { return T(length_ % rhs.length_); }
|
||||
forceinline constexpr T operator &(const T &rhs) const { return T(length_ & rhs.length_); }
|
||||
|
||||
constexpr bool operator <(const T &rhs) const { return length_ < rhs.length_; }
|
||||
constexpr bool operator >(const T &rhs) const { return length_ > rhs.length_; }
|
||||
constexpr bool operator <=(const T &rhs) const { return length_ <= rhs.length_; }
|
||||
constexpr bool operator >=(const T &rhs) const { return length_ >= rhs.length_; }
|
||||
constexpr bool operator ==(const T &rhs) const { return length_ == rhs.length_; }
|
||||
constexpr bool operator !=(const T &rhs) const { return length_ != rhs.length_; }
|
||||
forceinline constexpr T operator -() const { return T(- length_); }
|
||||
|
||||
constexpr bool operator !() const { return !length_; }
|
||||
forceinline constexpr bool operator <(const T &rhs) const { return length_ < rhs.length_; }
|
||||
forceinline constexpr bool operator >(const T &rhs) const { return length_ > rhs.length_; }
|
||||
forceinline constexpr bool operator <=(const T &rhs) const { return length_ <= rhs.length_; }
|
||||
forceinline constexpr bool operator >=(const T &rhs) const { return length_ >= rhs.length_; }
|
||||
forceinline constexpr bool operator ==(const T &rhs) const { return length_ == rhs.length_; }
|
||||
forceinline constexpr bool operator !=(const T &rhs) const { return length_ != rhs.length_; }
|
||||
|
||||
forceinline constexpr bool operator !() const { return !length_; }
|
||||
// bool operator () is not supported because it offers an implicit cast to int, which is prone silently to permit misuse
|
||||
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/*!
|
||||
Flushes the value in @c this. The current value is returned, and the internal value
|
||||
is reset to zero.
|
||||
*/
|
||||
T flush() {
|
||||
T result(length_);
|
||||
length_ = 0;
|
||||
return result;
|
||||
template <typename Result> Result flush() {
|
||||
// Jiggery pokery here; switching to function overloading avoids
|
||||
// the namespace-level requirement for template specialisation.
|
||||
Result r;
|
||||
static_cast<T *>(this)->fill(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
// operator int() is deliberately not provided, to avoid accidental subtitution of
|
||||
// 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:
|
||||
constexpr Cycles(int l) : WrappedInt<Cycles>(l) {}
|
||||
constexpr Cycles() : WrappedInt<Cycles>() {}
|
||||
constexpr Cycles(const Cycles &cycles) : WrappedInt<Cycles>(cycles.length_) {}
|
||||
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_) {}
|
||||
|
||||
private:
|
||||
friend WrappedInt;
|
||||
void fill(Cycles &result) {
|
||||
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:
|
||||
constexpr HalfCycles(int l) : WrappedInt<HalfCycles>(l) {}
|
||||
constexpr HalfCycles() : WrappedInt<HalfCycles>() {}
|
||||
forceinline constexpr HalfCycles(IntType l) noexcept : WrappedInt<HalfCycles>(l) {}
|
||||
forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {}
|
||||
|
||||
constexpr HalfCycles(const Cycles cycles) : WrappedInt<HalfCycles>(cycles.as_int() * 2) {}
|
||||
constexpr HalfCycles(const HalfCycles &half_cycles) : WrappedInt<HalfCycles>(half_cycles.length_) {}
|
||||
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.
|
||||
constexpr Cycles cycles() const {
|
||||
forceinline constexpr Cycles cycles() const {
|
||||
return Cycles(length_ >> 1);
|
||||
}
|
||||
|
||||
/// Flushes the whole cycles in @c this, subtracting that many from the total stored here.
|
||||
Cycles flush_cycles() {
|
||||
Cycles result(length_ >> 1);
|
||||
length_ &= 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Flushes the half cycles in @c this, returning the number stored and setting this total to zero.
|
||||
HalfCycles flush() {
|
||||
HalfCycles result(length_);
|
||||
length_ = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
Cycles divide_cycles(const Cycles &divisor) {
|
||||
HalfCycles half_divisor = HalfCycles(divisor);
|
||||
Cycles result(length_ / half_divisor.length_);
|
||||
forceinline Cycles divide_cycles(const Cycles &divisor) {
|
||||
const HalfCycles half_divisor = HalfCycles(divisor);
|
||||
const Cycles result(length_ / half_divisor.length_);
|
||||
length_ %= half_divisor.length_;
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
friend WrappedInt;
|
||||
void fill(Cycles &result) {
|
||||
result = Cycles(length_ >> 1);
|
||||
length_ &= 1;
|
||||
}
|
||||
|
||||
void fill(HalfCycles &result) {
|
||||
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
|
||||
// without losing the fractional part.
|
||||
|
||||
/*!
|
||||
If a component implements only run_for(Cycles), an owner can wrap it in HalfClockReceiver
|
||||
automatically to gain run_for(HalfCycles).
|
||||
@@ -203,9 +250,9 @@ template <class T> class HalfClockReceiver: public T {
|
||||
public:
|
||||
using T::T;
|
||||
|
||||
inline void run_for(const HalfCycles half_cycles) {
|
||||
forceinline void run_for(const HalfCycles half_cycles) {
|
||||
half_cycles_ += half_cycles;
|
||||
T::run_for(half_cycles_.flush_cycles());
|
||||
T::run_for(half_cycles_.flush<Cycles>());
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
82
ClockReceiver/DeferredQueue.hpp
Normal file
82
ClockReceiver/DeferredQueue.hpp
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// DeferredQueue.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 23/08/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef DeferredQueue_h
|
||||
#define DeferredQueue_h
|
||||
|
||||
#include <functional>
|
||||
#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.
|
||||
*/
|
||||
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);
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void(TimeUnit)> target_;
|
||||
|
||||
// The list of deferred actions.
|
||||
struct DeferredAction {
|
||||
TimeUnit delay;
|
||||
std::function<void(void)> action;
|
||||
|
||||
DeferredAction(TimeUnit delay, const std::function<void(void)> &action) : delay(delay), action(std::move(action)) {}
|
||||
};
|
||||
std::vector<DeferredAction> pending_actions_;
|
||||
};
|
||||
|
||||
#endif /* DeferredQueue_h */
|
||||
@@ -9,9 +9,9 @@
|
||||
#ifndef ForceInline_hpp
|
||||
#define ForceInline_hpp
|
||||
|
||||
#ifdef DEBUG
|
||||
#ifndef NDEBUG
|
||||
|
||||
#define forceinline
|
||||
#define forceinline inline
|
||||
|
||||
#else
|
||||
|
||||
|
||||
123
ClockReceiver/JustInTime.hpp
Normal file
123
ClockReceiver/JustInTime.hpp
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// JustInTime.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 28/07/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef JustInTime_h
|
||||
#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
|
||||
of time since run_for was last called.
|
||||
|
||||
Time can be added using the += operator. The -> operator can be used to access the
|
||||
embedded object. All time accumulated will be pushed to object before the pointer is returned.
|
||||
|
||||
Machines that accumulate HalfCycle time but supply to a Cycle-counted device may supply a
|
||||
separate @c TargetTimeScale at template declaration.
|
||||
*/
|
||||
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.
|
||||
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.
|
||||
forceinline T *operator->() {
|
||||
flush();
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Returns a pointer to the included object without flushing time.
|
||||
forceinline T *last_valid() {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time.
|
||||
forceinline void flush() {
|
||||
if(!is_flushed_) {
|
||||
is_flushed_ = true;
|
||||
if constexpr (divider == 1) {
|
||||
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
||||
} else {
|
||||
const auto duration = time_since_update_.template divide<TargetTimeScale>(LocalTimeScale(divider));
|
||||
if(duration > TargetTimeScale(0))
|
||||
object_.run_for(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale time_since_update_;
|
||||
bool is_flushed_ = true;
|
||||
};
|
||||
|
||||
/*!
|
||||
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 = 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) :
|
||||
object_(std::forward<Args>(args)...),
|
||||
threshold_(threshold) {}
|
||||
|
||||
/// Adds time to the actor.
|
||||
inline void operator += (const LocalTimeScale &rhs) {
|
||||
time_since_update_ += rhs;
|
||||
if(time_since_update_ >= threshold_) {
|
||||
time_since_update_ -= threshold_;
|
||||
task_queue_.enqueue([this] () {
|
||||
object_.run_for(threshold_);
|
||||
});
|
||||
}
|
||||
is_flushed_ = false;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time and returns a pointer to the included object.
|
||||
inline T *operator->() {
|
||||
flush();
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Returns a pointer to the included object without flushing time.
|
||||
inline T *last_valid() {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time.
|
||||
inline void flush() {
|
||||
if(!is_flushed_) {
|
||||
task_queue_.flush();
|
||||
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
||||
is_flushed_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale time_since_update_;
|
||||
TargetTimeScale threshold_;
|
||||
bool is_flushed_ = true;
|
||||
Concurrency::AsyncTaskQueue task_queue_;
|
||||
};
|
||||
|
||||
#endif /* JustInTime_h */
|
||||
@@ -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_; \
|
||||
} \
|
||||
}
|
||||
|
||||
@@ -141,28 +158,28 @@ void WD1770::run_for(const Cycles cycles) {
|
||||
WAIT_FOR_EVENT(Event1770::IndexHoleTarget); \
|
||||
status_.spin_up = true;
|
||||
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
|
||||
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
|
||||
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
|
||||
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
|
||||
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
|
||||
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
|
||||
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
|
||||
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
|
||||
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
|
||||
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
|
||||
// ! 4 ! Forc int ! 1 1 0 1 i3 i2 i1 i0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
|
||||
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
|
||||
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
|
||||
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
|
||||
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
|
||||
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
|
||||
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
|
||||
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
|
||||
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
|
||||
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
|
||||
// ! 4 ! Forc int ! 1 1 0 1 i3 i2 i1 i0 !
|
||||
// +--------+----------+-------------------------+
|
||||
|
||||
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,19 +194,19 @@ 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;
|
||||
}
|
||||
|
||||
Status new_status;
|
||||
BEGIN_SECTION()
|
||||
|
||||
// Wait for a new command, branch to the appropriate handler.
|
||||
@@ -209,9 +226,10 @@ 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 " << std::hex << command_ << std::endl);
|
||||
LOG("Starting " << PADHEX(2) << int(command_));
|
||||
|
||||
if(!(command_ & 0x80)) goto begin_type_1;
|
||||
if(!(command_ & 0x40)) goto begin_type_2;
|
||||
@@ -221,16 +239,16 @@ void WD1770::posit_event(int new_event_type) {
|
||||
/*
|
||||
Type 1 entry point.
|
||||
*/
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
|
||||
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
|
||||
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
|
||||
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
|
||||
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
|
||||
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
|
||||
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
|
||||
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
|
||||
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
|
||||
// +--------+----------+-------------------------+
|
||||
|
||||
begin_type_1:
|
||||
// Set initial flags, skip spin-up if possible.
|
||||
@@ -241,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;
|
||||
@@ -273,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;
|
||||
@@ -294,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;
|
||||
}
|
||||
@@ -310,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;
|
||||
@@ -329,14 +354,12 @@ void WD1770::posit_event(int new_event_type) {
|
||||
}
|
||||
|
||||
if(header_[0] == track_) {
|
||||
LOG("Reached track " << std::dec << track_);
|
||||
LOG("Reached track " << std::dec << int(track_));
|
||||
update_status([] (Status &status) {
|
||||
status.crc_error = false;
|
||||
});
|
||||
goto wait_for_command;
|
||||
}
|
||||
|
||||
distance_into_section_ = 0;
|
||||
}
|
||||
goto verify_read_data;
|
||||
|
||||
@@ -344,13 +367,13 @@ void WD1770::posit_event(int new_event_type) {
|
||||
/*
|
||||
Type 2 entry point.
|
||||
*/
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
|
||||
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
|
||||
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
|
||||
// +--------+----------+-------------------------+
|
||||
|
||||
begin_type_2:
|
||||
update_status([] (Status &status) {
|
||||
@@ -393,23 +416,28 @@ 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) {
|
||||
LOG("Failed to find sector " << std::dec << sector_);
|
||||
LOG("Failed to find sector " << std::dec << int(sector_));
|
||||
update_status([] (Status &status) {
|
||||
status.record_not_found = true;
|
||||
});
|
||||
goto wait_for_command;
|
||||
}
|
||||
if(distance_into_section_ == 7) {
|
||||
LOG("Considering " << std::dec << header_[0] << "/" << 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 << header_[0] << "/" << header_[2]);
|
||||
LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||
if(get_crc_generator().get_value()) {
|
||||
LOG("CRC error; back to searching");
|
||||
update_status([] (Status &status) {
|
||||
@@ -423,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;
|
||||
|
||||
@@ -466,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) {
|
||||
@@ -474,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 << sector_);
|
||||
goto wait_for_command;
|
||||
}
|
||||
goto type2_check_crc;
|
||||
@@ -560,21 +592,21 @@ void WD1770::posit_event(int new_event_type) {
|
||||
sector_++;
|
||||
goto test_type2_write_protection;
|
||||
}
|
||||
LOG("Wrote sector " << std::dec << sector_);
|
||||
LOG("Wrote sector " << std::dec << int(sector_));
|
||||
goto wait_for_command;
|
||||
|
||||
|
||||
/*
|
||||
Type 3 entry point.
|
||||
*/
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
|
||||
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
|
||||
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
|
||||
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
|
||||
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
begin_type_3:
|
||||
update_status([] (Status &status) {
|
||||
status.type = Status::Three;
|
||||
@@ -611,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) {
|
||||
@@ -626,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;
|
||||
@@ -652,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;
|
||||
}
|
||||
@@ -719,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) |
|
||||
@@ -768,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) {}
|
||||
@@ -784,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,10 +36,10 @@ 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);
|
||||
@@ -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 */
|
||||
@@ -47,6 +47,12 @@ class PortHandler {
|
||||
|
||||
/// Sets the current logical value of the interrupt line.
|
||||
void set_interrupt_status(bool status) {}
|
||||
|
||||
/// Provides a measure of time elapsed between other calls.
|
||||
void run_for(HalfCycles duration) {}
|
||||
|
||||
/// Receives passed-on flush() calls from the 6522.
|
||||
void flush() {}
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -71,8 +77,31 @@ class IRQDelegatePortHandler: public PortHandler {
|
||||
Delegate *delegate_ = nullptr;
|
||||
};
|
||||
|
||||
class MOS6522Base: public MOS6522Storage {
|
||||
/*!
|
||||
Implements a template for emulation of the MOS 6522 Versatile Interface Adaptor ('VIA').
|
||||
|
||||
The VIA provides:
|
||||
* two timers, each of which may trigger interrupts and one of which may repeat;
|
||||
* two digial input/output ports; and
|
||||
* a serial-to-parallel shifter.
|
||||
|
||||
Consumers should derive their own curiously-recurring-template-pattern subclass,
|
||||
implementing bus communications as required.
|
||||
*/
|
||||
template <class T> class MOS6522: public MOS6522Storage {
|
||||
public:
|
||||
MOS6522(T &bus_handler) noexcept : bus_handler_(bus_handler) {}
|
||||
MOS6522(const MOS6522 &) = delete;
|
||||
|
||||
/*! Sets a register value. */
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*! Gets a register value. */
|
||||
uint8_t read(int address);
|
||||
|
||||
/*! @returns the bus handler. */
|
||||
T &bus_handler();
|
||||
|
||||
/// Sets the input value of line @c line on port @c port.
|
||||
void set_control_line_input(Port port, Line line, bool value);
|
||||
|
||||
@@ -85,47 +114,32 @@ class MOS6522Base: public MOS6522Storage {
|
||||
/// @returns @c true if the IRQ line is currently active; @c false otherwise.
|
||||
bool get_interrupt_line();
|
||||
|
||||
private:
|
||||
inline void do_phase1();
|
||||
inline void do_phase2();
|
||||
virtual void reevaluate_interrupts() = 0;
|
||||
};
|
||||
|
||||
/*!
|
||||
Implements a template for emulation of the MOS 6522 Versatile Interface Adaptor ('VIA').
|
||||
|
||||
The VIA provides:
|
||||
* two timers, each of which may trigger interrupts and one of which may repeat;
|
||||
* two digial input/output ports; and
|
||||
* a serial-to-parallel shifter.
|
||||
|
||||
Consumers should derive their own curiously-recurring-template-pattern subclass,
|
||||
implementing bus communications as required.
|
||||
*/
|
||||
template <class T> class MOS6522: public MOS6522Base {
|
||||
public:
|
||||
MOS6522(T &bus_handler) noexcept : bus_handler_(bus_handler) {}
|
||||
MOS6522(const MOS6522 &) = delete;
|
||||
|
||||
/*! Sets a register value. */
|
||||
void set_register(int address, uint8_t value);
|
||||
|
||||
/*! Gets a register value. */
|
||||
uint8_t get_register(int address);
|
||||
|
||||
/*! @returns the bus handler. */
|
||||
T &bus_handler();
|
||||
/// Updates the port handler to the current time and then requests that it flush.
|
||||
void flush();
|
||||
|
||||
private:
|
||||
void do_phase1();
|
||||
void do_phase2();
|
||||
void shift_in();
|
||||
void shift_out();
|
||||
|
||||
T &bus_handler_;
|
||||
HalfCycles time_since_bus_handler_call_;
|
||||
|
||||
void access(int address);
|
||||
|
||||
uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output);
|
||||
inline void reevaluate_interrupts();
|
||||
|
||||
/// Sets the current intended output value for the port and line;
|
||||
/// if this affects the visible output, it will be passed to the handler.
|
||||
void set_control_line_output(Port port, Line line, LineState value);
|
||||
void evaluate_cb2_output();
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#include "Implementation/6522Implementation.hpp"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* _522_hpp */
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
//
|
||||
// 6522Base.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/09/2017.
|
||||
// Copyright 2017 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "../6522.hpp"
|
||||
|
||||
using namespace MOS::MOS6522;
|
||||
|
||||
void MOS6522Base::set_control_line_input(Port port, Line line, bool value) {
|
||||
switch(line) {
|
||||
case Line::One:
|
||||
if( value != control_inputs_[port].line_one &&
|
||||
value == !!(registers_.peripheral_control & (port ? 0x10 : 0x01))
|
||||
) {
|
||||
registers_.interrupt_flags |= port ? InterruptFlag::CB1ActiveEdge : InterruptFlag::CA1ActiveEdge;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
control_inputs_[port].line_one = value;
|
||||
break;
|
||||
|
||||
case Line::Two:
|
||||
// TODO: output modes, but probably elsewhere?
|
||||
if( value != control_inputs_[port].line_two && // i.e. value has changed ...
|
||||
!(registers_.peripheral_control & (port ? 0x80 : 0x08)) && // ... and line is input ...
|
||||
value == !!(registers_.peripheral_control & (port ? 0x40 : 0x04)) // ... and it's either high or low, as required
|
||||
) {
|
||||
registers_.interrupt_flags |= port ? InterruptFlag::CB2ActiveEdge : InterruptFlag::CA2ActiveEdge;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
control_inputs_[port].line_two = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MOS6522Base::do_phase2() {
|
||||
registers_.last_timer[0] = registers_.timer[0];
|
||||
registers_.last_timer[1] = registers_.timer[1];
|
||||
|
||||
if(registers_.timer_needs_reload) {
|
||||
registers_.timer_needs_reload = false;
|
||||
registers_.timer[0] = registers_.timer_latch[0];
|
||||
} else {
|
||||
registers_.timer[0] --;
|
||||
}
|
||||
|
||||
registers_.timer[1] --;
|
||||
if(registers_.next_timer[0] >= 0) {
|
||||
registers_.timer[0] = static_cast<uint16_t>(registers_.next_timer[0]);
|
||||
registers_.next_timer[0] = -1;
|
||||
}
|
||||
if(registers_.next_timer[1] >= 0) {
|
||||
registers_.timer[1] = static_cast<uint16_t>(registers_.next_timer[1]);
|
||||
registers_.next_timer[1] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void MOS6522Base::do_phase1() {
|
||||
// IRQ is raised on the half cycle after overflow
|
||||
if((registers_.timer[1] == 0xffff) && !registers_.last_timer[1] && timer_is_running_[1]) {
|
||||
timer_is_running_[1] = false;
|
||||
registers_.interrupt_flags |= InterruptFlag::Timer2;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
|
||||
if((registers_.timer[0] == 0xffff) && !registers_.last_timer[0] && timer_is_running_[0]) {
|
||||
registers_.interrupt_flags |= InterruptFlag::Timer1;
|
||||
reevaluate_interrupts();
|
||||
|
||||
if(registers_.auxiliary_control&0x40)
|
||||
registers_.timer_needs_reload = true;
|
||||
else
|
||||
timer_is_running_[0] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/*! Runs for a specified number of half cycles. */
|
||||
void MOS6522Base::run_for(const HalfCycles half_cycles) {
|
||||
int number_of_half_cycles = half_cycles.as_int();
|
||||
|
||||
if(is_phase2_) {
|
||||
do_phase2();
|
||||
number_of_half_cycles--;
|
||||
}
|
||||
|
||||
while(number_of_half_cycles >= 2) {
|
||||
do_phase1();
|
||||
do_phase2();
|
||||
number_of_half_cycles -= 2;
|
||||
}
|
||||
|
||||
if(number_of_half_cycles) {
|
||||
do_phase1();
|
||||
is_phase2_ = true;
|
||||
} else {
|
||||
is_phase2_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
/*! Runs for a specified number of cycles. */
|
||||
void MOS6522Base::run_for(const Cycles cycles) {
|
||||
int number_of_cycles = cycles.as_int();
|
||||
while(number_of_cycles--) {
|
||||
do_phase1();
|
||||
do_phase2();
|
||||
}
|
||||
}
|
||||
|
||||
/*! @returns @c true if the IRQ line is currently active; @c false otherwise. */
|
||||
bool MOS6522Base::get_interrupt_line() {
|
||||
uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f;
|
||||
return !!interrupt_status;
|
||||
}
|
||||
@@ -6,29 +6,63 @@
|
||||
// Copyright 2017 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
namespace MOS {
|
||||
namespace MOS6522 {
|
||||
|
||||
template <typename T> void MOS6522<T>::access(int address) {
|
||||
switch(address) {
|
||||
case 0x0:
|
||||
// In both handshake and pulse modes, CB2 goes low on any read or write of Port B.
|
||||
if(handshake_modes_[1] != HandshakeMode::None) {
|
||||
set_control_line_output(Port::B, Line::Two, LineState::Off);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xf:
|
||||
case 0x1:
|
||||
// In both handshake and pulse modes, CA2 goes low on any read or write of Port A.
|
||||
if(handshake_modes_[0] != HandshakeMode::None) {
|
||||
set_control_line_output(Port::A, Line::Two, LineState::Off);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
access(address);
|
||||
switch(address) {
|
||||
case 0x0: // Write Port B.
|
||||
// Store locally and communicate outwards.
|
||||
registers_.output[1] = value;
|
||||
bus_handler_.set_port_output(Port::B, value, registers_.data_direction[1]); // TODO: handshake
|
||||
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_port_output(Port::B, value, registers_.data_direction[1]);
|
||||
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | ((registers_.peripheral_control&0x20) ? 0 : InterruptFlag::CB2ActiveEdge));
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
case 0xf:
|
||||
case 0x1:
|
||||
case 0x1: // Write Port A.
|
||||
registers_.output[0] = value;
|
||||
bus_handler_.set_port_output(Port::A, value, registers_.data_direction[0]); // TODO: handshake
|
||||
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_port_output(Port::A, value, registers_.data_direction[0]);
|
||||
|
||||
if(handshake_modes_[1] != HandshakeMode::None) {
|
||||
set_control_line_output(Port::A, Line::Two, LineState::Off);
|
||||
}
|
||||
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | ((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge));
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
|
||||
case 0x2:
|
||||
case 0x2: // Port B direction.
|
||||
registers_.data_direction[1] = value;
|
||||
break;
|
||||
case 0x3:
|
||||
case 0x3: // Port A direction.
|
||||
registers_.data_direction[0] = value;
|
||||
break;
|
||||
|
||||
@@ -54,32 +88,57 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value)
|
||||
break;
|
||||
|
||||
// Shift
|
||||
case 0xa: registers_.shift = value; break;
|
||||
case 0xa:
|
||||
registers_.shift = value;
|
||||
shift_bits_remaining_ = 8;
|
||||
registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister;
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
|
||||
// Control
|
||||
case 0xb:
|
||||
registers_.auxiliary_control = value;
|
||||
evaluate_cb2_output();
|
||||
break;
|
||||
case 0xc:
|
||||
// printf("Peripheral control %02x\n", value);
|
||||
case 0xc: {
|
||||
// const auto old_peripheral_control = registers_.peripheral_control;
|
||||
registers_.peripheral_control = value;
|
||||
|
||||
// TODO: simplify below; trying to avoid improper logging of unimplemented warnings in input mode
|
||||
if(value & 0x08) {
|
||||
switch(value & 0x0e) {
|
||||
default: printf("Unimplemented control line mode %d\n", (value >> 1)&7); break;
|
||||
case 0x0c: bus_handler_.set_control_line_output(Port::A, Line::Two, false); break;
|
||||
case 0x0e: bus_handler_.set_control_line_output(Port::A, Line::Two, true); break;
|
||||
int shift = 0;
|
||||
for(int port = 0; port < 2; ++port) {
|
||||
handshake_modes_[port] = HandshakeMode::None;
|
||||
switch((value >> shift) & 0x0e) {
|
||||
default: break;
|
||||
|
||||
case 0x00: // Negative interrupt input; set Cx2 interrupt on negative Cx2 transition, clear on access to Port x register.
|
||||
case 0x02: // Independent negative interrupt input; set Cx2 interrupt on negative transition, don't clear automatically.
|
||||
case 0x04: // Positive interrupt input; set Cx2 interrupt on positive Cx2 transition, clear on access to Port x register.
|
||||
case 0x06: // Independent positive interrupt input; set Cx2 interrupt on positive transition, don't clear automatically.
|
||||
set_control_line_output(Port(port), Line::Two, LineState::Input);
|
||||
break;
|
||||
|
||||
case 0x08: // Handshake: set Cx2 to low on any read or write of Port x; set to high on an active transition of Cx1.
|
||||
handshake_modes_[port] = HandshakeMode::Handshake;
|
||||
set_control_line_output(Port(port), Line::Two, LineState::Off); // At a guess.
|
||||
break;
|
||||
|
||||
case 0x0a: // Pulse output: Cx2 is low for one cycle following a read or write of Port x.
|
||||
handshake_modes_[port] = HandshakeMode::Pulse;
|
||||
set_control_line_output(Port(port), Line::Two, LineState::On);
|
||||
break;
|
||||
|
||||
case 0x0c: // Manual output: Cx2 low.
|
||||
set_control_line_output(Port(port), Line::Two, LineState::Off);
|
||||
break;
|
||||
|
||||
case 0x0e: // Manual output: Cx2 high.
|
||||
set_control_line_output(Port(port), Line::Two, LineState::On);
|
||||
break;
|
||||
}
|
||||
|
||||
shift += 4;
|
||||
}
|
||||
if(value & 0x80) {
|
||||
switch(value & 0xe0) {
|
||||
default: printf("Unimplemented control line mode %d\n", (value >> 5)&7); break;
|
||||
case 0xc0: bus_handler_.set_control_line_output(Port::B, Line::Two, false); break;
|
||||
case 0xe0: bus_handler_.set_control_line_output(Port::B, Line::Two, true); break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} break;
|
||||
|
||||
// Interrupt control
|
||||
case 0xd:
|
||||
@@ -96,14 +155,15 @@ 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) {
|
||||
case 0x0:
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge);
|
||||
reevaluate_interrupts();
|
||||
return get_port_input(Port::B, registers_.data_direction[1], registers_.output[1]);
|
||||
case 0xf: // TODO: handshake, latching
|
||||
case 0xf:
|
||||
case 0x1:
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge);
|
||||
reevaluate_interrupts();
|
||||
@@ -128,7 +188,11 @@ template <typename T> uint8_t MOS6522<T>::get_register(int address) {
|
||||
return registers_.timer[1] & 0x00ff;
|
||||
case 0x9: return registers_.timer[1] >> 8;
|
||||
|
||||
case 0xa: return registers_.shift;
|
||||
case 0xa:
|
||||
shift_bits_remaining_ = 8;
|
||||
registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister;
|
||||
reevaluate_interrupts();
|
||||
return registers_.shift;
|
||||
|
||||
case 0xb: return registers_.auxiliary_control;
|
||||
case 0xc: return registers_.peripheral_control;
|
||||
@@ -141,7 +205,8 @@ template <typename T> uint8_t MOS6522<T>::get_register(int address) {
|
||||
}
|
||||
|
||||
template <typename T> uint8_t MOS6522<T>::get_port_input(Port port, uint8_t output_mask, uint8_t output) {
|
||||
uint8_t input = bus_handler_.get_port_input(port);
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
const uint8_t input = bus_handler_.get_port_input(port);
|
||||
return (input & ~output_mask) | (output & output_mask);
|
||||
}
|
||||
|
||||
@@ -154,6 +219,238 @@ template <typename T> void MOS6522<T>::reevaluate_interrupts() {
|
||||
bool new_interrupt_status = get_interrupt_line();
|
||||
if(new_interrupt_status != last_posted_interrupt_status_) {
|
||||
last_posted_interrupt_status_ = new_interrupt_status;
|
||||
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_interrupt_status(new_interrupt_status);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::set_control_line_input(Port port, Line line, bool value) {
|
||||
switch(line) {
|
||||
case Line::One:
|
||||
if(value != control_inputs_[port].lines[line]) {
|
||||
// In handshake mode, any transition on C[A/B]1 sets output high on C[A/B]2.
|
||||
if(handshake_modes_[port] == HandshakeMode::Handshake) {
|
||||
set_control_line_output(port, Line::Two, LineState::On);
|
||||
}
|
||||
|
||||
// Set the proper transition interrupt bit if enabled.
|
||||
if(value == !!(registers_.peripheral_control & (port ? 0x10 : 0x01))) {
|
||||
registers_.interrupt_flags |= port ? InterruptFlag::CB1ActiveEdge : InterruptFlag::CA1ActiveEdge;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
|
||||
// If this is a transition on CB1, consider updating the shift register.
|
||||
// TODO: and at least one full clock since the shift register was written?
|
||||
if(port == Port::B) {
|
||||
switch(shift_mode()) {
|
||||
default: break;
|
||||
case ShiftMode::InUnderCB1: if(value) shift_in(); break; // Shifts in are captured on a low-to-high transition.
|
||||
case ShiftMode::OutUnderCB1: if(!value) shift_out(); break; // Shifts out are updated on a high-to-low transition.
|
||||
}
|
||||
}
|
||||
}
|
||||
control_inputs_[port].lines[line] = value;
|
||||
break;
|
||||
|
||||
case Line::Two:
|
||||
if( value != control_inputs_[port].lines[line] && // i.e. value has changed ...
|
||||
!(registers_.peripheral_control & (port ? 0x80 : 0x08)) && // ... and line is input ...
|
||||
value == !!(registers_.peripheral_control & (port ? 0x40 : 0x04)) // ... and it's either high or low, as required
|
||||
) {
|
||||
registers_.interrupt_flags |= port ? InterruptFlag::CB2ActiveEdge : InterruptFlag::CA2ActiveEdge;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
control_inputs_[port].lines[line] = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::do_phase2() {
|
||||
++ time_since_bus_handler_call_;
|
||||
|
||||
registers_.last_timer[0] = registers_.timer[0];
|
||||
registers_.last_timer[1] = registers_.timer[1];
|
||||
|
||||
if(registers_.timer_needs_reload) {
|
||||
registers_.timer_needs_reload = false;
|
||||
registers_.timer[0] = registers_.timer_latch[0];
|
||||
} else {
|
||||
registers_.timer[0] --;
|
||||
}
|
||||
|
||||
registers_.timer[1] --;
|
||||
if(registers_.next_timer[0] >= 0) {
|
||||
registers_.timer[0] = static_cast<uint16_t>(registers_.next_timer[0]);
|
||||
registers_.next_timer[0] = -1;
|
||||
}
|
||||
if(registers_.next_timer[1] >= 0) {
|
||||
registers_.timer[1] = static_cast<uint16_t>(registers_.next_timer[1]);
|
||||
registers_.next_timer[1] = -1;
|
||||
}
|
||||
|
||||
// In pulse modes, CA2 and CB2 go high again on the next clock edge.
|
||||
if(handshake_modes_[1] == HandshakeMode::Pulse) {
|
||||
set_control_line_output(Port::B, Line::Two, LineState::On);
|
||||
}
|
||||
if(handshake_modes_[0] == HandshakeMode::Pulse) {
|
||||
set_control_line_output(Port::A, Line::Two, LineState::On);
|
||||
}
|
||||
|
||||
// If the shift register is shifting according to the input clock, do a shift.
|
||||
switch(shift_mode()) {
|
||||
default: break;
|
||||
case ShiftMode::InUnderPhase2: shift_in(); break;
|
||||
case ShiftMode::OutUnderPhase2: shift_out(); break;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::do_phase1() {
|
||||
++ time_since_bus_handler_call_;
|
||||
|
||||
// IRQ is raised on the half cycle after overflow
|
||||
if((registers_.timer[1] == 0xffff) && !registers_.last_timer[1] && timer_is_running_[1]) {
|
||||
timer_is_running_[1] = false;
|
||||
|
||||
// If the shift register is shifting according to this timer, do a shift.
|
||||
// TODO: "shift register is driven by only the low order 8 bits of timer 2"?
|
||||
switch(shift_mode()) {
|
||||
default: break;
|
||||
case ShiftMode::InUnderT2: shift_in(); break;
|
||||
case ShiftMode::OutUnderT2FreeRunning: shift_out(); break;
|
||||
case ShiftMode::OutUnderT2: shift_out(); break; // TODO: present a clock on CB1.
|
||||
}
|
||||
|
||||
registers_.interrupt_flags |= InterruptFlag::Timer2;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
|
||||
if((registers_.timer[0] == 0xffff) && !registers_.last_timer[0] && timer_is_running_[0]) {
|
||||
registers_.interrupt_flags |= InterruptFlag::Timer1;
|
||||
reevaluate_interrupts();
|
||||
|
||||
// Determine whether to reload.
|
||||
if(registers_.auxiliary_control&0x40)
|
||||
registers_.timer_needs_reload = true;
|
||||
else
|
||||
timer_is_running_[0] = false;
|
||||
|
||||
// Determine whether to toggle PB7.
|
||||
if(registers_.auxiliary_control&0x80) {
|
||||
registers_.output[1] ^= 0x80;
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_port_output(Port::B, registers_.output[1], registers_.data_direction[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*! Runs for a specified number of half cycles. */
|
||||
template <typename T> void MOS6522<T>::run_for(const HalfCycles half_cycles) {
|
||||
auto number_of_half_cycles = half_cycles.as_integral();
|
||||
if(!number_of_half_cycles) return;
|
||||
|
||||
if(is_phase2_) {
|
||||
do_phase2();
|
||||
number_of_half_cycles--;
|
||||
}
|
||||
|
||||
while(number_of_half_cycles >= 2) {
|
||||
do_phase1();
|
||||
do_phase2();
|
||||
number_of_half_cycles -= 2;
|
||||
}
|
||||
|
||||
if(number_of_half_cycles) {
|
||||
do_phase1();
|
||||
is_phase2_ = true;
|
||||
} else {
|
||||
is_phase2_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::flush() {
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.flush();
|
||||
}
|
||||
|
||||
/*! Runs for a specified number of cycles. */
|
||||
template <typename T> void MOS6522<T>::run_for(const Cycles cycles) {
|
||||
auto number_of_cycles = cycles.as_integral();
|
||||
while(number_of_cycles--) {
|
||||
do_phase1();
|
||||
do_phase2();
|
||||
}
|
||||
}
|
||||
|
||||
/*! @returns @c true if the IRQ line is currently active; @c false otherwise. */
|
||||
template <typename T> bool MOS6522<T>::get_interrupt_line() {
|
||||
uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f;
|
||||
return !!interrupt_status;
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::evaluate_cb2_output() {
|
||||
// CB2 is a special case, being both the line the shift register can output to,
|
||||
// and one that can be used as an input or handshaking output according to the
|
||||
// peripheral control register.
|
||||
|
||||
// My guess: other CB2 functions work only if the shift register is disabled (?).
|
||||
if(shift_mode() != ShiftMode::Disabled) {
|
||||
// Shift register is enabled, one way or the other; but announce only output.
|
||||
if(is_shifting_out()) {
|
||||
// Output mode; set the level according to the current top of the shift register.
|
||||
bus_handler_.set_control_line_output(Port::B, Line::Two, !!(registers_.shift & 0x80));
|
||||
} else {
|
||||
// Input mode.
|
||||
bus_handler_.set_control_line_output(Port::B, Line::Two, true);
|
||||
}
|
||||
} else {
|
||||
// Shift register is disabled.
|
||||
bus_handler_.set_control_line_output(Port::B, Line::Two, control_outputs_[1].lines[1] != LineState::Off);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::set_control_line_output(Port port, Line line, LineState value) {
|
||||
if(port == Port::B && line == Line::Two) {
|
||||
control_outputs_[port].lines[line] = value;
|
||||
evaluate_cb2_output();
|
||||
} else {
|
||||
// Do nothing if unchanged.
|
||||
if(value == control_outputs_[port].lines[line]) {
|
||||
return;
|
||||
}
|
||||
|
||||
control_outputs_[port].lines[line] = value;
|
||||
|
||||
if(value != LineState::Input) {
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_control_line_output(port, line, value != LineState::Off);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::shift_in() {
|
||||
registers_.shift = uint8_t((registers_.shift << 1) | (control_inputs_[1].lines[1] ? 1 : 0));
|
||||
--shift_bits_remaining_;
|
||||
if(!shift_bits_remaining_) {
|
||||
registers_.interrupt_flags |= InterruptFlag::ShiftRegister;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::shift_out() {
|
||||
// When shifting out, the shift register rotates rather than strictly shifts.
|
||||
// TODO: is that true for all modes?
|
||||
if(shift_mode() == ShiftMode::OutUnderT2FreeRunning || shift_bits_remaining_) {
|
||||
registers_.shift = uint8_t((registers_.shift << 1) | (registers_.shift >> 7));
|
||||
evaluate_cb2_output();
|
||||
|
||||
--shift_bits_remaining_;
|
||||
if(!shift_bits_remaining_) {
|
||||
registers_.interrupt_flags |= InterruptFlag::ShiftRegister;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class MOS6522Storage {
|
||||
|
||||
// The registers
|
||||
struct Registers {
|
||||
// "A low reset (RES) input clears all R6522 internal registers to logic 0"
|
||||
// "A low reset (RES) input clears all R6522 internal registers to logic 0"
|
||||
uint8_t output[2] = {0, 0};
|
||||
uint8_t input[2] = {0, 0};
|
||||
uint8_t data_direction[2] = {0, 0};
|
||||
@@ -37,14 +37,27 @@ class MOS6522Storage {
|
||||
bool timer_needs_reload = false;
|
||||
} registers_;
|
||||
|
||||
// control state
|
||||
// Control state.
|
||||
struct {
|
||||
bool line_one = false;
|
||||
bool line_two = false;
|
||||
bool lines[2] = {false, false};
|
||||
} control_inputs_[2];
|
||||
|
||||
enum class LineState {
|
||||
On, Off, Input
|
||||
};
|
||||
struct {
|
||||
LineState lines[2] = {LineState::Input, LineState::Input};
|
||||
} control_outputs_[2];
|
||||
|
||||
enum class HandshakeMode {
|
||||
None,
|
||||
Handshake,
|
||||
Pulse
|
||||
} handshake_modes_[2] = { HandshakeMode::None, HandshakeMode::None };
|
||||
|
||||
bool timer_is_running_[2] = {false, false};
|
||||
bool last_posted_interrupt_status_ = false;
|
||||
int shift_bits_remaining_ = 8;
|
||||
|
||||
enum InterruptFlag: uint8_t {
|
||||
CA2ActiveEdge = 1 << 0,
|
||||
@@ -55,6 +68,23 @@ class MOS6522Storage {
|
||||
Timer2 = 1 << 5,
|
||||
Timer1 = 1 << 6,
|
||||
};
|
||||
|
||||
enum class ShiftMode {
|
||||
Disabled = 0,
|
||||
InUnderT2 = 1,
|
||||
InUnderPhase2 = 2,
|
||||
InUnderCB1 = 3,
|
||||
OutUnderT2FreeRunning = 4,
|
||||
OutUnderT2 = 5,
|
||||
OutUnderPhase2 = 6,
|
||||
OutUnderCB1 = 7
|
||||
};
|
||||
ShiftMode shift_mode() const {
|
||||
return ShiftMode((registers_.auxiliary_control >> 2) & 7);
|
||||
}
|
||||
bool is_shifting_out() const {
|
||||
return registers_.auxiliary_control & 0x10;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -34,7 +34,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels
|
||||
unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels
|
||||
unsigned int shift_registers_[4] = {0, 0, 0, 0};
|
||||
uint8_t control_registers_[4] = {0, 0, 0, 0};
|
||||
int16_t volume_ = 0;
|
||||
@@ -58,29 +58,18 @@ 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:
|
||||
MOS6560(BusHandler &bus_handler) :
|
||||
bus_handler_(bus_handler),
|
||||
crt_(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::DisplayType::NTSC60, 2)),
|
||||
crt_(65*4, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8),
|
||||
audio_generator_(audio_queue_),
|
||||
speaker_(audio_generator_)
|
||||
{
|
||||
crt_->set_svideo_sampling_function(
|
||||
"vec2 svideo_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
|
||||
"{"
|
||||
"vec2 yc = texture(texID, coordinate).rg / vec2(255.0);"
|
||||
|
||||
"float phaseOffset = 6.283185308 * 2.0 * yc.y;"
|
||||
"float chroma = step(yc.y, 0.75) * cos(phase + phaseOffset);"
|
||||
|
||||
"return vec2(yc.x, chroma);"
|
||||
"}");
|
||||
|
||||
// default to s-video output
|
||||
crt_->set_video_signal(Outputs::CRT::VideoSignal::SVideo);
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::SVideo);
|
||||
|
||||
// default to NTSC
|
||||
set_output_mode(OutputMode::NTSC);
|
||||
@@ -94,7 +83,8 @@ template <class BusHandler> class MOS6560 {
|
||||
speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0));
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *get_crt() { return crt_.get(); }
|
||||
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); }
|
||||
Outputs::Speaker::Speaker *get_speaker() { return &speaker_; }
|
||||
|
||||
void set_high_frequency_cutoff(float cutoff) {
|
||||
@@ -117,12 +107,12 @@ template <class BusHandler> class MOS6560 {
|
||||
|
||||
// Chrominances are encoded such that 0-128 is a complete revolution of phase;
|
||||
// anything above 191 disables the colour subcarrier. Phase is relative to the
|
||||
// colour burst, so 0 is green.
|
||||
// colour burst, so 0 is green (NTSC) or blue/violet (PAL).
|
||||
const uint8_t pal_chrominances[16] = {
|
||||
255, 255, 37, 101,
|
||||
19, 86, 123, 59,
|
||||
46, 53, 37, 101,
|
||||
19, 86, 123, 59,
|
||||
255, 255, 90, 20,
|
||||
96, 42, 8, 72,
|
||||
84, 90, 90, 20,
|
||||
96, 42, 8, 72,
|
||||
};
|
||||
const uint8_t ntsc_chrominances[16] = {
|
||||
255, 255, 121, 57,
|
||||
@@ -131,12 +121,12 @@ template <class BusHandler> class MOS6560 {
|
||||
103, 42, 80, 16,
|
||||
};
|
||||
const uint8_t *chrominances;
|
||||
Outputs::CRT::DisplayType display_type;
|
||||
Outputs::Display::Type display_type;
|
||||
|
||||
switch(output_mode) {
|
||||
default:
|
||||
chrominances = pal_chrominances;
|
||||
display_type = Outputs::CRT::DisplayType::PAL50;
|
||||
display_type = Outputs::Display::Type::PAL50;
|
||||
timing_.cycles_per_line = 71;
|
||||
timing_.line_counter_increment_offset = 4;
|
||||
timing_.final_line_increment_position = timing_.cycles_per_line - timing_.line_counter_increment_offset;
|
||||
@@ -146,7 +136,7 @@ template <class BusHandler> class MOS6560 {
|
||||
|
||||
case OutputMode::NTSC:
|
||||
chrominances = ntsc_chrominances;
|
||||
display_type = Outputs::CRT::DisplayType::NTSC60;
|
||||
display_type = Outputs::Display::Type::NTSC60;
|
||||
timing_.cycles_per_line = 65;
|
||||
timing_.line_counter_increment_offset = 40;
|
||||
timing_.final_line_increment_position = 58;
|
||||
@@ -155,14 +145,14 @@ template <class BusHandler> class MOS6560 {
|
||||
break;
|
||||
}
|
||||
|
||||
crt_->set_new_display_type(static_cast<unsigned int>(timing_.cycles_per_line*4), display_type);
|
||||
crt_.set_new_display_type(timing_.cycles_per_line*4, display_type);
|
||||
|
||||
switch(output_mode) {
|
||||
case OutputMode::PAL:
|
||||
crt_->set_visible_area(Outputs::CRT::Rect(0.1f, 0.07f, 0.9f, 0.9f));
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.07f, 0.9f, 0.9f));
|
||||
break;
|
||||
case OutputMode::NTSC:
|
||||
crt_->set_visible_area(Outputs::CRT::Rect(0.05f, 0.05f, 0.9f, 0.9f));
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -180,7 +170,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_;
|
||||
@@ -284,17 +274,17 @@ template <class BusHandler> class MOS6560 {
|
||||
// update the CRT
|
||||
if(this_state_ != output_state_) {
|
||||
switch(output_state_) {
|
||||
case State::Sync: crt_->output_sync(cycles_in_state_ * 4); break;
|
||||
case State::ColourBurst: crt_->output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0); break;
|
||||
case State::Border: output_border(cycles_in_state_ * 4); break;
|
||||
case State::Pixels: crt_->output_data(cycles_in_state_ * 4); break;
|
||||
case State::Sync: crt_.output_sync(cycles_in_state_ * 4); break;
|
||||
case State::ColourBurst: crt_.output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0); break;
|
||||
case State::Border: output_border(cycles_in_state_ * 4); break;
|
||||
case State::Pixels: crt_.output_data(cycles_in_state_ * 4); break;
|
||||
}
|
||||
output_state_ = this_state_;
|
||||
cycles_in_state_ = 0;
|
||||
|
||||
pixel_pointer = nullptr;
|
||||
if(output_state_ == State::Pixels) {
|
||||
pixel_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(260));
|
||||
pixel_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(260));
|
||||
}
|
||||
}
|
||||
cycles_in_state_++;
|
||||
@@ -363,7 +353,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) {
|
||||
@@ -427,7 +417,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];
|
||||
@@ -438,7 +428,7 @@ template <class BusHandler> class MOS6560 {
|
||||
|
||||
private:
|
||||
BusHandler &bus_handler_;
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
AudioGenerator audio_generator_;
|
||||
@@ -451,12 +441,12 @@ template <class BusHandler> class MOS6560 {
|
||||
|
||||
// register state
|
||||
struct {
|
||||
bool interlaced, tall_characters;
|
||||
bool interlaced = false, tall_characters = false;
|
||||
uint8_t first_column_location, first_row_location;
|
||||
uint8_t number_of_columns, number_of_rows;
|
||||
uint16_t character_cell_start_address, video_matrix_start_address;
|
||||
uint16_t backgroundColour, borderColour, auxiliary_colour;
|
||||
bool invertedCells;
|
||||
bool invertedCells = false;
|
||||
|
||||
uint8_t direct_values[16];
|
||||
} registers_;
|
||||
@@ -465,7 +455,7 @@ template <class BusHandler> class MOS6560 {
|
||||
enum State {
|
||||
Sync, ColourBurst, Border, Pixels
|
||||
} this_state_, output_state_;
|
||||
unsigned int cycles_in_state_;
|
||||
int cycles_in_state_;
|
||||
|
||||
// counters that cover an entire field
|
||||
int horizontal_counter_ = 0, vertical_counter_ = 0;
|
||||
@@ -493,7 +483,7 @@ template <class BusHandler> class MOS6560 {
|
||||
}
|
||||
|
||||
// latches dictating start and length of drawing
|
||||
bool vertical_drawing_latch_, horizontal_drawing_latch_;
|
||||
bool vertical_drawing_latch_ = false, horizontal_drawing_latch_ = false;
|
||||
int rows_this_field_, columns_this_line_;
|
||||
|
||||
// current drawing position counter
|
||||
@@ -511,10 +501,10 @@ template <class BusHandler> class MOS6560 {
|
||||
uint16_t colours_[16];
|
||||
|
||||
uint16_t *pixel_pointer;
|
||||
void output_border(unsigned int number_of_cycles) {
|
||||
uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(1));
|
||||
void output_border(int number_of_cycles) {
|
||||
uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = registers_.borderColour;
|
||||
crt_->output_level(number_of_cycles);
|
||||
crt_.output_level(number_of_cycles);
|
||||
}
|
||||
|
||||
struct {
|
||||
|
||||
@@ -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;
|
||||
@@ -73,7 +73,7 @@ class i8272: public Storage::Disk::MFMController {
|
||||
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];
|
||||
|
||||
268
Components/8530/z8530.cpp
Normal file
268
Components/8530/z8530.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
//
|
||||
// 8530.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "z8530.hpp"
|
||||
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
using namespace Zilog::SCC;
|
||||
|
||||
void z8530::reset() {
|
||||
// TODO.
|
||||
}
|
||||
|
||||
bool z8530::get_interrupt_line() {
|
||||
return
|
||||
(master_interrupt_control_ & 0x8) &&
|
||||
(
|
||||
channels_[0].get_interrupt_line() ||
|
||||
channels_[1].get_interrupt_line()
|
||||
);
|
||||
}
|
||||
|
||||
std::uint8_t z8530::read(int address) {
|
||||
if(address & 2) {
|
||||
// Read data register for channel
|
||||
return 0x00;
|
||||
} else {
|
||||
// Read control register for channel.
|
||||
uint8_t result = 0;
|
||||
|
||||
switch(pointer_) {
|
||||
default:
|
||||
result = channels_[address & 1].read(address & 2, pointer_);
|
||||
break;
|
||||
|
||||
case 2: // Handled non-symmetrically between channels.
|
||||
if(address & 1) {
|
||||
LOG("[SCC] Unimplemented: register 2 status bits");
|
||||
} else {
|
||||
result = interrupt_vector_;
|
||||
|
||||
// Modify the vector if permitted.
|
||||
// if(master_interrupt_control_ & 1) {
|
||||
for(int port = 0; port < 2; ++port) {
|
||||
// TODO: the logic below assumes that DCD is the only implemented interrupt. Fix.
|
||||
if(channels_[port].get_interrupt_line()) {
|
||||
const uint8_t shift = 1 + 3*((master_interrupt_control_ & 0x10) >> 4);
|
||||
const uint8_t mask = uint8_t(~(7 << shift));
|
||||
result = uint8_t(
|
||||
(result & mask) |
|
||||
((1 | ((port == 1) ? 4 : 0)) << shift)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
pointer_ = 0;
|
||||
update_delegate();
|
||||
return result;
|
||||
}
|
||||
|
||||
return 0x00;
|
||||
}
|
||||
|
||||
void z8530::write(int address, std::uint8_t value) {
|
||||
if(address & 2) {
|
||||
// Write data register for channel.
|
||||
} else {
|
||||
// Write control register for channel.
|
||||
|
||||
// Most registers are per channel, but a couple are shared; sever
|
||||
// them here.
|
||||
switch(pointer_) {
|
||||
default:
|
||||
channels_[address & 1].write(address & 2, pointer_, value);
|
||||
break;
|
||||
|
||||
case 2: // Interrupt vector register; shared between both channels.
|
||||
interrupt_vector_ = value;
|
||||
LOG("[SCC] Interrupt vector set to " << PADHEX(2) << int(value));
|
||||
break;
|
||||
|
||||
case 9: // Master interrupt and reset register; also shared between both channels.
|
||||
LOG("[SCC] Master interrupt and reset register: " << PADHEX(2) << int(value));
|
||||
master_interrupt_control_ = value;
|
||||
break;
|
||||
}
|
||||
|
||||
// The pointer number resets to 0 after every access, but if it is zero
|
||||
// then crib at least the next set of pointer bits (which, similarly, are shared
|
||||
// between the two channels).
|
||||
if(pointer_) {
|
||||
pointer_ = 0;
|
||||
} else {
|
||||
// The lowest three bits are the lowest three bits of the pointer.
|
||||
pointer_ = value & 7;
|
||||
|
||||
// If the command part of the byte is a 'point high', also set the
|
||||
// top bit of the pointer.
|
||||
if(((value >> 3)&7) == 1) {
|
||||
pointer_ |= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
update_delegate();
|
||||
}
|
||||
|
||||
void z8530::set_dcd(int port, bool level) {
|
||||
channels_[port].set_dcd(level);
|
||||
update_delegate();
|
||||
}
|
||||
|
||||
// MARK: - Channel implementations
|
||||
|
||||
uint8_t z8530::Channel::read(bool data, uint8_t pointer) {
|
||||
// If this is a data read, just return it.
|
||||
if(data) {
|
||||
return data_;
|
||||
} else {
|
||||
// Otherwise, this is a control read...
|
||||
switch(pointer) {
|
||||
default:
|
||||
LOG("[SCC] Unrecognised control read from register " << int(pointer));
|
||||
return 0x00;
|
||||
|
||||
case 0:
|
||||
return dcd_ ? 0x8 : 0x0;
|
||||
|
||||
case 0xf:
|
||||
return external_interrupt_status_;
|
||||
}
|
||||
}
|
||||
|
||||
return 0x00;
|
||||
}
|
||||
|
||||
void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) {
|
||||
if(data) {
|
||||
data_ = value;
|
||||
return;
|
||||
} else {
|
||||
switch(pointer) {
|
||||
default:
|
||||
LOG("[SCC] Unrecognised control write: " << PADHEX(2) << int(value) << " to register " << int(pointer));
|
||||
break;
|
||||
|
||||
case 0x0: // Write register 0 — CRC reset and other functions.
|
||||
// Decode CRC reset instructions.
|
||||
switch(value >> 6) {
|
||||
default: /* Do nothing. */ break;
|
||||
case 1:
|
||||
LOG("[SCC] TODO: reset Rx CRC checker.");
|
||||
break;
|
||||
case 2:
|
||||
LOG("[SCC] TODO: reset Tx CRC checker.");
|
||||
break;
|
||||
case 3:
|
||||
LOG("[SCC] TODO: reset Tx underrun/EOM latch.");
|
||||
break;
|
||||
}
|
||||
|
||||
// Decode command code.
|
||||
switch((value >> 3)&7) {
|
||||
default: /* Do nothing. */ break;
|
||||
case 2:
|
||||
// LOG("[SCC] reset ext/status interrupts.");
|
||||
external_status_interrupt_ = false;
|
||||
external_interrupt_status_ = 0;
|
||||
break;
|
||||
case 3:
|
||||
LOG("[SCC] TODO: send abort (SDLC).");
|
||||
break;
|
||||
case 4:
|
||||
LOG("[SCC] TODO: enable interrupt on next Rx character.");
|
||||
break;
|
||||
case 5:
|
||||
LOG("[SCC] TODO: reset Tx interrupt pending.");
|
||||
break;
|
||||
case 6:
|
||||
LOG("[SCC] TODO: reset error.");
|
||||
break;
|
||||
case 7:
|
||||
LOG("[SCC] TODO: reset highest IUS.");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x1: // Write register 1 — Transmit/Receive Interrupt and Data Transfer Mode Definition.
|
||||
interrupt_mask_ = value;
|
||||
break;
|
||||
|
||||
case 0x4: // Write register 4 — Transmit/Receive Miscellaneous Parameters and Modes.
|
||||
// Bits 0 and 1 select parity mode.
|
||||
if(!(value&1)) {
|
||||
parity_ = Parity::Off;
|
||||
} else {
|
||||
parity_ = (value&2) ? Parity::Even : Parity::Odd;
|
||||
}
|
||||
|
||||
// Bits 2 and 3 select stop bits.
|
||||
switch((value >> 2)&3) {
|
||||
default: stop_bits_ = StopBits::Synchronous; break;
|
||||
case 1: stop_bits_ = StopBits::OneBit; break;
|
||||
case 2: stop_bits_ = StopBits::OneAndAHalfBits; break;
|
||||
case 3: stop_bits_ = StopBits::TwoBits; break;
|
||||
}
|
||||
|
||||
// Bits 4 and 5 pick a sync mode.
|
||||
switch((value >> 4)&3) {
|
||||
default: sync_mode_ = Sync::Monosync; break;
|
||||
case 1: sync_mode_ = Sync::Bisync; break;
|
||||
case 2: sync_mode_ = Sync::SDLC; break;
|
||||
case 3: sync_mode_ = Sync::External; break;
|
||||
}
|
||||
|
||||
// Bits 6 and 7 select a clock rate multiplier, unless synchronous
|
||||
// mode is enabled (and this is ignored if sync mode is external).
|
||||
if(stop_bits_ == StopBits::Synchronous) {
|
||||
clock_rate_multiplier_ = 1;
|
||||
} else {
|
||||
switch((value >> 6)&3) {
|
||||
default: clock_rate_multiplier_ = 1; break;
|
||||
case 1: clock_rate_multiplier_ = 16; break;
|
||||
case 2: clock_rate_multiplier_ = 32; break;
|
||||
case 3: clock_rate_multiplier_ = 64; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xf: // Write register 15 — External/Status Interrupt Control.
|
||||
external_interrupt_mask_ = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void z8530::Channel::set_dcd(bool level) {
|
||||
if(dcd_ == level) return;
|
||||
dcd_ = level;
|
||||
|
||||
if(external_interrupt_mask_ & 0x8) {
|
||||
external_status_interrupt_ = true;
|
||||
external_interrupt_status_ |= 0x8;
|
||||
}
|
||||
}
|
||||
|
||||
bool z8530::Channel::get_interrupt_line() {
|
||||
return
|
||||
(interrupt_mask_ & 1) && external_status_interrupt_;
|
||||
// TODO: other potential causes of an interrupt.
|
||||
}
|
||||
|
||||
void z8530::update_delegate() {
|
||||
const bool interrupt_line = get_interrupt_line();
|
||||
if(interrupt_line != previous_interrupt_line_) {
|
||||
previous_interrupt_line_ = interrupt_line;
|
||||
if(delegate_) delegate_->did_change_interrupt_status(this, interrupt_line);
|
||||
}
|
||||
}
|
||||
99
Components/8530/z8530.hpp
Normal file
99
Components/8530/z8530.hpp
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// z8530.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef z8530_hpp
|
||||
#define z8530_hpp
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Zilog {
|
||||
namespace SCC {
|
||||
|
||||
/*!
|
||||
Models the Zilog 8530 SCC, a serial adaptor.
|
||||
*/
|
||||
class z8530 {
|
||||
public:
|
||||
/*
|
||||
**Interface for emulated machine.**
|
||||
|
||||
Notes on addressing below:
|
||||
|
||||
There's no inherent ordering of the two 'address' lines,
|
||||
A/B and C/D, but the methods below assume:
|
||||
|
||||
A/B = A0
|
||||
C/D = A1
|
||||
*/
|
||||
std::uint8_t read(int address);
|
||||
void write(int address, std::uint8_t value);
|
||||
void reset();
|
||||
bool get_interrupt_line();
|
||||
|
||||
struct Delegate {
|
||||
virtual void did_change_interrupt_status(z8530 *, bool new_status) = 0;
|
||||
};
|
||||
void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
|
||||
/*
|
||||
**Interface for serial port input.**
|
||||
*/
|
||||
void set_dcd(int port, bool level);
|
||||
|
||||
private:
|
||||
class Channel {
|
||||
public:
|
||||
uint8_t read(bool data, uint8_t pointer);
|
||||
void write(bool data, uint8_t pointer, uint8_t value);
|
||||
void set_dcd(bool level);
|
||||
bool get_interrupt_line();
|
||||
|
||||
private:
|
||||
uint8_t data_ = 0xff;
|
||||
|
||||
enum class Parity {
|
||||
Even, Odd, Off
|
||||
} parity_ = Parity::Off;
|
||||
|
||||
enum class StopBits {
|
||||
Synchronous, OneBit, OneAndAHalfBits, TwoBits
|
||||
} stop_bits_ = StopBits::Synchronous;
|
||||
|
||||
enum class Sync {
|
||||
Monosync, Bisync, SDLC, External
|
||||
} sync_mode_ = Sync::Monosync;
|
||||
|
||||
int clock_rate_multiplier_ = 1;
|
||||
|
||||
uint8_t interrupt_mask_ = 0; // i.e. Write Register 0x1.
|
||||
|
||||
uint8_t external_interrupt_mask_ = 0; // i.e. Write Register 0xf.
|
||||
bool external_status_interrupt_ = false;
|
||||
uint8_t external_interrupt_status_ = 0;
|
||||
|
||||
bool dcd_ = false;
|
||||
} channels_[2];
|
||||
|
||||
uint8_t pointer_ = 0;
|
||||
|
||||
uint8_t interrupt_vector_ = 0;
|
||||
|
||||
uint8_t master_interrupt_control_ = 0;
|
||||
|
||||
bool previous_interrupt_line_ = false;
|
||||
void update_delegate();
|
||||
Delegate *delegate_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif /* z8530_hpp */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
||||
#include <cstdint>
|
||||
|
||||
namespace TI {
|
||||
namespace TMS {
|
||||
|
||||
/*!
|
||||
Provides emulation of the TMS9918a, TMS9928 and TMS9929. Likely in the future to be the
|
||||
@@ -29,30 +30,22 @@ namespace TI {
|
||||
These chips have only one non-on-demand interaction with the outside world: an interrupt line.
|
||||
See get_time_until_interrupt and get_interrupt_line for asynchronous operation options.
|
||||
*/
|
||||
class TMS9918: public TMS9918Base {
|
||||
class TMS9918: public Base {
|
||||
public:
|
||||
enum Personality {
|
||||
TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired.
|
||||
};
|
||||
|
||||
/*!
|
||||
Constructs an instance of the drive controller that behaves according to personality @c p.
|
||||
@param p The type of controller to emulate.
|
||||
*/
|
||||
TMS9918(Personality p);
|
||||
|
||||
enum TVStandard {
|
||||
/*! i.e. 50Hz output at around 312.5 lines/field */
|
||||
PAL,
|
||||
/*! i.e. 60Hz output at around 262.5 lines/field */
|
||||
NTSC
|
||||
};
|
||||
|
||||
/*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */
|
||||
void set_tv_standard(TVStandard standard);
|
||||
|
||||
/*! Provides the CRT this TMS is connected to. */
|
||||
Outputs::CRT::CRT *get_crt();
|
||||
/*! Sets the scan target this TMS will post content to. */
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
|
||||
/*! Sets the type of display the CRT will request. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
/*!
|
||||
Runs the VCP for the number of cycles indicate; it is an implicit assumption of the code
|
||||
@@ -61,26 +54,46 @@ class TMS9918: public TMS9918Base {
|
||||
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();
|
||||
|
||||
/*! Gets the current latched horizontal counter; provided by the Master System only. */
|
||||
uint8_t get_latched_horizontal_counter();
|
||||
|
||||
/*! Latches the current horizontal counter. */
|
||||
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.
|
||||
*/
|
||||
HalfCycles get_time_until_interrupt();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until the nominated line interrupt position is
|
||||
reached on line @c line. If no line interrupt position is defined for
|
||||
this VDP, returns the time until the 'beginning' of that line, whatever
|
||||
that may mean.
|
||||
|
||||
@line is relative to the first pixel line of the display and may be negative.
|
||||
*/
|
||||
HalfCycles get_time_until_line(int line);
|
||||
|
||||
/*!
|
||||
@returns @c true if the interrupt line is currently active; @c false otherwise.
|
||||
*/
|
||||
bool get_interrupt_line();
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* TMS9918_hpp */
|
||||
|
||||
@@ -12,90 +12,848 @@
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace TI {
|
||||
namespace TMS {
|
||||
|
||||
enum Personality {
|
||||
TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired.
|
||||
V9938,
|
||||
V9958,
|
||||
SMSVDP,
|
||||
SMS2VDP,
|
||||
GGVDP,
|
||||
};
|
||||
|
||||
enum class TVStandard {
|
||||
/*! i.e. 50Hz output at around 312.5 lines/field */
|
||||
PAL,
|
||||
/*! i.e. 60Hz output at around 262.5 lines/field */
|
||||
NTSC
|
||||
};
|
||||
|
||||
#define is_sega_vdp(x) ((x) >= SMSVDP)
|
||||
|
||||
class Base {
|
||||
public:
|
||||
static const uint32_t palette_pack(uint8_t r, uint8_t g, uint8_t b) {
|
||||
uint32_t result = 0;
|
||||
uint8_t *const result_ptr = reinterpret_cast<uint8_t *>(&result);
|
||||
result_ptr[0] = r;
|
||||
result_ptr[1] = g;
|
||||
result_ptr[2] = b;
|
||||
result_ptr[3] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
class TMS9918Base {
|
||||
protected:
|
||||
TMS9918Base();
|
||||
const static int output_lag = 11; // i.e. pixel output will occur 11 cycles after corresponding data read.
|
||||
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
// The default TMS palette.
|
||||
const uint32_t palette[16] = {
|
||||
palette_pack(0, 0, 0),
|
||||
palette_pack(0, 0, 0),
|
||||
palette_pack(33, 200, 66),
|
||||
palette_pack(94, 220, 120),
|
||||
|
||||
uint8_t ram_[16384];
|
||||
palette_pack(84, 85, 237),
|
||||
palette_pack(125, 118, 252),
|
||||
palette_pack(212, 82, 77),
|
||||
palette_pack(66, 235, 245),
|
||||
|
||||
palette_pack(252, 85, 84),
|
||||
palette_pack(255, 121, 120),
|
||||
palette_pack(212, 193, 84),
|
||||
palette_pack(230, 206, 128),
|
||||
|
||||
palette_pack(33, 176, 59),
|
||||
palette_pack(201, 91, 186),
|
||||
palette_pack(204, 204, 204),
|
||||
palette_pack(255, 255, 255)
|
||||
};
|
||||
|
||||
Base(Personality p);
|
||||
|
||||
const Personality personality_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
TVStandard tv_standard_ = TVStandard::NTSC;
|
||||
|
||||
// Holds the contents of this VDP's connected DRAM.
|
||||
std::vector<uint8_t> ram_;
|
||||
|
||||
// Holds the state of the DRAM/CRAM-access mechanism.
|
||||
uint16_t ram_pointer_ = 0;
|
||||
uint8_t read_ahead_buffer_ = 0;
|
||||
enum class MemoryAccess {
|
||||
Read, Write, None
|
||||
} queued_access_ = MemoryAccess::None;
|
||||
int cycles_until_access_ = 0;
|
||||
int minimum_access_column_ = 0;
|
||||
int vram_access_delay() {
|
||||
// This seems to be correct for all currently-modelled VDPs;
|
||||
// it's the delay between an external device scheduling a
|
||||
// read or write and the very first time that can occur
|
||||
// (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 6;
|
||||
}
|
||||
|
||||
// Holds the main status register.
|
||||
uint8_t status_ = 0;
|
||||
|
||||
bool write_phase_ = false;
|
||||
uint8_t low_write_ = 0;
|
||||
// Current state of programmer input.
|
||||
bool write_phase_ = false; // Determines whether the VDP is expecting the low or high byte of a write.
|
||||
uint8_t low_write_ = 0; // Buffers the low byte of a write.
|
||||
|
||||
// The various register flags.
|
||||
int next_screen_mode_ = 0, screen_mode_ = 0;
|
||||
bool next_blank_screen_ = true, blank_screen_ = true;
|
||||
// Various programmable flags.
|
||||
bool mode1_enable_ = false;
|
||||
bool mode2_enable_ = false;
|
||||
bool mode3_enable_ = false;
|
||||
bool blank_display_ = false;
|
||||
bool sprites_16x16_ = false;
|
||||
bool sprites_magnified_ = false;
|
||||
bool generate_interrupts_ = false;
|
||||
int sprite_height_ = 8;
|
||||
uint16_t pattern_name_address_ = 0;
|
||||
uint16_t colour_table_address_ = 0;
|
||||
uint16_t pattern_generator_table_address_ = 0;
|
||||
uint16_t sprite_attribute_table_address_ = 0;
|
||||
uint16_t sprite_generator_table_address_ = 0;
|
||||
|
||||
size_t pattern_name_address_ = 0; // i.e. address of the tile map.
|
||||
size_t colour_table_address_ = 0; // address of the colour map (if applicable).
|
||||
size_t pattern_generator_table_address_ = 0; // address of the tile contents.
|
||||
size_t sprite_attribute_table_address_ = 0; // address of the sprite list.
|
||||
size_t sprite_generator_table_address_ = 0; // address of the sprite contents.
|
||||
|
||||
uint8_t text_colour_ = 0;
|
||||
uint8_t background_colour_ = 0;
|
||||
|
||||
HalfCycles half_cycles_into_frame_;
|
||||
int column_ = 0, row_ = 0, output_column_ = 0;
|
||||
// This implementation of this chip officially accepts a 3.58Mhz clock, but runs
|
||||
// internally at 5.37Mhz. The following two help to maintain a lossless conversion
|
||||
// from the one to the other.
|
||||
int cycles_error_ = 0;
|
||||
uint32_t *pixel_target_ = nullptr, *pixel_base_ = nullptr;
|
||||
HalfCycles half_cycles_before_internal_cycles(int internal_cycles);
|
||||
|
||||
void output_border(int cycles);
|
||||
// Internal mechanisms for position tracking.
|
||||
int latched_column_ = 0;
|
||||
|
||||
// Vertical timing details.
|
||||
int frame_lines_ = 262;
|
||||
int first_vsync_line_ = 227;
|
||||
// A helper function to output the current border colour for
|
||||
// the number of cycles supplied.
|
||||
void output_border(int cycles, uint32_t cram_dot);
|
||||
|
||||
// A struct to contain timing information for the current mode.
|
||||
struct {
|
||||
/*
|
||||
Vertical layout:
|
||||
|
||||
Lines 0 to [pixel_lines]: standard data fetch and drawing will occur.
|
||||
... to [first_vsync_line]: refresh fetches will occur and border will be output.
|
||||
.. to [2.5 or 3 lines later]: vertical sync is output.
|
||||
... to [total lines - 1]: refresh fetches will occur and border will be output.
|
||||
... for one line: standard data fetch will occur, without drawing.
|
||||
*/
|
||||
int total_lines = 262;
|
||||
int pixel_lines = 192;
|
||||
int first_vsync_line = 227;
|
||||
|
||||
// Maximum number of sprite slots to populate;
|
||||
// if sprites beyond this number should be visible
|
||||
// then the appropriate status information will be set.
|
||||
int maximum_visible_sprites = 4;
|
||||
|
||||
// Set the position, in cycles, of the two interrupts,
|
||||
// within a line.
|
||||
struct {
|
||||
int column = 4;
|
||||
int row = 193;
|
||||
} end_of_frame_interrupt_position;
|
||||
int line_interrupt_position = -1;
|
||||
|
||||
// Enables or disabled the recognition of the sprite
|
||||
// list terminator, and sets the terminator value.
|
||||
bool allow_sprite_terminator = true;
|
||||
uint8_t sprite_terminator = 0xd0;
|
||||
} mode_timing_;
|
||||
|
||||
uint8_t line_interrupt_target = 0xff;
|
||||
uint8_t line_interrupt_counter = 0;
|
||||
bool enable_line_interrupts_ = false;
|
||||
bool line_interrupt_pending_ = false;
|
||||
|
||||
// The screen mode is a necessary predecessor to picking the line mode,
|
||||
// which is the thing latched per line.
|
||||
enum class ScreenMode {
|
||||
Blank,
|
||||
Text,
|
||||
MultiColour,
|
||||
ColouredText,
|
||||
Graphics,
|
||||
SMSMode4
|
||||
} screen_mode_;
|
||||
|
||||
// Horizontal selections.
|
||||
enum class LineMode {
|
||||
Text = 0,
|
||||
Character = 1,
|
||||
Refresh = 2
|
||||
} line_mode_ = LineMode::Text;
|
||||
int first_pixel_column_, first_right_border_column_;
|
||||
Text,
|
||||
Character,
|
||||
Refresh,
|
||||
SMS
|
||||
};
|
||||
|
||||
uint8_t pattern_names_[40];
|
||||
uint8_t pattern_buffer_[40];
|
||||
uint8_t colour_buffer_[40];
|
||||
// Temporary buffers collect a representation of this line prior to pixel serialisation.
|
||||
struct LineBuffer {
|
||||
// The line mode describes the proper timing diagram for this line.
|
||||
LineMode line_mode = LineMode::Text;
|
||||
|
||||
struct SpriteSet {
|
||||
// Holds the horizontal scroll position to apply to this line;
|
||||
// of those VDPs currently implemented, affects the Master System only.
|
||||
uint8_t latched_horizontal_scroll = 0;
|
||||
|
||||
// The names array holds pattern names, as an offset into memory, and
|
||||
// potentially flags also.
|
||||
struct {
|
||||
size_t offset = 0;
|
||||
uint8_t flags = 0;
|
||||
} names[40];
|
||||
|
||||
// The patterns array holds tile patterns, corresponding 1:1 with names.
|
||||
// Four bytes per pattern is the maximum required by any
|
||||
// currently-implemented VDP.
|
||||
uint8_t patterns[40][4];
|
||||
|
||||
/*
|
||||
Horizontal layout (on a 342-cycle clock):
|
||||
|
||||
15 cycles right border
|
||||
58 cycles blanking & sync
|
||||
13 cycles left border
|
||||
|
||||
... i.e. to cycle 86, then:
|
||||
|
||||
border up to first_pixel_output_column;
|
||||
pixels up to next_border_column;
|
||||
border up to the end.
|
||||
|
||||
e.g. standard 256-pixel modes will want to set
|
||||
first_pixel_output_column = 86, next_border_column = 342.
|
||||
*/
|
||||
int first_pixel_output_column = 94;
|
||||
int next_border_column = 334;
|
||||
|
||||
// An active sprite is one that has been selected for composition onto
|
||||
// this line.
|
||||
struct ActiveSprite {
|
||||
int index = 0;
|
||||
int row = 0;
|
||||
int index = 0; // The original in-table index of this sprite.
|
||||
int row = 0; // The row of the sprite that should be drawn.
|
||||
int x = 0; // The sprite's x position on screen.
|
||||
|
||||
uint8_t info[4];
|
||||
uint8_t image[2];
|
||||
uint8_t image[4]; // Up to four bytes of image information.
|
||||
int shift_position = 0; // An offset representing how much of the image information has already been drawn.
|
||||
} active_sprites[8];
|
||||
|
||||
int shift_position = 0;
|
||||
} active_sprites[4];
|
||||
int active_sprite_slot = 0;
|
||||
} sprite_sets_[2];
|
||||
int active_sprite_set_ = 0;
|
||||
bool sprites_stopped_ = false;
|
||||
int active_sprite_slot = 0; // A pointer to the slot into which a new active sprite will be deposited, if required.
|
||||
bool sprites_stopped = false; // A special TMS feature is that a sentinel value can be used to prevent any further sprites
|
||||
// being evaluated for display. This flag determines whether the sentinel has yet been reached.
|
||||
|
||||
int access_pointer_ = 0;
|
||||
void reset_sprite_collection();
|
||||
} line_buffers_[313];
|
||||
void posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_y, int screen_row);
|
||||
|
||||
inline void test_sprite(int sprite_number, int screen_row);
|
||||
inline void get_sprite_contents(int start, int cycles, int screen_row);
|
||||
// There is a delay between reading into the line buffer and outputting from there to the screen. That delay
|
||||
// is observeable because reading time affects availability of memory accesses and therefore time in which
|
||||
// to update sprites and tiles, but writing time affects when the palette is used and when the collision flag
|
||||
// may end up being set. So the two processes are slightly decoupled. The end of reading one line may overlap
|
||||
// with the beginning of writing the next, hence the two separate line buffers.
|
||||
struct LineBufferPointer {
|
||||
int row, column;
|
||||
} read_pointer_, write_pointer_;
|
||||
|
||||
// The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly
|
||||
// fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there
|
||||
// isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps
|
||||
// track of pending collisions, for visual reproduction.
|
||||
struct CRAMDot {
|
||||
LineBufferPointer location;
|
||||
uint32_t value;
|
||||
};
|
||||
std::vector<CRAMDot> upcoming_cram_dots_;
|
||||
|
||||
// Extra information that affects the Master System output mode.
|
||||
struct {
|
||||
// Programmer-set flags.
|
||||
bool vertical_scroll_lock = false;
|
||||
bool horizontal_scroll_lock = false;
|
||||
bool hide_left_column = false;
|
||||
bool shift_sprites_8px_left = false;
|
||||
bool mode4_enable = false;
|
||||
uint8_t horizontal_scroll = 0;
|
||||
uint8_t vertical_scroll = 0;
|
||||
|
||||
// The Master System's additional colour RAM.
|
||||
uint32_t colour_ram[32];
|
||||
bool cram_is_selected = false;
|
||||
|
||||
// Holds the vertical scroll position for this frame; this is latched
|
||||
// once and cannot dynamically be changed until the next frame.
|
||||
uint8_t latched_vertical_scroll = 0;
|
||||
|
||||
size_t pattern_name_address;
|
||||
size_t sprite_attribute_table_address;
|
||||
size_t sprite_generator_table_address;
|
||||
} master_system_;
|
||||
|
||||
void set_current_screen_mode() {
|
||||
if(blank_display_) {
|
||||
screen_mode_ = ScreenMode::Blank;
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_sega_vdp(personality_) && master_system_.mode4_enable) {
|
||||
screen_mode_ = ScreenMode::SMSMode4;
|
||||
mode_timing_.maximum_visible_sprites = 8;
|
||||
return;
|
||||
}
|
||||
|
||||
mode_timing_.maximum_visible_sprites = 4;
|
||||
if(!mode1_enable_ && !mode2_enable_ && !mode3_enable_) {
|
||||
screen_mode_ = ScreenMode::ColouredText;
|
||||
return;
|
||||
}
|
||||
|
||||
if(mode1_enable_ && !mode2_enable_ && !mode3_enable_) {
|
||||
screen_mode_ = ScreenMode::Text;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!mode1_enable_ && mode2_enable_ && !mode3_enable_) {
|
||||
screen_mode_ = ScreenMode::Graphics;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!mode1_enable_ && !mode2_enable_ && mode3_enable_) {
|
||||
screen_mode_ = ScreenMode::MultiColour;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: undocumented TMS modes.
|
||||
screen_mode_ = ScreenMode::Blank;
|
||||
}
|
||||
|
||||
void do_external_slot(int access_column) {
|
||||
// Don't do anything if the required time for the access to become executable
|
||||
// has yet to pass.
|
||||
if(access_column < minimum_access_column_) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(queued_access_) {
|
||||
default: return;
|
||||
|
||||
case MemoryAccess::Write:
|
||||
if(master_system_.cram_is_selected) {
|
||||
// Adjust the palette.
|
||||
master_system_.colour_ram[ram_pointer_ & 0x1f] = palette_pack(
|
||||
static_cast<uint8_t>(((read_ahead_buffer_ >> 0) & 3) * 255 / 3),
|
||||
static_cast<uint8_t>(((read_ahead_buffer_ >> 2) & 3) * 255 / 3),
|
||||
static_cast<uint8_t>(((read_ahead_buffer_ >> 4) & 3) * 255 / 3)
|
||||
);
|
||||
|
||||
// Schedule a CRAM dot; this is scheduled for wherever it should appear
|
||||
// on screen. So it's wherever the output stream would be now. Which
|
||||
// is output_lag cycles ago from the point of view of the input stream.
|
||||
upcoming_cram_dots_.emplace_back();
|
||||
CRAMDot &dot = upcoming_cram_dots_.back();
|
||||
|
||||
dot.location.column = write_pointer_.column - output_lag;
|
||||
dot.location.row = write_pointer_.row;
|
||||
|
||||
// Handle before this row conditionally; then handle after (or, more realistically,
|
||||
// exactly at the end of) naturally.
|
||||
if(dot.location.column < 0) {
|
||||
--dot.location.row;
|
||||
dot.location.column += 342;
|
||||
}
|
||||
dot.location.row += dot.location.column / 342;
|
||||
dot.location.column %= 342;
|
||||
|
||||
dot.value = master_system_.colour_ram[ram_pointer_ & 0x1f];
|
||||
} else {
|
||||
ram_[ram_pointer_ & 16383] = read_ahead_buffer_;
|
||||
}
|
||||
break;
|
||||
case MemoryAccess::Read:
|
||||
read_ahead_buffer_ = ram_[ram_pointer_ & 16383];
|
||||
break;
|
||||
}
|
||||
++ram_pointer_;
|
||||
queued_access_ = MemoryAccess::None;
|
||||
}
|
||||
|
||||
/*
|
||||
Fetching routines follow below; they obey the following rules:
|
||||
|
||||
1) input is a start position and an end position; they should perform the proper
|
||||
operations for the period: start <= time < end.
|
||||
2) times are measured relative to a 172-cycles-per-line clock (so: they directly
|
||||
count access windows on the TMS and Master System).
|
||||
3) time 0 is the beginning of the access window immediately after the last pattern/data
|
||||
block fetch that would contribute to this line, in a normal 32-column mode. So:
|
||||
|
||||
* it's cycle 309 on Mattias' TMS diagram;
|
||||
* it's cycle 1238 on his V9938 diagram;
|
||||
* it's after the last background render block in Mask of Destiny's Master System timing diagram.
|
||||
|
||||
That division point was selected, albeit arbitrarily, because it puts all the tile
|
||||
fetches for a single line into the same [0, 171] period.
|
||||
|
||||
4) all of these functions are templated with a `use_end` parameter. That will be true if
|
||||
end is < 172, false otherwise. So functions can use it to eliminate should-exit-not checks,
|
||||
for the more usual path of execution.
|
||||
|
||||
Provided for the benefit of the methods below:
|
||||
|
||||
* the function external_slot(), which will perform any pending VRAM read/write.
|
||||
* the macros slot(n) and external_slot(n) which can be used to schedule those things inside a
|
||||
switch(start)-based implementation.
|
||||
|
||||
All functions should just spool data to intermediary storage. This is because for most VDPs there is
|
||||
a decoupling between fetch pattern and output pattern, and it's neater to keep the same division
|
||||
for the exceptions.
|
||||
*/
|
||||
|
||||
#define slot(n) \
|
||||
if(use_end && end == n) return;\
|
||||
case n
|
||||
|
||||
#define external_slot(n) \
|
||||
slot(n): do_external_slot((n)*2);
|
||||
|
||||
#define external_slots_2(n) \
|
||||
external_slot(n); \
|
||||
external_slot(n+1);
|
||||
|
||||
#define external_slots_4(n) \
|
||||
external_slots_2(n); \
|
||||
external_slots_2(n+2);
|
||||
|
||||
#define external_slots_8(n) \
|
||||
external_slots_4(n); \
|
||||
external_slots_4(n+4);
|
||||
|
||||
#define external_slots_16(n) \
|
||||
external_slots_8(n); \
|
||||
external_slots_8(n+8);
|
||||
|
||||
#define external_slots_32(n) \
|
||||
external_slots_16(n); \
|
||||
external_slots_16(n+16);
|
||||
|
||||
|
||||
/***********************************************
|
||||
TMS9918 Fetching Code
|
||||
************************************************/
|
||||
|
||||
template<bool use_end> void fetch_tms_refresh(int start, int end) {
|
||||
#define refresh(location) \
|
||||
slot(location): \
|
||||
external_slot(location+1);
|
||||
|
||||
#define refreshes_2(location) \
|
||||
refresh(location); \
|
||||
refresh(location+2);
|
||||
|
||||
#define refreshes_4(location) \
|
||||
refreshes_2(location); \
|
||||
refreshes_2(location+4);
|
||||
|
||||
#define refreshes_8(location) \
|
||||
refreshes_4(location); \
|
||||
refreshes_4(location+8);
|
||||
|
||||
switch(start) {
|
||||
default: assert(false);
|
||||
|
||||
/* 44 external slots */
|
||||
external_slots_32(0)
|
||||
external_slots_8(32)
|
||||
external_slots_4(40)
|
||||
|
||||
/* 64 refresh/external slot pairs (= 128 windows) */
|
||||
refreshes_8(44);
|
||||
refreshes_8(60);
|
||||
refreshes_8(76);
|
||||
refreshes_8(92);
|
||||
refreshes_8(108);
|
||||
refreshes_8(124);
|
||||
refreshes_8(140);
|
||||
refreshes_8(156);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#undef refreshes_8
|
||||
#undef refreshes_4
|
||||
#undef refreshes_2
|
||||
#undef refresh
|
||||
}
|
||||
|
||||
template<bool use_end> void fetch_tms_text(int start, int end) {
|
||||
#define fetch_tile_name(location, column) slot(location): line_buffer.names[column].offset = ram_[row_base + column];
|
||||
#define fetch_tile_pattern(location, column) slot(location): line_buffer.patterns[column][0] = ram_[row_offset + size_t(line_buffer.names[column].offset << 3)];
|
||||
|
||||
#define fetch_column(location, column) \
|
||||
fetch_tile_name(location, column); \
|
||||
external_slot(location+1); \
|
||||
fetch_tile_pattern(location+2, column);
|
||||
|
||||
#define fetch_columns_2(location, column) \
|
||||
fetch_column(location, column); \
|
||||
fetch_column(location+3, column+1);
|
||||
|
||||
#define fetch_columns_4(location, column) \
|
||||
fetch_columns_2(location, column); \
|
||||
fetch_columns_2(location+6, column+2);
|
||||
|
||||
#define fetch_columns_8(location, column) \
|
||||
fetch_columns_4(location, column); \
|
||||
fetch_columns_4(location+12, column+4);
|
||||
|
||||
LineBuffer &line_buffer = line_buffers_[write_pointer_.row];
|
||||
const size_t row_base = pattern_name_address_ & (0x3c00 | static_cast<size_t>(write_pointer_.row >> 3) * 40);
|
||||
const size_t row_offset = pattern_generator_table_address_ & (0x3800 | (write_pointer_.row & 7));
|
||||
|
||||
switch(start) {
|
||||
default: assert(false);
|
||||
|
||||
/* 47 external slots (= 47 windows) */
|
||||
external_slots_32(0)
|
||||
external_slots_8(32)
|
||||
external_slots_4(40)
|
||||
external_slots_2(44)
|
||||
external_slot(46)
|
||||
|
||||
/* 40 column fetches (= 120 windows) */
|
||||
fetch_columns_8(47, 0);
|
||||
fetch_columns_8(71, 8);
|
||||
fetch_columns_8(95, 16);
|
||||
fetch_columns_8(119, 24);
|
||||
fetch_columns_8(143, 32);
|
||||
|
||||
/* 5 more external slots */
|
||||
external_slots_4(167);
|
||||
external_slot(171);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#undef fetch_columns_8
|
||||
#undef fetch_columns_4
|
||||
#undef fetch_columns_2
|
||||
#undef fetch_column
|
||||
#undef fetch_tile_pattern
|
||||
#undef fetch_tile_name
|
||||
}
|
||||
|
||||
template<bool use_end> void fetch_tms_character(int start, int end) {
|
||||
#define sprite_fetch_coordinates(location, sprite) \
|
||||
slot(location): \
|
||||
slot(location+1): \
|
||||
line_buffer.active_sprites[sprite].x = \
|
||||
ram_[\
|
||||
sprite_attribute_table_address_ & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 2))\
|
||||
];
|
||||
|
||||
// This implementation doesn't refetch Y; it's unclear to me
|
||||
// whether it's refetched.
|
||||
|
||||
#define sprite_fetch_graphics(location, sprite) \
|
||||
slot(location): \
|
||||
slot(location+1): \
|
||||
slot(location+2): \
|
||||
slot(location+3): {\
|
||||
const uint8_t name = ram_[\
|
||||
sprite_attribute_table_address_ & size_t(0x3f82 | (line_buffer.active_sprites[sprite].index << 2))\
|
||||
] & (sprites_16x16_ ? ~3 : ~0);\
|
||||
line_buffer.active_sprites[sprite].image[2] = ram_[\
|
||||
sprite_attribute_table_address_ & size_t(0x3f83 | (line_buffer.active_sprites[sprite].index << 2))\
|
||||
];\
|
||||
line_buffer.active_sprites[sprite].x -= (line_buffer.active_sprites[sprite].image[2] & 0x80) >> 2;\
|
||||
const size_t graphic_location = sprite_generator_table_address_ & size_t(0x3800 | (name << 3) | line_buffer.active_sprites[sprite].row); \
|
||||
line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location];\
|
||||
line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+16];\
|
||||
}
|
||||
|
||||
#define sprite_fetch_block(location, sprite) \
|
||||
sprite_fetch_coordinates(location, sprite) \
|
||||
sprite_fetch_graphics(location+2, sprite)
|
||||
|
||||
#define sprite_y_read(location, sprite) \
|
||||
slot(location): posit_sprite(sprite_selection_buffer, sprite, ram_[sprite_attribute_table_address_ & (((sprite) << 2) | 0x3f80)], write_pointer_.row);
|
||||
|
||||
#define fetch_tile_name(column) line_buffer.names[column].offset = ram_[(row_base + column) & 0x3fff];
|
||||
|
||||
#define fetch_tile(column) {\
|
||||
line_buffer.patterns[column][1] = ram_[(colour_base + size_t((line_buffer.names[column].offset << 3) >> colour_name_shift)) & 0x3fff]; \
|
||||
line_buffer.patterns[column][0] = ram_[(pattern_base + size_t(line_buffer.names[column].offset << 3)) & 0x3fff]; \
|
||||
}
|
||||
|
||||
#define background_fetch_block(location, column, sprite) \
|
||||
slot(location): fetch_tile_name(column) \
|
||||
external_slot(location+1); \
|
||||
slot(location+2): \
|
||||
slot(location+3): fetch_tile(column) \
|
||||
slot(location+4): fetch_tile_name(column+1) \
|
||||
sprite_y_read(location+5, sprite); \
|
||||
slot(location+6): \
|
||||
slot(location+7): fetch_tile(column+1) \
|
||||
slot(location+8): fetch_tile_name(column+2) \
|
||||
sprite_y_read(location+9, sprite+1); \
|
||||
slot(location+10): \
|
||||
slot(location+11): fetch_tile(column+2) \
|
||||
slot(location+12): fetch_tile_name(column+3) \
|
||||
sprite_y_read(location+13, sprite+2); \
|
||||
slot(location+14): \
|
||||
slot(location+15): fetch_tile(column+3)
|
||||
|
||||
LineBuffer &line_buffer = line_buffers_[write_pointer_.row];
|
||||
LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines];
|
||||
const size_t row_base = pattern_name_address_ & (size_t((write_pointer_.row << 2)&~31) | 0x3c00);
|
||||
|
||||
size_t pattern_base = pattern_generator_table_address_;
|
||||
size_t colour_base = colour_table_address_;
|
||||
int colour_name_shift = 6;
|
||||
|
||||
if(screen_mode_ == ScreenMode::Graphics) {
|
||||
// If this is high resolution mode, allow the row number to affect the pattern and colour addresses.
|
||||
pattern_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5));
|
||||
colour_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5));
|
||||
|
||||
colour_base += size_t(write_pointer_.row & 7);
|
||||
colour_name_shift = 0;
|
||||
} else {
|
||||
colour_base &= size_t(0xffc0);
|
||||
pattern_base &= size_t(0x3800);
|
||||
}
|
||||
|
||||
if(screen_mode_ == ScreenMode::MultiColour) {
|
||||
pattern_base += size_t((write_pointer_.row >> 2) & 7);
|
||||
} else {
|
||||
pattern_base += size_t(write_pointer_.row & 7);
|
||||
}
|
||||
|
||||
switch(start) {
|
||||
default: assert(false);
|
||||
|
||||
external_slots_2(0);
|
||||
|
||||
sprite_fetch_block(2, 0);
|
||||
sprite_fetch_block(8, 1);
|
||||
sprite_fetch_coordinates(14, 2);
|
||||
|
||||
external_slots_4(16);
|
||||
external_slot(20);
|
||||
|
||||
sprite_fetch_graphics(21, 2);
|
||||
sprite_fetch_block(25, 3);
|
||||
|
||||
slot(31):
|
||||
sprite_selection_buffer.reset_sprite_collection();
|
||||
do_external_slot(31*2);
|
||||
external_slots_2(32);
|
||||
external_slot(34);
|
||||
|
||||
sprite_y_read(35, 0);
|
||||
sprite_y_read(36, 1);
|
||||
sprite_y_read(37, 2);
|
||||
sprite_y_read(38, 3);
|
||||
sprite_y_read(39, 4);
|
||||
sprite_y_read(40, 5);
|
||||
sprite_y_read(41, 6);
|
||||
sprite_y_read(42, 7);
|
||||
|
||||
background_fetch_block(43, 0, 8);
|
||||
background_fetch_block(59, 4, 11);
|
||||
background_fetch_block(75, 8, 14);
|
||||
background_fetch_block(91, 12, 17);
|
||||
background_fetch_block(107, 16, 20);
|
||||
background_fetch_block(123, 20, 23);
|
||||
background_fetch_block(139, 24, 26);
|
||||
background_fetch_block(155, 28, 29);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#undef background_fetch_block
|
||||
#undef fetch_tile
|
||||
#undef fetch_tile_name
|
||||
#undef sprite_y_read
|
||||
#undef sprite_fetch_block
|
||||
#undef sprite_fetch_graphics
|
||||
#undef sprite_fetch_coordinates
|
||||
}
|
||||
|
||||
|
||||
/***********************************************
|
||||
Master System Fetching Code
|
||||
************************************************/
|
||||
|
||||
template<bool use_end> void fetch_sms(int start, int end) {
|
||||
#define sprite_fetch(sprite) {\
|
||||
line_buffer.active_sprites[sprite].x = \
|
||||
ram_[\
|
||||
master_system_.sprite_attribute_table_address & size_t(0x3f80 | (line_buffer.active_sprites[sprite].index << 1))\
|
||||
] - (master_system_.shift_sprites_8px_left ? 8 : 0); \
|
||||
const uint8_t name = ram_[\
|
||||
master_system_.sprite_attribute_table_address & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 1))\
|
||||
] & (sprites_16x16_ ? ~1 : ~0);\
|
||||
const size_t graphic_location = master_system_.sprite_generator_table_address & size_t(0x2000 | (name << 5) | (line_buffer.active_sprites[sprite].row << 2)); \
|
||||
line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location]; \
|
||||
line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+1]; \
|
||||
line_buffer.active_sprites[sprite].image[2] = ram_[graphic_location+2]; \
|
||||
line_buffer.active_sprites[sprite].image[3] = ram_[graphic_location+3]; \
|
||||
}
|
||||
|
||||
#define sprite_fetch_block(location, sprite) \
|
||||
slot(location): \
|
||||
slot(location+1): \
|
||||
slot(location+2): \
|
||||
slot(location+3): \
|
||||
slot(location+4): \
|
||||
slot(location+5): \
|
||||
sprite_fetch(sprite);\
|
||||
sprite_fetch(sprite+1);
|
||||
|
||||
#define sprite_y_read(location, sprite) \
|
||||
slot(location): \
|
||||
posit_sprite(sprite_selection_buffer, sprite, ram_[master_system_.sprite_attribute_table_address & ((sprite) | 0x3f00)], write_pointer_.row); \
|
||||
posit_sprite(sprite_selection_buffer, sprite+1, ram_[master_system_.sprite_attribute_table_address & ((sprite + 1) | 0x3f00)], write_pointer_.row); \
|
||||
|
||||
#define fetch_tile_name(column, row_info) {\
|
||||
const size_t scrolled_column = (column - horizontal_offset) & 0x1f;\
|
||||
const size_t address = row_info.pattern_address_base + (scrolled_column << 1); \
|
||||
line_buffer.names[column].flags = ram_[address+1]; \
|
||||
line_buffer.names[column].offset = static_cast<size_t>( \
|
||||
(((line_buffer.names[column].flags&1) << 8) | ram_[address]) << 5 \
|
||||
) + row_info.sub_row[(line_buffer.names[column].flags&4) >> 2]; \
|
||||
}
|
||||
|
||||
#define fetch_tile(column) \
|
||||
line_buffer.patterns[column][0] = ram_[line_buffer.names[column].offset]; \
|
||||
line_buffer.patterns[column][1] = ram_[line_buffer.names[column].offset+1]; \
|
||||
line_buffer.patterns[column][2] = ram_[line_buffer.names[column].offset+2]; \
|
||||
line_buffer.patterns[column][3] = ram_[line_buffer.names[column].offset+3];
|
||||
|
||||
#define background_fetch_block(location, column, sprite, row_info) \
|
||||
slot(location): fetch_tile_name(column, row_info) \
|
||||
external_slot(location+1); \
|
||||
slot(location+2): \
|
||||
slot(location+3): \
|
||||
slot(location+4): \
|
||||
fetch_tile(column) \
|
||||
fetch_tile_name(column+1, row_info) \
|
||||
sprite_y_read(location+5, sprite); \
|
||||
slot(location+6): \
|
||||
slot(location+7): \
|
||||
slot(location+8): \
|
||||
fetch_tile(column+1) \
|
||||
fetch_tile_name(column+2, row_info) \
|
||||
sprite_y_read(location+9, sprite+2); \
|
||||
slot(location+10): \
|
||||
slot(location+11): \
|
||||
slot(location+12): \
|
||||
fetch_tile(column+2) \
|
||||
fetch_tile_name(column+3, row_info) \
|
||||
sprite_y_read(location+13, sprite+4); \
|
||||
slot(location+14): \
|
||||
slot(location+15): fetch_tile(column+3)
|
||||
|
||||
// Determine the coarse horizontal scrolling offset; this isn't applied on the first two lines if the programmer has requested it.
|
||||
LineBuffer &line_buffer = line_buffers_[write_pointer_.row];
|
||||
LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines];
|
||||
const int horizontal_offset = (write_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) ? (line_buffer.latched_horizontal_scroll >> 3) : 0;
|
||||
|
||||
// Limit address bits in use if this is a SMS2 mode.
|
||||
const bool is_tall_mode = mode_timing_.pixel_lines != 192;
|
||||
const size_t pattern_name_address = master_system_.pattern_name_address | (is_tall_mode ? 0x800 : 0);
|
||||
const size_t pattern_name_offset = is_tall_mode ? 0x100 : 0;
|
||||
|
||||
// Determine row info for the screen both (i) if vertical scrolling is applied; and (ii) if it isn't.
|
||||
// The programmer can opt out of applying vertical scrolling to the right-hand portion of the display.
|
||||
const int scrolled_row = (write_pointer_.row + master_system_.latched_vertical_scroll) % (is_tall_mode ? 256 : 224);
|
||||
struct RowInfo {
|
||||
size_t pattern_address_base;
|
||||
size_t sub_row[2];
|
||||
};
|
||||
const RowInfo scrolled_row_info = {
|
||||
(pattern_name_address & size_t(((scrolled_row & ~7) << 3) | 0x3800)) - pattern_name_offset,
|
||||
{static_cast<size_t>((scrolled_row & 7) << 2), 28 ^ static_cast<size_t>((scrolled_row & 7) << 2)}
|
||||
};
|
||||
RowInfo row_info;
|
||||
if(master_system_.vertical_scroll_lock) {
|
||||
row_info.pattern_address_base = (pattern_name_address & size_t(((write_pointer_.row & ~7) << 3) | 0x3800)) - pattern_name_offset;
|
||||
row_info.sub_row[0] = size_t((write_pointer_.row & 7) << 2);
|
||||
row_info.sub_row[1] = 28 ^ size_t((write_pointer_.row & 7) << 2);
|
||||
} else row_info = scrolled_row_info;
|
||||
|
||||
// ... and do the actual fetching, which follows this routine:
|
||||
switch(start) {
|
||||
default: assert(false);
|
||||
|
||||
sprite_fetch_block(0, 0);
|
||||
sprite_fetch_block(6, 2);
|
||||
|
||||
external_slots_4(12);
|
||||
external_slot(16);
|
||||
|
||||
sprite_fetch_block(17, 4);
|
||||
sprite_fetch_block(23, 6);
|
||||
|
||||
slot(29):
|
||||
sprite_selection_buffer.reset_sprite_collection();
|
||||
do_external_slot(29*2);
|
||||
external_slot(30);
|
||||
|
||||
sprite_y_read(31, 0);
|
||||
sprite_y_read(32, 2);
|
||||
sprite_y_read(33, 4);
|
||||
sprite_y_read(34, 6);
|
||||
sprite_y_read(35, 8);
|
||||
sprite_y_read(36, 10);
|
||||
sprite_y_read(37, 12);
|
||||
sprite_y_read(38, 14);
|
||||
|
||||
background_fetch_block(39, 0, 16, scrolled_row_info);
|
||||
background_fetch_block(55, 4, 22, scrolled_row_info);
|
||||
background_fetch_block(71, 8, 28, scrolled_row_info);
|
||||
background_fetch_block(87, 12, 34, scrolled_row_info);
|
||||
background_fetch_block(103, 16, 40, scrolled_row_info);
|
||||
background_fetch_block(119, 20, 46, scrolled_row_info);
|
||||
background_fetch_block(135, 24, 52, row_info);
|
||||
background_fetch_block(151, 28, 58, row_info);
|
||||
|
||||
external_slots_4(167);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#undef background_fetch_block
|
||||
#undef fetch_tile
|
||||
#undef fetch_tile_name
|
||||
#undef sprite_y_read
|
||||
#undef sprite_fetch_block
|
||||
#undef sprite_fetch
|
||||
}
|
||||
|
||||
#undef external_slot
|
||||
#undef slot
|
||||
|
||||
uint32_t *pixel_target_ = nullptr, *pixel_origin_ = nullptr;
|
||||
bool asked_for_write_area_ = false;
|
||||
void draw_tms_character(int start, int end);
|
||||
void draw_tms_text(int start, int end);
|
||||
void draw_sms(int start, int end, uint32_t cram_dot);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* TMS9918Base_hpp */
|
||||
|
||||
@@ -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),
|
||||
@@ -173,11 +204,15 @@ void AY38910::select_register(uint8_t r) {
|
||||
}
|
||||
|
||||
void AY38910::set_register_value(uint8_t value) {
|
||||
// There are only 16 registers.
|
||||
if(selected_register_ > 15) return;
|
||||
registers_[selected_register_] = value;
|
||||
|
||||
// If this is a register that affects audio output, enqueue a mutation onto the
|
||||
// audio generation thread.
|
||||
if(selected_register_ < 14) {
|
||||
int selected_register = selected_register_;
|
||||
const int selected_register = selected_register_;
|
||||
task_queue_.defer([=] () {
|
||||
// Perform any register-specific mutation to output generation.
|
||||
uint8_t masked_value = value;
|
||||
switch(selected_register) {
|
||||
case 0: case 2: case 4:
|
||||
@@ -208,12 +243,34 @@ void AY38910::set_register_value(uint8_t value) {
|
||||
envelope_position_ = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Store a copy of the current register within the storage used by the audio generation
|
||||
// thread, and apply any changes to output volume.
|
||||
output_registers_[selected_register] = masked_value;
|
||||
evaluate_output_volume();
|
||||
});
|
||||
} else {
|
||||
if(port_handler_) port_handler_->set_port_output(selected_register_ == 15, value);
|
||||
}
|
||||
|
||||
// Decide which outputs are going to need updating (if any).
|
||||
bool update_port_a = false;
|
||||
bool update_port_b = true;
|
||||
if(port_handler_) {
|
||||
if(selected_register_ == 7) {
|
||||
const uint8_t io_change = registers_[7] ^ value;
|
||||
update_port_b = !!(io_change&0x80);
|
||||
update_port_a = !!(io_change&0x40);
|
||||
} else {
|
||||
update_port_b = selected_register_ == 15;
|
||||
update_port_a = selected_register_ != 15;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep a copy of the new value that is usable from the emulation thread.
|
||||
registers_[selected_register_] = value;
|
||||
|
||||
// Update ports as required.
|
||||
if(update_port_b) set_port_output(true);
|
||||
if(update_port_a) set_port_output(false);
|
||||
}
|
||||
|
||||
uint8_t AY38910::get_register_value() {
|
||||
@@ -238,6 +295,8 @@ uint8_t AY38910::get_port_output(bool port_b) {
|
||||
|
||||
void AY38910::set_port_handler(PortHandler *handler) {
|
||||
port_handler_ = handler;
|
||||
set_port_output(true);
|
||||
set_port_output(false);
|
||||
}
|
||||
|
||||
void AY38910::set_data_input(uint8_t r) {
|
||||
@@ -245,6 +304,16 @@ void AY38910::set_data_input(uint8_t r) {
|
||||
update_bus();
|
||||
}
|
||||
|
||||
void AY38910::set_port_output(bool port_b) {
|
||||
// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor,
|
||||
// so that when in the "input" mode, all pins will read normally high". Therefore,
|
||||
// report programmer selection of input mode as creating an output of 0xff.
|
||||
if(port_handler_) {
|
||||
const bool is_output = !!(registers_[7] & (port_b ? 0x80 : 0x40));
|
||||
port_handler_->set_port_output(port_b, is_output ? registers_[port_b ? 15 : 14] : 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t AY38910::get_data_output() {
|
||||
if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) {
|
||||
// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the
|
||||
@@ -253,7 +322,7 @@ uint8_t AY38910::get_data_output() {
|
||||
const uint8_t mask = port_handler_ ? port_handler_->get_port_input(selected_register_ == 15) : 0xff;
|
||||
|
||||
switch(selected_register_) {
|
||||
default: break;
|
||||
default: break;
|
||||
case 14: return mask & ((registers_[0x7] & 0x40) ? registers_[14] : 0xff);
|
||||
case 15: return mask & ((registers_[0x7] & 0x80) ? registers_[15] : 0xff);
|
||||
}
|
||||
@@ -280,7 +349,7 @@ void AY38910::update_bus() {
|
||||
// Assume no output, unless this turns out to be a read.
|
||||
data_output_ = 0xff;
|
||||
switch(control_state_) {
|
||||
default: break;
|
||||
default: break;
|
||||
case LatchAddress: select_register(data_input_); break;
|
||||
case Write: set_register_value(data_input_); break;
|
||||
case Read: data_output_ = get_register_value(); break;
|
||||
|
||||
@@ -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);
|
||||
@@ -83,7 +91,7 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
*/
|
||||
void set_port_handler(PortHandler *);
|
||||
|
||||
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter; not for public consumption
|
||||
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
bool is_zero_level();
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
@@ -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,
|
||||
@@ -128,10 +136,11 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
uint8_t data_input_, data_output_;
|
||||
|
||||
int16_t output_volume_;
|
||||
inline void evaluate_output_volume();
|
||||
void evaluate_output_volume();
|
||||
|
||||
inline void update_bus();
|
||||
void update_bus();
|
||||
PortHandler *port_handler_ = nullptr;
|
||||
void set_port_output(bool port_b);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace {
|
||||
DiskII::DiskII(int clock_rate) :
|
||||
clock_rate_(clock_rate),
|
||||
inputs_(input_command),
|
||||
drives_{{static_cast<unsigned int>(clock_rate), 300, 1}, {static_cast<unsigned int>(clock_rate), 300, 1}}
|
||||
drives_{{clock_rate, 300, 1}, {clock_rate, 300, 1}}
|
||||
{
|
||||
drives_[0].set_clocking_hint_observer(this);
|
||||
drives_[1].set_clocking_hint_observer(this);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -211,7 +211,7 @@ void DiskII::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int driv
|
||||
drives_[drive].set_disk(disk);
|
||||
}
|
||||
|
||||
void DiskII::process_event(const Storage::Disk::Track::Event &event) {
|
||||
void DiskII::process_event(const Storage::Disk::Drive::Event &event) {
|
||||
if(event.type == Storage::Disk::Track::Event::FluxTransition) {
|
||||
inputs_ &= ~input_flux;
|
||||
flux_duration_ = 2; // Upon detection of a flux transition, the flux flag should stay set for 1us. Emulate that as two cycles.
|
||||
@@ -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 final:
|
||||
public Storage::Disk::Drive::EventDelegate,
|
||||
public ClockingHint::Source,
|
||||
public ClockingHint::Observer {
|
||||
@@ -48,13 +48,13 @@ 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);
|
||||
|
||||
/*!
|
||||
Supplies the image of the state machine (i.e. P6) ROM,
|
||||
Supplies the image of the state machine (i.e. P6) ROM,
|
||||
which dictates how the Disk II will respond to input.
|
||||
|
||||
To reduce processing costs, some assumptions are made by
|
||||
@@ -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::Track::Event &event) override;
|
||||
void process_event(const Storage::Disk::Drive::Event &event) override;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) override;
|
||||
|
||||
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_;
|
||||
|
||||
406
Components/DiskII/IWM.cpp
Normal file
406
Components/DiskII/IWM.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
//
|
||||
// IWM.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 05/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "IWM.hpp"
|
||||
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
using namespace Apple;
|
||||
|
||||
namespace {
|
||||
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) :
|
||||
clock_rate_(clock_rate) {}
|
||||
|
||||
// MARK: - Bus accessors
|
||||
|
||||
uint8_t IWM::read(int address) {
|
||||
access(address);
|
||||
|
||||
// Per Inside Macintosh:
|
||||
//
|
||||
// "Before you can read from any of the disk registers you must set up the state of the IWM so that it
|
||||
// can pass the data through to the MC68000's address space where you'll be able to read it. To do that,
|
||||
// you must first turn off Q7 by reading or writing dBase+q7L. Then turn on Q6 by accessing dBase+q6H.
|
||||
// After that, the IWM will be able to pass data from the disk's RD/SENSE line through to you."
|
||||
//
|
||||
// My understanding:
|
||||
//
|
||||
// Q6 = 1, Q7 = 0 reads the status register. The meaning of the top 'SENSE' bit is then determined by
|
||||
// the CA0,1,2 and SEL switches as described in Inside Macintosh, summarised above as RD/SENSE.
|
||||
|
||||
if(address&1) {
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
switch(state_ & (Q6 | Q7 | ENABLE)) {
|
||||
default:
|
||||
LOG("[IWM] Invalid read\n");
|
||||
return 0xff;
|
||||
|
||||
// "Read all 1s".
|
||||
// printf("Reading all 1s\n");
|
||||
// return 0xff;
|
||||
|
||||
case 0:
|
||||
case ENABLE: { /* Read data register. Zeroing afterwards is a guess. */
|
||||
const auto result = data_register_;
|
||||
|
||||
if(data_register_ & 0x80) {
|
||||
// printf("\n\nIWM:%02x\n\n", data_register_);
|
||||
// printf(".");
|
||||
data_register_ = 0;
|
||||
}
|
||||
// LOG("Reading data register: " << PADHEX(2) << int(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case Q6: case Q6|ENABLE: {
|
||||
/*
|
||||
[If A = 0], Read status register:
|
||||
|
||||
bits 0-4: same as mode register.
|
||||
bit 5: 1 = either /ENBL1 or /ENBL2 is currently low.
|
||||
bit 6: 1 = MZ (reserved for future compatibility; should always be read as 0).
|
||||
bit 7: 1 = SENSE input high; 0 = SENSE input low.
|
||||
|
||||
(/ENBL1 is low when the first drive's motor is on; /ENBL2 is low when the second drive's motor is on.
|
||||
If the 1-second timer is enabled, motors remain on for one second after being programmatically disabled.)
|
||||
*/
|
||||
|
||||
return uint8_t(
|
||||
(mode_&0x1f) |
|
||||
((state_ & ENABLE) ? 0x20 : 0x00) |
|
||||
(sense() & 0x80)
|
||||
);
|
||||
} break;
|
||||
|
||||
case Q7: case Q7|ENABLE:
|
||||
/*
|
||||
Read write-handshake register:
|
||||
|
||||
bits 0-5: reserved for future use (currently read as 1).
|
||||
bit 6: 1 = write state (0 = underrun has occurred; 1 = no underrun so far).
|
||||
bit 7: 1 = write data buffer ready for data (1 = ready; 0 = busy).
|
||||
*/
|
||||
// LOG("Reading write handshake: " << PADHEX(2) << (0x3f | write_handshake_));
|
||||
return 0x3f | write_handshake_;
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
void IWM::write(int address, uint8_t input) {
|
||||
access(address);
|
||||
|
||||
switch(state_ & (Q6 | Q7 | ENABLE)) {
|
||||
default: break;
|
||||
|
||||
case Q7|Q6:
|
||||
/*
|
||||
Write mode register:
|
||||
|
||||
bit 0: 1 = latch mode (should be set in asynchronous mode).
|
||||
bit 1: 0 = synchronous handshake protocol; 1 = asynchronous.
|
||||
bit 2: 0 = 1-second on-board timer enable; 1 = timer disable.
|
||||
bit 3: 0 = slow mode; 1 = fast mode.
|
||||
bit 4: 0 = 7Mhz; 1 = 8Mhz (7 or 8 mHz clock descriptor).
|
||||
bit 5: 1 = test mode; 0 = normal operation.
|
||||
bit 6: 1 = MZ-reset.
|
||||
bit 7: reserved for future expansion.
|
||||
*/
|
||||
|
||||
mode_ = input;
|
||||
|
||||
switch(mode_ & 0x18) {
|
||||
case 0x00: bit_length_ = Cycles(24); break; // slow mode, 7Mhz
|
||||
case 0x08: bit_length_ = Cycles(12); break; // fast mode, 7Mhz
|
||||
case 0x10: bit_length_ = Cycles(32); break; // slow mode, 8Mhz
|
||||
case 0x18: bit_length_ = Cycles(16); break; // fast mode, 8Mhz
|
||||
}
|
||||
LOG("IWM mode is now " << PADHEX(2) << int(mode_));
|
||||
break;
|
||||
|
||||
case Q7|Q6|ENABLE: // Write data register.
|
||||
next_output_ = input;
|
||||
write_handshake_ &= ~0x80;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Switch access
|
||||
|
||||
void IWM::access(int address) {
|
||||
// Keep a record of switch state; bits in state_
|
||||
// should correlate with the anonymous namespace constants
|
||||
// defined at the top of this file — CA0, CA1, etc.
|
||||
address &= 0xf;
|
||||
const auto mask = 1 << (address >> 1);
|
||||
const auto old_state = state_;
|
||||
|
||||
if(address & 1) {
|
||||
state_ |= mask;
|
||||
} else {
|
||||
state_ &= ~mask;
|
||||
}
|
||||
|
||||
// React appropriately to ENABLE and DRIVESEL changes, and changes into/out of write mode.
|
||||
if(old_state != state_) {
|
||||
push_drive_state();
|
||||
|
||||
switch(mask) {
|
||||
default: break;
|
||||
|
||||
case ENABLE:
|
||||
if(address & 1) {
|
||||
if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(true);
|
||||
} else {
|
||||
// If the 1-second delay is enabled, set up a timer for that.
|
||||
if(!(mode_ & 4)) {
|
||||
cycles_until_disable_ = Cycles(clock_rate_);
|
||||
} else {
|
||||
if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DRIVESEL: {
|
||||
const int new_drive = address & 1;
|
||||
if(new_drive != active_drive_) {
|
||||
if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(false);
|
||||
active_drive_ = new_drive;
|
||||
if(drives_[active_drive_]) {
|
||||
drives_[active_drive_]->set_enabled(state_ & ENABLE || (cycles_until_disable_ > Cycles(0)));
|
||||
push_drive_state();
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case Q6:
|
||||
case Q7:
|
||||
select_shift_mode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::set_select(bool enabled) {
|
||||
// Store SEL as an extra state bit.
|
||||
if(enabled) state_ |= SEL;
|
||||
else state_ &= ~SEL;
|
||||
push_drive_state();
|
||||
}
|
||||
|
||||
void IWM::push_drive_state() {
|
||||
if(drives_[active_drive_]) {
|
||||
const uint8_t drive_control_lines =
|
||||
((state_ & CA0) ? IWMDrive::CA0 : 0) |
|
||||
((state_ & CA1) ? IWMDrive::CA1 : 0) |
|
||||
((state_ & CA2) ? IWMDrive::CA2 : 0) |
|
||||
((state_ & SEL) ? IWMDrive::SEL : 0) |
|
||||
((state_ & LSTRB) ? IWMDrive::LSTRB : 0);
|
||||
drives_[active_drive_]->set_control_lines(drive_control_lines);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Active logic
|
||||
|
||||
void IWM::run_for(const Cycles cycles) {
|
||||
// Check for a timeout of the motor-off timer.
|
||||
if(cycles_until_disable_ > Cycles(0)) {
|
||||
cycles_until_disable_ -= cycles;
|
||||
if(cycles_until_disable_ <= Cycles(0)) {
|
||||
cycles_until_disable_ = Cycles(0);
|
||||
if(drives_[active_drive_])
|
||||
drives_[active_drive_]->set_enabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Activity otherwise depends on mode and motor state.
|
||||
auto integer_cycles = cycles.as_integral();
|
||||
switch(shift_mode_) {
|
||||
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_ + error_margin) {
|
||||
propose_shift(0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
cycles_since_shift_ += Cycles(integer_cycles);
|
||||
}
|
||||
} break;
|
||||
|
||||
case ShiftMode::Writing:
|
||||
if(drives_[active_drive_]->is_writing()) {
|
||||
while(cycles_since_shift_ + integer_cycles >= bit_length_) {
|
||||
const auto cycles_until_write = bit_length_ - cycles_since_shift_;
|
||||
drives_[active_drive_]->run_for(cycles_until_write);
|
||||
|
||||
// Output a flux transition if the top bit is set.
|
||||
drives_[active_drive_]->write_bit(shift_register_ & 0x80);
|
||||
shift_register_ <<= 1;
|
||||
|
||||
integer_cycles -= cycles_until_write.as_integral();
|
||||
cycles_since_shift_ = Cycles(0);
|
||||
|
||||
--output_bits_remaining_;
|
||||
if(!output_bits_remaining_) {
|
||||
if(!(write_handshake_ & 0x80)) {
|
||||
write_handshake_ |= 0x80;
|
||||
shift_register_ = next_output_;
|
||||
output_bits_remaining_ = 8;
|
||||
// LOG("Next byte: " << PADHEX(2) << int(shift_register_));
|
||||
} else {
|
||||
write_handshake_ &= ~0x40;
|
||||
drives_[active_drive_]->end_writing();
|
||||
// printf("\n");
|
||||
LOG("Overrun; done.");
|
||||
select_shift_mode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cycles_since_shift_ = integer_cycles;
|
||||
if(integer_cycles) {
|
||||
drives_[active_drive_]->run_for(cycles_since_shift_);
|
||||
}
|
||||
} else {
|
||||
drives_[active_drive_]->run_for(cycles);
|
||||
}
|
||||
break;
|
||||
|
||||
case ShiftMode::CheckingWriteProtect:
|
||||
if(integer_cycles < 8) {
|
||||
shift_register_ = (shift_register_ >> integer_cycles) | (sense() & (0xff << (8 - integer_cycles)));
|
||||
} else {
|
||||
shift_register_ = sense();
|
||||
}
|
||||
|
||||
/* Deliberate fallthrough. */
|
||||
default:
|
||||
if(drive_is_rotating_[active_drive_]) drives_[active_drive_]->run_for(cycles);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::select_shift_mode() {
|
||||
// Don't allow an ongoing write to be interrupted.
|
||||
if(shift_mode_ == ShiftMode::Writing && drives_[active_drive_] && drives_[active_drive_]->is_writing()) return;
|
||||
|
||||
const auto old_shift_mode = shift_mode_;
|
||||
|
||||
switch(state_ & (Q6|Q7)) {
|
||||
default: shift_mode_ = ShiftMode::CheckingWriteProtect; break;
|
||||
case 0: shift_mode_ = ShiftMode::Reading; break;
|
||||
case Q7:
|
||||
// "The IWM is put into the write state by a transition from the write protect sense state to the
|
||||
// write load state".
|
||||
if(shift_mode_ == ShiftMode::CheckingWriteProtect) shift_mode_ = ShiftMode::Writing;
|
||||
break;
|
||||
}
|
||||
|
||||
// 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_integral()), false);
|
||||
shift_register_ = next_output_;
|
||||
write_handshake_ |= 0x80 | 0x40;
|
||||
output_bits_remaining_ = 8;
|
||||
LOG("Seeding output with " << PADHEX(2) << shift_register_);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t IWM::sense() {
|
||||
return drives_[active_drive_] ? (drives_[active_drive_]->read() ? 0xff : 0x00) : 0xff;
|
||||
}
|
||||
|
||||
void IWM::process_event(const Storage::Disk::Drive::Event &event) {
|
||||
if(shift_mode_ != ShiftMode::Reading) return;
|
||||
|
||||
switch(event.type) {
|
||||
case Storage::Disk::Track::Event::IndexHole: return;
|
||||
case Storage::Disk::Track::Event::FluxTransition:
|
||||
propose_shift(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if(bit)
|
||||
cycles_since_shift_ = Cycles(0);
|
||||
else
|
||||
cycles_since_shift_ -= bit_length_;
|
||||
}
|
||||
|
||||
void IWM::set_drive(int slot, IWMDrive *drive) {
|
||||
drives_[slot] = drive;
|
||||
drive->set_event_delegate(this);
|
||||
drive->set_clocking_hint_observer(this);
|
||||
}
|
||||
|
||||
void IWM::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) {
|
||||
const bool is_rotating = clocking != ClockingHint::Preference::None;
|
||||
|
||||
if(component == static_cast<ClockingHint::Source *>(drives_[0])) {
|
||||
drive_is_rotating_[0] = is_rotating;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
124
Components/DiskII/IWM.hpp
Normal file
124
Components/DiskII/IWM.hpp
Normal file
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// IWM.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 05/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#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>
|
||||
|
||||
namespace Apple {
|
||||
|
||||
/*!
|
||||
Defines the drive interface used by the IWM, derived from the external pinout as
|
||||
per e.g. https://old.pinouts.ru/HD/MacExtDrive_pinout.shtml
|
||||
|
||||
These are subclassed of Storage::Disk::Drive, so accept any disk the emulator supports,
|
||||
and provide the usual read/write interface for on-disk data.
|
||||
*/
|
||||
struct IWMDrive: public Storage::Disk::Drive {
|
||||
IWMDrive(int input_clock_rate, int number_of_heads) : Storage::Disk::Drive(input_clock_rate, number_of_heads) {}
|
||||
|
||||
enum Line: int {
|
||||
CA0 = 1 << 0,
|
||||
CA1 = 1 << 1,
|
||||
CA2 = 1 << 2,
|
||||
LSTRB = 1 << 3,
|
||||
SEL = 1 << 4,
|
||||
};
|
||||
|
||||
virtual void set_enabled(bool) = 0;
|
||||
virtual void set_control_lines(int) = 0;
|
||||
virtual bool read() = 0;
|
||||
};
|
||||
|
||||
class IWM:
|
||||
public Storage::Disk::Drive::EventDelegate,
|
||||
public ClockingHint::Observer {
|
||||
public:
|
||||
IWM(int clock_rate);
|
||||
|
||||
/// Sets the current external value of the data bus.
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*!
|
||||
Submits an access to address @c address.
|
||||
|
||||
@returns The 8-bit value loaded to the data bus by the IWM.
|
||||
*/
|
||||
uint8_t read(int address);
|
||||
|
||||
/*!
|
||||
Sets the current input of the IWM's SEL line.
|
||||
*/
|
||||
void set_select(bool enabled);
|
||||
|
||||
/// Advances the controller by @c cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
/// 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;
|
||||
|
||||
const int clock_rate_;
|
||||
|
||||
uint8_t data_register_ = 0;
|
||||
uint8_t mode_ = 0;
|
||||
bool read_write_ready_ = true;
|
||||
bool write_overran_ = false;
|
||||
|
||||
int state_ = 0;
|
||||
|
||||
int active_drive_ = 0;
|
||||
IWMDrive *drives_[2] = {nullptr, nullptr};
|
||||
bool drive_is_rotating_[2] = {false, false};
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override;
|
||||
|
||||
Cycles cycles_until_disable_;
|
||||
uint8_t write_handshake_ = 0x80;
|
||||
|
||||
void access(int address);
|
||||
|
||||
uint8_t shift_register_ = 0;
|
||||
uint8_t next_output_ = 0;
|
||||
int output_bits_remaining_ = 0;
|
||||
|
||||
void propose_shift(uint8_t bit);
|
||||
Cycles cycles_since_shift_;
|
||||
Cycles bit_length_ = Cycles(16);
|
||||
|
||||
void push_drive_state();
|
||||
|
||||
enum class ShiftMode {
|
||||
Reading,
|
||||
Writing,
|
||||
CheckingWriteProtect
|
||||
} shift_mode_;
|
||||
|
||||
uint8_t sense();
|
||||
void select_shift_mode();
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endif /* IWM_hpp */
|
||||
181
Components/DiskII/MacintoshDoubleDensityDrive.cpp
Normal file
181
Components/DiskII/MacintoshDoubleDensityDrive.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
//
|
||||
// MacintoshDoubleDensityDrive.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 10/07/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "MacintoshDoubleDensityDrive.hpp"
|
||||
|
||||
/*
|
||||
Sources used pervasively:
|
||||
|
||||
http://members.iinet.net.au/~kalandi/apple/AUG/1991/11%20NOV.DEC/DISK.STUFF.html
|
||||
Apple Guide to the Macintosh Family Hardware
|
||||
Inside Macintosh III
|
||||
*/
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
DoubleDensityDrive::DoubleDensityDrive(int input_clock_rate, bool is_800k) :
|
||||
IWMDrive(input_clock_rate, is_800k ? 2 : 1), // Only 800kb drives are double sided.
|
||||
is_800k_(is_800k) {
|
||||
// Start with a valid rotation speed.
|
||||
if(is_800k) {
|
||||
Drive::set_rotation_speed(393.3807f);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Speed Selection
|
||||
|
||||
void DoubleDensityDrive::did_step(Storage::Disk::HeadPosition to_position) {
|
||||
// printf("At track %d\n", to_position.as_int());
|
||||
// The 800kb drive automatically selects rotation speed as a function of
|
||||
// head position; the 400kb drive doesn't do so.
|
||||
if(is_800k_) {
|
||||
/*
|
||||
Numbers below cribbed from the Kryoflux forums; specifically:
|
||||
https://forum.kryoflux.com/viewtopic.php?t=1090
|
||||
|
||||
They can almost be worked out algorithmically, since the point is to
|
||||
produce an almost-constant value for speed*(number of sectors), and:
|
||||
|
||||
393.3807 * 12 = 4720.5684
|
||||
429.1723 * 11 = 4720.895421
|
||||
472.1435 * 10 = 4721.435
|
||||
524.5672 * 9 = 4721.1048
|
||||
590.1098 * 8 = 4720.8784
|
||||
|
||||
So 4721 / (number of sectors per track in zone) would give essentially
|
||||
the same results.
|
||||
*/
|
||||
const int zone = to_position.as_int() >> 4;
|
||||
switch(zone) {
|
||||
case 0: Drive::set_rotation_speed(393.3807f); break;
|
||||
case 1: Drive::set_rotation_speed(429.1723f); break;
|
||||
case 2: Drive::set_rotation_speed(472.1435f); break;
|
||||
case 3: Drive::set_rotation_speed(524.5672f); break;
|
||||
default: Drive::set_rotation_speed(590.1098f); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DoubleDensityDrive::set_rotation_speed(float revolutions_per_minute) {
|
||||
if(!is_800k_) {
|
||||
// Don't allow drive speeds to drop below 10 RPM, as a temporary sop
|
||||
// to sanity.
|
||||
Drive::set_rotation_speed(std::max(10.0f, revolutions_per_minute));
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Control input/output.
|
||||
|
||||
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) {
|
||||
const auto old_state = control_state_;
|
||||
control_state_ = lines;
|
||||
|
||||
// Catch low-to-high LSTRB transitions.
|
||||
if((old_state ^ control_state_) & control_state_ & Line::LSTRB) {
|
||||
switch(control_state_ & (Line::CA2 | Line::CA1 | Line::CA0 | Line::SEL)) {
|
||||
default:
|
||||
break;
|
||||
|
||||
case 0: // Set step direction — CA2 set => step outward.
|
||||
case Line::CA2:
|
||||
step_direction_ = (control_state_ & Line::CA2) ? -1 : 1;
|
||||
break;
|
||||
|
||||
case Line::CA1: // Set drive motor — CA2 set => motor off.
|
||||
case Line::CA1|Line::CA2:
|
||||
set_motor_on(!(control_state_ & Line::CA2));
|
||||
break;
|
||||
|
||||
case Line::CA0: // Initiate a step.
|
||||
step(Storage::Disk::HeadPosition(step_direction_));
|
||||
break;
|
||||
|
||||
case Line::SEL|Line::CA2: // Reset new disk flag.
|
||||
has_new_disk_ = false;
|
||||
break;
|
||||
|
||||
case Line::CA2 | Line::CA1 | Line::CA0: // Eject the disk.
|
||||
set_disk(nullptr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DoubleDensityDrive::read() {
|
||||
switch(control_state_ & (CA2 | CA1 | CA0 | SEL)) {
|
||||
default:
|
||||
return false;
|
||||
|
||||
case 0: // Head step direction.
|
||||
// (0 = inward)
|
||||
return step_direction_ <= 0;
|
||||
|
||||
case SEL: // Disk in place.
|
||||
// (0 = disk present)
|
||||
return !has_disk();
|
||||
|
||||
case CA0: // Disk head step completed.
|
||||
// (0 = still stepping)
|
||||
return true; // TODO: stepping delay. But at the main Drive level.
|
||||
|
||||
case CA0|SEL: // Disk locked.
|
||||
// (0 = write protected)
|
||||
return !get_is_read_only();
|
||||
|
||||
case CA1: // Disk motor running.
|
||||
// (0 = motor on)
|
||||
return !get_motor_on();
|
||||
|
||||
case CA1|SEL: // Head at track 0.
|
||||
// (0 = at track 0)
|
||||
// "This bit becomes valid beginning 12 msec after the step that places the head at track 0."
|
||||
return !get_is_track_zero();
|
||||
|
||||
case CA1|CA0: // Disk has been ejected.
|
||||
// (0 = user has ejected disk)
|
||||
return !has_new_disk_;
|
||||
|
||||
case CA1|CA0|SEL: // Tachometer.
|
||||
// (arbitrary)
|
||||
return get_tachometer();
|
||||
|
||||
case CA2: // Read data, lower head.
|
||||
set_head(0);
|
||||
return false;
|
||||
|
||||
case CA2|SEL: // Read data, upper head.
|
||||
set_head(1);
|
||||
return false;
|
||||
|
||||
case CA2|CA1: // Single- or double-sided drive.
|
||||
// (0 = single sided)
|
||||
return get_head_count() != 1;
|
||||
|
||||
case CA2|CA1|CA0: // "Present/HD" (per the Mac Plus ROM)
|
||||
// (0 = ??HD??)
|
||||
//
|
||||
// Alternative explanation: "Disk ready for reading?"
|
||||
// (0 = ready)
|
||||
return false;
|
||||
|
||||
case CA2|CA1|CA0|SEL: // Drive installed.
|
||||
// (0 = present, 1 = missing)
|
||||
//
|
||||
// TODO: why do I need to return this the wrong way around for the Mac Plus?
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void DoubleDensityDrive::did_set_disk() {
|
||||
has_new_disk_ = true;
|
||||
}
|
||||
53
Components/DiskII/MacintoshDoubleDensityDrive.hpp
Normal file
53
Components/DiskII/MacintoshDoubleDensityDrive.hpp
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// MacintoshDoubleDensityDrive.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 10/07/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef MacintoshDoubleDensityDrive_hpp
|
||||
#define MacintoshDoubleDensityDrive_hpp
|
||||
|
||||
#include "IWM.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
class DoubleDensityDrive: public IWMDrive {
|
||||
public:
|
||||
DoubleDensityDrive(int input_clock_rate, bool is_800k);
|
||||
|
||||
/*!
|
||||
@returns @c true if this is an 800kb drive; @c false otherwise.
|
||||
*/
|
||||
bool is_800k() {
|
||||
return is_800k_;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current rotation speed of this drive only if it is a 400kb drive.
|
||||
800kb drives select their own rotation speed based on head position,
|
||||
and ignore this input.
|
||||
*/
|
||||
void set_rotation_speed(float revolutions_per_minute);
|
||||
|
||||
void set_enabled(bool) override;
|
||||
void set_control_lines(int) override;
|
||||
bool read() override;
|
||||
|
||||
private:
|
||||
// To receive the proper notifications from Storage::Disk::Drive.
|
||||
void did_step(Storage::Disk::HeadPosition to_position) override;
|
||||
void did_set_disk() override;
|
||||
|
||||
const bool is_800k_;
|
||||
bool has_new_disk_ = false;
|
||||
int control_state_ = 0;
|
||||
int step_direction_ = 1;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* MacintoshDoubleDensityDrive_hpp */
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ ListSelection *ListSelection::list_selection() {
|
||||
}
|
||||
|
||||
BooleanSelection *ListSelection::boolean_selection() {
|
||||
return new BooleanSelection(value != "no" && value != "n");
|
||||
return new BooleanSelection(value != "no" && value != "n" && value != "false" && value != "f");
|
||||
}
|
||||
|
||||
BooleanSelection *BooleanSelection::boolean_selection() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -33,14 +33,16 @@ bool get_bool(const Configurable::SelectionSet &selections_by_option, const std:
|
||||
std::vector<std::unique_ptr<Configurable::Option>> Configurable::standard_options(Configurable::StandardOptions mask) {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> options;
|
||||
if(mask & QuickLoadTape) options.emplace_back(new Configurable::BooleanOption("Load Tapes Quickly", "quickload"));
|
||||
if(mask & (DisplayRGB | DisplayComposite | DisplaySVideo)) {
|
||||
if(mask & (DisplayRGB | DisplayCompositeColour | DisplayCompositeMonochrome | DisplaySVideo)) {
|
||||
std::vector<std::string> display_options;
|
||||
if(mask & DisplayComposite) display_options.emplace_back("composite");
|
||||
if(mask & DisplaySVideo) display_options.emplace_back("svideo");
|
||||
if(mask & DisplayRGB) display_options.emplace_back("rgb");
|
||||
if(mask & DisplayCompositeColour) display_options.emplace_back("composite");
|
||||
if(mask & DisplayCompositeMonochrome) display_options.emplace_back("composite-mono");
|
||||
if(mask & DisplaySVideo) display_options.emplace_back("svideo");
|
||||
if(mask & DisplayRGB) display_options.emplace_back("rgb");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -57,11 +59,16 @@ void Configurable::append_display_selection(Configurable::SelectionSet &selectio
|
||||
std::string string_selection;
|
||||
switch(selection) {
|
||||
default:
|
||||
case Display::RGB: string_selection = "rgb"; break;
|
||||
case Display::SVideo: string_selection = "svideo"; break;
|
||||
case Display::Composite: string_selection = "composite"; break;
|
||||
case Display::RGB: string_selection = "rgb"; break;
|
||||
case Display::SVideo: string_selection = "svideo"; break;
|
||||
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
|
||||
@@ -85,9 +92,17 @@ bool Configurable::get_display(const Configurable::SelectionSet &selections_by_o
|
||||
return true;
|
||||
}
|
||||
if(display->value == "composite") {
|
||||
result = Configurable::Display::Composite;
|
||||
result = Configurable::Display::CompositeColour;
|
||||
return true;
|
||||
}
|
||||
if(display->value == "composite-mono") {
|
||||
result = Configurable::Display::CompositeMonochrome;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Configurable::get_quick_boot(const Configurable::SelectionSet &selections_by_option, bool &result) {
|
||||
return get_bool(selections_by_option, "quickboot", result);
|
||||
}
|
||||
|
||||
@@ -16,15 +16,18 @@ namespace Configurable {
|
||||
enum StandardOptions {
|
||||
DisplayRGB = (1 << 0),
|
||||
DisplaySVideo = (1 << 1),
|
||||
DisplayComposite = (1 << 2),
|
||||
QuickLoadTape = (1 << 3),
|
||||
AutomaticTapeMotorControl = (1 << 4)
|
||||
DisplayCompositeColour = (1 << 2),
|
||||
DisplayCompositeMonochrome = (1 << 3),
|
||||
QuickLoadTape = (1 << 4),
|
||||
AutomaticTapeMotorControl = (1 << 5),
|
||||
QuickBoot = (1 << 6),
|
||||
};
|
||||
|
||||
enum class Display {
|
||||
RGB,
|
||||
SVideo,
|
||||
Composite
|
||||
CompositeColour,
|
||||
CompositeMonochrome
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -47,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.
|
||||
|
||||
@@ -74,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 */
|
||||
|
||||
@@ -185,7 +185,7 @@ class ConcreteJoystick: public Joystick {
|
||||
// convenient hard-coded values. TODO: make these a function of time.
|
||||
using Type = Joystick::Input::Type;
|
||||
switch(input.type) {
|
||||
default: did_set_input(input, is_active ? 1.0f : 0.0f); break;
|
||||
default: did_set_input(input, is_active ? 1.0f : 0.0f); break;
|
||||
case Type::Left: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.1f : 0.5f); break;
|
||||
case Type::Right: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.9f : 0.5f); break;
|
||||
case Type::Up: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.1f : 0.5f); break;
|
||||
@@ -203,7 +203,7 @@ class ConcreteJoystick: public Joystick {
|
||||
// Otherwise apply a threshold test to convert to digital, with remapping from axes to digital inputs.
|
||||
using Type = Joystick::Input::Type;
|
||||
switch(input.type) {
|
||||
default: did_set_input(input, value > 0.5f); break;
|
||||
default: did_set_input(input, value > 0.5f); break;
|
||||
case Type::Horizontal:
|
||||
did_set_input(Input(Type::Left, input.info.control.index), value <= 0.25f);
|
||||
did_set_input(Input(Type::Right, input.info.control.index), value >= 0.75f);
|
||||
|
||||
@@ -10,7 +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, 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);
|
||||
@@ -22,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);
|
||||
@@ -36,3 +47,11 @@ bool Keyboard::get_key_state(Key key) {
|
||||
if(key_offset >= key_states_.size()) return false;
|
||||
return key_states_[key_offset];
|
||||
}
|
||||
|
||||
const std::set<Keyboard::Key> &Keyboard::observed_keys() {
|
||||
return observed_keys_;
|
||||
}
|
||||
|
||||
bool Keyboard::is_exclusive() {
|
||||
return is_exclusive_;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
// Copyright 2017 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Keyboard_hpp
|
||||
#define Keyboard_hpp
|
||||
#ifndef Inputs_Keyboard_hpp
|
||||
#define Inputs_Keyboard_hpp
|
||||
|
||||
#include <vector>
|
||||
#include <set>
|
||||
|
||||
namespace Inputs {
|
||||
|
||||
@@ -20,29 +21,51 @@ namespace Inputs {
|
||||
*/
|
||||
class Keyboard {
|
||||
public:
|
||||
Keyboard();
|
||||
|
||||
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(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, const std::set<Key> &essential_modifiers);
|
||||
|
||||
// Host interface.
|
||||
virtual void set_key_pressed(Key key, char value, bool is_pressed);
|
||||
virtual void reset_all_keys();
|
||||
|
||||
/// @returns a set of all Keys that this keyboard responds to.
|
||||
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();
|
||||
|
||||
// Delegate interface.
|
||||
struct Delegate {
|
||||
virtual void keyboard_did_change_key(Keyboard *keyboard, Key key, bool is_pressed) = 0;
|
||||
@@ -52,10 +75,13 @@ class Keyboard {
|
||||
bool get_key_state(Key key);
|
||||
|
||||
private:
|
||||
std::set<Key> observed_keys_;
|
||||
std::set<Key> essential_modifiers_;
|
||||
std::vector<bool> key_states_;
|
||||
Delegate *delegate_ = nullptr;
|
||||
bool is_exclusive_ = true;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* Keyboard_hpp */
|
||||
#endif /* Inputs_Keyboard_hpp */
|
||||
|
||||
47
Inputs/Mouse.hpp
Normal file
47
Inputs/Mouse.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Mouse.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 11/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Mouse_h
|
||||
#define Mouse_h
|
||||
|
||||
namespace Inputs {
|
||||
|
||||
/*!
|
||||
Models a classic-era mouse: something that provides 2d relative motion plus
|
||||
some quantity of buttons.
|
||||
*/
|
||||
class Mouse {
|
||||
public:
|
||||
/*!
|
||||
Indicates a movement of the mouse.
|
||||
*/
|
||||
virtual void move(int x, int y) {}
|
||||
|
||||
/*!
|
||||
@returns the number of buttons on this mouse.
|
||||
*/
|
||||
virtual int get_number_of_buttons() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*!
|
||||
Indicates that button @c index is now either pressed or unpressed.
|
||||
The intention is that @c index be semantic, not positional:
|
||||
0 for the primary button, 1 for the secondary, 2 for the tertiary, etc.
|
||||
*/
|
||||
virtual void set_button_pressed(int index, bool is_pressed) {}
|
||||
|
||||
/*!
|
||||
Releases all depressed buttons.
|
||||
*/
|
||||
virtual void reset_all_buttons() {}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* Mouse_h */
|
||||
123
Inputs/QuadratureMouse/QuadratureMouse.hpp
Normal file
123
Inputs/QuadratureMouse/QuadratureMouse.hpp
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// QuadratureMouse.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 11/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef QuadratureMouse_hpp
|
||||
#define QuadratureMouse_hpp
|
||||
|
||||
#include "../Mouse.hpp"
|
||||
#include <atomic>
|
||||
|
||||
namespace Inputs {
|
||||
|
||||
/*!
|
||||
Provides a simple implementation of a Mouse, designed for simple
|
||||
thread-safe feeding to a machine that accepts quadrature-encoded input.
|
||||
|
||||
TEMPORARY SIMPLIFICATION: it is assumed that the caller will be interested
|
||||
in observing a signal that dictates velocity, sampling the other to
|
||||
obtain direction only on transitions in the velocity signal.
|
||||
|
||||
Or, more concretely, of the two channels per axis, one is accurate only when
|
||||
the other transitions. Hence the discussion of 'primary' and 'secondary'
|
||||
channels below. This is intended to be fixed.
|
||||
*/
|
||||
class QuadratureMouse: public Mouse {
|
||||
public:
|
||||
QuadratureMouse(int number_of_buttons) :
|
||||
number_of_buttons_(number_of_buttons) {}
|
||||
|
||||
/*
|
||||
Inputs, to satisfy the Mouse interface.
|
||||
*/
|
||||
void move(int x, int y) override {
|
||||
// Accumulate all provided motion.
|
||||
axes_[0] += x;
|
||||
axes_[1] += y;
|
||||
}
|
||||
|
||||
int get_number_of_buttons() override {
|
||||
return number_of_buttons_;
|
||||
}
|
||||
|
||||
void set_button_pressed(int index, bool is_pressed) override {
|
||||
if(is_pressed)
|
||||
button_flags_ |= (1 << index);
|
||||
else
|
||||
button_flags_ &= ~(1 << index);
|
||||
}
|
||||
|
||||
void reset_all_buttons() override {
|
||||
button_flags_ = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Outputs.
|
||||
*/
|
||||
|
||||
/*!
|
||||
Applies a single step from the current accumulated mouse movement, which
|
||||
might involve the mouse moving right, or left, or not at all.
|
||||
*/
|
||||
void prepare_step() {
|
||||
for(int axis = 0; axis < 2; ++axis) {
|
||||
// Do nothing if there's no motion to communicate.
|
||||
const int axis_value = axes_[axis];
|
||||
if(!axis_value) continue;
|
||||
|
||||
// Toggle the primary channel and set the secondary for
|
||||
// negative motion. At present the y axis signals the
|
||||
// secondary channel the opposite way around from the
|
||||
// primary.
|
||||
primaries_[axis] ^= 1;
|
||||
secondaries_[axis] = primaries_[axis] ^ axis;
|
||||
if(axis_value > 0) {
|
||||
-- axes_[axis];
|
||||
secondaries_[axis] ^= 1; // Switch to positive motion.
|
||||
} else {
|
||||
++ axes_[axis];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the two quadrature channels — bit 0 is the 'primary' channel
|
||||
(i.e. the one that can be monitored to observe velocity) and
|
||||
bit 1 is the 'secondary' (i.e. that which can be queried to
|
||||
observe direction).
|
||||
*/
|
||||
int get_channel(int axis) {
|
||||
return primaries_[axis] | (secondaries_[axis] << 1);
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns a bit mask of the currently pressed buttons.
|
||||
*/
|
||||
int get_button_mask() {
|
||||
return button_flags_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns @c true if any mouse motion is waiting to be communicated;
|
||||
@c false otherwise.
|
||||
*/
|
||||
bool has_steps() {
|
||||
return axes_[0] || axes_[1];
|
||||
}
|
||||
|
||||
private:
|
||||
int number_of_buttons_ = 0;
|
||||
std::atomic<int> button_flags_;
|
||||
std::atomic<int> axes_[2];
|
||||
|
||||
int primaries_[2] = {0, 0};
|
||||
int secondaries_[2] = {0, 0};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* QuadratureMouse_hpp */
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
|
||||
#include "../../Analyser/Static/AmstradCPC/Target.hpp"
|
||||
|
||||
@@ -40,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::DisplayComposite)
|
||||
Configurable::StandardOptions(Configurable::DisplayRGB | Configurable::DisplayCompositeColour)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,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);
|
||||
}
|
||||
|
||||
@@ -170,11 +171,15 @@ 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) {
|
||||
establish_palette_hits();
|
||||
build_mode_table();
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f));
|
||||
crt_.set_brightness(3.0f / 2.0f); // As only the values 0, 1 and 2 will be used in each channel,
|
||||
// whereas Red2Green2Blue2 defines a range of 0-3.
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -217,12 +222,12 @@ class CRTCBusHandler {
|
||||
if(cycles_) {
|
||||
switch(previous_output_mode_) {
|
||||
default:
|
||||
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::ColourBurst: crt_->output_default_colour_burst(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::ColourBurst: crt_.output_default_colour_burst(cycles_ * 16); break;
|
||||
case OutputMode::Pixels:
|
||||
crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_);
|
||||
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
|
||||
pixel_pointer_ = pixel_data_ = nullptr;
|
||||
break;
|
||||
}
|
||||
@@ -238,52 +243,54 @@ class CRTCBusHandler {
|
||||
// collect some more pixels if output is ongoing
|
||||
if(previous_output_mode_ == OutputMode::Pixels) {
|
||||
if(!pixel_data_) {
|
||||
pixel_pointer_ = pixel_data_ = crt_->allocate_write_area(320, 8);
|
||||
pixel_pointer_ = pixel_data_ = crt_.begin_data(320, 8);
|
||||
}
|
||||
if(pixel_pointer_) {
|
||||
// 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, cycles_ * 16 / pixel_divider_);
|
||||
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
|
||||
pixel_pointer_ = pixel_data_ = nullptr;
|
||||
cycles_ = 0;
|
||||
}
|
||||
@@ -323,27 +330,14 @@ class CRTCBusHandler {
|
||||
was_hsync_ = state.hsync;
|
||||
}
|
||||
|
||||
/// Constructs an appropriate CRT for video output.
|
||||
void setup_output(float aspect_ratio) {
|
||||
crt_.reset(new Outputs::CRT::CRT(1024, 16, Outputs::CRT::DisplayType::PAL50, 1));
|
||||
crt_->set_rgb_sampling_function(
|
||||
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)"
|
||||
"{"
|
||||
"uint sample = texture(texID, coordinate).r;"
|
||||
"return vec3(float((sample >> 4) & 3u), float((sample >> 2) & 3u), float(sample & 3u)) / 2.0;"
|
||||
"}");
|
||||
crt_->set_visible_area(Outputs::CRT::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f));
|
||||
crt_->set_video_signal(Outputs::CRT::VideoSignal::RGB);
|
||||
/// Sets the destination for output.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// Destructs the CRT.
|
||||
void close_output() {
|
||||
crt_.reset();
|
||||
}
|
||||
|
||||
/// @returns the CRT.
|
||||
Outputs::CRT::CRT *get_crt() {
|
||||
return crt_.get();
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -376,10 +370,18 @@ class CRTCBusHandler {
|
||||
}
|
||||
|
||||
private:
|
||||
void output_border(unsigned int length) {
|
||||
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_->allocate_write_area(1));
|
||||
if(colour_pointer) *colour_pointer = border_;
|
||||
crt_->output_level(length * 16);
|
||||
void output_border(int length) {
|
||||
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)
|
||||
@@ -395,16 +397,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,7 +416,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)];
|
||||
}
|
||||
@@ -423,7 +425,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)];
|
||||
@@ -434,7 +436,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)];
|
||||
@@ -449,7 +451,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)];
|
||||
}
|
||||
@@ -461,7 +463,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)];
|
||||
}
|
||||
@@ -469,7 +471,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)];
|
||||
@@ -486,7 +488,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)];
|
||||
}
|
||||
@@ -507,7 +509,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,19 +530,19 @@ class CRTCBusHandler {
|
||||
Border,
|
||||
Pixels
|
||||
} previous_output_mode_ = OutputMode::Sync;
|
||||
unsigned int cycles_ = 0;
|
||||
int cycles_ = 0;
|
||||
|
||||
bool was_hsync_ = false, was_vsync_ = false;
|
||||
int cycles_into_hsync_ = 0;
|
||||
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
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;
|
||||
|
||||
unsigned int pixel_divider_ = 1;
|
||||
int pixel_divider_ = 1;
|
||||
uint16_t mode0_output_[256];
|
||||
uint32_t mode1_output_[256];
|
||||
uint64_t mode2_output_[256];
|
||||
@@ -572,7 +574,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);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -591,7 +593,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;
|
||||
}
|
||||
|
||||
@@ -602,7 +604,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_;
|
||||
}
|
||||
|
||||
@@ -757,7 +759,7 @@ class i8255PortHandler : public Intel::i8255::PortHandler {
|
||||
template <bool has_fdc> class ConcreteMachine:
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::Machine,
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public Utility::TypeRecipient,
|
||||
public CPU::Z80::BusHandler,
|
||||
public ClockingHint::Observer,
|
||||
@@ -789,33 +791,43 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
ay_.ay().set_port_handler(&key_state_);
|
||||
|
||||
// construct the list of necessary ROMs
|
||||
std::vector<std::string> required_roms = {"amsdos.rom"};
|
||||
const std::string machine_name = "AmstradCPC";
|
||||
std::vector<ROMMachine::ROM> required_roms = {
|
||||
ROMMachine::ROM(machine_name, "the Amstrad Disk Operating System", "amsdos.rom", 16*1024, 0x1fe22ecd)
|
||||
};
|
||||
std::string model_number;
|
||||
uint32_t crcs[2];
|
||||
switch(target.model) {
|
||||
default:
|
||||
model_number = "6128";
|
||||
has_128k_ = true;
|
||||
crcs[0] = 0x0219bb74;
|
||||
crcs[1] = 0xca6af63d;
|
||||
break;
|
||||
case Analyser::Static::AmstradCPC::Target::Model::CPC464:
|
||||
model_number = "464";
|
||||
has_128k_ = false;
|
||||
crcs[0] = 0x815752df;
|
||||
crcs[1] = 0x7d9a3bac;
|
||||
break;
|
||||
case Analyser::Static::AmstradCPC::Target::Model::CPC664:
|
||||
model_number = "664";
|
||||
has_128k_ = false;
|
||||
crcs[0] = 0x3f5a6dc4;
|
||||
crcs[1] = 0x32fee492;
|
||||
break;
|
||||
}
|
||||
required_roms.push_back("os" + model_number + ".rom");
|
||||
required_roms.push_back("basic" + model_number + ".rom");
|
||||
required_roms.emplace_back(machine_name, "the CPC " + model_number + " firmware", "os" + model_number + ".rom", 16*1024, crcs[0]);
|
||||
required_roms.emplace_back(machine_name, "the CPC " + model_number + " BASIC ROM", "basic" + model_number + ".rom", 16*1024, crcs[1]);
|
||||
|
||||
// fetch and verify the ROMs
|
||||
const auto roms = rom_fetcher("AmstradCPC", required_roms);
|
||||
const auto roms = rom_fetcher(required_roms);
|
||||
|
||||
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
|
||||
@@ -860,13 +872,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);
|
||||
@@ -892,9 +906,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
|
||||
@@ -908,19 +924,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:
|
||||
@@ -929,13 +947,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
|
||||
@@ -981,19 +1001,14 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
flush_fdc();
|
||||
}
|
||||
|
||||
/// A CRTMachine function; indicates that outputs should be created now.
|
||||
void setup_output(float aspect_ratio) override final {
|
||||
crtc_bus_handler_.setup_output(aspect_ratio);
|
||||
/// A CRTMachine function; sets the destination for video.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
|
||||
crtc_bus_handler_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// A CRTMachine function; indicates that outputs should be destroyed now.
|
||||
void close_output() override final {
|
||||
crtc_bus_handler_.close_output();
|
||||
}
|
||||
|
||||
/// @returns the CRT in use.
|
||||
Outputs::CRT::CRT *get_crt() override final {
|
||||
return crtc_bus_handler_.get_crt();
|
||||
/// A CRTMachine function; sets the output display type.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override final {
|
||||
crtc_bus_handler_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/// @returns the speaker in use.
|
||||
@@ -1058,7 +1073,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
if(has_fdc) fdc_.set_activity_observer(observer);
|
||||
if constexpr (has_fdc) fdc_.set_activity_observer(observer);
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
@@ -1086,7 +1101,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Joysticks
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return key_state_.get_joysticks();
|
||||
}
|
||||
|
||||
@@ -1148,11 +1163,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_;
|
||||
@@ -1192,7 +1209,7 @@ Machine *Machine::AmstradCPC(const Analyser::Static::Target *target, const ROMMa
|
||||
using Target = Analyser::Static::AmstradCPC::Target;
|
||||
const Target *const cpc_target = dynamic_cast<const Target *>(target);
|
||||
switch(cpc_target->model) {
|
||||
default: return new AmstradCPC::ConcreteMachine<true>(*cpc_target, rom_fetcher);
|
||||
default: return new AmstradCPC::ConcreteMachine<true>(*cpc_target, rom_fetcher);
|
||||
case Target::Model::CPC464: return new AmstradCPC::ConcreteMachine<false>(*cpc_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,27 +57,27 @@ 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
|
||||
}
|
||||
|
||||
uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
#define KEYS(...) {__VA_ARGS__, KeyboardMachine::Machine::KeyEndSequence}
|
||||
#define SHIFT(...) {KeyShift, __VA_ARGS__, KeyboardMachine::Machine::KeyEndSequence}
|
||||
#define X {KeyboardMachine::Machine::KeyNotMapped}
|
||||
#define KEYS(...) {__VA_ARGS__, KeyboardMachine::MappedMachine::KeyEndSequence}
|
||||
#define SHIFT(...) {KeyShift, __VA_ARGS__, KeyboardMachine::MappedMachine::KeyEndSequence}
|
||||
#define X {KeyboardMachine::MappedMachine::KeyNotMapped}
|
||||
static KeySequence key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
|
||||
@@ -33,7 +33,7 @@ enum Key: uint16_t {
|
||||
#undef Line
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public KeyboardMachine::Machine::KeyboardMapper {
|
||||
struct KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,74 +8,86 @@
|
||||
|
||||
#include "AppleII.hpp"
|
||||
|
||||
#include "../../Activity/Source.hpp"
|
||||
#include "../MediaTarget.hpp"
|
||||
#include "../CRTMachine.hpp"
|
||||
#include "../JoystickMachine.hpp"
|
||||
#include "../KeyboardMachine.hpp"
|
||||
#include "../Utility/MemoryFuzzer.hpp"
|
||||
#include "../Utility/StringSerialiser.hpp"
|
||||
#include "../../../Activity/Source.hpp"
|
||||
#include "../../MediaTarget.hpp"
|
||||
#include "../../CRTMachine.hpp"
|
||||
#include "../../JoystickMachine.hpp"
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
#include "../../Utility/MemoryFuzzer.hpp"
|
||||
#include "../../Utility/StringSerialiser.hpp"
|
||||
|
||||
#include "../../Processors/6502/6502.hpp"
|
||||
#include "../../Components/AudioToggle/AudioToggle.hpp"
|
||||
#include "../../../Processors/6502/6502.hpp"
|
||||
#include "../../../Components/AudioToggle/AudioToggle.hpp"
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include "Card.hpp"
|
||||
#include "DiskIICard.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#include "../../Analyser/Static/AppleII/Target.hpp"
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../../Analyser/Static/AppleII/Target.hpp"
|
||||
#include "../../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../../Configurable/StandardOptions.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
template <bool is_iie> class ConcreteMachine:
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayCompositeMonochrome | Configurable::DisplayCompositeColour)
|
||||
);
|
||||
}
|
||||
|
||||
#define is_iie() ((model == Analyser::Static::AppleII::Target::Model::IIe) || (model == Analyser::Static::AppleII::Target::Model::EnhancedIIe))
|
||||
|
||||
template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::Machine,
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Inputs::Keyboard,
|
||||
public AppleII::Machine,
|
||||
public Configurable::Device,
|
||||
public Apple::II::Machine,
|
||||
public Activity::Source,
|
||||
public JoystickMachine::Machine,
|
||||
public AppleII::Card::Delegate {
|
||||
public Apple::II::Card::Delegate {
|
||||
private:
|
||||
struct VideoBusHandler : public AppleII::Video::BusHandler {
|
||||
struct VideoBusHandler : public Apple::II::Video::BusHandler {
|
||||
public:
|
||||
VideoBusHandler(uint8_t *ram, uint8_t *aux_ram) : ram_(ram), aux_ram_(aux_ram) {}
|
||||
|
||||
uint8_t perform_read(uint16_t address) {
|
||||
return ram_[address];
|
||||
}
|
||||
uint16_t perform_aux_read(uint16_t address) {
|
||||
return static_cast<uint16_t>(ram_[address] | (aux_ram_[address] << 8));
|
||||
void perform_read(uint16_t address, size_t count, uint8_t *base_target, uint8_t *auxiliary_target) {
|
||||
memcpy(base_target, &ram_[address], count);
|
||||
memcpy(auxiliary_target, &aux_ram_[address], count);
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t *ram_, *aux_ram_;
|
||||
};
|
||||
|
||||
CPU::MOS6502::Processor<ConcreteMachine, false> m6502_;
|
||||
CPU::MOS6502::Processor<(model == Analyser::Static::AppleII::Target::Model::EnhancedIIe) ? CPU::MOS6502::Personality::PSynertek65C02 : CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
VideoBusHandler video_bus_handler_;
|
||||
std::unique_ptr<AppleII::Video::Video<VideoBusHandler, is_iie>> video_;
|
||||
Apple::II::Video::Video<VideoBusHandler, is_iie()> video_;
|
||||
int cycles_into_current_line_ = 0;
|
||||
Cycles cycles_since_video_update_;
|
||||
|
||||
void update_video() {
|
||||
video_->run_for(cycles_since_video_update_.flush());
|
||||
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;
|
||||
@@ -83,51 +95,64 @@ template <bool is_iie> class ConcreteMachine:
|
||||
|
||||
uint8_t ram_[65536], aux_ram_[65536];
|
||||
std::vector<uint8_t> rom_;
|
||||
std::vector<uint8_t> character_rom_;
|
||||
uint8_t keyboard_input_ = 0x00;
|
||||
bool key_is_down_ = false;
|
||||
|
||||
uint8_t get_keyboard_input() {
|
||||
if(string_serialiser_) {
|
||||
return string_serialiser_->head() | 0x80;
|
||||
} else {
|
||||
return keyboard_input_;
|
||||
}
|
||||
}
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
Outputs::Speaker::LowpassSpeaker<Audio::Toggle> speaker_;
|
||||
Cycles cycles_since_audio_update_;
|
||||
|
||||
// MARK: - Cards
|
||||
std::array<std::unique_ptr<AppleII::Card>, 7> cards_;
|
||||
std::array<std::unique_ptr<Apple::II::Card>, 7> cards_;
|
||||
Cycles cycles_since_card_update_;
|
||||
std::vector<AppleII::Card *> every_cycle_cards_;
|
||||
std::vector<AppleII::Card *> just_in_time_cards_;
|
||||
std::vector<Apple::II::Card *> every_cycle_cards_;
|
||||
std::vector<Apple::II::Card *> just_in_time_cards_;
|
||||
|
||||
int stretched_cycles_since_card_update_ = 0;
|
||||
|
||||
void install_card(std::size_t slot, AppleII::Card *card) {
|
||||
void install_card(std::size_t slot, Apple::II::Card *card) {
|
||||
assert(slot >= 1 && slot < 8);
|
||||
cards_[slot - 1].reset(card);
|
||||
card->set_delegate(this);
|
||||
pick_card_messaging_group(card);
|
||||
}
|
||||
|
||||
bool is_every_cycle_card(AppleII::Card *card) {
|
||||
bool is_every_cycle_card(const Apple::II::Card *card) {
|
||||
return !card->get_select_constraints();
|
||||
}
|
||||
|
||||
void pick_card_messaging_group(AppleII::Card *card) {
|
||||
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<AppleII::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_;
|
||||
std::vector<AppleII::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_;
|
||||
std::vector<Apple::II::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_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(AppleII::Card *card) override {
|
||||
void card_did_change_select_constraints(Apple::II::Card *card) override {
|
||||
pick_card_messaging_group(card);
|
||||
}
|
||||
|
||||
AppleII::DiskIICard *diskii_card() {
|
||||
return dynamic_cast<AppleII::DiskIICard *>(cards_[5].get());
|
||||
Apple::II::DiskIICard *diskii_card() {
|
||||
return dynamic_cast<Apple::II::DiskIICard *>(cards_[5].get());
|
||||
}
|
||||
|
||||
// MARK: - Memory Map.
|
||||
@@ -145,7 +170,7 @@ template <bool is_iie> class ConcreteMachine:
|
||||
|
||||
On a IIe with auxiliary memory the following orthogonal changes also need to be factored in:
|
||||
|
||||
0000 to 0200 : can be paged independently of the rest of RAM, other than part of the language card area which pages with it
|
||||
0000 to 0200 : can be paged independently of the rest of RAM, other than part of the language card area which pages with it
|
||||
0400 to 0800 : the text screen, can be configured to write to auxiliary RAM
|
||||
2000 to 4000 : the graphics screen, which can be configured to write to auxiliary RAM
|
||||
c100 to d000 : can be used to page an additional 3.75kb of ROM, replacing the IO area
|
||||
@@ -179,7 +204,7 @@ template <bool is_iie> class ConcreteMachine:
|
||||
bool has_language_card_ = true;
|
||||
void set_language_card_paging() {
|
||||
uint8_t *const ram = alternative_zero_page_ ? aux_ram_ : ram_;
|
||||
uint8_t *const rom = is_iie ? &rom_[3840] : rom_.data();
|
||||
uint8_t *const rom = is_iie() ? &rom_[3840] : rom_.data();
|
||||
|
||||
page(0xd0, 0xe0,
|
||||
language_card_.read ? &ram[language_card_.bank1 ? 0xd000 : 0xc000] : rom,
|
||||
@@ -225,13 +250,13 @@ template <bool is_iie> class ConcreteMachine:
|
||||
read_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200],
|
||||
write_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200]);
|
||||
|
||||
if(video_ && video_->get_80_store()) {
|
||||
bool use_aux_ram = video_->get_page2();
|
||||
if(video_.get_80_store()) {
|
||||
bool use_aux_ram = video_.get_page2();
|
||||
page(0x04, 0x08,
|
||||
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400],
|
||||
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400]);
|
||||
|
||||
if(video_->get_high_resolution()) {
|
||||
if(video_.get_high_resolution()) {
|
||||
page(0x20, 0x40,
|
||||
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000],
|
||||
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000]);
|
||||
@@ -298,16 +323,17 @@ template <bool is_iie> class ConcreteMachine:
|
||||
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::AppleII::Target &target, const ROMMachine::ROMFetcher &rom_fetcher):
|
||||
m6502_(*this),
|
||||
video_bus_handler_(ram_, aux_ram_),
|
||||
audio_toggle_(audio_queue_),
|
||||
speaker_(audio_toggle_) {
|
||||
// The system's master clock rate.
|
||||
const float master_clock = 14318180.0;
|
||||
m6502_(*this),
|
||||
video_bus_handler_(ram_, aux_ram_),
|
||||
video_(video_bus_handler_),
|
||||
audio_toggle_(audio_queue_),
|
||||
speaker_(audio_toggle_) {
|
||||
// The system's master clock rate.
|
||||
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
|
||||
// the master clock rate divided by 14 because every 65th cycle is extended by one seventh.
|
||||
// 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
|
||||
// the master clock rate divided by 14 because every 65th cycle is extended by one seventh.
|
||||
set_clock_rate((master_clock / 14.0) * 65.0 / (65.0 + 1.0 / 7.0));
|
||||
|
||||
// The speaker, however, should think it is clocked at half the master clock, per a general
|
||||
@@ -324,30 +350,44 @@ template <bool is_iie> class ConcreteMachine:
|
||||
Memory::Fuzz(aux_ram_, sizeof(aux_ram_));
|
||||
|
||||
// Add a couple of joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
|
||||
// Pick the required ROMs.
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
std::vector<std::string> rom_names;
|
||||
const std::string machine_name = "AppleII";
|
||||
std::vector<ROMMachine::ROM> rom_descriptions;
|
||||
size_t rom_size = 12*1024;
|
||||
switch(target.model) {
|
||||
default:
|
||||
rom_names.push_back("apple2-character.rom");
|
||||
rom_names.push_back("apple2o.rom");
|
||||
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588);
|
||||
break;
|
||||
case Target::Model::IIplus:
|
||||
rom_names.push_back("apple2-character.rom");
|
||||
rom_names.push_back("apple2.rom");
|
||||
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26);
|
||||
break;
|
||||
case Target::Model::IIe:
|
||||
rom_size += 3840;
|
||||
rom_names.push_back("apple2eu-character.rom");
|
||||
rom_names.push_back("apple2eu.rom");
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1);
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d);
|
||||
break;
|
||||
case Target::Model::EnhancedIIe:
|
||||
rom_size += 3840;
|
||||
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d);
|
||||
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942);
|
||||
break;
|
||||
}
|
||||
const auto roms = rom_fetcher("AppleII", rom_names);
|
||||
const auto roms = rom_fetcher(rom_descriptions);
|
||||
|
||||
// Try to install a Disk II card now, before checking the ROM list,
|
||||
// to make sure that Disk II dependencies have been communicated.
|
||||
if(target.disk_controller != Target::DiskController::None) {
|
||||
// Apple recommended slot 6 for the (first) Disk II.
|
||||
install_card(6, new Apple::II::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector));
|
||||
}
|
||||
|
||||
// Now, check and move the ROMs.
|
||||
if(!roms[0] || !roms[1]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
@@ -357,12 +397,7 @@ template <bool is_iie> class ConcreteMachine:
|
||||
rom_.erase(rom_.begin(), rom_.end() - static_cast<off_t>(rom_size));
|
||||
}
|
||||
|
||||
character_rom_ = std::move(*roms[0]);
|
||||
|
||||
if(target.disk_controller != Target::DiskController::None) {
|
||||
// Apple recommended slot 6 for the (first) Disk II.
|
||||
install_card(6, new AppleII::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector));
|
||||
}
|
||||
video_.set_character_rom(*roms[0]);
|
||||
|
||||
// Set up the default memory blocks. On a II or II+ these values will never change.
|
||||
// On a IIe they'll be affected by selection of auxiliary RAM.
|
||||
@@ -382,17 +417,13 @@ template <bool is_iie> class ConcreteMachine:
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
void setup_output(float aspect_ratio) override {
|
||||
video_.reset(new AppleII::Video::Video<VideoBusHandler, is_iie>(video_bus_handler_));
|
||||
video_->set_character_rom(character_rom_);
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void close_output() override {
|
||||
video_.reset();
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *get_crt() override {
|
||||
return video_->get_crt();
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
video_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
@@ -420,9 +451,12 @@ template <bool is_iie> class ConcreteMachine:
|
||||
bool has_updated_cards = false;
|
||||
if(read_pages_[address >> 8]) {
|
||||
if(isReadOperation(operation)) *value = read_pages_[address >> 8][address & 0xff];
|
||||
else if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value;
|
||||
else {
|
||||
if(address >= 0x200 && address < 0x6000) update_video();
|
||||
if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value;
|
||||
}
|
||||
|
||||
if(is_iie && address >= 0xc300 && address < 0xd000) {
|
||||
if(is_iie() && address >= 0xc300 && address < 0xd000) {
|
||||
bool internal_c8_rom = internal_c8_rom_;
|
||||
internal_c8_rom |= ((address >> 8) == 0xc3) && !slot_C3_rom_;
|
||||
internal_c8_rom &= (address != 0xcfff);
|
||||
@@ -446,7 +480,7 @@ template <bool is_iie> class ConcreteMachine:
|
||||
// actor, but this will actually be the result most of the time so it's not
|
||||
// too terrible.
|
||||
if(isReadOperation(operation) && address != 0xc000) {
|
||||
*value = video_->get_last_read_value(cycles_since_video_update_);
|
||||
*value = video_.get_last_read_value(cycles_since_video_update_);
|
||||
}
|
||||
|
||||
switch(address) {
|
||||
@@ -457,18 +491,18 @@ template <bool is_iie> class ConcreteMachine:
|
||||
default: break;
|
||||
|
||||
case 0xc000:
|
||||
if(string_serialiser_) {
|
||||
*value = string_serialiser_->head() | 0x80;
|
||||
} else {
|
||||
*value = keyboard_input_;
|
||||
}
|
||||
*value = get_keyboard_input();
|
||||
break;
|
||||
case 0xc001: case 0xc002: case 0xc003: case 0xc004: case 0xc005: case 0xc006: case 0xc007:
|
||||
case 0xc008: case 0xc009: case 0xc00a: case 0xc00b: case 0xc00c: case 0xc00d: case 0xc00e: case 0xc00f:
|
||||
*value = (*value & 0x80) | (get_keyboard_input() & 0x7f);
|
||||
break;
|
||||
|
||||
case 0xc061: // Switch input 0.
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
static_cast<Joystick *>(joysticks_[0].get())->buttons[0] || static_cast<Joystick *>(joysticks_[1].get())->buttons[2] ||
|
||||
(is_iie && open_apple_is_pressed_)
|
||||
(is_iie() && open_apple_is_pressed_)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
@@ -476,7 +510,7 @@ template <bool is_iie> class ConcreteMachine:
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
static_cast<Joystick *>(joysticks_[0].get())->buttons[1] || static_cast<Joystick *>(joysticks_[1].get())->buttons[1] ||
|
||||
(is_iie && closed_apple_is_pressed_)
|
||||
(is_iie() && closed_apple_is_pressed_)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
@@ -498,33 +532,38 @@ template <bool is_iie> class ConcreteMachine:
|
||||
} break;
|
||||
|
||||
// The IIe-only state reads follow...
|
||||
case 0xc011: if(is_iie) *value = (*value & 0x7f) | (language_card_.bank1 ? 0x80 : 0x00); break;
|
||||
case 0xc012: if(is_iie) *value = (*value & 0x7f) | (language_card_.read ? 0x80 : 0x00); break;
|
||||
case 0xc013: if(is_iie) *value = (*value & 0x7f) | (read_auxiliary_memory_ ? 0x80 : 0x00); break;
|
||||
case 0xc014: if(is_iie) *value = (*value & 0x7f) | (write_auxiliary_memory_ ? 0x80 : 0x00); break;
|
||||
case 0xc015: if(is_iie) *value = (*value & 0x7f) | (internal_CX_rom_ ? 0x80 : 0x00); break;
|
||||
case 0xc016: if(is_iie) *value = (*value & 0x7f) | (alternative_zero_page_ ? 0x80 : 0x00); break;
|
||||
case 0xc017: if(is_iie) *value = (*value & 0x7f) | (slot_C3_rom_ ? 0x80 : 0x00); break;
|
||||
case 0xc018: if(is_iie) *value = (*value & 0x7f) | (video_->get_80_store() ? 0x80 : 0x00); break;
|
||||
case 0xc019: if(is_iie) *value = (*value & 0x7f) | (video_->get_is_vertical_blank(cycles_since_video_update_) ? 0x00 : 0x80); break;
|
||||
case 0xc01a: if(is_iie) *value = (*value & 0x7f) | (video_->get_text() ? 0x80 : 0x00); break;
|
||||
case 0xc01b: if(is_iie) *value = (*value & 0x7f) | (video_->get_mixed() ? 0x80 : 0x00); break;
|
||||
case 0xc01c: if(is_iie) *value = (*value & 0x7f) | (video_->get_page2() ? 0x80 : 0x00); break;
|
||||
case 0xc01d: if(is_iie) *value = (*value & 0x7f) | (video_->get_high_resolution() ? 0x80 : 0x00); break;
|
||||
case 0xc01e: if(is_iie) *value = (*value & 0x7f) | (video_->get_alternative_character_set() ? 0x80 : 0x00); break;
|
||||
case 0xc01f: if(is_iie) *value = (*value & 0x7f) | (video_->get_80_columns() ? 0x80 : 0x00); break;
|
||||
case 0xc07f: if(is_iie) *value = (*value & 0x7f) | (video_->get_double_high_resolution() ? 0x80 : 0x00); break;
|
||||
#define IIeSwitchRead(s) *value = get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
|
||||
case 0xc011: IIeSwitchRead(language_card_.bank1); break;
|
||||
case 0xc012: IIeSwitchRead(language_card_.read); break;
|
||||
case 0xc013: IIeSwitchRead(read_auxiliary_memory_); break;
|
||||
case 0xc014: IIeSwitchRead(write_auxiliary_memory_); break;
|
||||
case 0xc015: IIeSwitchRead(internal_CX_rom_); break;
|
||||
case 0xc016: IIeSwitchRead(alternative_zero_page_); break;
|
||||
case 0xc017: IIeSwitchRead(slot_C3_rom_); break;
|
||||
case 0xc018: IIeSwitchRead(video_.get_80_store()); break;
|
||||
case 0xc019: IIeSwitchRead(video_.get_is_vertical_blank(cycles_since_video_update_)); break;
|
||||
case 0xc01a: IIeSwitchRead(video_.get_text()); break;
|
||||
case 0xc01b: IIeSwitchRead(video_.get_mixed()); break;
|
||||
case 0xc01c: IIeSwitchRead(video_.get_page2()); break;
|
||||
case 0xc01d: IIeSwitchRead(video_.get_high_resolution()); break;
|
||||
case 0xc01e: IIeSwitchRead(video_.get_alternative_character_set()); break;
|
||||
case 0xc01f: IIeSwitchRead(video_.get_80_columns()); break;
|
||||
#undef IIeSwitchRead
|
||||
|
||||
case 0xc07f:
|
||||
if(is_iie()) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Write-only switches. All IIe as currently implemented.
|
||||
if(is_iie) {
|
||||
if(is_iie()) {
|
||||
switch(address) {
|
||||
default: printf("Write %04x?\n", address); break;
|
||||
default: break;
|
||||
|
||||
case 0xc000:
|
||||
case 0xc001:
|
||||
update_video();
|
||||
video_->set_80_store(!!(address&1));
|
||||
video_.set_80_store(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
@@ -564,13 +603,13 @@ template <bool is_iie> class ConcreteMachine:
|
||||
case 0xc00c:
|
||||
case 0xc00d:
|
||||
update_video();
|
||||
video_->set_80_columns(!!(address&1));
|
||||
video_.set_80_columns(!!(address&1));
|
||||
break;
|
||||
|
||||
case 0xc00e:
|
||||
case 0xc00f:
|
||||
update_video();
|
||||
video_->set_alternative_character_set(!!(address&1));
|
||||
video_.set_alternative_character_set(!!(address&1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -589,32 +628,32 @@ template <bool is_iie> class ConcreteMachine:
|
||||
analogue_charge_ = 0.0f;
|
||||
} break;
|
||||
|
||||
/* Read-write switches. */
|
||||
/* Switches triggered by reading or writing. */
|
||||
case 0xc050:
|
||||
case 0xc051:
|
||||
update_video();
|
||||
video_->set_text(!!(address&1));
|
||||
video_.set_text(!!(address&1));
|
||||
break;
|
||||
case 0xc052: update_video(); video_->set_mixed(false); break;
|
||||
case 0xc053: update_video(); video_->set_mixed(true); break;
|
||||
case 0xc052: update_video(); video_.set_mixed(false); break;
|
||||
case 0xc053: update_video(); video_.set_mixed(true); break;
|
||||
case 0xc054:
|
||||
case 0xc055:
|
||||
update_video();
|
||||
video_->set_page2(!!(address&1));
|
||||
video_.set_page2(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
case 0xc056:
|
||||
case 0xc057:
|
||||
update_video();
|
||||
video_->set_high_resolution(!!(address&1));
|
||||
video_.set_high_resolution(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc05e:
|
||||
case 0xc05f:
|
||||
if(is_iie) {
|
||||
if(is_iie()) {
|
||||
update_video();
|
||||
video_->set_double_high_resolution(!(address&1));
|
||||
video_.set_annunciator_3(!(address&1));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -626,12 +665,13 @@ template <bool is_iie> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// On the IIe, reading C010 returns additional key info.
|
||||
if(is_iie && isReadOperation(operation)) {
|
||||
if(is_iie() && isReadOperation(operation)) {
|
||||
*value = (key_is_down_ ? 0x80 : 0x00) | (keyboard_input_ & 0x7f);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc030:
|
||||
case 0xc030: case 0xc031: case 0xc032: case 0xc033: case 0xc034: case 0xc035: case 0xc036: case 0xc037:
|
||||
case 0xc038: case 0xc039: case 0xc03a: case 0xc03b: case 0xc03c: case 0xc03d: case 0xc03e: case 0xc03f:
|
||||
update_audio();
|
||||
audio_toggle_.set_output(!audio_toggle_.get_output());
|
||||
break;
|
||||
@@ -673,7 +713,7 @@ template <bool is_iie> class ConcreteMachine:
|
||||
// If this is a card access, figure out which card is at play before determining
|
||||
// the totality of who needs messaging.
|
||||
size_t card_number = 0;
|
||||
AppleII::Card::Select select = AppleII::Card::None;
|
||||
Apple::II::Card::Select select = Apple::II::Card::None;
|
||||
|
||||
if(address >= 0xc100) {
|
||||
/*
|
||||
@@ -681,20 +721,20 @@ template <bool is_iie> class ConcreteMachine:
|
||||
0xCn00 to 0xCnff: card n.
|
||||
*/
|
||||
card_number = (address - 0xc100) >> 8;
|
||||
select = AppleII::Card::Device;
|
||||
select = Apple::II::Card::Device;
|
||||
} else {
|
||||
/*
|
||||
Decode the area conventionally used by cards for registers:
|
||||
C0n0 to C0nF: card n - 8.
|
||||
*/
|
||||
card_number = (address - 0xc090) >> 4;
|
||||
select = AppleII::Card::IO;
|
||||
select = Apple::II::Card::IO;
|
||||
}
|
||||
|
||||
// If the selected card is a just-in-time card, update the just-in-time cards,
|
||||
// and then message it specifically.
|
||||
const bool is_read = isReadOperation(operation);
|
||||
AppleII::Card *const target = cards_[static_cast<size_t>(card_number)].get();
|
||||
Apple::II::Card *const target = cards_[static_cast<size_t>(card_number)].get();
|
||||
if(target && !is_every_cycle_card(target)) {
|
||||
update_just_in_time_cards();
|
||||
target->perform_bus_operation(select, is_read, address, value);
|
||||
@@ -705,7 +745,7 @@ template <bool is_iie> class ConcreteMachine:
|
||||
for(const auto &card: every_cycle_cards_) {
|
||||
card->run_for(Cycles(1), is_stretched_cycle);
|
||||
card->perform_bus_operation(
|
||||
(card == target) ? select : AppleII::Card::None,
|
||||
(card == target) ? select : Apple::II::Card::None,
|
||||
is_read, address, value);
|
||||
}
|
||||
has_updated_cards = true;
|
||||
@@ -717,7 +757,32 @@ template <bool is_iie> class ConcreteMachine:
|
||||
const bool is_read = isReadOperation(operation);
|
||||
for(const auto &card: every_cycle_cards_) {
|
||||
card->run_for(Cycles(1), is_stretched_cycle);
|
||||
card->perform_bus_operation(AppleII::Card::None, is_read, address, value);
|
||||
card->perform_bus_operation(Apple::II::Card::None, is_read, address, value);
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,13 +828,13 @@ template <bool is_iie> 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie) value = static_cast<char>(toupper(value));
|
||||
if(!is_iie()) value = static_cast<char>(toupper(value));
|
||||
|
||||
if(is_pressed) {
|
||||
keyboard_input_ = static_cast<uint8_t>(value | 0x80);
|
||||
@@ -786,7 +851,29 @@ template <bool is_iie> class ConcreteMachine:
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) override {
|
||||
string_serialiser_.reset(new Utility::StringSerialiser(string, true));
|
||||
string_serialiser_ = std::make_unique<Utility::StringSerialiser>(string, true);
|
||||
}
|
||||
|
||||
// MARK:: Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
return Apple::II::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
Configurable::Display display;
|
||||
if(Configurable::get_display(selections_by_option, display)) {
|
||||
set_video_signal_configurable(display);
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
return get_accurate_selections();
|
||||
}
|
||||
|
||||
// MARK: MediaTarget
|
||||
@@ -806,22 +893,25 @@ template <bool is_iie> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: JoystickMachine
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return joysticks_;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
using namespace AppleII;
|
||||
using namespace Apple::II;
|
||||
|
||||
Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
const Target *const appleii_target = dynamic_cast<const Target *>(target);
|
||||
if(appleii_target->model == Target::Model::IIe) {
|
||||
return new ConcreteMachine<true>(*appleii_target, rom_fetcher);
|
||||
} else {
|
||||
return new ConcreteMachine<false>(*appleii_target, rom_fetcher);
|
||||
switch(appleii_target->model) {
|
||||
default: return nullptr;
|
||||
case Target::Model::II: return new ConcreteMachine<Target::Model::II>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIplus: return new ConcreteMachine<Target::Model::IIplus>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIe: return new ConcreteMachine<Target::Model::IIe>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::EnhancedIIe: return new ConcreteMachine<Target::Model::EnhancedIIe>(*appleii_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,18 @@
|
||||
#ifndef AppleII_hpp
|
||||
#define AppleII_hpp
|
||||
|
||||
#include "../../Configurable/Configurable.hpp"
|
||||
#include "../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../ROMMachine.hpp"
|
||||
#include "../../../Configurable/Configurable.hpp"
|
||||
#include "../../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace AppleII {
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
/// @returns The options available for an Apple II.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options();
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
@@ -26,6 +30,7 @@ class Machine {
|
||||
static Machine *AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* AppleII_hpp */
|
||||
@@ -9,11 +9,12 @@
|
||||
#ifndef Card_h
|
||||
#define Card_h
|
||||
|
||||
#include "../../Processors/6502/6502.hpp"
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../Activity/Observer.hpp"
|
||||
#include "../../../Processors/6502/6502.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../Activity/Observer.hpp"
|
||||
|
||||
namespace AppleII {
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
/*!
|
||||
This provides a small subset of the interface offered to cards installed in
|
||||
@@ -39,6 +40,7 @@ namespace AppleII {
|
||||
*/
|
||||
class Card {
|
||||
public:
|
||||
virtual ~Card() {}
|
||||
enum Select: int {
|
||||
None = 0, // No select line is active
|
||||
IO = 1 << 0, // IO select is active
|
||||
@@ -81,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_;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ class Card {
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Card_h */
|
||||
@@ -8,15 +8,25 @@
|
||||
|
||||
#include "DiskIICard.hpp"
|
||||
|
||||
using namespace AppleII;
|
||||
using namespace Apple::II;
|
||||
|
||||
DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector) : diskii_(2045454) {
|
||||
const auto roms = rom_fetcher(
|
||||
"DiskII",
|
||||
{
|
||||
is_16_sector ? "boot-16.rom" : "boot-13.rom",
|
||||
is_16_sector ? "state-machine-16.rom" : "state-machine-13.rom"
|
||||
std::vector<std::unique_ptr<std::vector<uint8_t>>> roms;
|
||||
if(is_16_sector) {
|
||||
roms = rom_fetcher({
|
||||
{"DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6},
|
||||
{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } }
|
||||
});
|
||||
} else {
|
||||
roms = rom_fetcher({
|
||||
{"DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ff},
|
||||
{"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 }
|
||||
});
|
||||
}
|
||||
if(!roms[0] || !roms[1]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
boot_ = std::move(*roms[0]);
|
||||
diskii_.set_state_machine(*roms[1]);
|
||||
set_select_constraints(None);
|
||||
@@ -42,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) {
|
||||
@@ -55,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) {
|
||||
@@ -10,17 +10,18 @@
|
||||
#define DiskIICard_hpp
|
||||
|
||||
#include "Card.hpp"
|
||||
#include "../ROMMachine.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
#include "../../Components/DiskII/DiskII.hpp"
|
||||
#include "../../Storage/Disk/Disk.hpp"
|
||||
#include "../../ClockReceiver/ClockingHintSource.hpp"
|
||||
#include "../../../Components/DiskII/DiskII.hpp"
|
||||
#include "../../../Storage/Disk/Disk.hpp"
|
||||
#include "../../../ClockReceiver/ClockingHintSource.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace AppleII {
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
class DiskIICard: public Card, public ClockingHint::Observer {
|
||||
public:
|
||||
@@ -41,6 +42,7 @@ class DiskIICard: public Card, public ClockingHint::Observer {
|
||||
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* DiskIICard_hpp */
|
||||
340
Machines/Apple/AppleII/Video.cpp
Normal file
340
Machines/Apple/AppleII/Video.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
//
|
||||
// Video.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/04/2018.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Video.hpp"
|
||||
|
||||
using namespace Apple::II::Video;
|
||||
|
||||
VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
|
||||
crt_(910, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance1),
|
||||
is_iie_(is_iie),
|
||||
deferrer_(std::move(target)) {
|
||||
|
||||
// Show only the centre 75% of the TV frame.
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.118f, 0.122f, 0.77f, 0.77f));
|
||||
|
||||
// TODO: there seems to be some sort of bug whereby switching modes can cause
|
||||
// a signal discontinuity that knocks phase out of whack. So it isn't safe to
|
||||
// use default_colour_bursts elsewhere, though it otherwise should be. If/when
|
||||
// it is, start doing so and return to setting the immediate phase up here.
|
||||
// crt_.set_immediate_default_phase(0.5f);
|
||||
|
||||
character_zones[0].xor_mask = 0;
|
||||
character_zones[0].address_mask = 0x3f;
|
||||
character_zones[1].xor_mask = 0;
|
||||
character_zones[1].address_mask = 0x3f;
|
||||
character_zones[2].xor_mask = 0;
|
||||
character_zones[2].address_mask = 0x3f;
|
||||
character_zones[3].xor_mask = 0;
|
||||
character_zones[3].address_mask = 0x3f;
|
||||
|
||||
if(is_iie) {
|
||||
character_zones[0].xor_mask =
|
||||
character_zones[2].xor_mask =
|
||||
character_zones[3].xor_mask = 0xff;
|
||||
character_zones[2].address_mask =
|
||||
character_zones[3].address_mask = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/*
|
||||
Rote setters and getters.
|
||||
*/
|
||||
void VideoBase::set_alternative_character_set(bool alternative_character_set) {
|
||||
set_alternative_character_set_ = alternative_character_set;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
alternative_character_set_ = alternative_character_set;
|
||||
if(alternative_character_set) {
|
||||
character_zones[1].address_mask = 0xff;
|
||||
character_zones[1].xor_mask = 0;
|
||||
} else {
|
||||
character_zones[1].address_mask = 0x3f;
|
||||
character_zones[1].xor_mask = flash_mask();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_alternative_character_set() {
|
||||
return set_alternative_character_set_;
|
||||
}
|
||||
|
||||
void VideoBase::set_80_columns(bool columns_80) {
|
||||
set_columns_80_ = columns_80;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
columns_80_ = columns_80;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_80_columns() {
|
||||
return set_columns_80_;
|
||||
}
|
||||
|
||||
void VideoBase::set_80_store(bool store_80) {
|
||||
set_store_80_ = store_80_ = store_80;
|
||||
}
|
||||
|
||||
bool VideoBase::get_80_store() {
|
||||
return set_store_80_;
|
||||
}
|
||||
|
||||
void VideoBase::set_page2(bool page2) {
|
||||
set_page2_ = page2_ = page2;
|
||||
}
|
||||
|
||||
bool VideoBase::get_page2() {
|
||||
return set_page2_;
|
||||
}
|
||||
|
||||
void VideoBase::set_text(bool text) {
|
||||
set_text_ = text;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
text_ = text;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_text() {
|
||||
return set_text_;
|
||||
}
|
||||
|
||||
void VideoBase::set_mixed(bool mixed) {
|
||||
set_mixed_ = mixed;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
mixed_ = mixed;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_mixed() {
|
||||
return set_mixed_;
|
||||
}
|
||||
|
||||
void VideoBase::set_high_resolution(bool high_resolution) {
|
||||
set_high_resolution_ = high_resolution;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
high_resolution_ = high_resolution;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_high_resolution() {
|
||||
return set_high_resolution_;
|
||||
}
|
||||
|
||||
void VideoBase::set_annunciator_3(bool annunciator_3) {
|
||||
set_annunciator_3_ = annunciator_3;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
annunciator_3_ = annunciator_3;
|
||||
high_resolution_mask_ = annunciator_3_ ? 0x7f : 0xff;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_annunciator_3() {
|
||||
return set_annunciator_3_;
|
||||
}
|
||||
|
||||
void VideoBase::set_character_rom(const std::vector<uint8_t> &character_rom) {
|
||||
character_rom_ = character_rom;
|
||||
|
||||
// Flip all character contents based on the second line of the $ graphic.
|
||||
if(character_rom_[0x121] == 0x3c || character_rom_[0x122] == 0x3c) {
|
||||
for(auto &graphic : character_rom_) {
|
||||
graphic =
|
||||
((graphic & 0x01) ? 0x40 : 0x00) |
|
||||
((graphic & 0x02) ? 0x20 : 0x00) |
|
||||
((graphic & 0x04) ? 0x10 : 0x00) |
|
||||
((graphic & 0x08) ? 0x08 : 0x00) |
|
||||
((graphic & 0x10) ? 0x04 : 0x00) |
|
||||
((graphic & 0x20) ? 0x02 : 0x00) |
|
||||
((graphic & 0x40) ? 0x01 : 0x00);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_text(uint8_t *target, const uint8_t *const source, size_t length, size_t pixel_row) const {
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
const int character = source[c] & character_zones[source[c] >> 6].address_mask;
|
||||
const uint8_t xor_mask = character_zones[source[c] >> 6].xor_mask;
|
||||
const std::size_t character_address = static_cast<std::size_t>(character << 3) + pixel_row;
|
||||
const uint8_t character_pattern = character_rom_[character_address] ^ xor_mask;
|
||||
|
||||
// The character ROM is output MSB to LSB rather than LSB to MSB.
|
||||
target[0] = target[1] = character_pattern & 0x40;
|
||||
target[2] = target[3] = character_pattern & 0x20;
|
||||
target[4] = target[5] = character_pattern & 0x10;
|
||||
target[6] = target[7] = character_pattern & 0x08;
|
||||
target[8] = target[9] = character_pattern & 0x04;
|
||||
target[10] = target[11] = character_pattern & 0x02;
|
||||
target[12] = target[13] = character_pattern & 0x01;
|
||||
graphics_carry_ = character_pattern & 0x01;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_double_text(uint8_t *target, const uint8_t *const source, const uint8_t *const auxiliary_source, size_t length, size_t pixel_row) const {
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
const std::size_t character_addresses[2] = {
|
||||
static_cast<std::size_t>(
|
||||
(auxiliary_source[c] & character_zones[auxiliary_source[c] >> 6].address_mask) << 3
|
||||
) + pixel_row,
|
||||
static_cast<std::size_t>(
|
||||
(source[c] & character_zones[source[c] >> 6].address_mask) << 3
|
||||
) + pixel_row
|
||||
};
|
||||
|
||||
const uint8_t character_patterns[2] = {
|
||||
static_cast<uint8_t>(
|
||||
character_rom_[character_addresses[0]] ^ character_zones[auxiliary_source[c] >> 6].xor_mask
|
||||
),
|
||||
static_cast<uint8_t>(
|
||||
character_rom_[character_addresses[1]] ^ character_zones[source[c] >> 6].xor_mask
|
||||
)
|
||||
};
|
||||
|
||||
// The character ROM is output MSB to LSB rather than LSB to MSB.
|
||||
target[0] = character_patterns[0] & 0x40;
|
||||
target[1] = character_patterns[0] & 0x20;
|
||||
target[2] = character_patterns[0] & 0x10;
|
||||
target[3] = character_patterns[0] & 0x08;
|
||||
target[4] = character_patterns[0] & 0x04;
|
||||
target[5] = character_patterns[0] & 0x02;
|
||||
target[6] = character_patterns[0] & 0x01;
|
||||
target[7] = character_patterns[1] & 0x40;
|
||||
target[8] = character_patterns[1] & 0x20;
|
||||
target[9] = character_patterns[1] & 0x10;
|
||||
target[10] = character_patterns[1] & 0x08;
|
||||
target[11] = character_patterns[1] & 0x04;
|
||||
target[12] = character_patterns[1] & 0x02;
|
||||
target[13] = character_patterns[1] & 0x01;
|
||||
graphics_carry_ = character_patterns[1] & 0x01;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_low_resolution(uint8_t *target, const uint8_t *const source, size_t length, int column, int row) const {
|
||||
const int row_shift = row&4;
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
// Low-resolution graphics mode shifts the colour code on a loop, but has to account for whether this
|
||||
// 14-sample output window is starting at the beginning of a colour cycle or halfway through.
|
||||
if((column + static_cast<int>(c))&1) {
|
||||
target[0] = target[4] = target[8] = target[12] = (source[c] >> row_shift) & 4;
|
||||
target[1] = target[5] = target[9] = target[13] = (source[c] >> row_shift) & 8;
|
||||
target[2] = target[6] = target[10] = (source[c] >> row_shift) & 1;
|
||||
target[3] = target[7] = target[11] = (source[c] >> row_shift) & 2;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 8;
|
||||
} else {
|
||||
target[0] = target[4] = target[8] = target[12] = (source[c] >> row_shift) & 1;
|
||||
target[1] = target[5] = target[9] = target[13] = (source[c] >> row_shift) & 2;
|
||||
target[2] = target[6] = target[10] = (source[c] >> row_shift) & 4;
|
||||
target[3] = target[7] = target[11] = (source[c] >> row_shift) & 8;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 2;
|
||||
}
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_fat_low_resolution(uint8_t *target, const uint8_t *const source, size_t length, int column, int row) const {
|
||||
const int row_shift = row&4;
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
// Fat low-resolution mode appears not to do anything to try to make odd and
|
||||
// even columns compatible.
|
||||
target[0] = target[1] = target[8] = target[9] = (source[c] >> row_shift) & 1;
|
||||
target[2] = target[3] = target[10] = target[11] = (source[c] >> row_shift) & 2;
|
||||
target[4] = target[5] = target[12] = target[13] = (source[c] >> row_shift) & 4;
|
||||
target[6] = target[7] = (source[c] >> row_shift) & 8;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 4;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_double_low_resolution(uint8_t *target, const uint8_t *const source, const uint8_t *const auxiliary_source, size_t length, int column, int row) const {
|
||||
const int row_shift = row&4;
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
if((column + static_cast<int>(c))&1) {
|
||||
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 2;
|
||||
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 4;
|
||||
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 8;
|
||||
target[3] = (auxiliary_source[c] >> row_shift) & 1;
|
||||
|
||||
target[8] = target[12] = (source[c] >> row_shift) & 4;
|
||||
target[9] = target[13] = (source[c] >> row_shift) & 8;
|
||||
target[10] = (source[c] >> row_shift) & 1;
|
||||
target[7] = target[11] = (source[c] >> row_shift) & 2;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 8;
|
||||
} else {
|
||||
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 8;
|
||||
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 1;
|
||||
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 2;
|
||||
target[3] = (auxiliary_source[c] >> row_shift) & 4;
|
||||
|
||||
target[8] = target[12] = (source[c] >> row_shift) & 1;
|
||||
target[9] = target[13] = (source[c] >> row_shift) & 2;
|
||||
target[10] = (source[c] >> row_shift) & 4;
|
||||
target[7] = target[11] = (source[c] >> row_shift) & 8;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 2;
|
||||
}
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_high_resolution(uint8_t *target, const uint8_t *const source, size_t length) const {
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
// High resolution graphics shift out LSB to MSB, optionally with a delay of half a pixel.
|
||||
// If there is a delay, the previous output level is held to bridge the gap.
|
||||
// Delays may be ignored on a IIe if Annunciator 3 is set; that's the state that
|
||||
// high_resolution_mask_ models.
|
||||
if(source[c] & high_resolution_mask_ & 0x80) {
|
||||
target[0] = graphics_carry_;
|
||||
target[1] = target[2] = source[c] & 0x01;
|
||||
target[3] = target[4] = source[c] & 0x02;
|
||||
target[5] = target[6] = source[c] & 0x04;
|
||||
target[7] = target[8] = source[c] & 0x08;
|
||||
target[9] = target[10] = source[c] & 0x10;
|
||||
target[11] = target[12] = source[c] & 0x20;
|
||||
target[13] = source[c] & 0x40;
|
||||
} else {
|
||||
target[0] = target[1] = source[c] & 0x01;
|
||||
target[2] = target[3] = source[c] & 0x02;
|
||||
target[4] = target[5] = source[c] & 0x04;
|
||||
target[6] = target[7] = source[c] & 0x08;
|
||||
target[8] = target[9] = source[c] & 0x10;
|
||||
target[10] = target[11] = source[c] & 0x20;
|
||||
target[12] = target[13] = source[c] & 0x40;
|
||||
}
|
||||
graphics_carry_ = source[c] & 0x40;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_double_high_resolution(uint8_t *target, const uint8_t *const source, const uint8_t *const auxiliary_source, size_t length) const {
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
target[0] = auxiliary_source[c] & 0x01;
|
||||
target[1] = auxiliary_source[c] & 0x02;
|
||||
target[2] = auxiliary_source[c] & 0x04;
|
||||
target[3] = auxiliary_source[c] & 0x08;
|
||||
target[4] = auxiliary_source[c] & 0x10;
|
||||
target[5] = auxiliary_source[c] & 0x20;
|
||||
target[6] = auxiliary_source[c] & 0x40;
|
||||
target[7] = source[c] & 0x01;
|
||||
target[8] = source[c] & 0x02;
|
||||
target[9] = source[c] & 0x04;
|
||||
target[10] = source[c] & 0x08;
|
||||
target[11] = source[c] & 0x10;
|
||||
target[12] = source[c] & 0x20;
|
||||
target[13] = source[c] & 0x40;
|
||||
|
||||
graphics_carry_ = auxiliary_source[c] & 0x40;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
608
Machines/Apple/AppleII/Video.hpp
Normal file
608
Machines/Apple/AppleII/Video.hpp
Normal file
@@ -0,0 +1,608 @@
|
||||
//
|
||||
// Video.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/04/2018.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Video_hpp
|
||||
#define Video_hpp
|
||||
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../ClockReceiver/DeferredQueue.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
namespace Video {
|
||||
|
||||
class BusHandler {
|
||||
public:
|
||||
/*!
|
||||
Requests fetching of the @c count bytes starting from @c address.
|
||||
|
||||
The handler should write the values from base memory to @c base_target, and those
|
||||
from auxiliary memory to @c auxiliary_target. If the machine has no axiliary memory,
|
||||
it needn't write anything to auxiliary_target.
|
||||
*/
|
||||
void perform_read(uint16_t address, size_t count, uint8_t *base_target, uint8_t *auxiliary_target) {
|
||||
}
|
||||
};
|
||||
|
||||
class VideoBase {
|
||||
public:
|
||||
VideoBase(bool is_iie, std::function<void(Cycles)> &&target);
|
||||
|
||||
/// Sets the scan target.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
/*
|
||||
Descriptions for the setters below are taken verbatim from
|
||||
the Apple IIe Technical Reference. Addresses are the conventional
|
||||
locations within the Apple II memory map. Only those which affect
|
||||
video output are implemented here.
|
||||
|
||||
Those registers which don't exist on a II/II+ are marked.
|
||||
*/
|
||||
|
||||
/*!
|
||||
Setter for ALTCHAR ($C00E/$C00F; triggers on write only):
|
||||
|
||||
* Off: display text using primary character set.
|
||||
* On: display text using alternate character set.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_alternative_character_set(bool);
|
||||
bool get_alternative_character_set();
|
||||
|
||||
/*!
|
||||
Setter for 80COL ($C00C/$C00D; triggers on write only).
|
||||
|
||||
* Off: display 40 columns.
|
||||
* On: display 80 columns.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_columns(bool);
|
||||
bool get_80_columns();
|
||||
|
||||
/*!
|
||||
Setter for 80STORE ($C000/$C001; triggers on write only).
|
||||
|
||||
* Off: cause PAGE2 to select auxiliary RAM.
|
||||
* On: cause PAGE2 to switch main RAM areas.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_store(bool);
|
||||
bool get_80_store();
|
||||
|
||||
/*!
|
||||
Setter for PAGE2 ($C054/$C055; triggers on read or write).
|
||||
|
||||
* Off: select Page 1.
|
||||
* On: select Page 2 or, if 80STORE on, Page 1 in auxiliary memory.
|
||||
|
||||
80STORE doesn't exist on a II/II+; therefore this always selects
|
||||
either Page 1 or Page 2 on those machines.
|
||||
*/
|
||||
void set_page2(bool);
|
||||
bool get_page2();
|
||||
|
||||
/*!
|
||||
Setter for TEXT ($C050/$C051; triggers on read or write).
|
||||
|
||||
* Off: display graphics or, if MIXED on, mixed.
|
||||
* On: display text.
|
||||
*/
|
||||
void set_text(bool);
|
||||
bool get_text();
|
||||
|
||||
/*!
|
||||
Setter for MIXED ($C052/$C053; triggers on read or write).
|
||||
|
||||
* Off: display only text or only graphics.
|
||||
* On: if TEXT off, display text and graphics.
|
||||
*/
|
||||
void set_mixed(bool);
|
||||
bool get_mixed();
|
||||
|
||||
/*!
|
||||
Setter for HIRES ($C056/$C057; triggers on read or write).
|
||||
|
||||
* Off: if TEXT off, display low-resolution graphics.
|
||||
* On: if TEXT off, display high-resolution or, if DHIRES on, double high-resolution graphics.
|
||||
|
||||
DHIRES doesn't exist on a II/II+; therefore this always selects
|
||||
either high- or low-resolution graphics on those machines.
|
||||
|
||||
Despite Apple's documentation, the IIe also supports double low-resolution
|
||||
graphics, which are the 80-column analogue to ordinary low-resolution 40-column
|
||||
low-resolution graphics.
|
||||
*/
|
||||
void set_high_resolution(bool);
|
||||
bool get_high_resolution();
|
||||
|
||||
/*!
|
||||
Setter for annunciator 3.
|
||||
|
||||
* On: turn on annunciator 3.
|
||||
* Off: turn off annunciator 3.
|
||||
|
||||
This exists on both the II/II+ and the IIe, but has no effect on
|
||||
video on the older machines. It's intended to be used on the IIe
|
||||
to confirm double-high resolution mode but has side effects in
|
||||
selecting mixed mode output and discarding high-resolution
|
||||
delay bits.
|
||||
*/
|
||||
void set_annunciator_3(bool);
|
||||
bool get_annunciator_3();
|
||||
|
||||
// Setup for text mode.
|
||||
void set_character_rom(const std::vector<uint8_t> &);
|
||||
|
||||
protected:
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
// State affecting output video stream generation.
|
||||
uint8_t *pixel_pointer_ = nullptr;
|
||||
|
||||
// State affecting logical state.
|
||||
int row_ = 0, column_ = 0, flash_ = 0;
|
||||
uint8_t flash_mask() {
|
||||
return static_cast<uint8_t>((flash_ / flash_length) * 0xff);
|
||||
}
|
||||
|
||||
// Enumerates all Apple II and IIe display modes.
|
||||
enum class GraphicsMode {
|
||||
Text = 0,
|
||||
DoubleText,
|
||||
HighRes,
|
||||
DoubleHighRes,
|
||||
LowRes,
|
||||
DoubleLowRes,
|
||||
FatLowRes
|
||||
};
|
||||
bool is_text_mode(GraphicsMode m) { return m <= GraphicsMode::DoubleText; }
|
||||
bool is_double_mode(GraphicsMode m) { return !!(static_cast<int>(m)&1); }
|
||||
|
||||
// Various soft-switch values.
|
||||
bool alternative_character_set_ = false, set_alternative_character_set_ = false;
|
||||
bool columns_80_ = false, set_columns_80_ = false;
|
||||
bool store_80_ = false, set_store_80_ = false;
|
||||
bool page2_ = false, set_page2_ = false;
|
||||
bool text_ = true, set_text_ = true;
|
||||
bool mixed_ = false, set_mixed_ = false;
|
||||
bool high_resolution_ = false, set_high_resolution_ = false;
|
||||
bool annunciator_3_ = false, set_annunciator_3_ = false;
|
||||
|
||||
// Graphics carry is the final level output in a fetch window;
|
||||
// it carries on into the next if it's high resolution with
|
||||
// the delay bit set.
|
||||
mutable uint8_t graphics_carry_ = 0;
|
||||
bool was_double_ = false;
|
||||
uint8_t high_resolution_mask_ = 0xff;
|
||||
|
||||
// This holds a copy of the character ROM. The regular character
|
||||
// set is assumed to be in the first 64*8 bytes; the alternative
|
||||
// is in the 128*8 bytes after that.
|
||||
std::vector<uint8_t> character_rom_;
|
||||
|
||||
// Memory is fetched ahead of time into this array;
|
||||
// this permits the correct delay between fetching
|
||||
// without having to worry about a rolling buffer.
|
||||
std::array<uint8_t, 40> base_stream_;
|
||||
std::array<uint8_t, 40> auxiliary_stream_;
|
||||
|
||||
bool is_iie_ = false;
|
||||
static constexpr int flash_length = 8406;
|
||||
|
||||
// Describes the current text mode mapping from in-memory character index
|
||||
// to output character.
|
||||
struct CharacterMapping {
|
||||
uint8_t address_mask;
|
||||
uint8_t xor_mask;
|
||||
};
|
||||
CharacterMapping character_zones[4];
|
||||
|
||||
/*!
|
||||
Outputs 40-column text to @c target, using @c length bytes from @c source.
|
||||
*/
|
||||
void output_text(uint8_t *target, const uint8_t *source, size_t length, size_t pixel_row) const;
|
||||
|
||||
/*!
|
||||
Outputs 80-column text to @c target, drawing @c length columns from @c source and @c auxiliary_source.
|
||||
*/
|
||||
void output_double_text(uint8_t *target, const uint8_t *source, const uint8_t *auxiliary_source, size_t length, size_t pixel_row) const;
|
||||
|
||||
/*!
|
||||
Outputs 40-column low-resolution graphics to @c target, drawing @c length columns from @c source.
|
||||
*/
|
||||
void output_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
|
||||
|
||||
/*!
|
||||
Outputs 80-column low-resolution graphics to @c target, drawing @c length columns from @c source and @c auxiliary_source.
|
||||
*/
|
||||
void output_double_low_resolution(uint8_t *target, const uint8_t *source, const uint8_t *auxiliary_source, size_t length, int column, int row) const;
|
||||
|
||||
/*!
|
||||
Outputs 40-column high-resolution graphics to @c target, drawing @c length columns from @c source.
|
||||
*/
|
||||
void output_high_resolution(uint8_t *target, const uint8_t *source, size_t length) const;
|
||||
|
||||
/*!
|
||||
Outputs 80-column double-high-resolution graphics to @c target, drawing @c length columns from @c source.
|
||||
*/
|
||||
void output_double_high_resolution(uint8_t *target, const uint8_t *source, const uint8_t *auxiliary_source, size_t length) const;
|
||||
|
||||
/*!
|
||||
Outputs 40-column "fat low resolution" graphics to @c target, drawing @c length columns from @c source.
|
||||
|
||||
Fat low-resolution mode is like regular low-resolution mode except that data is shifted out on the 7M
|
||||
clock rather than the 14M.
|
||||
*/
|
||||
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_;
|
||||
};
|
||||
|
||||
template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
public:
|
||||
/// Constructs an instance of the video feed; a CRT is also created.
|
||||
Video(BusHandler &bus_handler) :
|
||||
VideoBase(is_iie, [=] (Cycles cycles) { advance(cycles); }),
|
||||
bus_handler_(bus_handler) {}
|
||||
|
||||
/*!
|
||||
Runs video for @c cycles.
|
||||
*/
|
||||
void run_for(Cycles cycles) {
|
||||
deferrer_.run_for(cycles);
|
||||
}
|
||||
|
||||
/*!
|
||||
Obtains the last value the video read prior to time now+offset.
|
||||
*/
|
||||
uint8_t get_last_read_value(Cycles offset) {
|
||||
// Rules of generation:
|
||||
// (1) a complete sixty-five-cycle scan line consists of sixty-five consecutive bytes of
|
||||
// display buffer memory that starts twenty-five bytes prior to the actual data to be displayed.
|
||||
// (2) During VBL the data acts just as if it were starting a whole new frame from the beginning, but
|
||||
// it never finishes this pseudo-frame. After getting one third of the way through the frame (to
|
||||
// scan line $3F), it suddenly repeats the previous six scan lines ($3A through $3F) before aborting
|
||||
// to begin the next true frame.
|
||||
//
|
||||
// Source: Have an Apple Split by Bob Bishop; http://rich12345.tripod.com/aiivideo/softalk.html
|
||||
|
||||
// Determine column at offset.
|
||||
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).
|
||||
mapped_column += 25;
|
||||
|
||||
// Apply carry into the row counter.
|
||||
int mapped_row = row_ + (mapped_column / 65);
|
||||
mapped_column %= 65;
|
||||
mapped_row %= 262;
|
||||
|
||||
// Apple out-of-bounds row logic.
|
||||
if(mapped_row >= 256) {
|
||||
mapped_row = 0x3a + (mapped_row&255);
|
||||
} else {
|
||||
mapped_row %= 192;
|
||||
}
|
||||
|
||||
// Calculate the address and return the value.
|
||||
uint16_t read_address = static_cast<uint16_t>(get_row_address(mapped_row) + mapped_column - 25);
|
||||
uint8_t value, aux_value;
|
||||
bus_handler_.perform_read(read_address, 1, &value, &aux_value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns @c true if the display will be within vertical blank at now + @c offset; @c false otherwise.
|
||||
*/
|
||||
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_ + 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).
|
||||
mapped_column += 25;
|
||||
|
||||
// Apply carry into the row counter and test it for location.
|
||||
int mapped_row = row_ + (mapped_column / 65);
|
||||
return (mapped_row % 262) >= 192;
|
||||
}
|
||||
|
||||
private:
|
||||
/*!
|
||||
Advances time by @c cycles; expects to be fed by the CPU clock.
|
||||
Implicitly adds an extra half a colour clock at the end of
|
||||
line.
|
||||
*/
|
||||
void advance(Cycles cycles) {
|
||||
/*
|
||||
Addressing scheme used throughout is that column 0 is the first column with pixels in it;
|
||||
row 0 is the first row with pixels in it.
|
||||
|
||||
A frame is oriented around 65 cycles across, 262 lines down.
|
||||
*/
|
||||
constexpr int first_sync_line = 220; // A complete guess. Information needed.
|
||||
constexpr int first_sync_column = 49; // Also a guess.
|
||||
constexpr int sync_length = 4; // One of the two likely candidates.
|
||||
|
||||
int int_cycles = int(cycles.as_integral());
|
||||
while(int_cycles) {
|
||||
const int cycles_this_line = std::min(65 - column_, int_cycles);
|
||||
const int ending_column = column_ + cycles_this_line;
|
||||
const bool is_vertical_sync_line = (row_ >= first_sync_line && row_ < first_sync_line + 3);
|
||||
|
||||
if(is_vertical_sync_line) {
|
||||
// In effect apply an XOR to HSYNC and VSYNC flags in order to include equalising
|
||||
// pulses (and hencce keep hsync approximately where it should be during vsync).
|
||||
const int blank_start = std::max(first_sync_column - sync_length, column_);
|
||||
const int blank_end = std::min(first_sync_column, ending_column);
|
||||
if(blank_end > blank_start) {
|
||||
if(blank_start > column_) {
|
||||
crt_.output_sync((blank_start - column_) * 14);
|
||||
}
|
||||
crt_.output_blank((blank_end - blank_start) * 14);
|
||||
if(blank_end < ending_column) {
|
||||
crt_.output_sync((ending_column - blank_end) * 14);
|
||||
}
|
||||
} else {
|
||||
crt_.output_sync(cycles_this_line * 14);
|
||||
}
|
||||
} else {
|
||||
const GraphicsMode line_mode = graphics_mode(row_);
|
||||
|
||||
// Determine whether there's any fetching to do. Fetching occurs during the first
|
||||
// 40 columns of rows prior to 192.
|
||||
if(row_ < 192 && column_ < 40) {
|
||||
const int character_row = row_ >> 3;
|
||||
const uint16_t row_address = static_cast<uint16_t>((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
|
||||
// Grab the memory contents that'll be needed momentarily.
|
||||
const int fetch_end = std::min(40, ending_column);
|
||||
uint16_t fetch_address;
|
||||
switch(line_mode) {
|
||||
default:
|
||||
case GraphicsMode::Text:
|
||||
case GraphicsMode::DoubleText:
|
||||
case GraphicsMode::LowRes:
|
||||
case GraphicsMode::FatLowRes:
|
||||
case GraphicsMode::DoubleLowRes: {
|
||||
const uint16_t text_address = static_cast<uint16_t>(((video_page()+1) * 0x400) + row_address);
|
||||
fetch_address = static_cast<uint16_t>(text_address + column_);
|
||||
} break;
|
||||
|
||||
case GraphicsMode::HighRes:
|
||||
case GraphicsMode::DoubleHighRes:
|
||||
fetch_address = static_cast<uint16_t>(((video_page()+1) * 0x2000) + row_address + ((row_&7) << 10) + column_);
|
||||
break;
|
||||
}
|
||||
|
||||
bus_handler_.perform_read(
|
||||
fetch_address,
|
||||
static_cast<size_t>(fetch_end - column_),
|
||||
&base_stream_[static_cast<size_t>(column_)],
|
||||
&auxiliary_stream_[static_cast<size_t>(column_)]);
|
||||
}
|
||||
|
||||
if(row_ < 192) {
|
||||
// The pixel area is the first 40.5 columns; base contents
|
||||
// remain where they would naturally be but auxiliary
|
||||
// graphics appear to the left of that.
|
||||
if(!column_) {
|
||||
pixel_pointer_ = crt_.begin_data(568);
|
||||
graphics_carry_ = 0;
|
||||
was_double_ = true;
|
||||
}
|
||||
|
||||
if(column_ < 40) {
|
||||
const int pixel_start = std::max(0, column_);
|
||||
const int pixel_end = std::min(40, ending_column);
|
||||
const int pixel_row = row_ & 7;
|
||||
|
||||
const bool is_double = Video::is_double_mode(line_mode);
|
||||
if(!is_double && was_double_ && pixel_pointer_) {
|
||||
pixel_pointer_[pixel_start*14 + 0] =
|
||||
pixel_pointer_[pixel_start*14 + 1] =
|
||||
pixel_pointer_[pixel_start*14 + 2] =
|
||||
pixel_pointer_[pixel_start*14 + 3] =
|
||||
pixel_pointer_[pixel_start*14 + 4] =
|
||||
pixel_pointer_[pixel_start*14 + 5] =
|
||||
pixel_pointer_[pixel_start*14 + 6] = 0;
|
||||
}
|
||||
was_double_ = is_double;
|
||||
|
||||
if(pixel_pointer_) {
|
||||
switch(line_mode) {
|
||||
case GraphicsMode::Text:
|
||||
output_text(
|
||||
&pixel_pointer_[pixel_start * 14 + 7],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
static_cast<size_t>(pixel_row));
|
||||
break;
|
||||
|
||||
case GraphicsMode::DoubleText:
|
||||
output_double_text(
|
||||
&pixel_pointer_[pixel_start * 14],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
static_cast<size_t>(pixel_row));
|
||||
break;
|
||||
|
||||
case GraphicsMode::LowRes:
|
||||
output_low_resolution(
|
||||
&pixel_pointer_[pixel_start * 14 + 7],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
pixel_start,
|
||||
pixel_row);
|
||||
break;
|
||||
|
||||
case GraphicsMode::FatLowRes:
|
||||
output_fat_low_resolution(
|
||||
&pixel_pointer_[pixel_start * 14 + 7],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
pixel_start,
|
||||
pixel_row);
|
||||
break;
|
||||
|
||||
case GraphicsMode::DoubleLowRes:
|
||||
output_double_low_resolution(
|
||||
&pixel_pointer_[pixel_start * 14],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
pixel_start,
|
||||
pixel_row);
|
||||
break;
|
||||
|
||||
case GraphicsMode::HighRes:
|
||||
output_high_resolution(
|
||||
&pixel_pointer_[pixel_start * 14 + 7],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start));
|
||||
break;
|
||||
|
||||
case GraphicsMode::DoubleHighRes:
|
||||
output_double_high_resolution(
|
||||
&pixel_pointer_[pixel_start * 14],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start));
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
if(pixel_end == 40) {
|
||||
if(pixel_pointer_) {
|
||||
if(was_double_) {
|
||||
pixel_pointer_[560] = pixel_pointer_[561] = pixel_pointer_[562] = pixel_pointer_[563] =
|
||||
pixel_pointer_[564] = pixel_pointer_[565] = pixel_pointer_[566] = pixel_pointer_[567] = 0;
|
||||
} else {
|
||||
if(line_mode == GraphicsMode::HighRes && base_stream_[39]&0x80)
|
||||
pixel_pointer_[567] = graphics_carry_;
|
||||
else
|
||||
pixel_pointer_[567] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
crt_.output_data(568, 568);
|
||||
pixel_pointer_ = nullptr;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(column_ < 40 && ending_column >= 40) {
|
||||
crt_.output_blank(568);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The left border, sync, right border pattern doesn't depend on whether
|
||||
there were pixels this row and is output as soon as it is known.
|
||||
*/
|
||||
|
||||
if(column_ < first_sync_column && ending_column >= first_sync_column) {
|
||||
crt_.output_blank(first_sync_column*14 - 568);
|
||||
}
|
||||
|
||||
if(column_ < (first_sync_column + sync_length) && ending_column >= (first_sync_column + sync_length)) {
|
||||
crt_.output_sync(sync_length*14);
|
||||
}
|
||||
|
||||
int second_blank_start;
|
||||
if(!is_text_mode(graphics_mode(row_+1))) {
|
||||
const int colour_burst_start = std::max(first_sync_column + sync_length + 1, column_);
|
||||
const int colour_burst_end = std::min(first_sync_column + sync_length + 4, ending_column);
|
||||
if(colour_burst_end > colour_burst_start) {
|
||||
crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, 0);
|
||||
}
|
||||
|
||||
second_blank_start = std::max(first_sync_column + sync_length + 3, column_);
|
||||
} else {
|
||||
second_blank_start = std::max(first_sync_column + sync_length, column_);
|
||||
}
|
||||
|
||||
if(ending_column > second_blank_start) {
|
||||
crt_.output_blank((ending_column - second_blank_start) * 14);
|
||||
}
|
||||
}
|
||||
|
||||
int_cycles -= cycles_this_line;
|
||||
column_ = (column_ + cycles_this_line) % 65;
|
||||
if(!column_) {
|
||||
row_ = (row_ + 1) % 262;
|
||||
flash_ = (flash_ + 1) % (2 * flash_length);
|
||||
if(!alternative_character_set_) {
|
||||
character_zones[1].xor_mask = flash_mask();
|
||||
}
|
||||
|
||||
// Add an extra half a colour cycle of blank; this isn't counted in the run_for
|
||||
// count explicitly but is promised. If this is a vertical sync line, output sync
|
||||
// instead of blank, taking that to be the default level.
|
||||
if(is_vertical_sync_line) {
|
||||
crt_.output_sync(2);
|
||||
} else {
|
||||
crt_.output_blank(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GraphicsMode graphics_mode(int row) {
|
||||
if(
|
||||
text_ ||
|
||||
(mixed_ && row >= 160 && row < 192)
|
||||
) return columns_80_ ? GraphicsMode::DoubleText : GraphicsMode::Text;
|
||||
if(high_resolution_) {
|
||||
return (annunciator_3_ && columns_80_) ? GraphicsMode::DoubleHighRes : GraphicsMode::HighRes;
|
||||
} else {
|
||||
if(columns_80_) return GraphicsMode::DoubleLowRes;
|
||||
if(annunciator_3_) return GraphicsMode::FatLowRes;
|
||||
return GraphicsMode::LowRes;
|
||||
}
|
||||
}
|
||||
|
||||
int video_page() {
|
||||
return (store_80_ || !page2_) ? 0 : 1;
|
||||
}
|
||||
|
||||
uint16_t get_row_address(int row) {
|
||||
const int character_row = row >> 3;
|
||||
const int pixel_row = row & 7;
|
||||
const uint16_t row_address = static_cast<uint16_t>((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
|
||||
const GraphicsMode pixel_mode = graphics_mode(row);
|
||||
return ((pixel_mode == GraphicsMode::HighRes) || (pixel_mode == GraphicsMode::DoubleHighRes)) ?
|
||||
static_cast<uint16_t>(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
|
||||
static_cast<uint16_t>(((video_page()+1) * 0x400) + row_address);
|
||||
}
|
||||
|
||||
BusHandler &bus_handler_;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Video_hpp */
|
||||
97
Machines/Apple/Macintosh/Audio.cpp
Normal file
97
Machines/Apple/Macintosh/Audio.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// Audio.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 31/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Audio.hpp"
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
namespace {
|
||||
|
||||
// The sample_length is coupled with the clock rate selected within the Macintosh proper;
|
||||
// as per the header-declaration a divide-by-two clock is expected to arrive here.
|
||||
const std::size_t sample_length = 352 / 2;
|
||||
|
||||
}
|
||||
|
||||
Audio::Audio(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {}
|
||||
|
||||
// MARK: - Inputs
|
||||
|
||||
void Audio::post_sample(uint8_t sample) {
|
||||
// Store sample directly indexed by current write pointer; this ensures that collected samples
|
||||
// directly map to volume and enabled/disabled states.
|
||||
sample_queue_.buffer[sample_queue_.write_pointer] = sample;
|
||||
sample_queue_.write_pointer = (sample_queue_.write_pointer + 1) % sample_queue_.buffer.size();
|
||||
}
|
||||
|
||||
void Audio::set_volume(int volume) {
|
||||
// Do nothing if the volume hasn't changed.
|
||||
if(posted_volume_ == volume) return;
|
||||
posted_volume_ = volume;
|
||||
|
||||
// Post the volume change as a deferred event.
|
||||
task_queue_.defer([=] () {
|
||||
volume_ = volume;
|
||||
set_volume_multiplier();
|
||||
});
|
||||
}
|
||||
|
||||
void Audio::set_enabled(bool on) {
|
||||
// Do nothing if the mask hasn't changed.
|
||||
if(posted_enable_mask_ == int(on)) return;
|
||||
posted_enable_mask_ = int(on);
|
||||
|
||||
// Post the enabled mask change as a deferred event.
|
||||
task_queue_.defer([=] () {
|
||||
enabled_mask_ = int(on);
|
||||
set_volume_multiplier();
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: - Output generation
|
||||
|
||||
bool Audio::is_zero_level() {
|
||||
return !volume_ || !enabled_mask_;
|
||||
}
|
||||
|
||||
void Audio::set_sample_volume_range(std::int16_t range) {
|
||||
// Some underflow here doesn't really matter.
|
||||
output_volume_ = range / (7 * 255);
|
||||
set_volume_multiplier();
|
||||
}
|
||||
|
||||
void Audio::set_volume_multiplier() {
|
||||
volume_multiplier_ = int16_t(output_volume_ * volume_ * enabled_mask_);
|
||||
}
|
||||
|
||||
void Audio::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
// TODO: the implementation below acts as if the hardware uses pulse-amplitude modulation;
|
||||
// in fact it uses pulse-width modulation. But the scale for pulses isn't specified, so
|
||||
// that's something to return to.
|
||||
|
||||
while(number_of_samples) {
|
||||
// Determine how many output samples will be at the same level.
|
||||
const auto cycles_left_in_sample = std::min(number_of_samples, sample_length - subcycle_offset_);
|
||||
|
||||
// Determine the output level, and output that many samples.
|
||||
// (Hoping that the copiler substitutes an effective memset16-type operation here).
|
||||
const int16_t output_level = volume_multiplier_ * (int16_t(sample_queue_.buffer[sample_queue_.read_pointer]) - 128);
|
||||
for(size_t c = 0; c < cycles_left_in_sample; ++c) {
|
||||
target[c] = output_level;
|
||||
}
|
||||
target += cycles_left_in_sample;
|
||||
|
||||
// Advance the sample pointer.
|
||||
subcycle_offset_ += cycles_left_in_sample;
|
||||
sample_queue_.read_pointer = (sample_queue_.read_pointer + (subcycle_offset_ / sample_length)) % sample_queue_.buffer.size();
|
||||
subcycle_offset_ %= sample_length;
|
||||
|
||||
// Decreate the number of samples left to write.
|
||||
number_of_samples -= cycles_left_in_sample;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user