mirror of
				https://github.com/TomHarte/CLK.git
				synced 2025-10-31 20:16:07 +00:00 
			
		
		
		
	Compare commits
	
		
			1538 Commits
		
	
	
		
			2018-10-29
			...
			2020-01-16
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 1422f8a93a | ||
|  | f0da75f8e9 | ||
|  | cb8a7a4137 | ||
|  | efd684dc56 | ||
|  | aeac6b5888 | ||
|  | 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 | ||
|  | 4701aa149a | 
							
								
								
									
										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; | 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()) { | 	machines_(machines), machines_mutex_(machines_mutex), queues_(machines.size()) { | ||||||
| 	speaker_ = MultiSpeaker::create(machines); | 	speaker_ = MultiSpeaker::create(machines); | ||||||
| } | } | ||||||
| @@ -25,7 +25,7 @@ void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Ma | |||||||
| 	std::condition_variable condition; | 	std::condition_variable condition; | ||||||
| 	std::mutex mutex; | 	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); | 		std::lock_guard<std::mutex> lock(mutex); | ||||||
| 		outstanding_machines = machines_.size(); | 		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) { | 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_) { | 	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); | 		if(crt_machine) function(crt_machine); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| void MultiCRTMachine::setup_output(float aspect_ratio) { | void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||||
| 	perform_serial([=](::CRTMachine::Machine *machine) { | 	scan_target_ = scan_target; | ||||||
| 		machine->setup_output(aspect_ratio); |  | ||||||
| 	}); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MultiCRTMachine::close_output() { | 	CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine(); | ||||||
| 	perform_serial([=](::CRTMachine::Machine *machine) { | 	if(crt_machine) crt_machine->set_scan_target(scan_target); | ||||||
| 		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; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() { | Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() { | ||||||
| @@ -84,6 +73,14 @@ void MultiCRTMachine::run_for(Time::Seconds duration) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void MultiCRTMachine::did_change_machine_order() { | void MultiCRTMachine::did_change_machine_order() { | ||||||
|  | 	if(scan_target_) scan_target_->will_change_owner(); | ||||||
|  |  | ||||||
|  | 	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_) { | 	if(speaker_) { | ||||||
| 		speaker_->set_new_front_machine(machines_.front().get()); | 		speaker_->set_new_front_machine(machines_.front().get()); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ namespace Dynamic { | |||||||
| */ | */ | ||||||
| class MultiCRTMachine: public CRTMachine::Machine { | class MultiCRTMachine: public CRTMachine::Machine { | ||||||
| 	public: | 	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 | 			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. | 		// Below is the standard CRTMachine::Machine interface; see there for documentation. | ||||||
| 		void setup_output(float aspect_ratio) override; | 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override; | ||||||
| 		void close_output() override; |  | ||||||
| 		Outputs::CRT::CRT *get_crt() override; |  | ||||||
| 		Outputs::Speaker::Speaker *get_speaker() override; | 		Outputs::Speaker::Speaker *get_speaker() override; | ||||||
| 		void run_for(Time::Seconds duration) override; | 		void run_for(Time::Seconds duration) override; | ||||||
|  |  | ||||||
| 	private: | 	private: | ||||||
| 		void run_for(const Cycles cycles) override {} | 		void run_for(const Cycles cycles) override {} | ||||||
| 		const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_; | 		const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_; | ||||||
| 		std::mutex &machines_mutex_; | 		std::recursive_mutex &machines_mutex_; | ||||||
| 		std::vector<Concurrency::AsyncTaskQueue> queues_; | 		std::vector<Concurrency::AsyncTaskQueue> queues_; | ||||||
| 		MultiSpeaker *speaker_ = nullptr; | 		MultiSpeaker *speaker_ = nullptr; | ||||||
| 		Delegate *delegate_ = nullptr; | 		Delegate *delegate_ = nullptr; | ||||||
|  | 		Outputs::Display::ScanTarget *scan_target_ = nullptr; | ||||||
|  |  | ||||||
| 		/*! | 		/*! | ||||||
| 			Performs a parallel for operation across all machines, performing the supplied | 			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_; | 	return joysticks_; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ class MultiJoystickMachine: public JoystickMachine::Machine { | |||||||
| 		MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | 		MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||||
|  |  | ||||||
| 		// Below is the standard JoystickMachine::Machine interface; see there for documentation. | 		// Below is the standard JoystickMachine::Machine interface; see there for documentation. | ||||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override; | 		const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override; | ||||||
|  |  | ||||||
| 	private: | 	private: | ||||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_; | 		std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_; | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ | |||||||
| // | // | ||||||
|  |  | ||||||
| #include "MultiMachine.hpp" | #include "MultiMachine.hpp" | ||||||
|  | #include "../../../Outputs/Log.hpp" | ||||||
|  |  | ||||||
| #include <algorithm> | #include <algorithm> | ||||||
|  |  | ||||||
| @@ -58,6 +59,11 @@ KeyboardMachine::Machine *MultiMachine::keyboard_machine() { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | MouseMachine::Machine *MultiMachine::mouse_machine() { | ||||||
|  | 	// TODO. | ||||||
|  | 	return nullptr; | ||||||
|  | } | ||||||
|  |  | ||||||
| Configurable::Device *MultiMachine::configurable_device() { | Configurable::Device *MultiMachine::configurable_device() { | ||||||
| 	if(has_picked_) { | 	if(has_picked_) { | ||||||
| 		return machines_.front()->configurable_device(); | 		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() { | void MultiMachine::multi_crt_did_run_machines() { | ||||||
| 	std::lock_guard<std::mutex> machines_lock(machines_mutex_); | 	std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||||
| #ifdef DEBUG | #ifndef NDEBUG | ||||||
| 	for(const auto &machine: machines_) { | 	for(const auto &machine: machines_) { | ||||||
| 		CRTMachine::Machine *crt = machine->crt_machine(); | 		CRTMachine::Machine *crt = machine->crt_machine(); | ||||||
| 		printf("%0.2f ", crt->get_confidence()); | 		LOGNBR(PADHEX(2) << crt->get_confidence() << " " << crt->debug_type() << "; "); | ||||||
| 		crt->print_type(); |  | ||||||
| 		printf("; "); |  | ||||||
| 	} | 	} | ||||||
| 	printf("\n"); | 	LOGNBR(std::endl); | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| 	DynamicMachine *front = machines_.front().get(); | 	DynamicMachine *front = machines_.front().get(); | ||||||
|   | |||||||
| @@ -54,6 +54,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De | |||||||
| 		Configurable::Device *configurable_device() override; | 		Configurable::Device *configurable_device() override; | ||||||
| 		CRTMachine::Machine *crt_machine() override; | 		CRTMachine::Machine *crt_machine() override; | ||||||
| 		JoystickMachine::Machine *joystick_machine() override; | 		JoystickMachine::Machine *joystick_machine() override; | ||||||
|  | 		MouseMachine::Machine *mouse_machine() override; | ||||||
| 		KeyboardMachine::Machine *keyboard_machine() override; | 		KeyboardMachine::Machine *keyboard_machine() override; | ||||||
| 		MediaTarget::Machine *media_target() override; | 		MediaTarget::Machine *media_target() override; | ||||||
| 		void *raw_pointer() override; | 		void *raw_pointer() override; | ||||||
| @@ -62,7 +63,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De | |||||||
| 		void multi_crt_did_run_machines() override; | 		void multi_crt_did_run_machines() override; | ||||||
|  |  | ||||||
| 		std::vector<std::unique_ptr<DynamicMachine>> machines_; | 		std::vector<std::unique_ptr<DynamicMachine>> machines_; | ||||||
| 		std::mutex machines_mutex_; | 		std::recursive_mutex machines_mutex_; | ||||||
|  |  | ||||||
| 		MultiConfigurable configurable_; | 		MultiConfigurable configurable_; | ||||||
| 		MultiCRTMachine crt_machine_; | 		MultiCRTMachine crt_machine_; | ||||||
|   | |||||||
| @@ -15,8 +15,10 @@ enum class Machine { | |||||||
| 	AmstradCPC, | 	AmstradCPC, | ||||||
| 	AppleII, | 	AppleII, | ||||||
| 	Atari2600, | 	Atari2600, | ||||||
|  | 	AtariST, | ||||||
| 	ColecoVision, | 	ColecoVision, | ||||||
| 	Electron, | 	Electron, | ||||||
|  | 	Macintosh, | ||||||
| 	MasterSystem, | 	MasterSystem, | ||||||
| 	MSX, | 	MSX, | ||||||
| 	Oric, | 	Oric, | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ using namespace Analyser::Static::Acorn; | |||||||
|  |  | ||||||
| std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) { | std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) { | ||||||
| 	// c.f. http://beebwiki.mdfs.net/Acorn_DFS_disc_format | 	// c.f. http://beebwiki.mdfs.net/Acorn_DFS_disc_format | ||||||
| 	std::unique_ptr<Catalogue> catalogue(new Catalogue); | 	auto catalogue = std::make_unique<Catalogue>(); | ||||||
| 	Storage::Encodings::MFM::Parser parser(false, disk); | 	Storage::Encodings::MFM::Parser parser(false, disk); | ||||||
|  |  | ||||||
| 	Storage::Encodings::MFM::Sector *names = parser.get_sector(0, 0, 0); | 	Storage::Encodings::MFM::Sector *names = parser.get_sector(0, 0, 0); | ||||||
| @@ -75,7 +75,7 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s | |||||||
| 	return catalogue; | 	return catalogue; | ||||||
| } | } | ||||||
| std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) { | std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) { | ||||||
| 	std::unique_ptr<Catalogue> catalogue(new Catalogue); | 	auto catalogue = std::make_unique<Catalogue>(); | ||||||
| 	Storage::Encodings::MFM::Parser parser(true, disk); | 	Storage::Encodings::MFM::Parser parser(true, disk); | ||||||
|  |  | ||||||
| 	Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.get_sector(0, 0, 1); | 	Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.get_sector(0, 0, 1); | ||||||
|   | |||||||
| @@ -58,7 +58,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | |||||||
| } | } | ||||||
|  |  | ||||||
| Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||||
| 	std::unique_ptr<Target> target(new Target); | 	auto target = std::make_unique<Target>(); | ||||||
| 	target->machine = Machine::Electron; | 	target->machine = Machine::Electron; | ||||||
| 	target->confidence = 0.5; // TODO: a proper estimation | 	target->confidence = 0.5; // TODO: a proper estimation | ||||||
| 	target->has_dfs = false; | 	target->has_dfs = false; | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ | |||||||
| using namespace Analyser::Static::Acorn; | using namespace Analyser::Static::Acorn; | ||||||
|  |  | ||||||
| static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::Tape::Tape> &tape, Storage::Tape::Acorn::Parser &parser) { | static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::Tape::Tape> &tape, Storage::Tape::Acorn::Parser &parser) { | ||||||
| 	std::unique_ptr<File::Chunk> new_chunk(new File::Chunk); | 	auto new_chunk = std::make_unique<File::Chunk>(); | ||||||
| 	int shift_register = 0; | 	int shift_register = 0; | ||||||
|  |  | ||||||
| // TODO: move this into the parser | // TODO: move this into the parser | ||||||
| @@ -90,7 +90,7 @@ static std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &chunks) { | |||||||
| 	if(!chunks.size()) return nullptr; | 	if(!chunks.size()) return nullptr; | ||||||
|  |  | ||||||
| 	// accumulate chunks for as long as block number is sequential and the end-of-file bit isn't set | 	// accumulate chunks for as long as block number is sequential and the end-of-file bit isn't set | ||||||
| 	std::unique_ptr<File> file(new File); | 	auto file = std::make_unique<File>(); | ||||||
|  |  | ||||||
| 	uint16_t block_number = 0; | 	uint16_t block_number = 0; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -181,7 +181,7 @@ static bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, co | |||||||
|  |  | ||||||
| Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||||
| 	TargetList destination; | 	TargetList destination; | ||||||
| 	std::unique_ptr<Target> target(new Target); | 	auto target = std::make_unique<Target>(); | ||||||
| 	target->machine = Machine::AmstradCPC; | 	target->machine = Machine::AmstradCPC; | ||||||
| 	target->confidence = 0.5; | 	target->confidence = 0.5; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ | |||||||
| #include "Target.hpp" | #include "Target.hpp" | ||||||
|  |  | ||||||
| Analyser::Static::TargetList Analyser::Static::AppleII::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | Analyser::Static::TargetList Analyser::Static::AppleII::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||||
| 	auto target = std::unique_ptr<Target>(new Target); | 	auto target = std::make_unique<Target>(); | ||||||
| 	target->machine = Machine::AppleII; | 	target->machine = Machine::AppleII; | ||||||
| 	target->media = media; | 	target->media = media; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,9 +12,10 @@ | |||||||
| 
 | 
 | ||||||
| #include "../Disassembler/6502.hpp" | #include "../Disassembler/6502.hpp" | ||||||
| 
 | 
 | ||||||
| using namespace Analyser::Static::Atari; | using namespace Analyser::Static::Atari2600; | ||||||
|  | using Target = Analyser::Static::Atari2600::Target; | ||||||
| 
 | 
 | ||||||
| static void DeterminePagingFor2kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | static void DeterminePagingFor2kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||||
| 	// if this is a 2kb cartridge then it's definitely either unpaged or a CommaVid
 | 	// if this is a 2kb cartridge then it's definitely either unpaged or a CommaVid
 | ||||||
| 	uint16_t entry_address, break_address; | 	uint16_t entry_address, break_address; | ||||||
| 
 | 
 | ||||||
| @@ -48,10 +49,10 @@ static void DeterminePagingFor2kCartridge(Analyser::Static::Atari::Target &targe | |||||||
| 	// caveat: false positives aren't likely to be problematic; a false positive is a 2KB ROM that always addresses
 | 	// caveat: false positives aren't likely to be problematic; a false positive is a 2KB ROM that always addresses
 | ||||||
| 	// itself so as to land in ROM even if mapped as a CommaVid and this code is on the fence as to whether it
 | 	// itself so as to land in ROM even if mapped as a CommaVid and this code is on the fence as to whether it
 | ||||||
| 	// attempts to modify itself but it probably doesn't
 | 	// attempts to modify itself but it probably doesn't
 | ||||||
| 	if(has_wide_area_store) target.paging_model = Analyser::Static::Atari::Target::PagingModel::CommaVid; | 	if(has_wide_area_store) target.paging_model = Target::PagingModel::CommaVid; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void DeterminePagingFor8kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | static void DeterminePagingFor8kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||||
| 	// Activision stack titles have their vectors at the top of the low 4k, not the top, and
 | 	// Activision stack titles have their vectors at the top of the low 4k, not the top, and
 | ||||||
| 	// always list 0xf000 as both vectors; they do not repeat them, and, inexplicably, they all
 | 	// always list 0xf000 as both vectors; they do not repeat them, and, inexplicably, they all
 | ||||||
| 	// issue an SEI as their first instruction (maybe some sort of relic of the development environment?)
 | 	// issue an SEI as their first instruction (maybe some sort of relic of the development environment?)
 | ||||||
| @@ -60,12 +61,12 @@ static void DeterminePagingFor8kCartridge(Analyser::Static::Atari::Target &targe | |||||||
| 		(segment.data[8191] != 0xf0 || segment.data[8189] != 0xf0 || segment.data[8190] != 0x00 || segment.data[8188] != 0x00) && | 		(segment.data[8191] != 0xf0 || segment.data[8189] != 0xf0 || segment.data[8190] != 0x00 || segment.data[8188] != 0x00) && | ||||||
| 		segment.data[0] == 0x78 | 		segment.data[0] == 0x78 | ||||||
| 	) { | 	) { | ||||||
| 		target.paging_model = Analyser::Static::Atari::Target::PagingModel::ActivisionStack; | 		target.paging_model = Target::PagingModel::ActivisionStack; | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// make an assumption that this is the Atari paging model
 | 	// make an assumption that this is the Atari paging model
 | ||||||
| 	target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari8k; | 	target.paging_model = Target::PagingModel::Atari8k; | ||||||
| 
 | 
 | ||||||
| 	std::set<uint16_t> internal_accesses; | 	std::set<uint16_t> internal_accesses; | ||||||
| 	internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end()); | 	internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end()); | ||||||
| @@ -85,13 +86,13 @@ static void DeterminePagingFor8kCartridge(Analyser::Static::Atari::Target &targe | |||||||
| 		tigervision_access_count += masked_address == 0x3f; | 		tigervision_access_count += masked_address == 0x3f; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if(parker_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::ParkerBros; | 	if(parker_access_count > atari_access_count) target.paging_model = Target::PagingModel::ParkerBros; | ||||||
| 	else if(tigervision_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::Tigervision; | 	else if(tigervision_access_count > atari_access_count) target.paging_model = Target::PagingModel::Tigervision; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void DeterminePagingFor16kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | static void DeterminePagingFor16kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||||
| 	// make an assumption that this is the Atari paging model
 | 	// make an assumption that this is the Atari paging model
 | ||||||
| 	target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari16k; | 	target.paging_model = Target::PagingModel::Atari16k; | ||||||
| 
 | 
 | ||||||
| 	std::set<uint16_t> internal_accesses; | 	std::set<uint16_t> internal_accesses; | ||||||
| 	internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end()); | 	internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end()); | ||||||
| @@ -106,17 +107,17 @@ static void DeterminePagingFor16kCartridge(Analyser::Static::Atari::Target &targ | |||||||
| 		mnetwork_access_count += masked_address >= 0x1fe0 && masked_address < 0x1ffb; | 		mnetwork_access_count += masked_address >= 0x1fe0 && masked_address < 0x1ffb; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if(mnetwork_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::MNetwork; | 	if(mnetwork_access_count > atari_access_count) target.paging_model = Target::PagingModel::MNetwork; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void DeterminePagingFor64kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | static void DeterminePagingFor64kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||||
| 	// make an assumption that this is a Tigervision if there is a write to 3F
 | 	// make an assumption that this is a Tigervision if there is a write to 3F
 | ||||||
| 	target.paging_model = | 	target.paging_model = | ||||||
| 		(disassembly.external_stores.find(0x3f) != disassembly.external_stores.end()) ? | 		(disassembly.external_stores.find(0x3f) != disassembly.external_stores.end()) ? | ||||||
| 			Analyser::Static::Atari::Target::PagingModel::Tigervision : Analyser::Static::Atari::Target::PagingModel::MegaBoy; | 			Target::PagingModel::Tigervision : Target::PagingModel::MegaBoy; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | static void DeterminePagingForCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||||
| 	if(segment.data.size() == 2048) { | 	if(segment.data.size() == 2048) { | ||||||
| 		DeterminePagingFor2kCartridge(target, segment); | 		DeterminePagingFor2kCartridge(target, segment); | ||||||
| 		return; | 		return; | ||||||
| @@ -140,16 +141,16 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, | |||||||
| 			DeterminePagingFor8kCartridge(target, segment, disassembly); | 			DeterminePagingFor8kCartridge(target, segment, disassembly); | ||||||
| 		break; | 		break; | ||||||
| 		case 10495: | 		case 10495: | ||||||
| 			target.paging_model = Analyser::Static::Atari::Target::PagingModel::Pitfall2; | 			target.paging_model = Target::PagingModel::Pitfall2; | ||||||
| 		break; | 		break; | ||||||
| 		case 12288: | 		case 12288: | ||||||
| 			target.paging_model = Analyser::Static::Atari::Target::PagingModel::CBSRamPlus; | 			target.paging_model = Target::PagingModel::CBSRamPlus; | ||||||
| 		break; | 		break; | ||||||
| 		case 16384: | 		case 16384: | ||||||
| 			DeterminePagingFor16kCartridge(target, segment, disassembly); | 			DeterminePagingFor16kCartridge(target, segment, disassembly); | ||||||
| 		break; | 		break; | ||||||
| 		case 32768: | 		case 32768: | ||||||
| 			target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari32k; | 			target.paging_model = Target::PagingModel::Atari32k; | ||||||
| 		break; | 		break; | ||||||
| 		case 65536: | 		case 65536: | ||||||
| 			DeterminePagingFor64kCartridge(target, segment, disassembly); | 			DeterminePagingFor64kCartridge(target, segment, disassembly); | ||||||
| @@ -161,8 +162,8 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, | |||||||
| 	// check for a Super Chip. Atari ROM images [almost] always have the same value stored over RAM
 | 	// check for a Super Chip. Atari ROM images [almost] always have the same value stored over RAM
 | ||||||
| 	// regions; when they don't they at least seem to have the first 128 bytes be the same as the
 | 	// regions; when they don't they at least seem to have the first 128 bytes be the same as the
 | ||||||
| 	// next 128 bytes. So check for that.
 | 	// next 128 bytes. So check for that.
 | ||||||
| 	if(	target.paging_model != Analyser::Static::Atari::Target::PagingModel::CBSRamPlus && | 	if(	target.paging_model != Target::PagingModel::CBSRamPlus && | ||||||
| 		target.paging_model != Analyser::Static::Atari::Target::PagingModel::MNetwork) { | 		target.paging_model != Target::PagingModel::MNetwork) { | ||||||
| 		bool has_superchip = true; | 		bool has_superchip = true; | ||||||
| 		for(std::size_t address = 0; address < 128; address++) { | 		for(std::size_t address = 0; address < 128; address++) { | ||||||
| 			if(segment.data[address] != segment.data[address+128]) { | 			if(segment.data[address] != segment.data[address+128]) { | ||||||
| @@ -174,19 +175,19 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// check for a Tigervision or Tigervision-esque scheme
 | 	// check for a Tigervision or Tigervision-esque scheme
 | ||||||
| 	if(target.paging_model == Analyser::Static::Atari::Target::PagingModel::None && segment.data.size() > 4096) { | 	if(target.paging_model == Target::PagingModel::None && segment.data.size() > 4096) { | ||||||
| 		bool looks_like_tigervision = disassembly.external_stores.find(0x3f) != disassembly.external_stores.end(); | 		bool looks_like_tigervision = disassembly.external_stores.find(0x3f) != disassembly.external_stores.end(); | ||||||
| 		if(looks_like_tigervision) target.paging_model = Analyser::Static::Atari::Target::PagingModel::Tigervision; | 		if(looks_like_tigervision) target.paging_model = Target::PagingModel::Tigervision; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Analyser::Static::TargetList Analyser::Static::Atari::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | Analyser::Static::TargetList Analyser::Static::Atari2600::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||||
| 	// TODO: sanity checking; is this image really for an Atari 2600?
 | 	// TODO: sanity checking; is this image really for an Atari 2600?
 | ||||||
| 	std::unique_ptr<Analyser::Static::Atari::Target> target(new Analyser::Static::Atari::Target); | 	auto target = std::make_unique<Target>(); | ||||||
| 	target->machine = Machine::Atari2600; | 	target->machine = Machine::Atari2600; | ||||||
| 	target->confidence = 0.5; | 	target->confidence = 0.5; | ||||||
| 	target->media.cartridges = media.cartridges; | 	target->media.cartridges = media.cartridges; | ||||||
| 	target->paging_model = Analyser::Static::Atari::Target::PagingModel::None; | 	target->paging_model = Target::PagingModel::None; | ||||||
| 	target->uses_superchip = false; | 	target->uses_superchip = false; | ||||||
| 
 | 
 | ||||||
| 	// try to figure out the paging scheme
 | 	// try to figure out the paging scheme
 | ||||||
| @@ -15,7 +15,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace Analyser { | namespace Analyser { | ||||||
| namespace Static { | namespace Static { | ||||||
| namespace Atari { | namespace Atari2600 { | ||||||
| 
 | 
 | ||||||
| TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); | TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); | ||||||
| 
 | 
 | ||||||
| @@ -6,14 +6,14 @@ | |||||||
| //  Copyright 2018 Thomas Harte. All rights reserved.
 | //  Copyright 2018 Thomas Harte. All rights reserved.
 | ||||||
| //
 | //
 | ||||||
| 
 | 
 | ||||||
| #ifndef Analyser_Static_Atari_Target_h | #ifndef Analyser_Static_Atari2600_Target_h | ||||||
| #define Analyser_Static_Atari_Target_h | #define Analyser_Static_Atari2600_Target_h | ||||||
| 
 | 
 | ||||||
| #include "../StaticAnalyser.hpp" | #include "../StaticAnalyser.hpp" | ||||||
| 
 | 
 | ||||||
| namespace Analyser { | namespace Analyser { | ||||||
| namespace Static { | namespace Static { | ||||||
| namespace Atari { | namespace Atari2600 { | ||||||
| 
 | 
 | ||||||
| struct Target: public ::Analyser::Static::Target { | struct Target: public ::Analyser::Static::Target { | ||||||
| 	enum class PagingModel { | 	enum class PagingModel { | ||||||
							
								
								
									
										26
									
								
								Analyser/Static/AtariST/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Analyser/Static/AtariST/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | // | ||||||
|  | //  StaticAnalyser.cpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 03/10/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #include "StaticAnalyser.hpp" | ||||||
|  | #include "Target.hpp" | ||||||
|  |  | ||||||
|  | Analyser::Static::TargetList Analyser::Static::AtariST::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||||
|  | 	// This analyser can comprehend disks and mass-storage devices only. | ||||||
|  | 	if(media.disks.empty()) return {}; | ||||||
|  |  | ||||||
|  | 	// As there is at least one usable media image, wave it through. | ||||||
|  | 	Analyser::Static::TargetList targets; | ||||||
|  |  | ||||||
|  | 	using Target = Analyser::Static::Target; | ||||||
|  | 	auto *target = new Target; | ||||||
|  | 	target->machine = Analyser::Machine::AtariST; | ||||||
|  | 	target->media = media; | ||||||
|  | 	targets.push_back(std::unique_ptr<Analyser::Static::Target>(target)); | ||||||
|  |  | ||||||
|  | 	return targets; | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								Analyser/Static/AtariST/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Analyser/Static/AtariST/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | // | ||||||
|  | //  StaticAnalyser.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 03/10/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef Analyser_Static_AtariST_StaticAnalyser_hpp | ||||||
|  | #define Analyser_Static_AtariST_StaticAnalyser_hpp | ||||||
|  |  | ||||||
|  | #include "../StaticAnalyser.hpp" | ||||||
|  | #include "../../../Storage/TargetPlatforms.hpp" | ||||||
|  | #include <string> | ||||||
|  |  | ||||||
|  | namespace Analyser { | ||||||
|  | namespace Static { | ||||||
|  | namespace AtariST { | ||||||
|  |  | ||||||
|  | TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #endif /* Analyser_Static_AtariST_StaticAnalyser_hpp */ | ||||||
							
								
								
									
										23
									
								
								Analyser/Static/AtariST/Target.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								Analyser/Static/AtariST/Target.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | // | ||||||
|  | //  Target.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 03/06/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef Analyser_Static_AtariST_Target_h | ||||||
|  | #define Analyser_Static_AtariST_Target_h | ||||||
|  |  | ||||||
|  | namespace Analyser { | ||||||
|  | namespace Static { | ||||||
|  | namespace AtariST { | ||||||
|  |  | ||||||
|  | struct Target: public ::Analyser::Static::Target { | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* Analyser_Static_AtariST_Target_h */ | ||||||
| @@ -54,7 +54,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | |||||||
|  |  | ||||||
| Analyser::Static::TargetList Analyser::Static::Coleco::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | Analyser::Static::TargetList Analyser::Static::Coleco::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||||
| 	TargetList targets; | 	TargetList targets; | ||||||
| 	std::unique_ptr<Target> target(new Target); | 	auto target = std::make_unique<Target>(); | ||||||
| 	target->machine = Machine::ColecoVision; | 	target->machine = Machine::ColecoVision; | ||||||
| 	target->confidence = 1.0f - 1.0f / 32768.0f; | 	target->confidence = 1.0f - 1.0f / 32768.0f; | ||||||
| 	target->media.cartridges = ColecoCartridgesFrom(media.cartridges); | 	target->media.cartridges = ColecoCartridgesFrom(media.cartridges); | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | |||||||
| 		std::shared_ptr<Storage::Disk::Drive> drive; | 		std::shared_ptr<Storage::Disk::Drive> drive; | ||||||
|  |  | ||||||
| 		CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) { | 		CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) { | ||||||
| 			drive.reset(new Storage::Disk::Drive(4000000, 300, 2)); | 			drive = std::make_shared<Storage::Disk::Drive>(4000000, 300, 2); | ||||||
| 			set_drive(drive); | 			set_drive(drive); | ||||||
| 			drive->set_motor_on(true); | 			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> get_next_sector() { | ||||||
| 			std::shared_ptr<Sector> sector(new Sector); | 			auto sector = std::make_shared<Sector>(); | ||||||
| 			const int max_index_count = index_count_ + 2; | 			const int max_index_count = index_count_ + 2; | ||||||
|  |  | ||||||
| 			while(index_count_ < max_index_count) { | 			while(index_count_ < max_index_count) { | ||||||
|   | |||||||
| @@ -13,8 +13,10 @@ | |||||||
| #include "Tape.hpp" | #include "Tape.hpp" | ||||||
| #include "Target.hpp" | #include "Target.hpp" | ||||||
| #include "../../../Storage/Cartridge/Encodings/CommodoreROM.hpp" | #include "../../../Storage/Cartridge/Encodings/CommodoreROM.hpp" | ||||||
|  | #include "../../../Outputs/Log.hpp" | ||||||
|  |  | ||||||
| #include <algorithm> | #include <algorithm> | ||||||
|  | #include <cstring> | ||||||
| #include <sstream> | #include <sstream> | ||||||
|  |  | ||||||
| using namespace Analyser::Static::Commodore; | using namespace Analyser::Static::Commodore; | ||||||
| @@ -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) { | Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||||
| 	TargetList destination; | 	TargetList destination; | ||||||
|  |  | ||||||
| 	std::unique_ptr<Target> target(new Target); | 	auto target = std::make_unique<Target>(); | ||||||
| 	target->machine = Machine::Vic20;	// TODO: machine estimation | 	target->machine = Machine::Vic20;	// TODO: machine estimation | ||||||
| 	target->confidence = 0.5; // TODO: a proper estimation | 	target->confidence = 0.5; // TODO: a proper estimation | ||||||
|  |  | ||||||
| @@ -77,7 +79,7 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if(!files.empty()) { | 	if(!files.empty()) { | ||||||
| 		target->memory_model = Target::MemoryModel::Unexpanded; | 		auto memory_model = Target::MemoryModel::Unexpanded; | ||||||
| 		std::ostringstream string_stream; | 		std::ostringstream string_stream; | ||||||
| 		string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ","; | 		string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ","; | ||||||
| 		if(files.front().is_basic()) { | 		if(files.front().is_basic()) { | ||||||
| @@ -91,18 +93,20 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media | |||||||
| 		// make a first guess based on loading address | 		// make a first guess based on loading address | ||||||
| 		switch(files.front().starting_address) { | 		switch(files.front().starting_address) { | ||||||
| 			default: | 			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: | 			case 0x1001: | ||||||
| 				target->memory_model = Target::MemoryModel::Unexpanded; | 				memory_model = Target::MemoryModel::Unexpanded; | ||||||
| 			break; | 			break; | ||||||
| 			case 0x1201: | 			case 0x1201: | ||||||
| 				target->memory_model = Target::MemoryModel::ThirtyTwoKB; | 				memory_model = Target::MemoryModel::ThirtyTwoKB; | ||||||
| 			break; | 			break; | ||||||
| 			case 0x0401: | 			case 0x0401: | ||||||
| 				target->memory_model = Target::MemoryModel::EightKB; | 				memory_model = Target::MemoryModel::EightKB; | ||||||
| 			break; | 			break; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		target->set_memory_model(memory_model); | ||||||
|  |  | ||||||
| 		// General approach: increase memory size conservatively such that the largest file found will fit. | 		// General approach: increase memory size conservatively such that the largest file found will fit. | ||||||
| //		for(File &file : files) { | //		for(File &file : files) { | ||||||
| //			std::size_t file_size = file.data.size(); | //			std::size_t file_size = file.data.size(); | ||||||
| @@ -144,13 +148,52 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if(!target->media.empty()) { | 	if(!target->media.empty()) { | ||||||
| 		// Inspect filename for a region hint. | 		// Inspect filename for configuration hints. | ||||||
| 		std::string lowercase_name = file_name; | 		std::string lowercase_name = file_name; | ||||||
| 		std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower); | 		std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower); | ||||||
|  |  | ||||||
|  | 		// Hint 1: 'ntsc' anywhere in the name implies America. | ||||||
| 		if(lowercase_name.find("ntsc") != std::string::npos) { | 		if(lowercase_name.find("ntsc") != std::string::npos) { | ||||||
| 			target->region = Analyser::Static::Commodore::Target::Region::American; | 			target->region = Analyser::Static::Commodore::Target::Region::American; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// Potential additional hints: check for TheC64 tags. | ||||||
|  | 		auto final_underscore = lowercase_name.find_last_of('_'); | ||||||
|  | 		if(final_underscore != std::string::npos) { | ||||||
|  | 			auto iterator = lowercase_name.begin() + ssize_t(final_underscore) + 1; | ||||||
|  |  | ||||||
|  | 			while(iterator != lowercase_name.end()) { | ||||||
|  | 				// Grab the next tag. | ||||||
|  | 				char next_tag[3] = {0, 0, 0}; | ||||||
|  | 				next_tag[0] = *iterator++; | ||||||
|  | 				if(iterator == lowercase_name.end()) break; | ||||||
|  | 				next_tag[1] = *iterator++; | ||||||
|  |  | ||||||
|  | 				// Exit early if attempting to read another tag has run over the file extension. | ||||||
|  | 				if(next_tag[0] == '.' || next_tag[1] == '.') break; | ||||||
|  |  | ||||||
|  | 				// Check whether it's anything. | ||||||
|  | 				target->enabled_ram.bank0 |= !strcmp(next_tag, "b0"); | ||||||
|  | 				target->enabled_ram.bank1 |= !strcmp(next_tag, "b1"); | ||||||
|  | 				target->enabled_ram.bank2 |= !strcmp(next_tag, "b2"); | ||||||
|  | 				target->enabled_ram.bank3 |= !strcmp(next_tag, "b3"); | ||||||
|  | 				target->enabled_ram.bank5 |= !strcmp(next_tag, "b5"); | ||||||
|  | 				if(!strcmp(next_tag, "tn")) {	// i.e. NTSC. | ||||||
|  | 					target->region = Analyser::Static::Commodore::Target::Region::American; | ||||||
|  | 				} | ||||||
|  | 				if(!strcmp(next_tag, "tp")) {	// i.e. PAL. | ||||||
|  | 					target->region = Analyser::Static::Commodore::Target::Region::European; | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// Unhandled: | ||||||
|  | 				// | ||||||
|  | 				//	M6: 	this is a C64 file. | ||||||
|  | 				//	MV: 	this is a Vic-20 file. | ||||||
|  | 				//	J1/J2:	this C64 file should have the primary joystick in slot 1/2. | ||||||
|  | 				//	RO:		this disk image should be treated as read-only. | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Attach a 1540 if there are any disks here. | 		// Attach a 1540 if there are any disks here. | ||||||
| 		target->has_c1540 = !target->media.disks.empty(); | 		target->has_c1540 = !target->media.disks.empty(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,7 +31,26 @@ struct Target: public ::Analyser::Static::Target { | |||||||
| 		Swedish | 		Swedish | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	MemoryModel memory_model = MemoryModel::Unexpanded; | 	/// Maps from a named memory model to a bank enabled/disabled set. | ||||||
|  | 	void set_memory_model(MemoryModel memory_model) { | ||||||
|  | 		// This is correct for unexpanded and 32kb memory models. | ||||||
|  | 		enabled_ram.bank0 = enabled_ram.bank1 = | ||||||
|  | 		enabled_ram.bank2 = enabled_ram.bank3 = | ||||||
|  | 		enabled_ram.bank5 = memory_model == MemoryModel::ThirtyTwoKB; | ||||||
|  |  | ||||||
|  | 		// Bank 0 will need to be enabled if this is an 8kb machine. | ||||||
|  | 		enabled_ram.bank0 |= memory_model == MemoryModel::EightKB; | ||||||
|  | 	} | ||||||
|  | 	struct { | ||||||
|  | 		bool bank0 = false; | ||||||
|  | 		bool bank1 = false; | ||||||
|  | 		bool bank2 = false; | ||||||
|  | 		bool bank3 = false; | ||||||
|  | 		bool bank5 = false; | ||||||
|  | 					// Sic. There is no bank 4; this is because the area that logically would be | ||||||
|  | 					// bank 4 is occupied by the character ROM, colour RAM, hardware registers, etc. | ||||||
|  | 	} enabled_ram; | ||||||
|  |  | ||||||
| 	Region region = Region::European; | 	Region region = Region::European; | ||||||
| 	bool has_c1540 = false; | 	bool has_c1540 = false; | ||||||
| 	std::string loading_command; | 	std::string loading_command; | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ static std::unique_ptr<Analyser::Static::Target> CartridgeTarget( | |||||||
| 		output_segments.emplace_back(start_address, segment.data); | 		output_segments.emplace_back(start_address, segment.data); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	std::unique_ptr<Analyser::Static::MSX::Target> target(new Analyser::Static::MSX::Target); | 	auto target = std::make_unique<Analyser::Static::MSX::Target>(); | ||||||
| 	target->machine = Analyser::Machine::MSX; | 	target->machine = Analyser::Machine::MSX; | ||||||
| 	target->confidence = confidence; | 	target->confidence = confidence; | ||||||
|  |  | ||||||
| @@ -269,7 +269,7 @@ Analyser::Static::TargetList Analyser::Static::MSX::GetTargets(const Media &medi | |||||||
| 	std::move(cartridge_targets.begin(), cartridge_targets.end(), std::back_inserter(destination)); | 	std::move(cartridge_targets.begin(), cartridge_targets.end(), std::back_inserter(destination)); | ||||||
|  |  | ||||||
| 	// Consider building a target for disks and/or tapes. | 	// Consider building a target for disks and/or tapes. | ||||||
| 	std::unique_ptr<Target> target(new Target); | 	auto target = std::make_unique<Target>(); | ||||||
|  |  | ||||||
| 	// Check tapes for loadable files. | 	// Check tapes for loadable files. | ||||||
| 	for(auto &tape : media.tapes) { | 	for(auto &tape : media.tapes) { | ||||||
| @@ -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. | 	// Blindly accept disks for now. | ||||||
|  | 	// TODO: how to spot an MSX disk? | ||||||
| 	target->media.disks = media.disks; | 	target->media.disks = media.disks; | ||||||
| 	target->has_disk_drive = !media.disks.empty(); | 	target->has_disk_drive = !media.disks.empty(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,6 +19,12 @@ namespace MSX { | |||||||
| struct Target: public ::Analyser::Static::Target { | struct Target: public ::Analyser::Static::Target { | ||||||
| 	bool has_disk_drive = false; | 	bool has_disk_drive = false; | ||||||
| 	std::string loading_command; | 	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; | using namespace Analyser::Static::Oric; | ||||||
|  |  | ||||||
| static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) { | namespace { | ||||||
|  |  | ||||||
|  | int score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) { | ||||||
| 	int score = 0; | 	int score = 0; | ||||||
|  |  | ||||||
| 	for(const auto address : disassembly.outward_calls)		score += (rom_functions.find(address) != rom_functions.end()) ? 1 : -1; | 	for(const auto address : disassembly.outward_calls)		score += (rom_functions.find(address) != rom_functions.end()) ? 1 : -1; | ||||||
| @@ -30,7 +32,7 @@ static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, cons | |||||||
| 	return score; | 	return score; | ||||||
| } | } | ||||||
|  |  | ||||||
| static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | int basic10_score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||||
| 	const std::set<uint16_t> rom_functions = { | 	const std::set<uint16_t> rom_functions = { | ||||||
| 		0x0228,	0x022b, | 		0x0228,	0x022b, | ||||||
| 		0xc3ca,	0xc3f8,	0xc448,	0xc47c,	0xc4b5,	0xc4e3,	0xc4e0,	0xc524,	0xc56f,	0xc5a2,	0xc5f8,	0xc60a,	0xc6a5,	0xc6de,	0xc719,	0xc738, | 		0xc3ca,	0xc3f8,	0xc448,	0xc47c,	0xc4b5,	0xc4e3,	0xc4e0,	0xc524,	0xc56f,	0xc5a2,	0xc5f8,	0xc60a,	0xc6a5,	0xc6de,	0xc719,	0xc738, | ||||||
| @@ -51,10 +53,10 @@ static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembl | |||||||
| 		0x0228, 0x0229, 0x022a, 0x022b, 0x022c, 0x022d, 0x0230 | 		0x0228, 0x0229, 0x022a, 0x022b, 0x022c, 0x022d, 0x0230 | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	return Score(disassembly, rom_functions, variable_locations); | 	return score(disassembly, rom_functions, variable_locations); | ||||||
| } | } | ||||||
|  |  | ||||||
| static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | int basic11_score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||||
| 	const std::set<uint16_t> rom_functions = { | 	const std::set<uint16_t> rom_functions = { | ||||||
| 		0x0238,	0x023b,	0x023e,	0x0241,	0x0244,	0x0247, | 		0x0238,	0x023b,	0x023e,	0x0241,	0x0244,	0x0247, | ||||||
| 		0xc3c6,	0xc3f4,	0xc444,	0xc47c,	0xc4a8,	0xc4d3,	0xc4e0,	0xc524,	0xc55f,	0xc592,	0xc5e8,	0xc5fa,	0xc692,	0xc6b3,	0xc6ee,	0xc70d, | 		0xc3c6,	0xc3f4,	0xc444,	0xc47c,	0xc4a8,	0xc4d3,	0xc4e0,	0xc524,	0xc55f,	0xc592,	0xc5e8,	0xc5fa,	0xc692,	0xc6b3,	0xc6ee,	0xc70d, | ||||||
| @@ -76,10 +78,10 @@ static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembl | |||||||
| 		0x0244, 0x0245, 0x0246, 0x0247, 0x0248, 0x0249, 0x024a, 0x024b, 0x024c | 		0x0244, 0x0245, 0x0246, 0x0247, 0x0248, 0x0249, 0x024a, 0x024b, 0x024c | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	return Score(disassembly, rom_functions, variable_locations); | 	return score(disassembly, rom_functions, variable_locations); | ||||||
| } | } | ||||||
|  |  | ||||||
| static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) { | bool is_microdisc(Storage::Encodings::MFM::Parser &parser) { | ||||||
| 	/* | 	/* | ||||||
| 		The Microdisc boot sector is sector 2 of track 0 and contains a 23-byte signature. | 		The Microdisc boot sector is sector 2 of track 0 and contains a 23-byte signature. | ||||||
| 	*/ | 	*/ | ||||||
| @@ -100,8 +102,51 @@ static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) { | |||||||
| 	return !std::memcmp(signature, first_sample.data(), sizeof(signature)); | 	return !std::memcmp(signature, first_sample.data(), sizeof(signature)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | bool is_400_loader(Storage::Encodings::MFM::Parser &parser, uint16_t range_start, uint16_t range_end) { | ||||||
|  | 	/* | ||||||
|  | 		Both the Jasmin and BD-DOS boot sectors are sector 1 of track 0 and are loaded at $400; | ||||||
|  | 		use disassembly to test for likely matches. | ||||||
|  | 	*/ | ||||||
|  |  | ||||||
|  | 	Storage::Encodings::MFM::Sector *sector = parser.get_sector(0, 0, 1); | ||||||
|  | 	if(!sector) return false; | ||||||
|  | 	if(sector->samples.empty()) return false; | ||||||
|  |  | ||||||
|  | 	// Take a copy of the first sampling, and keep only the final 256 bytes (assuming at least that many were found). | ||||||
|  | 	std::vector<uint8_t> first_sample = sector->samples[0]; | ||||||
|  | 	if(first_sample.size() < 256) return false; | ||||||
|  | 	if(first_sample.size() > 256) { | ||||||
|  | 		first_sample.erase(first_sample.end() - 256, first_sample.end()); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Grab a disassembly. | ||||||
|  | 	const auto disassembly = | ||||||
|  | 		Analyser::Static::MOS6502::Disassemble(first_sample, Analyser::Static::Disassembler::OffsetMapper(0x400), {0x400}); | ||||||
|  |  | ||||||
|  | 	// Check for references to the Jasmin registers. | ||||||
|  | 	int register_hits = 0; | ||||||
|  | 	for(auto list : {disassembly.external_stores, disassembly.external_loads, disassembly.external_modifies}) { | ||||||
|  | 		for(auto address : list) { | ||||||
|  | 			register_hits += (address >= range_start && address <= range_end); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Arbitrary, sure, but as long as at least two accesses to the requested register range are found, accept this. | ||||||
|  | 	return register_hits >= 2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool is_jasmin(Storage::Encodings::MFM::Parser &parser) { | ||||||
|  | 	return is_400_loader(parser, 0x3f4, 0x3ff); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool is_bd500(Storage::Encodings::MFM::Parser &parser) { | ||||||
|  | 	return is_400_loader(parser, 0x310, 0x323); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||||
| 	std::unique_ptr<Target> target(new Target); | 	auto target = std::make_unique<Target>(); | ||||||
| 	target->machine = Machine::Oric; | 	target->machine = Machine::Oric; | ||||||
| 	target->confidence = 0.5; | 	target->confidence = 0.5; | ||||||
|  |  | ||||||
| @@ -115,12 +160,10 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med | |||||||
| 			for(const auto &file : tape_files) { | 			for(const auto &file : tape_files) { | ||||||
| 				if(file.data_type == File::MachineCode) { | 				if(file.data_type == File::MachineCode) { | ||||||
| 					std::vector<uint16_t> entry_points = {file.starting_address}; | 					std::vector<uint16_t> entry_points = {file.starting_address}; | ||||||
| 					Analyser::Static::MOS6502::Disassembly disassembly = | 					const Analyser::Static::MOS6502::Disassembly disassembly = | ||||||
| 						Analyser::Static::MOS6502::Disassemble(file.data, Analyser::Static::Disassembler::OffsetMapper(file.starting_address), entry_points); | 						Analyser::Static::MOS6502::Disassemble(file.data, Analyser::Static::Disassembler::OffsetMapper(file.starting_address), entry_points); | ||||||
|  |  | ||||||
| 					int basic10_score = Basic10Score(disassembly); | 					if(basic10_score(disassembly) > basic11_score(disassembly)) ++basic10_votes; else ++basic11_votes; | ||||||
| 					int basic11_score = Basic11Score(disassembly); |  | ||||||
| 					if(basic10_score > basic11_score) basic10_votes++; else basic11_votes++; |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @@ -130,12 +173,22 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if(!media.disks.empty()) { | 	if(!media.disks.empty()) { | ||||||
| 		// Only the Microdisc is emulated right now, so accept only disks that it can boot from. | 		// 8-DOS is recognised by a dedicated Disk II analyser, so check only for Microdisc, | ||||||
|  | 		// Jasmin and BD-DOS formats here. | ||||||
| 		for(auto &disk: media.disks) { | 		for(auto &disk: media.disks) { | ||||||
| 			Storage::Encodings::MFM::Parser parser(true, disk); | 			Storage::Encodings::MFM::Parser parser(true, disk); | ||||||
| 			if(IsMicrodisc(parser)) { |  | ||||||
|  | 			if(is_microdisc(parser)) { | ||||||
| 				target->disk_interface = Target::DiskInterface::Microdisc; | 				target->disk_interface = Target::DiskInterface::Microdisc; | ||||||
| 				target->media.disks.push_back(disk); | 				target->media.disks.push_back(disk); | ||||||
|  | 			} else if(is_jasmin(parser)) { | ||||||
|  | 				target->disk_interface = Target::DiskInterface::Jasmin; | ||||||
|  | 				target->should_start_jasmin = true; | ||||||
|  | 				target->media.disks.push_back(disk); | ||||||
|  | 			} else if(is_bd500(parser)) { | ||||||
|  | 				target->disk_interface = Target::DiskInterface::BD500; | ||||||
|  | 				target->media.disks.push_back(disk); | ||||||
|  | 				target->rom = Target::ROM::BASIC10; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -26,12 +26,15 @@ struct Target: public ::Analyser::Static::Target { | |||||||
| 	enum class DiskInterface { | 	enum class DiskInterface { | ||||||
| 		Microdisc, | 		Microdisc, | ||||||
| 		Pravetz, | 		Pravetz, | ||||||
|  | 		Jasmin, | ||||||
|  | 		BD500, | ||||||
| 		None | 		None | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	ROM rom = ROM::BASIC11; | 	ROM rom = ROM::BASIC11; | ||||||
| 	DiskInterface disk_interface = DiskInterface::None; | 	DiskInterface disk_interface = DiskInterface::None; | ||||||
| 	std::string loading_command; | 	std::string loading_command; | ||||||
|  | 	bool should_start_jasmin = false; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &med | |||||||
| 		return {}; | 		return {}; | ||||||
|  |  | ||||||
| 	TargetList targets; | 	TargetList targets; | ||||||
| 	std::unique_ptr<Target> target(new Target); | 	auto target = std::make_unique<Target>(); | ||||||
|  |  | ||||||
| 	target->machine = Machine::MasterSystem; | 	target->machine = Machine::MasterSystem; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,10 +17,12 @@ | |||||||
| #include "Acorn/StaticAnalyser.hpp" | #include "Acorn/StaticAnalyser.hpp" | ||||||
| #include "AmstradCPC/StaticAnalyser.hpp" | #include "AmstradCPC/StaticAnalyser.hpp" | ||||||
| #include "AppleII/StaticAnalyser.hpp" | #include "AppleII/StaticAnalyser.hpp" | ||||||
| #include "Atari/StaticAnalyser.hpp" | #include "Atari2600/StaticAnalyser.hpp" | ||||||
|  | #include "AtariST/StaticAnalyser.hpp" | ||||||
| #include "Coleco/StaticAnalyser.hpp" | #include "Coleco/StaticAnalyser.hpp" | ||||||
| #include "Commodore/StaticAnalyser.hpp" | #include "Commodore/StaticAnalyser.hpp" | ||||||
| #include "DiskII/StaticAnalyser.hpp" | #include "DiskII/StaticAnalyser.hpp" | ||||||
|  | #include "Macintosh/StaticAnalyser.hpp" | ||||||
| #include "MSX/StaticAnalyser.hpp" | #include "MSX/StaticAnalyser.hpp" | ||||||
| #include "Oric/StaticAnalyser.hpp" | #include "Oric/StaticAnalyser.hpp" | ||||||
| #include "Sega/StaticAnalyser.hpp" | #include "Sega/StaticAnalyser.hpp" | ||||||
| @@ -35,15 +37,22 @@ | |||||||
| #include "../../Storage/Disk/DiskImage/Formats/AppleDSK.hpp" | #include "../../Storage/Disk/DiskImage/Formats/AppleDSK.hpp" | ||||||
| #include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp" | #include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp" | ||||||
| #include "../../Storage/Disk/DiskImage/Formats/D64.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/G64.hpp" | ||||||
| #include "../../Storage/Disk/DiskImage/Formats/DMK.hpp" | #include "../../Storage/Disk/DiskImage/Formats/DMK.hpp" | ||||||
| #include "../../Storage/Disk/DiskImage/Formats/HFE.hpp" | #include "../../Storage/Disk/DiskImage/Formats/HFE.hpp" | ||||||
|  | #include "../../Storage/Disk/DiskImage/Formats/MSA.hpp" | ||||||
| #include "../../Storage/Disk/DiskImage/Formats/MSXDSK.hpp" | #include "../../Storage/Disk/DiskImage/Formats/MSXDSK.hpp" | ||||||
| #include "../../Storage/Disk/DiskImage/Formats/NIB.hpp" | #include "../../Storage/Disk/DiskImage/Formats/NIB.hpp" | ||||||
| #include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp" | #include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp" | ||||||
| #include "../../Storage/Disk/DiskImage/Formats/SSD.hpp" | #include "../../Storage/Disk/DiskImage/Formats/SSD.hpp" | ||||||
|  | #include "../../Storage/Disk/DiskImage/Formats/ST.hpp" | ||||||
|  | #include "../../Storage/Disk/DiskImage/Formats/STX.hpp" | ||||||
| #include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp" | #include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp" | ||||||
|  |  | ||||||
|  | // Mass Storage Devices (i.e. usually, hard disks) | ||||||
|  | #include "../../Storage/MassStorage/Formats/HFV.hpp" | ||||||
|  |  | ||||||
| // Tapes | // Tapes | ||||||
| #include "../../Storage/Tape/Formats/CAS.hpp" | #include "../../Storage/Tape/Formats/CAS.hpp" | ||||||
| #include "../../Storage/Tape/Formats/CommodoreTAP.hpp" | #include "../../Storage/Tape/Formats/CommodoreTAP.hpp" | ||||||
| @@ -89,7 +98,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | |||||||
| 	Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// 81 | 	Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// 81 | ||||||
| 	Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600)							// A26 | 	Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600)							// A26 | ||||||
| 	Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn)			// ADF | 	Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn)			// ADF | ||||||
| 	Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge)					// BIN | 	Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge)						// BIN (cartridge dump) | ||||||
| 	Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX)													// CAS | 	Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX)													// CAS | ||||||
| 	Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC)											// CDT | 	Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC)											// CDT | ||||||
| 	Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision)						// COL | 	Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision)						// COL | ||||||
| @@ -99,7 +108,9 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | |||||||
| 	Format("do", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)			// DO | 	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("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)				// DSD | ||||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC)		// DSK (Amstrad CPC) | 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC)		// DSK (Amstrad CPC) | ||||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)		// DSK (Apple) | 	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::MSXDSK>, TargetPlatform::MSX)				// DSK (MSX) | ||||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric)			// DSK (Oric) | 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric)			// DSK (Oric) | ||||||
| 	Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore)			// G64 | 	Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore)			// G64 | ||||||
| @@ -108,6 +119,9 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | |||||||
| 			Disk::DiskImageHolder<Storage::Disk::HFE>, | 			Disk::DiskImageHolder<Storage::Disk::HFE>, | ||||||
| 			TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric) | 			TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric) | ||||||
| 			// HFE (TODO: switch to AllDisk once the MSX stops being so greedy) | 			// HFE (TODO: switch to AllDisk once the MSX stops being so greedy) | ||||||
|  | 	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)		// IMG (DiskCopy 4.2) | ||||||
|  | 	Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// IMG (DiskCopy 4.2) | ||||||
|  | 	Format("msa", result.disks, Disk::DiskImageHolder<Storage::Disk::MSA>, TargetPlatform::AtariST)				// MSA | ||||||
| 	Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII)				// NIB | 	Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII)				// NIB | ||||||
| 	Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// O | 	Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// O | ||||||
| 	Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// P | 	Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// P | ||||||
| @@ -133,6 +147,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | |||||||
| 	Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega)								// SG | 	Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega)								// SG | ||||||
| 	Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega)								// SMS | 	Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega)								// SMS | ||||||
| 	Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)				// SSD | 	Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)				// SSD | ||||||
|  | 	Format("st", result.disks, Disk::DiskImageHolder<Storage::Disk::ST>, TargetPlatform::AtariST)				// ST | ||||||
|  | 	Format("stx", result.disks, Disk::DiskImageHolder<Storage::Disk::STX>, TargetPlatform::AtariST)				// STX | ||||||
| 	Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore)									// TAP (Commodore) | 	Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore)									// TAP (Commodore) | ||||||
| 	Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric)											// TAP (Oric) | 	Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric)											// TAP (Oric) | ||||||
| 	Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX)													// TSX | 	Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX)													// TSX | ||||||
| @@ -155,7 +171,7 @@ Media Analyser::Static::GetMedia(const std::string &file_name) { | |||||||
| TargetList Analyser::Static::GetTargets(const std::string &file_name) { | TargetList Analyser::Static::GetTargets(const std::string &file_name) { | ||||||
| 	TargetList targets; | 	TargetList targets; | ||||||
|  |  | ||||||
| 	// Collect all disks, tapes and ROMs as can be extrapolated from this file, forming the | 	// Collect all disks, tapes ROMs, etc as can be extrapolated from this file, forming the | ||||||
| 	// union of all platforms this file might be a target for. | 	// union of all platforms this file might be a target for. | ||||||
| 	TargetPlatform::IntType potential_platforms = 0; | 	TargetPlatform::IntType potential_platforms = 0; | ||||||
| 	Media media = GetMediaAndPlatforms(file_name, potential_platforms); | 	Media media = GetMediaAndPlatforms(file_name, potential_platforms); | ||||||
| @@ -169,13 +185,15 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) { | |||||||
| 	if(potential_platforms & TargetPlatform::Acorn)			Append(Acorn); | 	if(potential_platforms & TargetPlatform::Acorn)			Append(Acorn); | ||||||
| 	if(potential_platforms & TargetPlatform::AmstradCPC)	Append(AmstradCPC); | 	if(potential_platforms & TargetPlatform::AmstradCPC)	Append(AmstradCPC); | ||||||
| 	if(potential_platforms & TargetPlatform::AppleII)		Append(AppleII); | 	if(potential_platforms & TargetPlatform::AppleII)		Append(AppleII); | ||||||
| 	if(potential_platforms & TargetPlatform::Atari2600)		Append(Atari); | 	if(potential_platforms & TargetPlatform::Atari2600)		Append(Atari2600); | ||||||
|  | 	if(potential_platforms & TargetPlatform::AtariST)		Append(AtariST); | ||||||
| 	if(potential_platforms & TargetPlatform::ColecoVision)	Append(Coleco); | 	if(potential_platforms & TargetPlatform::ColecoVision)	Append(Coleco); | ||||||
| 	if(potential_platforms & TargetPlatform::Commodore)		Append(Commodore); | 	if(potential_platforms & TargetPlatform::Commodore)		Append(Commodore); | ||||||
| 	if(potential_platforms & TargetPlatform::DiskII)		Append(DiskII); | 	if(potential_platforms & TargetPlatform::DiskII)		Append(DiskII); | ||||||
| 	if(potential_platforms & TargetPlatform::Sega)			Append(Sega); | 	if(potential_platforms & TargetPlatform::Macintosh)		Append(Macintosh); | ||||||
| 	if(potential_platforms & TargetPlatform::MSX)			Append(MSX); | 	if(potential_platforms & TargetPlatform::MSX)			Append(MSX); | ||||||
| 	if(potential_platforms & TargetPlatform::Oric)			Append(Oric); | 	if(potential_platforms & TargetPlatform::Oric)			Append(Oric); | ||||||
|  | 	if(potential_platforms & TargetPlatform::Sega)			Append(Sega); | ||||||
| 	if(potential_platforms & TargetPlatform::ZX8081)		Append(ZX8081); | 	if(potential_platforms & TargetPlatform::ZX8081)		Append(ZX8081); | ||||||
| 	#undef Append | 	#undef Append | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,9 +11,10 @@ | |||||||
|  |  | ||||||
| #include "../Machines.hpp" | #include "../Machines.hpp" | ||||||
|  |  | ||||||
| #include "../../Storage/Tape/Tape.hpp" |  | ||||||
| #include "../../Storage/Disk/Disk.hpp" |  | ||||||
| #include "../../Storage/Cartridge/Cartridge.hpp" | #include "../../Storage/Cartridge/Cartridge.hpp" | ||||||
|  | #include "../../Storage/Disk/Disk.hpp" | ||||||
|  | #include "../../Storage/MassStorage/MassStorageDevice.hpp" | ||||||
|  | #include "../../Storage/Tape/Tape.hpp" | ||||||
|  |  | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <string> | #include <string> | ||||||
| @@ -29,9 +30,10 @@ struct Media { | |||||||
| 	std::vector<std::shared_ptr<Storage::Disk::Disk>> disks; | 	std::vector<std::shared_ptr<Storage::Disk::Disk>> disks; | ||||||
| 	std::vector<std::shared_ptr<Storage::Tape::Tape>> tapes; | 	std::vector<std::shared_ptr<Storage::Tape::Tape>> tapes; | ||||||
| 	std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges; | 	std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges; | ||||||
|  | 	std::vector<std::shared_ptr<Storage::MassStorage::MassStorageDevice>> mass_storage_devices; | ||||||
|  |  | ||||||
| 	bool empty() const { | 	bool empty() const { | ||||||
| 		return disks.empty() && tapes.empty() && cartridges.empty(); | 		return disks.empty() && tapes.empty() && cartridges.empty() && mass_storage_devices.empty(); | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,6 +9,9 @@ | |||||||
| #ifndef ClockReceiver_hpp | #ifndef ClockReceiver_hpp | ||||||
| #define ClockReceiver_hpp | #define ClockReceiver_hpp | ||||||
|  |  | ||||||
|  | #include "ForceInline.hpp" | ||||||
|  | #include <cstdint> | ||||||
|  |  | ||||||
| /* | /* | ||||||
| 	Informal pattern for all classes that run from a clock cycle: | 	Informal pattern for all classes that run from a clock cycle: | ||||||
|  |  | ||||||
| @@ -52,149 +55,193 @@ | |||||||
| */ | */ | ||||||
| template <class T> class WrappedInt { | template <class T> class WrappedInt { | ||||||
| 	public: | 	public: | ||||||
| 		constexpr WrappedInt(int l) : length_(l) {} | 		using IntType = int64_t; | ||||||
| 		constexpr WrappedInt() : length_(0) {} |  | ||||||
|  |  | ||||||
| 		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_; | 			length_ = rhs.length_; | ||||||
| 			return *this; | 			return *this; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		T &operator +=(const T &rhs) { | 		forceinline T &operator +=(const T &rhs) { | ||||||
| 			length_ += rhs.length_; | 			length_ += rhs.length_; | ||||||
| 			return *static_cast<T *>(this); | 			return *static_cast<T *>(this); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		T &operator -=(const T &rhs) { | 		forceinline T &operator -=(const T &rhs) { | ||||||
| 			length_ -= rhs.length_; | 			length_ -= rhs.length_; | ||||||
| 			return *static_cast<T *>(this); | 			return *static_cast<T *>(this); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		T &operator ++() { | 		forceinline T &operator ++() { | ||||||
| 			++ length_; | 			++ length_; | ||||||
| 			return *static_cast<T *>(this); | 			return *static_cast<T *>(this); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		T &operator ++(int) { | 		forceinline T &operator ++(int) { | ||||||
| 			length_ ++; | 			length_ ++; | ||||||
| 			return *static_cast<T *>(this); | 			return *static_cast<T *>(this); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		T &operator --() { | 		forceinline T &operator --() { | ||||||
| 			-- length_; | 			-- length_; | ||||||
| 			return *static_cast<T *>(this); | 			return *static_cast<T *>(this); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		T &operator --(int) { | 		forceinline T &operator --(int) { | ||||||
| 			length_ --; | 			length_ --; | ||||||
| 			return *static_cast<T *>(this); | 			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_; | 			length_ %= rhs.length_; | ||||||
| 			return *static_cast<T *>(this); | 			return *static_cast<T *>(this); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		T &operator &=(const T &rhs) { | 		forceinline T &operator &=(const T &rhs) { | ||||||
| 			length_ &= rhs.length_; | 			length_ &= rhs.length_; | ||||||
| 			return *static_cast<T *>(this); | 			return *static_cast<T *>(this); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		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_);	} | 		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_);	} | 		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_);	} | 		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_;		} | 		forceinline constexpr T operator -() const						{	return T(- 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					{	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 | 		// 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 | 			Severs from @c this the effect of dividing by @c divisor; @c this will end up with | ||||||
| 			the value of @c this modulo @c divisor and @c divided by @c divisor is returned. | 			the value of @c this modulo @c divisor and @c divided by @c divisor is returned. | ||||||
| 		*/ | 		*/ | ||||||
| 		T divide(const T &divisor) { | 		template <typename Result = T> forceinline Result divide(const T &divisor) { | ||||||
| 			T result(length_ / divisor.length_); | 			Result r; | ||||||
| 			length_ %= divisor.length_; | 			static_cast<T *>(this)->fill(r, divisor); | ||||||
| 			return result; | 			return r; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/*! | 		/*! | ||||||
| 			Flushes the value in @c this. The current value is returned, and the internal value | 			Flushes the value in @c this. The current value is returned, and the internal value | ||||||
| 			is reset to zero. | 			is reset to zero. | ||||||
| 		*/ | 		*/ | ||||||
| 		T flush() { | 		template <typename Result> Result flush() { | ||||||
| 			T result(length_); | 			// Jiggery pokery here; switching to function overloading avoids | ||||||
| 			length_ = 0; | 			// the namespace-level requirement for template specialisation. | ||||||
| 			return result; | 			Result r; | ||||||
|  | 			static_cast<T *>(this)->fill(r); | ||||||
|  | 			return r; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// operator int() is deliberately not provided, to avoid accidental subtitution of | 		// operator int() is deliberately not provided, to avoid accidental subtitution of | ||||||
| 		// classes that use this template. | 		// classes that use this template. | ||||||
|  |  | ||||||
| 	protected: | 	protected: | ||||||
| 		int length_; | 		IntType length_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /// Describes an integer number of whole cycles: pairs of clock signal transitions. | /// Describes an integer number of whole cycles: pairs of clock signal transitions. | ||||||
| class Cycles: public WrappedInt<Cycles> { | class Cycles: public WrappedInt<Cycles> { | ||||||
| 	public: | 	public: | ||||||
| 		constexpr Cycles(int l) : WrappedInt<Cycles>(l) {} | 		forceinline constexpr Cycles(IntType l) noexcept : WrappedInt<Cycles>(l) {} | ||||||
| 		constexpr Cycles() : WrappedInt<Cycles>() {} | 		forceinline constexpr Cycles() noexcept : WrappedInt<Cycles>() {} | ||||||
| 		constexpr Cycles(const Cycles &cycles) : WrappedInt<Cycles>(cycles.length_) {} | 		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. | /// Describes an integer number of half cycles: single clock signal transitions. | ||||||
| class HalfCycles: public WrappedInt<HalfCycles> { | class HalfCycles: public WrappedInt<HalfCycles> { | ||||||
| 	public: | 	public: | ||||||
| 		constexpr HalfCycles(int l) : WrappedInt<HalfCycles>(l) {} | 		forceinline constexpr HalfCycles(IntType l) noexcept : WrappedInt<HalfCycles>(l) {} | ||||||
| 		constexpr HalfCycles() : WrappedInt<HalfCycles>() {} | 		forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {} | ||||||
|  |  | ||||||
| 		constexpr HalfCycles(const Cycles cycles) : WrappedInt<HalfCycles>(cycles.as_int() * 2) {} | 		forceinline constexpr HalfCycles(const Cycles &cycles) noexcept : WrappedInt<HalfCycles>(cycles.as_integral() * 2) {} | ||||||
| 		constexpr HalfCycles(const HalfCycles &half_cycles) : WrappedInt<HalfCycles>(half_cycles.length_) {} | 		forceinline constexpr HalfCycles(const HalfCycles &half_cycles) noexcept : WrappedInt<HalfCycles>(half_cycles.length_) {} | ||||||
|  |  | ||||||
| 		/// @returns The number of whole cycles completely covered by this span of half cycles. | 		/// @returns The number of whole cycles completely covered by this span of half cycles. | ||||||
| 		constexpr Cycles cycles() const { | 		forceinline constexpr Cycles cycles() const { | ||||||
| 			return Cycles(length_ >> 1); | 			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 | 			Severs from @c this the effect of dividing by @c divisor; @c this will end up with | ||||||
| 			the value of @c this modulo @c divisor and @c divided by @c divisor is returned. | 			the value of @c this modulo @c divisor and @c divided by @c divisor is returned. | ||||||
| 		*/ | 		*/ | ||||||
| 		Cycles divide_cycles(const Cycles &divisor) { | 		forceinline Cycles divide_cycles(const Cycles &divisor) { | ||||||
| 			HalfCycles half_divisor = HalfCycles(divisor); | 			const HalfCycles half_divisor = HalfCycles(divisor); | ||||||
| 			Cycles result(length_ / half_divisor.length_); | 			const Cycles result(length_ / half_divisor.length_); | ||||||
| 			length_ %= half_divisor.length_; | 			length_ %= half_divisor.length_; | ||||||
| 			return result; | 			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 | 	If a component implements only run_for(Cycles), an owner can wrap it in HalfClockReceiver | ||||||
| 	automatically to gain run_for(HalfCycles). | 	automatically to gain run_for(HalfCycles). | ||||||
| @@ -203,9 +250,9 @@ template <class T> class HalfClockReceiver: public T { | |||||||
| 	public: | 	public: | ||||||
| 		using T::T; | 		using T::T; | ||||||
|  |  | ||||||
| 		inline void run_for(const HalfCycles half_cycles) { | 		forceinline void run_for(const HalfCycles half_cycles) { | ||||||
| 			half_cycles_ += half_cycles; | 			half_cycles_ += half_cycles; | ||||||
| 			T::run_for(half_cycles_.flush_cycles()); | 			T::run_for(half_cycles_.flush<Cycles>()); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	private: | 	private: | ||||||
|   | |||||||
| @@ -1,25 +1,26 @@ | |||||||
| //
 | //
 | ||||||
| //  ClockDeferrer.hpp
 | //  DeferredQueue.hpp
 | ||||||
| //  Clock Signal
 | //  Clock Signal
 | ||||||
| //
 | //
 | ||||||
| //  Created by Thomas Harte on 23/08/2018.
 | //  Created by Thomas Harte on 23/08/2018.
 | ||||||
| //  Copyright © 2018 Thomas Harte. All rights reserved.
 | //  Copyright © 2018 Thomas Harte. All rights reserved.
 | ||||||
| //
 | //
 | ||||||
| 
 | 
 | ||||||
| #ifndef ClockDeferrer_h | #ifndef DeferredQueue_h | ||||||
| #define ClockDeferrer_h | #define DeferredQueue_h | ||||||
| 
 | 
 | ||||||
|  | #include <functional> | ||||||
| #include <vector> | #include <vector> | ||||||
| 
 | 
 | ||||||
| /*!
 | /*!
 | ||||||
| 	A ClockDeferrer maintains a list of ordered actions and the times at which | 	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 | 	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. | 	that occur between those actions, triggering each action when it is reached. | ||||||
| */ | */ | ||||||
| template <typename TimeUnit> class ClockDeferrer { | template <typename TimeUnit> class DeferredQueue { | ||||||
| 	public: | 	public: | ||||||
| 		/// Constructs a ClockDeferrer that will call target(period) in between deferred actions.
 | 		/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
 | ||||||
| 		ClockDeferrer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {} | 		DeferredQueue(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {} | ||||||
| 
 | 
 | ||||||
| 		/*!
 | 		/*!
 | ||||||
| 			Schedules @c action to occur in @c delay units of time. | 			Schedules @c action to occur in @c delay units of time. | ||||||
| @@ -78,4 +79,4 @@ template <typename TimeUnit> class ClockDeferrer { | |||||||
| 		std::vector<DeferredAction> pending_actions_; | 		std::vector<DeferredAction> pending_actions_; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #endif /* ClockDeferrer_h */ | #endif /* DeferredQueue_h */ | ||||||
| @@ -9,9 +9,9 @@ | |||||||
| #ifndef ForceInline_hpp | #ifndef ForceInline_hpp | ||||||
| #define ForceInline_hpp | #define ForceInline_hpp | ||||||
|  |  | ||||||
| #ifdef DEBUG | #ifndef NDEBUG | ||||||
|  |  | ||||||
| #define forceinline | #define forceinline inline | ||||||
|  |  | ||||||
| #else | #else | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										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 "1770.hpp" | ||||||
|  |  | ||||||
| #include "../../Storage/Disk/Encodings/MFM/Constants.hpp" | #include "../../Storage/Disk/Encodings/MFM/Constants.hpp" | ||||||
|  |  | ||||||
|  | #define LOG_PREFIX "[WD FDC] " | ||||||
| #include "../../Outputs/Log.hpp" | #include "../../Outputs/Log.hpp" | ||||||
|  |  | ||||||
| using namespace WD; | using namespace WD; | ||||||
| @@ -16,19 +18,19 @@ using namespace WD; | |||||||
| WD1770::WD1770(Personality p) : | WD1770::WD1770(Personality p) : | ||||||
| 		Storage::Disk::MFMController(8000000), | 		Storage::Disk::MFMController(8000000), | ||||||
| 		personality_(p), | 		personality_(p), | ||||||
| 		interesting_event_mask_(static_cast<int>(Event1770::Command)) { | 		interesting_event_mask_(int(Event1770::Command)) { | ||||||
| 	set_is_double_density(false); | 	set_is_double_density(false); | ||||||
| 	posit_event(static_cast<int>(Event1770::Command)); | 	posit_event(int(Event1770::Command)); | ||||||
| } | } | ||||||
|  |  | ||||||
| void WD1770::set_register(int address, uint8_t value) { | void WD1770::write(int address, uint8_t value) { | ||||||
| 	switch(address&3) { | 	switch(address&3) { | ||||||
| 		case 0: { | 		case 0: { | ||||||
| 			if((value&0xf0) == 0xd0) { | 			if((value&0xf0) == 0xd0) { | ||||||
| 				if(value == 0xd0) { | 				if(value == 0xd0) { | ||||||
| 					// Force interrupt **immediately**. | 					// Force interrupt **immediately**. | ||||||
| 					LOG("Force interrupt immediately"); | 					LOG("Force interrupt immediately"); | ||||||
| 					posit_event(static_cast<int>(Event1770::ForceInterrupt)); | 					posit_event(int(Event1770::ForceInterrupt)); | ||||||
| 				} else { | 				} else { | ||||||
| 					ERROR("!!!TODO: force interrupt!!!"); | 					ERROR("!!!TODO: force interrupt!!!"); | ||||||
| 					update_status([] (Status &status) { | 					update_status([] (Status &status) { | ||||||
| @@ -37,7 +39,7 @@ void WD1770::set_register(int address, uint8_t value) { | |||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				command_ = value; | 				command_ = value; | ||||||
| 				posit_event(static_cast<int>(Event1770::Command)); | 				posit_event(int(Event1770::Command)); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		break; | 		break; | ||||||
| @@ -52,27 +54,35 @@ void WD1770::set_register(int address, uint8_t value) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| uint8_t WD1770::get_register(int address) { | uint8_t WD1770::read(int address) { | ||||||
| 	switch(address&3) { | 	switch(address&3) { | ||||||
| 		default: { | 		default: { | ||||||
| 			update_status([] (Status &status) { | 			update_status([] (Status &status) { | ||||||
| 				status.interrupt_request = false; | 				status.interrupt_request = false; | ||||||
| 			}); | 			}); | ||||||
| 			uint8_t status = | 			uint8_t status = | ||||||
| 					(status_.write_protect ? Flag::WriteProtect : 0) | |  | ||||||
| 				(status_.crc_error ? Flag::CRCError : 0) | | 				(status_.crc_error ? Flag::CRCError : 0) | | ||||||
| 				(status_.busy ? Flag::Busy : 0); | 				(status_.busy ? Flag::Busy : 0); | ||||||
|  |  | ||||||
|  | 			// Per Jean Louis-Guérin's documentation: | ||||||
|  | 			// | ||||||
|  | 			//	* 	the write-protect bit is locked into place by a type 2 or type 3 command, but is | ||||||
|  | 			//		read live after a type 1. | ||||||
|  | 			//	*	the track 0 bit is captured during a type 1 instruction and lost upon any other type, | ||||||
|  | 			//		it is not live sampled. | ||||||
| 			switch(status_.type) { | 			switch(status_.type) { | ||||||
| 				case Status::One: | 				case Status::One: | ||||||
| 					status |= | 					status |= | ||||||
| 						(get_drive().get_is_track_zero() ? Flag::TrackZero : 0) | | 						(status_.track_zero ? Flag::TrackZero : 0) | | ||||||
| 						(status_.seek_error ? Flag::SeekError : 0); | 						(status_.seek_error ? Flag::SeekError : 0) | | ||||||
| 						// TODO: index hole | 						(get_drive().get_is_read_only() ? Flag::WriteProtect : 0) | | ||||||
|  | 						(get_drive().get_index_pulse() ? Flag::Index : 0); | ||||||
| 				break; | 				break; | ||||||
|  |  | ||||||
| 				case Status::Two: | 				case Status::Two: | ||||||
| 				case Status::Three: | 				case Status::Three: | ||||||
| 					status |= | 					status |= | ||||||
|  | 						(status_.write_protect ? Flag::WriteProtect : 0) | | ||||||
| 						(status_.record_type ? Flag::RecordType : 0) | | 						(status_.record_type ? Flag::RecordType : 0) | | ||||||
| 						(status_.lost_data ? Flag::LostData : 0) | | 						(status_.lost_data ? Flag::LostData : 0) | | ||||||
| 						(status_.data_request ? Flag::DataRequest : 0) | | 						(status_.data_request ? Flag::DataRequest : 0) | | ||||||
| @@ -89,10 +99,15 @@ uint8_t WD1770::get_register(int address) { | |||||||
| 				if(status_.type == Status::One) | 				if(status_.type == Status::One) | ||||||
| 					status |= (status_.spin_up ? Flag::SpinUp : 0); | 					status |= (status_.spin_up ? Flag::SpinUp : 0); | ||||||
| 			} | 			} | ||||||
|  | //			LOG("Returned status " << PADHEX(2) << int(status) << " of type " << 1+int(status_.type)); | ||||||
| 			return status; | 			return status; | ||||||
| 		} | 		} | ||||||
| 		case 1:		return track_; | 		case 1: | ||||||
| 		case 2:		return sector_; | 			LOG("Returned track " << int(track_)); | ||||||
|  | 			return track_; | ||||||
|  | 		case 2: | ||||||
|  | 			LOG("Returned sector " << int(sector_)); | ||||||
|  | 			return sector_; | ||||||
| 		case 3: | 		case 3: | ||||||
| 			update_status([] (Status &status) { | 			update_status([] (Status &status) { | ||||||
| 				status.data_request = false; | 				status.data_request = false; | ||||||
| @@ -105,28 +120,30 @@ void WD1770::run_for(const Cycles cycles) { | |||||||
| 	Storage::Disk::Controller::run_for(cycles); | 	Storage::Disk::Controller::run_for(cycles); | ||||||
|  |  | ||||||
| 	if(delay_time_) { | 	if(delay_time_) { | ||||||
| 		unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_int()); | 		const auto number_of_cycles = cycles.as_integral(); | ||||||
| 		if(delay_time_ <= number_of_cycles) { | 		if(delay_time_ <= number_of_cycles) { | ||||||
| 			delay_time_ = 0; | 			delay_time_ = 0; | ||||||
| 			posit_event(static_cast<int>(Event1770::Timer)); | 			posit_event(int(Event1770::Timer)); | ||||||
| 		} else { | 		} else { | ||||||
| 			delay_time_ -= number_of_cycles; | 			delay_time_ -= number_of_cycles; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(mask); return; case __LINE__: | #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = int(mask); return; case __LINE__: | ||||||
| #define WAIT_FOR_TIME(ms)		resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer); | #define WAIT_FOR_TIME(ms)		resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer); | ||||||
| #define WAIT_FOR_BYTES(count)	resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = static_cast<int>(Event::Token); return; } | #define WAIT_FOR_BYTES(count)	resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = int(Event::Token); return; } | ||||||
| #define BEGIN_SECTION()	switch(resume_point_) { default: | #define BEGIN_SECTION()	switch(resume_point_) { default: | ||||||
| #define END_SECTION()	(void)0; } | #define END_SECTION()	(void)0; } | ||||||
|  |  | ||||||
| #define READ_ID()	\ | #define READ_ID()	\ | ||||||
| 		if(new_event_type == static_cast<int>(Event::Token)) {	\ | 		if(new_event_type == int(Event::Token)) {	\ | ||||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; }	\ | 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {\ | ||||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {	\ | 				set_data_mode(DataMode::Reading);	\ | ||||||
|  | 				++distance_into_section_;	\ | ||||||
|  | 			} else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {	\ | ||||||
| 				header_[distance_into_section_ - 1] = get_latest_token().byte_value;	\ | 				header_[distance_into_section_ - 1] = get_latest_token().byte_value;	\ | ||||||
| 				distance_into_section_++;	\ | 				++distance_into_section_;	\ | ||||||
| 			}	\ | 			}	\ | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -159,10 +176,10 @@ void WD1770::run_for(const Cycles cycles) { | |||||||
| // +--------+----------+-------------------------+ | // +--------+----------+-------------------------+ | ||||||
|  |  | ||||||
| void WD1770::posit_event(int new_event_type) { | void WD1770::posit_event(int new_event_type) { | ||||||
| 	if(new_event_type == static_cast<int>(Event::IndexHole)) { | 	if(new_event_type == int(Event::IndexHole)) { | ||||||
| 		index_hole_count_++; | 		index_hole_count_++; | ||||||
| 		if(index_hole_count_target_ == index_hole_count_) { | 		if(index_hole_count_target_ == index_hole_count_) { | ||||||
| 			posit_event(static_cast<int>(Event1770::IndexHoleTarget)); | 			posit_event(int(Event1770::IndexHoleTarget)); | ||||||
| 			index_hole_count_target_ = -1; | 			index_hole_count_target_ = -1; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -177,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; | 		interesting_event_mask_ = 0; | ||||||
| 		resume_point_ = 0; | 		resume_point_ = 0; | ||||||
| 		update_status([] (Status &status) { | 		update_status([] (Status &status) { | ||||||
| 			status.type = Status::One; | 			status.type = Status::One; | ||||||
| 			status.data_request = false; | 			status.data_request = false; | ||||||
|  | 			status.spin_up = false; | ||||||
| 		}); | 		}); | ||||||
| 	} else { | 	} else { | ||||||
| 		if(!(interesting_event_mask_ & static_cast<int>(new_event_type))) return; | 		if(!(interesting_event_mask_ & int(new_event_type))) return; | ||||||
| 		interesting_event_mask_ &= ~new_event_type; | 		interesting_event_mask_ &= ~new_event_type; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	Status new_status; |  | ||||||
| 	BEGIN_SECTION() | 	BEGIN_SECTION() | ||||||
|  |  | ||||||
| 	// Wait for a new command, branch to the appropriate handler. | 	// 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) { | 		update_status([] (Status &status) { | ||||||
| 			status.busy = true; | 			status.busy = true; | ||||||
| 			status.interrupt_request = false; | 			status.interrupt_request = false; | ||||||
|  | 			status.track_zero = false;	// Always reset by a non-type 1; so reset regardless and set properly later. | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		LOG("Starting " << std::hex << command_ << std::endl); | 		LOG("Starting " << PADHEX(2) << int(command_)); | ||||||
|  |  | ||||||
| 		if(!(command_ & 0x80)) goto begin_type_1; | 		if(!(command_ & 0x80)) goto begin_type_1; | ||||||
| 		if(!(command_ & 0x40)) goto begin_type_2; | 		if(!(command_ & 0x40)) goto begin_type_2; | ||||||
| @@ -241,6 +259,7 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 			status.data_request = false; | 			status.data_request = false; | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
|  | 		LOG("Step/Seek/Restore with track " << int(track_) << " data " << int(data_)); | ||||||
| 		if(!has_motor_on_line() && !has_head_load_line()) goto test_type1_type; | 		if(!has_motor_on_line() && !has_head_load_line()) goto test_type1_type; | ||||||
|  |  | ||||||
| 		if(has_motor_on_line()) goto begin_type1_spin_up; | 		if(has_motor_on_line()) goto begin_type1_spin_up; | ||||||
| @@ -273,19 +292,19 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	perform_seek_or_restore_command: | 	perform_seek_or_restore_command: | ||||||
| 		if(track_ == data_) goto verify; | 		if(track_ == data_) goto verify_seek; | ||||||
| 		step_direction_ = (data_ > track_); | 		step_direction_ = (data_ > track_); | ||||||
|  |  | ||||||
| 	adjust_track: | 	adjust_track: | ||||||
| 		if(step_direction_) track_++; else track_--; | 		if(step_direction_) ++track_; else --track_; | ||||||
|  |  | ||||||
| 	perform_step: | 	perform_step: | ||||||
| 		if(!step_direction_ && get_drive().get_is_track_zero()) { | 		if(!step_direction_ && get_drive().get_is_track_zero()) { | ||||||
| 			track_ = 0; | 			track_ = 0; | ||||||
| 			goto verify; | 			goto verify_seek; | ||||||
| 		} | 		} | ||||||
| 		get_drive().step(Storage::Disk::HeadPosition(step_direction_ ? 1 : -1)); | 		get_drive().step(Storage::Disk::HeadPosition(step_direction_ ? 1 : -1)); | ||||||
| 		unsigned int time_to_wait; | 		Cycles::IntType time_to_wait; | ||||||
| 		switch(command_ & 3) { | 		switch(command_ & 3) { | ||||||
| 			default: | 			default: | ||||||
| 			case 0: time_to_wait = 6;	break; | 			case 0: time_to_wait = 6;	break; | ||||||
| @@ -294,14 +313,17 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 			case 3: time_to_wait = (personality_ == P1772) ? 3 : 30;	break; | 			case 3: time_to_wait = (personality_ == P1772) ? 3 : 30;	break; | ||||||
| 		} | 		} | ||||||
| 		WAIT_FOR_TIME(time_to_wait); | 		WAIT_FOR_TIME(time_to_wait); | ||||||
| 		if(command_ >> 5) goto verify; | 		if(command_ >> 5) goto verify_seek; | ||||||
| 		goto perform_seek_or_restore_command; | 		goto perform_seek_or_restore_command; | ||||||
|  |  | ||||||
| 	perform_step_command: | 	perform_step_command: | ||||||
| 		if(command_ & 0x10) goto adjust_track; | 		if(command_ & 0x10) goto adjust_track; | ||||||
| 		goto perform_step; | 		goto perform_step; | ||||||
|  |  | ||||||
| 	verify: | 	verify_seek: | ||||||
|  | 		update_status([this] (Status &status) { | ||||||
|  | 			status.track_zero = get_drive().get_is_track_zero(); | ||||||
|  | 		}); | ||||||
| 		if(!(command_ & 0x04)) { | 		if(!(command_ & 0x04)) { | ||||||
| 			goto wait_for_command; | 			goto wait_for_command; | ||||||
| 		} | 		} | ||||||
| @@ -310,17 +332,20 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 		distance_into_section_ = 0; | 		distance_into_section_ = 0; | ||||||
|  |  | ||||||
| 	verify_read_data: | 	verify_read_data: | ||||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | 		WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token)); | ||||||
| 		READ_ID(); | 		READ_ID(); | ||||||
|  |  | ||||||
| 		if(index_hole_count_ == 6) { | 		if(index_hole_count_ == 6) { | ||||||
|  | 			LOG("Nothing found to verify"); | ||||||
| 			update_status([] (Status &status) { | 			update_status([] (Status &status) { | ||||||
| 				status.seek_error = true; | 				status.seek_error = true; | ||||||
| 			}); | 			}); | ||||||
| 			goto wait_for_command; | 			goto wait_for_command; | ||||||
| 		} | 		} | ||||||
| 		if(distance_into_section_ == 7) { | 		if(distance_into_section_ == 7) { | ||||||
|  | 			distance_into_section_ = 0; | ||||||
| 			set_data_mode(DataMode::Scanning); | 			set_data_mode(DataMode::Scanning); | ||||||
|  |  | ||||||
| 			if(get_crc_generator().get_value()) { | 			if(get_crc_generator().get_value()) { | ||||||
| 				update_status([] (Status &status) { | 				update_status([] (Status &status) { | ||||||
| 					status.crc_error = true; | 					status.crc_error = true; | ||||||
| @@ -329,14 +354,12 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if(header_[0] == track_) { | 			if(header_[0] == track_) { | ||||||
| 				LOG("Reached track " << std::dec << track_); | 				LOG("Reached track " << std::dec << int(track_)); | ||||||
| 				update_status([] (Status &status) { | 				update_status([] (Status &status) { | ||||||
| 					status.crc_error = false; | 					status.crc_error = false; | ||||||
| 				}); | 				}); | ||||||
| 				goto wait_for_command; | 				goto wait_for_command; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			distance_into_section_ = 0; |  | ||||||
| 		} | 		} | ||||||
| 		goto verify_read_data; | 		goto verify_read_data; | ||||||
|  |  | ||||||
| @@ -393,23 +416,28 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 			goto wait_for_command; | 			goto wait_for_command; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		distance_into_section_ = 0; | ||||||
|  | 		set_data_mode(DataMode::Scanning); | ||||||
|  |  | ||||||
| 	type2_get_header: | 	type2_get_header: | ||||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | 		WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token)); | ||||||
| 		READ_ID(); | 		READ_ID(); | ||||||
|  |  | ||||||
| 		if(index_hole_count_ == 5) { | 		if(index_hole_count_ == 5) { | ||||||
| 			LOG("Failed to find sector " << std::dec << sector_); | 			LOG("Failed to find sector " << std::dec << int(sector_)); | ||||||
| 			update_status([] (Status &status) { | 			update_status([] (Status &status) { | ||||||
| 				status.record_not_found = true; | 				status.record_not_found = true; | ||||||
| 			}); | 			}); | ||||||
| 			goto wait_for_command; | 			goto wait_for_command; | ||||||
| 		} | 		} | ||||||
| 		if(distance_into_section_ == 7) { | 		if(distance_into_section_ == 7) { | ||||||
| 			LOG("Considering " << std::dec << header_[0] << "/" << header_[2]); | 			distance_into_section_ = 0; | ||||||
| 			set_data_mode(DataMode::Scanning); | 			set_data_mode(DataMode::Scanning); | ||||||
|  |  | ||||||
|  | 			LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2])); | ||||||
| 			if(		header_[0] == track_ && header_[2] == sector_ && | 			if(		header_[0] == track_ && header_[2] == sector_ && | ||||||
| 					(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) { | 					(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) { | ||||||
| 				LOG("Found " << std::dec << header_[0] << "/" << header_[2]); | 				LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2])); | ||||||
| 				if(get_crc_generator().get_value()) { | 				if(get_crc_generator().get_value()) { | ||||||
| 					LOG("CRC error; back to searching"); | 					LOG("CRC error; back to searching"); | ||||||
| 					update_status([] (Status &status) { | 					update_status([] (Status &status) { | ||||||
| @@ -423,7 +451,6 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 				}); | 				}); | ||||||
| 				goto type2_read_or_write_data; | 				goto type2_read_or_write_data; | ||||||
| 			} | 			} | ||||||
| 			distance_into_section_ = 0; |  | ||||||
| 		} | 		} | ||||||
| 		goto type2_get_header; | 		goto type2_get_header; | ||||||
|  |  | ||||||
| @@ -466,6 +493,9 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 		header_[distance_into_section_] = get_latest_token().byte_value; | 		header_[distance_into_section_] = get_latest_token().byte_value; | ||||||
| 		distance_into_section_++; | 		distance_into_section_++; | ||||||
| 		if(distance_into_section_ == 2) { | 		if(distance_into_section_ == 2) { | ||||||
|  | 			distance_into_section_ = 0; | ||||||
|  | 			set_data_mode(DataMode::Scanning); | ||||||
|  |  | ||||||
| 			if(get_crc_generator().get_value()) { | 			if(get_crc_generator().get_value()) { | ||||||
| 				LOG("CRC error; terminating"); | 				LOG("CRC error; terminating"); | ||||||
| 				update_status([this] (Status &status) { | 				update_status([this] (Status &status) { | ||||||
| @@ -474,11 +504,13 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 				goto wait_for_command; | 				goto wait_for_command; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			LOG("Finished reading sector " << std::dec << int(sector_)); | ||||||
|  |  | ||||||
| 			if(command_ & 0x10) { | 			if(command_ & 0x10) { | ||||||
| 				sector_++; | 				sector_++; | ||||||
|  | 				LOG("Advancing to search for sector " << std::dec << int(sector_)); | ||||||
| 				goto test_type2_write_protection; | 				goto test_type2_write_protection; | ||||||
| 			} | 			} | ||||||
| 			LOG("Finished reading sector " << std::dec << sector_); |  | ||||||
| 			goto wait_for_command; | 			goto wait_for_command; | ||||||
| 		} | 		} | ||||||
| 		goto type2_check_crc; | 		goto type2_check_crc; | ||||||
| @@ -560,7 +592,7 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 			sector_++; | 			sector_++; | ||||||
| 			goto test_type2_write_protection; | 			goto test_type2_write_protection; | ||||||
| 		} | 		} | ||||||
| 		LOG("Wrote sector " << std::dec << sector_); | 		LOG("Wrote sector " << std::dec << int(sector_)); | ||||||
| 		goto wait_for_command; | 		goto wait_for_command; | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -611,8 +643,8 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 		distance_into_section_ = 0; | 		distance_into_section_ = 0; | ||||||
|  |  | ||||||
| 	read_address_get_header: | 	read_address_get_header: | ||||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | 		WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token)); | ||||||
| 		if(new_event_type == static_cast<int>(Event::Token)) { | 		if(new_event_type == int(Event::Token)) { | ||||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; } | 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; } | ||||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { | 			else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { | ||||||
| 				if(status_.data_request) { | 				if(status_.data_request) { | ||||||
| @@ -626,9 +658,11 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 				update_status([] (Status &status) { | 				update_status([] (Status &status) { | ||||||
| 					status.data_request = true; | 					status.data_request = true; | ||||||
| 				}); | 				}); | ||||||
| 				distance_into_section_++; | 				++distance_into_section_; | ||||||
|  |  | ||||||
| 				if(distance_into_section_ == 7) { | 				if(distance_into_section_ == 7) { | ||||||
|  | 					distance_into_section_ = 0; | ||||||
|  |  | ||||||
| 					if(get_crc_generator().get_value()) { | 					if(get_crc_generator().get_value()) { | ||||||
| 						update_status([] (Status &status) { | 						update_status([] (Status &status) { | ||||||
| 							status.crc_error = true; | 							status.crc_error = true; | ||||||
| @@ -652,7 +686,7 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 		index_hole_count_ = 0; | 		index_hole_count_ = 0; | ||||||
|  |  | ||||||
| 	read_track_read_byte: | 	read_track_read_byte: | ||||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); | 		WAIT_FOR_EVENT(int(Event::Token) | int(Event::IndexHole)); | ||||||
| 		if(index_hole_count_) { | 		if(index_hole_count_) { | ||||||
| 			goto wait_for_command; | 			goto wait_for_command; | ||||||
| 		} | 		} | ||||||
| @@ -719,7 +753,7 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| 				case 0xfd: case 0xfe: | 				case 0xfd: case 0xfe: | ||||||
| 					// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022 | 					// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022 | ||||||
| 					write_raw_short( | 					write_raw_short( | ||||||
| 						static_cast<uint16_t>( | 						uint16_t( | ||||||
| 							0xa022 | | 							0xa022 | | ||||||
| 							((data_ & 0x80) << 7) | | 							((data_ & 0x80) << 7) | | ||||||
| 							((data_ & 0x40) << 6) | | 							((data_ & 0x40) << 6) | | ||||||
| @@ -768,15 +802,18 @@ void WD1770::posit_event(int new_event_type) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void WD1770::update_status(std::function<void(Status &)> updater) { | void WD1770::update_status(std::function<void(Status &)> updater) { | ||||||
|  | 	const Status old_status = status_; | ||||||
|  |  | ||||||
| 	if(delegate_) { | 	if(delegate_) { | ||||||
| 		Status old_status = status_; |  | ||||||
| 		updater(status_); | 		updater(status_); | ||||||
| 		bool did_change = | 		const bool did_change = | ||||||
| 			(status_.busy != old_status.busy) || | 			(status_.busy != old_status.busy) || | ||||||
| 			(status_.data_request != old_status.data_request); | 			(status_.data_request != old_status.data_request) || | ||||||
|  | 			(status_.interrupt_request != old_status.interrupt_request); | ||||||
| 		if(did_change) delegate_->wd1770_did_change_output(this); | 		if(did_change) delegate_->wd1770_did_change_output(this); | ||||||
| 	} | 	} else updater(status_); | ||||||
| 	else updater(status_); |  | ||||||
|  | 	if(status_.busy != old_status.busy) update_clocking_observer(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void WD1770::set_head_load_request(bool head_load) {} | void WD1770::set_head_load_request(bool head_load) {} | ||||||
| @@ -784,5 +821,14 @@ void WD1770::set_motor_on(bool motor_on) {} | |||||||
|  |  | ||||||
| void WD1770::set_head_loaded(bool head_loaded) { | void WD1770::set_head_loaded(bool head_loaded) { | ||||||
| 	head_is_loaded_ = head_loaded; | 	head_is_loaded_ = head_loaded; | ||||||
| 	if(head_loaded) posit_event(static_cast<int>(Event1770::HeadLoad)); | 	if(head_loaded) posit_event(int(Event1770::HeadLoad)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool WD1770::get_head_loaded() { | ||||||
|  | 	return head_is_loaded_; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ClockingHint::Preference WD1770::preferred_clocking() { | ||||||
|  | 	if(status_.busy) return ClockingHint::Preference::RealTime; | ||||||
|  | 	return Storage::Disk::MFMController::preferred_clocking(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -36,10 +36,10 @@ class WD1770: public Storage::Disk::MFMController { | |||||||
| 		using Storage::Disk::MFMController::set_is_double_density; | 		using Storage::Disk::MFMController::set_is_double_density; | ||||||
|  |  | ||||||
| 		/// Writes @c value to the register at @c address. Only the low two bits of the address are decoded. | 		/// Writes @c value to the register at @c address. Only the low two bits of the address are decoded. | ||||||
| 		void set_register(int address, uint8_t value); | 		void write(int address, uint8_t value); | ||||||
|  |  | ||||||
| 		/// Fetches the value of the register @c address. Only the low two bits of the address are decoded. | 		/// Fetches the value of the register @c address. Only the low two bits of the address are decoded. | ||||||
| 		uint8_t get_register(int address); | 		uint8_t read(int address); | ||||||
|  |  | ||||||
| 		/// Runs the controller for @c number_of_cycles cycles. | 		/// Runs the controller for @c number_of_cycles cycles. | ||||||
| 		void run_for(const Cycles cycles); | 		void run_for(const Cycles cycles); | ||||||
| @@ -73,11 +73,16 @@ class WD1770: public Storage::Disk::MFMController { | |||||||
| 		}; | 		}; | ||||||
| 		inline void set_delegate(Delegate *delegate)	{	delegate_ = delegate;			} | 		inline void set_delegate(Delegate *delegate)	{	delegate_ = delegate;			} | ||||||
|  |  | ||||||
|  | 		ClockingHint::Preference preferred_clocking() final; | ||||||
|  |  | ||||||
| 	protected: | 	protected: | ||||||
| 		virtual void set_head_load_request(bool head_load); | 		virtual void set_head_load_request(bool head_load); | ||||||
| 		virtual void set_motor_on(bool motor_on); | 		virtual void set_motor_on(bool motor_on); | ||||||
| 		void set_head_loaded(bool head_loaded); | 		void set_head_loaded(bool head_loaded); | ||||||
|  |  | ||||||
|  | 		/// @returns The last value posted to @c set_head_loaded. | ||||||
|  | 		bool get_head_loaded(); | ||||||
|  |  | ||||||
| 	private: | 	private: | ||||||
| 		Personality personality_; | 		Personality personality_; | ||||||
| 		inline bool has_motor_on_line() { return (personality_ != P1793 ) && (personality_ != P1773); } | 		inline bool has_motor_on_line() { return (personality_ != P1793 ) && (personality_ != P1773); } | ||||||
| @@ -94,6 +99,7 @@ class WD1770: public Storage::Disk::MFMController { | |||||||
| 			bool data_request = false; | 			bool data_request = false; | ||||||
| 			bool interrupt_request = false; | 			bool interrupt_request = false; | ||||||
| 			bool busy = false; | 			bool busy = false; | ||||||
|  | 			bool track_zero = false; | ||||||
| 			enum { | 			enum { | ||||||
| 				One, Two, Three | 				One, Two, Three | ||||||
| 			} type = One; | 			} type = One; | ||||||
| @@ -121,7 +127,7 @@ class WD1770: public Storage::Disk::MFMController { | |||||||
| 		void posit_event(int type); | 		void posit_event(int type); | ||||||
| 		int interesting_event_mask_; | 		int interesting_event_mask_; | ||||||
| 		int resume_point_ = 0; | 		int resume_point_ = 0; | ||||||
| 		unsigned int delay_time_ = 0; | 		Cycles::IntType delay_time_ = 0; | ||||||
|  |  | ||||||
| 		// ID buffer | 		// ID buffer | ||||||
| 		uint8_t header_[6]; | 		uint8_t header_[6]; | ||||||
|   | |||||||
							
								
								
									
										312
									
								
								Components/5380/ncr5380.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								Components/5380/ncr5380.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,312 @@ | |||||||
|  | // | ||||||
|  | //  ncr5380.cpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 10/08/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #include "ncr5380.hpp" | ||||||
|  |  | ||||||
|  | #include "../../Outputs/Log.hpp" | ||||||
|  |  | ||||||
|  | using namespace NCR::NCR5380; | ||||||
|  | using SCSI::Line; | ||||||
|  |  | ||||||
|  | NCR5380::NCR5380(SCSI::Bus &bus, int clock_rate) : | ||||||
|  | 	bus_(bus), | ||||||
|  | 	clock_rate_(clock_rate) { | ||||||
|  | 	device_id_ = bus_.add_device(); | ||||||
|  | 	bus_.add_observer(this); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void NCR5380::write(int address, uint8_t value, bool dma_acknowledge) { | ||||||
|  | 	switch(address & 7) { | ||||||
|  | 		case 0: | ||||||
|  | //			LOG("[SCSI 0] Set current SCSI bus state to " << PADHEX(2) << int(value)); | ||||||
|  | 			data_bus_ = value; | ||||||
|  |  | ||||||
|  | 			if(dma_request_ && dma_operation_ == DMAOperation::Send) { | ||||||
|  | //				printf("w %02x\n", value); | ||||||
|  | 				dma_acknowledge_ = true; | ||||||
|  | 				dma_request_ = false; | ||||||
|  | 				update_control_output(); | ||||||
|  | 				bus_.set_device_output(device_id_, bus_output_); | ||||||
|  | 			} | ||||||
|  | 		break; | ||||||
|  |  | ||||||
|  | 		case 1: { | ||||||
|  | //			LOG("[SCSI 1] Initiator command register set: " << PADHEX(2) << int(value)); | ||||||
|  | 			initiator_command_ = value; | ||||||
|  |  | ||||||
|  | 			bus_output_ &= ~(Line::Reset | Line::Acknowledge | Line::Busy | Line::SelectTarget | Line::Attention); | ||||||
|  | 			if(value & 0x80) bus_output_ |= Line::Reset; | ||||||
|  | 			if(value & 0x08) bus_output_ |= Line::Busy; | ||||||
|  | 			if(value & 0x04) bus_output_ |= Line::SelectTarget; | ||||||
|  |  | ||||||
|  | 			/* bit 5 = differential enable if this were a 5381 */ | ||||||
|  |  | ||||||
|  | 			test_mode_ = value & 0x40; | ||||||
|  | 			assert_data_bus_ = value & 0x01; | ||||||
|  | 			update_control_output(); | ||||||
|  | 		} break; | ||||||
|  |  | ||||||
|  | 		case 2: | ||||||
|  | //			LOG("[SCSI 2] Set mode: " << PADHEX(2) << int(value)); | ||||||
|  | 			mode_ = value; | ||||||
|  |  | ||||||
|  | 			// bit 7: 1 = use block mode DMA mode (if DMA mode is also enabled) | ||||||
|  | 			// bit 6: 1 = be a SCSI target; 0 = be an initiator | ||||||
|  | 			// bit 5: 1 = check parity | ||||||
|  | 			// bit 4: 1 = generate an interrupt if parity checking is enabled and an error is found | ||||||
|  | 			// bit 3: 1 = generate an interrupt when an EOP is received from the DMA controller | ||||||
|  | 			// bit 2: 1 = generate an interrupt and reset low 6 bits of register 1 if an unexpected loss of Line::Busy occurs | ||||||
|  | 			// bit 1: 1 = use DMA mode | ||||||
|  | 			// bit 0: 1 = begin arbitration mode (device ID should be in register 0) | ||||||
|  | 			arbitration_in_progress_ = false; | ||||||
|  | 			switch(mode_ & 0x3) { | ||||||
|  | 				case 0x0: | ||||||
|  | 					bus_output_ &= ~SCSI::Line::Busy; | ||||||
|  | 					dma_request_ = false; | ||||||
|  | 					set_execution_state(ExecutionState::None); | ||||||
|  | 				break; | ||||||
|  |  | ||||||
|  | 				case 0x1: | ||||||
|  | 					arbitration_in_progress_ = true; | ||||||
|  | 					set_execution_state(ExecutionState::WaitingForBusy); | ||||||
|  | 					lost_arbitration_ = false; | ||||||
|  | 				break; | ||||||
|  |  | ||||||
|  | 				default: | ||||||
|  | 					assert_data_bus_ = false; | ||||||
|  | 					set_execution_state(ExecutionState::PerformingDMA); | ||||||
|  | 					bus_.update_observers(); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 			update_control_output(); | ||||||
|  | 		break; | ||||||
|  |  | ||||||
|  | 		case 3: { | ||||||
|  | //			LOG("[SCSI 3] Set target command: " << PADHEX(2) << int(value)); | ||||||
|  | 			target_command_ = value; | ||||||
|  | 			update_control_output(); | ||||||
|  | 		} break; | ||||||
|  |  | ||||||
|  | 		case 4: | ||||||
|  | //			LOG("[SCSI 4] Set select enabled: " << PADHEX(2) << int(value)); | ||||||
|  | 		break; | ||||||
|  |  | ||||||
|  | 		case 5: | ||||||
|  | //			LOG("[SCSI 5] Start DMA send: " << PADHEX(2) << int(value)); | ||||||
|  | 			dma_operation_ = DMAOperation::Send; | ||||||
|  | 		break; | ||||||
|  |  | ||||||
|  | 		case 6: | ||||||
|  | //			LOG("[SCSI 6] Start DMA target receive: " << PADHEX(2) << int(value)); | ||||||
|  | 			dma_operation_ = DMAOperation::TargetReceive; | ||||||
|  | 		break; | ||||||
|  |  | ||||||
|  | 		case 7: | ||||||
|  | //			LOG("[SCSI 7] Start DMA initiator receive: " << PADHEX(2) << int(value)); | ||||||
|  | 			dma_operation_ = DMAOperation::InitiatorReceive; | ||||||
|  | 		break; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Data is output only if the data bus is asserted. | ||||||
|  | 	if(assert_data_bus_) { | ||||||
|  | 		bus_output_ = (bus_output_ & ~SCSI::Line::Data) | data_bus_; | ||||||
|  | 	} else { | ||||||
|  | 		bus_output_ &= ~SCSI::Line::Data; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// In test mode, still nothing is output. Otherwise throw out | ||||||
|  | 	// the current value of bus_output_. | ||||||
|  | 	if(test_mode_) { | ||||||
|  | 		bus_.set_device_output(device_id_, SCSI::DefaultBusState); | ||||||
|  | 	} else { | ||||||
|  | 		bus_.set_device_output(device_id_, bus_output_); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t NCR5380::read(int address, bool dma_acknowledge) { | ||||||
|  | 	switch(address & 7) { | ||||||
|  | 		case 0: | ||||||
|  | //			LOG("[SCSI 0] Get current SCSI bus state: " << PADHEX(2) << (bus_.get_state() & 0xff)); | ||||||
|  |  | ||||||
|  | 			if(dma_request_ && dma_operation_ == DMAOperation::InitiatorReceive) { | ||||||
|  | 				dma_acknowledge_ = true; | ||||||
|  | 				dma_request_ = false; | ||||||
|  | 				update_control_output(); | ||||||
|  | 				bus_.set_device_output(device_id_, bus_output_); | ||||||
|  | 			} | ||||||
|  | 		return uint8_t(bus_.get_state()); | ||||||
|  |  | ||||||
|  | 		case 1: | ||||||
|  | //			LOG("[SCSI 1] Initiator command register get: " << (arbitration_in_progress_ ? 'p' : '-') <<  (lost_arbitration_ ? 'l' : '-')); | ||||||
|  | 		return | ||||||
|  | 			// Bits repeated as they were set. | ||||||
|  | 			(initiator_command_ & ~0x60) | | ||||||
|  |  | ||||||
|  | 			// Arbitration in progress. | ||||||
|  | 			(arbitration_in_progress_ ? 0x40 : 0x00) | | ||||||
|  |  | ||||||
|  | 			// Lost arbitration. | ||||||
|  | 			(lost_arbitration_ ? 0x20 : 0x00); | ||||||
|  |  | ||||||
|  | 		case 2: | ||||||
|  | //			LOG("[SCSI 2] Get mode"); | ||||||
|  | 		return mode_; | ||||||
|  |  | ||||||
|  | 		case 3: | ||||||
|  | //			LOG("[SCSI 3] Get target command"); | ||||||
|  | 		return target_command_; | ||||||
|  |  | ||||||
|  | 		case 4: { | ||||||
|  | 			const auto bus_state = bus_.get_state(); | ||||||
|  | 			const uint8_t result = | ||||||
|  | 				((bus_state & Line::Reset)			? 0x80 : 0x00) | | ||||||
|  | 				((bus_state & Line::Busy)			? 0x40 : 0x00) | | ||||||
|  | 				((bus_state & Line::Request)		? 0x20 : 0x00) | | ||||||
|  | 				((bus_state & Line::Message)		? 0x10 : 0x00) | | ||||||
|  | 				((bus_state & Line::Control)		? 0x08 : 0x00) | | ||||||
|  | 				((bus_state & Line::Input)			? 0x04 : 0x00) | | ||||||
|  | 				((bus_state & Line::SelectTarget)	? 0x02 : 0x00) | | ||||||
|  | 				((bus_state & Line::Parity)			? 0x01 : 0x00); | ||||||
|  | //			LOG("[SCSI 4] Get current bus state: " << PADHEX(2) << int(result)); | ||||||
|  | 			return result; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		case 5: { | ||||||
|  | 			const auto bus_state = bus_.get_state(); | ||||||
|  | 			const bool phase_matches = | ||||||
|  | 				(target_output() & (Line::Message | Line::Control | Line::Input)) == | ||||||
|  | 				(bus_state & (Line::Message | Line::Control | Line::Input)); | ||||||
|  |  | ||||||
|  | 			const uint8_t result = | ||||||
|  | 				/* b7 = end of DMA */ | ||||||
|  | 				((dma_request_ && state_ == ExecutionState::PerformingDMA) ? 0x40 : 0x00)	| | ||||||
|  | 				/* b5 = parity error */ | ||||||
|  | 				/* b4 = IRQ active */ | ||||||
|  | 				(phase_matches ? 0x08 : 0x00)	| | ||||||
|  | 				/* b2 = busy error */ | ||||||
|  | 				((bus_state & Line::Attention) ? 0x02 : 0x00) | | ||||||
|  | 				((bus_state & Line::Acknowledge) ? 0x01 : 0x00); | ||||||
|  | //			LOG("[SCSI 5] Get bus and status: " << PADHEX(2) << int(result)); | ||||||
|  | 			return result; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		case 6: | ||||||
|  | //			LOG("[SCSI 6] Get input data"); | ||||||
|  | 		return 0xff; | ||||||
|  |  | ||||||
|  | 		case 7: | ||||||
|  | //			LOG("[SCSI 7] Reset parity/interrupt"); | ||||||
|  | 		return 0xff; | ||||||
|  | 	} | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SCSI::BusState NCR5380::target_output() { | ||||||
|  | 	SCSI::BusState output = SCSI::DefaultBusState; | ||||||
|  | 	if(target_command_ & 0x08) output |= Line::Request; | ||||||
|  | 	if(target_command_ & 0x04) output |= Line::Message; | ||||||
|  | 	if(target_command_ & 0x02) output |= Line::Control; | ||||||
|  | 	if(target_command_ & 0x01) output |= Line::Input; | ||||||
|  | 	return output; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void NCR5380::update_control_output() { | ||||||
|  | 	bus_output_ &= ~(Line::Request | Line::Message | Line::Control | Line::Input | Line::Acknowledge | Line::Attention); | ||||||
|  | 	if(mode_ & 0x40) { | ||||||
|  | 		// This is a target; C/D, I/O, /MSG and /REQ are signalled on the bus. | ||||||
|  | 		bus_output_ |= target_output(); | ||||||
|  | 	} else { | ||||||
|  | 		// This is an initiator; /ATN and /ACK are signalled on the bus. | ||||||
|  | 		if( | ||||||
|  | 			(initiator_command_ & 0x10) || | ||||||
|  | 			(state_ == ExecutionState::PerformingDMA && dma_acknowledge_) | ||||||
|  | 		) bus_output_ |= Line::Acknowledge; | ||||||
|  | 		if(initiator_command_ & 0x02) bus_output_ |= Line::Attention; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void NCR5380::scsi_bus_did_change(SCSI::Bus *, SCSI::BusState new_state, double time_since_change) { | ||||||
|  | 	switch(state_) { | ||||||
|  | 		default: break; | ||||||
|  |  | ||||||
|  | 		/* | ||||||
|  | 			Official documentation: | ||||||
|  |  | ||||||
|  | 				Arbitration is accomplished using a bus-free filter to continuously monitor BSY. | ||||||
|  | 				If BSY remains inactive for at least 400 nsec then the SCSI bus is considered free | ||||||
|  | 				and arbitration may begin. Arbitration will begin if the bus is free, SEL is inactive | ||||||
|  | 				and the ARBITRATION bit (port 2, bit 0) is active. Once arbitration has begun | ||||||
|  | 				(BSY asserted), an arbitration delay of 2.2 /Lsec must elapse before the data bus | ||||||
|  | 				can be examined to deter- mine if arbitration has been won. This delay must be | ||||||
|  | 				implemented in the controlling software driver. | ||||||
|  |  | ||||||
|  | 			Personal notes: | ||||||
|  |  | ||||||
|  | 				I'm discounting that "arbitratation is accomplished" opening, and assuming that what needs | ||||||
|  | 				to happen is: | ||||||
|  |  | ||||||
|  | 					(i) wait for BSY to be inactive; | ||||||
|  | 					(ii) count 400 nsec; | ||||||
|  | 					(iii) check that BSY and SEL are inactive. | ||||||
|  | 		*/ | ||||||
|  |  | ||||||
|  | 		case ExecutionState::WaitingForBusy: | ||||||
|  | 			if(!(new_state & SCSI::Line::Busy) || time_since_change < SCSI::DeskewDelay) return; | ||||||
|  | 			state_ = ExecutionState::WatchingBusy; | ||||||
|  |  | ||||||
|  | 		case ExecutionState::WatchingBusy: | ||||||
|  | 			if(!(new_state & SCSI::Line::Busy)) { | ||||||
|  | 				lost_arbitration_ = true; | ||||||
|  | 				set_execution_state(ExecutionState::None); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Check for having hit 400ns (more or less) since BSY was inactive. | ||||||
|  | 			if(time_since_change >= SCSI::BusSettleDelay) { | ||||||
|  | //				arbitration_in_progress_ = false; | ||||||
|  | 				if(new_state & SCSI::Line::SelectTarget) { | ||||||
|  | 					lost_arbitration_ = true; | ||||||
|  | 					set_execution_state(ExecutionState::None); | ||||||
|  | 				} else { | ||||||
|  | 					bus_output_ &= ~SCSI::Line::Busy; | ||||||
|  | 					set_execution_state(ExecutionState::None); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			/* TODO: there's a bug here, given that the dropping of Busy isn't communicated onward. */ | ||||||
|  | 		break; | ||||||
|  |  | ||||||
|  | 		case ExecutionState::PerformingDMA: | ||||||
|  | 			if(time_since_change < SCSI::DeskewDelay) return; | ||||||
|  |  | ||||||
|  | 			// Signal a DMA request if the request line is active, i.e. meaningful data is | ||||||
|  | 			// on the bus, and this device hasn't yet acknowledged it. | ||||||
|  | 			switch(new_state & (SCSI::Line::Request | SCSI::Line::Acknowledge)) { | ||||||
|  | 				case 0: | ||||||
|  | 					dma_request_ = false; | ||||||
|  | 				break; | ||||||
|  | 				case SCSI::Line::Request: | ||||||
|  | 					dma_request_ = true; | ||||||
|  | 				break; | ||||||
|  | 				case SCSI::Line::Request | SCSI::Line::Acknowledge: | ||||||
|  | 					dma_request_ = false; | ||||||
|  | 				break; | ||||||
|  | 				case SCSI::Line::Acknowledge: | ||||||
|  | 					dma_acknowledge_ = false; | ||||||
|  | 					dma_request_ = false; | ||||||
|  | 					update_control_output(); | ||||||
|  | 					bus_.set_device_output(device_id_, bus_output_); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 		break; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void NCR5380::set_execution_state(ExecutionState state) { | ||||||
|  | 	state_ = state; | ||||||
|  | 	if(state != ExecutionState::PerformingDMA) dma_operation_ = DMAOperation::Ready; | ||||||
|  | } | ||||||
							
								
								
									
										75
									
								
								Components/5380/ncr5380.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								Components/5380/ncr5380.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | // | ||||||
|  | //  ncr5380.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 10/08/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef ncr5380_hpp | ||||||
|  | #define ncr5380_hpp | ||||||
|  |  | ||||||
|  | #include <cstdint> | ||||||
|  |  | ||||||
|  | #include "../../Storage/MassStorage/SCSI/SCSI.hpp" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | namespace NCR { | ||||||
|  | namespace NCR5380 { | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	Models the NCR 5380, a SCSI interface chip. | ||||||
|  | */ | ||||||
|  | class NCR5380 final: public SCSI::Bus::Observer { | ||||||
|  | 	public: | ||||||
|  | 		NCR5380(SCSI::Bus &bus, int clock_rate); | ||||||
|  |  | ||||||
|  | 		/*! Writes @c value to @c address.  */ | ||||||
|  | 		void write(int address, uint8_t value, bool dma_acknowledge = false); | ||||||
|  |  | ||||||
|  | 		/*! Reads from @c address. */ | ||||||
|  | 		uint8_t read(int address, bool dma_acknowledge = false); | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		SCSI::Bus &bus_; | ||||||
|  |  | ||||||
|  | 		const int clock_rate_; | ||||||
|  | 		size_t device_id_; | ||||||
|  |  | ||||||
|  | 		SCSI::BusState bus_output_ = SCSI::DefaultBusState; | ||||||
|  | 		SCSI::BusState expected_phase_ = SCSI::DefaultBusState; | ||||||
|  | 		uint8_t mode_ = 0xff; | ||||||
|  | 		uint8_t initiator_command_ = 0xff; | ||||||
|  | 		uint8_t data_bus_ = 0xff; | ||||||
|  | 		uint8_t target_command_ = 0xff; | ||||||
|  | 		bool test_mode_ = false; | ||||||
|  | 		bool assert_data_bus_ = false; | ||||||
|  | 		bool dma_request_ = false; | ||||||
|  | 		bool dma_acknowledge_ = false; | ||||||
|  |  | ||||||
|  | 		enum class ExecutionState { | ||||||
|  | 			None, | ||||||
|  | 			WaitingForBusy, | ||||||
|  | 			WatchingBusy, | ||||||
|  | 			PerformingDMA, | ||||||
|  | 		} state_ = ExecutionState::None; | ||||||
|  | 		enum class DMAOperation { | ||||||
|  | 			Ready, | ||||||
|  | 			Send, | ||||||
|  | 			TargetReceive, | ||||||
|  | 			InitiatorReceive | ||||||
|  | 		} dma_operation_ = DMAOperation::Ready; | ||||||
|  | 		bool lost_arbitration_ = false, arbitration_in_progress_ = false; | ||||||
|  |  | ||||||
|  | 		void set_execution_state(ExecutionState state); | ||||||
|  |  | ||||||
|  | 		SCSI::BusState target_output(); | ||||||
|  | 		void update_control_output(); | ||||||
|  |  | ||||||
|  | 		void scsi_bus_did_change(SCSI::Bus *, SCSI::BusState new_state, double time_since_change) final; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* ncr5380_hpp */ | ||||||
| @@ -47,6 +47,12 @@ class PortHandler { | |||||||
|  |  | ||||||
| 		/// Sets the current logical value of the interrupt line. | 		/// Sets the current logical value of the interrupt line. | ||||||
| 		void set_interrupt_status(bool status)									{} | 		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; | 		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: | 	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. | 		/// Sets the input value of line @c line on port @c port. | ||||||
| 		void set_control_line_input(Port port, Line line, bool value); | 		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. | 		/// @returns @c true if the IRQ line is currently active; @c false otherwise. | ||||||
| 		bool get_interrupt_line(); | 		bool get_interrupt_line(); | ||||||
|  |  | ||||||
| 	private: | 		/// Updates the port handler to the current time and then requests that it flush. | ||||||
| 		inline void do_phase1(); | 		void flush(); | ||||||
| 		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(); |  | ||||||
|  |  | ||||||
| 	private: | 	private: | ||||||
|  | 		void do_phase1(); | ||||||
|  | 		void do_phase2(); | ||||||
|  | 		void shift_in(); | ||||||
|  | 		void shift_out(); | ||||||
|  |  | ||||||
| 		T &bus_handler_; | 		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); | 		uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output); | ||||||
| 		inline void reevaluate_interrupts(); | 		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" | #include "Implementation/6522Implementation.hpp" | ||||||
|  |  | ||||||
| } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #endif /* _522_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. | //  Copyright 2017 Thomas Harte. All rights reserved. | ||||||
| // | // | ||||||
|  |  | ||||||
| template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) { | #include "../../../Outputs/Log.hpp" | ||||||
| 	address &= 0xf; |  | ||||||
|  | namespace MOS { | ||||||
|  | namespace MOS6522 { | ||||||
|  |  | ||||||
|  | template <typename T> void MOS6522<T>::access(int address) { | ||||||
| 	switch(address) { | 	switch(address) { | ||||||
| 		case 0x0: | 		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; | 			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)); | 			registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | ((registers_.peripheral_control&0x20) ? 0 : InterruptFlag::CB2ActiveEdge)); | ||||||
| 			reevaluate_interrupts(); | 			reevaluate_interrupts(); | ||||||
| 		break; | 		break; | ||||||
| 		case 0xf: | 		case 0xf: | ||||||
| 		case 0x1: | 		case 0x1:	// Write Port A. | ||||||
| 			registers_.output[0] = value; | 			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)); | 			registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | ((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge)); | ||||||
| 			reevaluate_interrupts(); | 			reevaluate_interrupts(); | ||||||
| 		break; | 		break; | ||||||
|  |  | ||||||
| 		case 0x2: | 		case 0x2:	// Port B direction. | ||||||
| 			registers_.data_direction[1] = value; | 			registers_.data_direction[1] = value; | ||||||
| 		break; | 		break; | ||||||
| 		case 0x3: | 		case 0x3:	// Port A direction. | ||||||
| 			registers_.data_direction[0] = value; | 			registers_.data_direction[0] = value; | ||||||
| 		break; | 		break; | ||||||
|  |  | ||||||
| @@ -54,33 +88,58 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) | |||||||
| 		break; | 		break; | ||||||
|  |  | ||||||
| 		// Shift | 		// 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 | 		// Control | ||||||
| 		case 0xb: | 		case 0xb: | ||||||
| 			registers_.auxiliary_control = value; | 			registers_.auxiliary_control = value; | ||||||
|  | 			evaluate_cb2_output(); | ||||||
| 		break; | 		break; | ||||||
| 		case 0xc: | 		case 0xc: { | ||||||
| //			printf("Peripheral control %02x\n", value); | //			const auto old_peripheral_control = registers_.peripheral_control; | ||||||
| 			registers_.peripheral_control = value; | 			registers_.peripheral_control = value; | ||||||
|  |  | ||||||
| 			// TODO: simplify below; trying to avoid improper logging of unimplemented warnings in input mode | 			int shift = 0; | ||||||
| 			if(value & 0x08) { | 			for(int port = 0; port < 2; ++port) { | ||||||
| 				switch(value & 0x0e) { | 				handshake_modes_[port] = HandshakeMode::None; | ||||||
| 					default: printf("Unimplemented control line mode %d\n", (value >> 1)&7);		break; | 				switch((value >> shift) & 0x0e) { | ||||||
| 					case 0x0c:	bus_handler_.set_control_line_output(Port::A, Line::Two, false);	break; | 					default: break; | ||||||
| 					case 0x0e:	bus_handler_.set_control_line_output(Port::A, Line::Two, true);		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. | ||||||
| 			if(value & 0x80) { | 					case 0x04:	// Positive interrupt input; set Cx2 interrupt on positive Cx2 transition, clear on access to Port x register. | ||||||
| 				switch(value & 0xe0) { | 					case 0x06:	// Independent positive interrupt input; set Cx2 interrupt on positive transition, don't clear automatically. | ||||||
| 					default: printf("Unimplemented control line mode %d\n", (value >> 5)&7);		break; | 						set_control_line_output(Port(port), Line::Two, LineState::Input); | ||||||
| 					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; | ||||||
|  |  | ||||||
|  | 					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; | ||||||
|  | 			} | ||||||
|  | 		} break; | ||||||
|  |  | ||||||
| 		// Interrupt control | 		// Interrupt control | ||||||
| 		case 0xd: | 		case 0xd: | ||||||
| 			registers_.interrupt_flags &= ~value; | 			registers_.interrupt_flags &= ~value; | ||||||
| @@ -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; | 	address &= 0xf; | ||||||
|  | 	access(address); | ||||||
| 	switch(address) { | 	switch(address) { | ||||||
| 		case 0x0: | 		case 0x0: | ||||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge); | 			registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge); | ||||||
| 			reevaluate_interrupts(); | 			reevaluate_interrupts(); | ||||||
| 		return get_port_input(Port::B, registers_.data_direction[1], registers_.output[1]); | 		return get_port_input(Port::B, registers_.data_direction[1], registers_.output[1]); | ||||||
| 		case 0xf:	// TODO: handshake, latching | 		case 0xf: | ||||||
| 		case 0x1: | 		case 0x1: | ||||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge); | 			registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge); | ||||||
| 			reevaluate_interrupts(); | 			reevaluate_interrupts(); | ||||||
| @@ -128,7 +188,11 @@ template <typename T> uint8_t MOS6522<T>::get_register(int address) { | |||||||
| 		return registers_.timer[1] & 0x00ff; | 		return registers_.timer[1] & 0x00ff; | ||||||
| 		case 0x9:	return registers_.timer[1] >> 8; | 		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 0xb:	return registers_.auxiliary_control; | ||||||
| 		case 0xc:	return registers_.peripheral_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) { | 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); | 	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(); | 	bool new_interrupt_status = get_interrupt_line(); | ||||||
| 	if(new_interrupt_status != last_posted_interrupt_status_) { | 	if(new_interrupt_status != last_posted_interrupt_status_) { | ||||||
| 		last_posted_interrupt_status_ = new_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); | 		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(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -37,14 +37,27 @@ class MOS6522Storage { | |||||||
| 			bool timer_needs_reload = false; | 			bool timer_needs_reload = false; | ||||||
| 		} registers_; | 		} registers_; | ||||||
|  |  | ||||||
| 		// control state | 		// Control state. | ||||||
| 		struct { | 		struct { | ||||||
| 			bool line_one = false; | 			bool lines[2] = {false, false}; | ||||||
| 			bool line_two = false; |  | ||||||
| 		} control_inputs_[2]; | 		} 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 timer_is_running_[2] = {false, false}; | ||||||
| 		bool last_posted_interrupt_status_ = false; | 		bool last_posted_interrupt_status_ = false; | ||||||
|  | 		int shift_bits_remaining_ = 8; | ||||||
|  |  | ||||||
| 		enum InterruptFlag: uint8_t { | 		enum InterruptFlag: uint8_t { | ||||||
| 			CA2ActiveEdge	= 1 << 0, | 			CA2ActiveEdge	= 1 << 0, | ||||||
| @@ -55,6 +68,23 @@ class MOS6522Storage { | |||||||
| 			Timer2			= 1 << 5, | 			Timer2			= 1 << 5, | ||||||
| 			Timer1			= 1 << 6, | 			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 void set_ram(uint16_t address, uint8_t value)	{	ram_[address&0x7f] = value;		} | ||||||
| 		inline uint8_t get_ram(uint16_t address)				{	return ram_[address & 0x7f];	} | 		inline uint8_t get_ram(uint16_t address)				{	return ram_[address & 0x7f];	} | ||||||
|  |  | ||||||
| 		inline void set_register(int address, uint8_t value) { | 		inline void write(int address, uint8_t value) { | ||||||
| 			const uint8_t decodedAddress = address & 0x07; | 			const uint8_t decodedAddress = address & 0x07; | ||||||
| 			switch(decodedAddress) { | 			switch(decodedAddress) { | ||||||
| 				// Port output | 				// Port output | ||||||
| @@ -63,7 +63,7 @@ template <class T> class MOS6532 { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		inline uint8_t get_register(int address) { | 		inline uint8_t read(int address) { | ||||||
| 			const uint8_t decodedAddress = address & 0x7; | 			const uint8_t decodedAddress = address & 0x7; | ||||||
| 			switch(decodedAddress) { | 			switch(decodedAddress) { | ||||||
| 				// Port input | 				// Port input | ||||||
| @@ -107,7 +107,7 @@ template <class T> class MOS6532 { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		inline void run_for(const Cycles cycles) { | 		inline void run_for(const Cycles cycles) { | ||||||
| 			unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_int()); | 			unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_integral()); | ||||||
|  |  | ||||||
| 			// permit counting _to_ zero; counting _through_ zero initiates the other behaviour | 			// permit counting _to_ zero; counting _through_ zero initiates the other behaviour | ||||||
| 			if(timer_.value >= number_of_cycles) { | 			if(timer_.value >= number_of_cycles) { | ||||||
|   | |||||||
| @@ -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 | 	To run the VIC for a cycle, the caller should call @c get_address, make the requested bus access | ||||||
| 	and call @c set_graphics_value with the result. | 	and call @c set_graphics_value with the result. | ||||||
|  |  | ||||||
| 	@c set_register and @c get_register provide register access. | 	@c write and @c read provide register access. | ||||||
| */ | */ | ||||||
| template <class BusHandler> class MOS6560 { | template <class BusHandler> class MOS6560 { | ||||||
| 	public: | 	public: | ||||||
| 		MOS6560(BusHandler &bus_handler) : | 		MOS6560(BusHandler &bus_handler) : | ||||||
| 				bus_handler_(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_), | 				audio_generator_(audio_queue_), | ||||||
| 				speaker_(audio_generator_) | 				speaker_(audio_generator_) | ||||||
| 		{ | 		{ | ||||||
| 			crt_->set_svideo_sampling_function( |  | ||||||
| 				"vec2 svideo_sample(usampler2D texID, vec2 coordinate, 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 | 			// default to s-video output | ||||||
| 			crt_->set_video_signal(Outputs::CRT::VideoSignal::SVideo); | 			crt_.set_display_type(Outputs::Display::DisplayType::SVideo); | ||||||
|  |  | ||||||
| 			// default to NTSC | 			// default to NTSC | ||||||
| 			set_output_mode(OutputMode::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)); | 			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_; } | 		Outputs::Speaker::Speaker *get_speaker() { return &speaker_; } | ||||||
|  |  | ||||||
| 		void set_high_frequency_cutoff(float cutoff) { | 		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; | 			// 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 | 			// 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] = { | 			const uint8_t pal_chrominances[16] = { | ||||||
| 				255,	255,	37,		101, | 				255,	255,	90,		20, | ||||||
| 				19,		86,		123,	59, | 				96,		42,		8,		72, | ||||||
| 				46,		53,		37,		101, | 				84,		90,		90,		20, | ||||||
| 				19,		86,		123,	59, | 				96,		42,		8,		72, | ||||||
| 			}; | 			}; | ||||||
| 			const uint8_t ntsc_chrominances[16] = { | 			const uint8_t ntsc_chrominances[16] = { | ||||||
| 				255,	255,	121,	57, | 				255,	255,	121,	57, | ||||||
| @@ -131,12 +121,12 @@ template <class BusHandler> class MOS6560 { | |||||||
| 				103,	42,		80,		16, | 				103,	42,		80,		16, | ||||||
| 			}; | 			}; | ||||||
| 			const uint8_t *chrominances; | 			const uint8_t *chrominances; | ||||||
| 			Outputs::CRT::DisplayType display_type; | 			Outputs::Display::Type display_type; | ||||||
|  |  | ||||||
| 			switch(output_mode) { | 			switch(output_mode) { | ||||||
| 				default: | 				default: | ||||||
| 					chrominances = pal_chrominances; | 					chrominances = pal_chrominances; | ||||||
| 					display_type = Outputs::CRT::DisplayType::PAL50; | 					display_type = Outputs::Display::Type::PAL50; | ||||||
| 					timing_.cycles_per_line = 71; | 					timing_.cycles_per_line = 71; | ||||||
| 					timing_.line_counter_increment_offset = 4; | 					timing_.line_counter_increment_offset = 4; | ||||||
| 					timing_.final_line_increment_position = timing_.cycles_per_line - timing_.line_counter_increment_offset; | 					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: | 				case OutputMode::NTSC: | ||||||
| 					chrominances = ntsc_chrominances; | 					chrominances = ntsc_chrominances; | ||||||
| 					display_type = Outputs::CRT::DisplayType::NTSC60; | 					display_type = Outputs::Display::Type::NTSC60; | ||||||
| 					timing_.cycles_per_line = 65; | 					timing_.cycles_per_line = 65; | ||||||
| 					timing_.line_counter_increment_offset = 40; | 					timing_.line_counter_increment_offset = 40; | ||||||
| 					timing_.final_line_increment_position = 58; | 					timing_.final_line_increment_position = 58; | ||||||
| @@ -155,14 +145,14 @@ template <class BusHandler> class MOS6560 { | |||||||
| 				break; | 				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) { | 			switch(output_mode) { | ||||||
| 				case OutputMode::PAL: | 				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; | 				break; | ||||||
| 				case OutputMode::NTSC: | 				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; | 				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 | 			// keep track of the amount of time since the speaker was updated; lazy updates are applied | ||||||
| 			cycles_since_speaker_update_ += cycles; | 			cycles_since_speaker_update_ += cycles; | ||||||
|  |  | ||||||
| 			int number_of_cycles = cycles.as_int(); | 			auto number_of_cycles = cycles.as_integral(); | ||||||
| 			while(number_of_cycles--) { | 			while(number_of_cycles--) { | ||||||
| 				// keep an old copy of the vertical count because that test is a cycle later than the actual changes | 				// keep an old copy of the vertical count because that test is a cycle later than the actual changes | ||||||
| 				int previous_vertical_counter = vertical_counter_; | 				int previous_vertical_counter = vertical_counter_; | ||||||
| @@ -284,17 +274,17 @@ template <class BusHandler> class MOS6560 { | |||||||
| 				// update the CRT | 				// update the CRT | ||||||
| 				if(this_state_ != output_state_) { | 				if(this_state_ != output_state_) { | ||||||
| 					switch(output_state_) { | 					switch(output_state_) { | ||||||
| 						case State::Sync:			crt_->output_sync(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::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::Border:			output_border(cycles_in_state_ * 4);														break; | ||||||
| 						case State::Pixels:			crt_->output_data(cycles_in_state_ * 4);														break; | 						case State::Pixels:			crt_.output_data(cycles_in_state_ * 4);														break; | ||||||
| 					} | 					} | ||||||
| 					output_state_ = this_state_; | 					output_state_ = this_state_; | ||||||
| 					cycles_in_state_ = 0; | 					cycles_in_state_ = 0; | ||||||
|  |  | ||||||
| 					pixel_pointer = nullptr; | 					pixel_pointer = nullptr; | ||||||
| 					if(output_state_ == State::Pixels) { | 					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_++; | 				cycles_in_state_++; | ||||||
| @@ -363,7 +353,7 @@ template <class BusHandler> class MOS6560 { | |||||||
| 		/*! | 		/*! | ||||||
| 			Writes to a 6560 register. | 			Writes to a 6560 register. | ||||||
| 		*/ | 		*/ | ||||||
| 		void set_register(int address, uint8_t value) { | 		void write(int address, uint8_t value) { | ||||||
| 			address &= 0xf; | 			address &= 0xf; | ||||||
| 			registers_.direct_values[address] = value; | 			registers_.direct_values[address] = value; | ||||||
| 			switch(address) { | 			switch(address) { | ||||||
| @@ -427,7 +417,7 @@ template <class BusHandler> class MOS6560 { | |||||||
| 		/* | 		/* | ||||||
| 			Reads from a 6560 register. | 			Reads from a 6560 register. | ||||||
| 		*/ | 		*/ | ||||||
| 		uint8_t get_register(int address) { | 		uint8_t read(int address) { | ||||||
| 			address &= 0xf; | 			address &= 0xf; | ||||||
| 			switch(address) { | 			switch(address) { | ||||||
| 				default: return registers_.direct_values[address]; | 				default: return registers_.direct_values[address]; | ||||||
| @@ -438,7 +428,7 @@ template <class BusHandler> class MOS6560 { | |||||||
|  |  | ||||||
| 	private: | 	private: | ||||||
| 		BusHandler &bus_handler_; | 		BusHandler &bus_handler_; | ||||||
| 		std::unique_ptr<Outputs::CRT::CRT> crt_; | 		Outputs::CRT::CRT crt_; | ||||||
|  |  | ||||||
| 		Concurrency::DeferringAsyncTaskQueue audio_queue_; | 		Concurrency::DeferringAsyncTaskQueue audio_queue_; | ||||||
| 		AudioGenerator audio_generator_; | 		AudioGenerator audio_generator_; | ||||||
| @@ -465,7 +455,7 @@ template <class BusHandler> class MOS6560 { | |||||||
| 		enum State { | 		enum State { | ||||||
| 			Sync, ColourBurst, Border, Pixels | 			Sync, ColourBurst, Border, Pixels | ||||||
| 		} this_state_, output_state_; | 		} this_state_, output_state_; | ||||||
| 		unsigned int cycles_in_state_; | 		int cycles_in_state_; | ||||||
|  |  | ||||||
| 		// counters that cover an entire field | 		// counters that cover an entire field | ||||||
| 		int horizontal_counter_ = 0, vertical_counter_ = 0; | 		int horizontal_counter_ = 0, vertical_counter_ = 0; | ||||||
| @@ -511,10 +501,10 @@ template <class BusHandler> class MOS6560 { | |||||||
| 		uint16_t colours_[16]; | 		uint16_t colours_[16]; | ||||||
|  |  | ||||||
| 		uint16_t *pixel_pointer; | 		uint16_t *pixel_pointer; | ||||||
| 		void output_border(unsigned int number_of_cycles) { | 		void output_border(int number_of_cycles) { | ||||||
| 			uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(1)); | 			uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1)); | ||||||
| 			if(colour_pointer) *colour_pointer = registers_.borderColour; | 			if(colour_pointer) *colour_pointer = registers_.borderColour; | ||||||
| 			crt_->output_level(number_of_cycles); | 			crt_.output_level(number_of_cycles); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		struct { | 		struct { | ||||||
|   | |||||||
| @@ -111,7 +111,7 @@ template <class T> class CRTC6845 { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		void run_for(Cycles cycles) { | 		void run_for(Cycles cycles) { | ||||||
| 			int cyles_remaining = cycles.as_int(); | 			auto cyles_remaining = cycles.as_integral(); | ||||||
| 			while(cyles_remaining--) { | 			while(cyles_remaining--) { | ||||||
| 				// check for end of visible characters | 				// check for end of visible characters | ||||||
| 				if(character_counter_ == registers_[1]) { | 				if(character_counter_ == registers_[1]) { | ||||||
|   | |||||||
							
								
								
									
										228
									
								
								Components/6850/6850.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								Components/6850/6850.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,228 @@ | |||||||
|  | // | ||||||
|  | //  6850.cpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 10/10/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #include "6850.hpp" | ||||||
|  |  | ||||||
|  | #include <cassert> | ||||||
|  |  | ||||||
|  | using namespace Motorola::ACIA; | ||||||
|  |  | ||||||
|  | const HalfCycles ACIA::SameAsTransmit; | ||||||
|  |  | ||||||
|  | ACIA::ACIA(HalfCycles transmit_clock_rate, HalfCycles receive_clock_rate) : | ||||||
|  | 	transmit_clock_rate_(transmit_clock_rate), | ||||||
|  | 	receive_clock_rate_((receive_clock_rate != SameAsTransmit) ? receive_clock_rate : transmit_clock_rate) { | ||||||
|  | 	transmit.set_writer_clock_rate(transmit_clock_rate); | ||||||
|  | 	request_to_send.set_writer_clock_rate(transmit_clock_rate); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t ACIA::read(int address) { | ||||||
|  | 	if(address&1) { | ||||||
|  | 		overran_ = false; | ||||||
|  | 		received_data_ |= NoValueMask; | ||||||
|  | 		update_interrupt_line(); | ||||||
|  | 		return uint8_t(received_data_); | ||||||
|  | 	} else { | ||||||
|  | 		return get_status(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ACIA::reset() { | ||||||
|  | 	transmit.reset_writing(); | ||||||
|  | 	transmit.write(true); | ||||||
|  | 	request_to_send.reset_writing(); | ||||||
|  |  | ||||||
|  | 	bits_received_ = bits_incoming_ = 0; | ||||||
|  | 	receive_interrupt_enabled_ = transmit_interrupt_enabled_ = false; | ||||||
|  | 	overran_ = false; | ||||||
|  | 	next_transmission_ = received_data_ = NoValueMask; | ||||||
|  |  | ||||||
|  | 	update_interrupt_line(); | ||||||
|  | 	assert(!interrupt_line_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ACIA::write(int address, uint8_t value) { | ||||||
|  | 	if(address&1) { | ||||||
|  | 		next_transmission_ = value; | ||||||
|  | 		consider_transmission(); | ||||||
|  | 		update_interrupt_line(); | ||||||
|  | 	} else { | ||||||
|  | 		if((value&3) == 3) { | ||||||
|  | 			reset(); | ||||||
|  | 		} else { | ||||||
|  | 			switch(value & 3) { | ||||||
|  | 				default: | ||||||
|  | 				case 0: divider_ = 1;	break; | ||||||
|  | 				case 1: divider_ = 16;	break; | ||||||
|  | 				case 2: divider_ = 64;	break; | ||||||
|  | 			} | ||||||
|  | 			switch((value >> 2) & 7) { | ||||||
|  | 				default: | ||||||
|  | 				case 0:	data_bits_ = 7; stop_bits_ = 2; parity_ = Parity::Even;	break; | ||||||
|  | 				case 1:	data_bits_ = 7; stop_bits_ = 2; parity_ = Parity::Odd;	break; | ||||||
|  | 				case 2:	data_bits_ = 7; stop_bits_ = 1; parity_ = Parity::Even;	break; | ||||||
|  | 				case 3:	data_bits_ = 7; stop_bits_ = 1; parity_ = Parity::Odd;	break; | ||||||
|  | 				case 4:	data_bits_ = 8; stop_bits_ = 2; parity_ = Parity::None;	break; | ||||||
|  | 				case 5:	data_bits_ = 8; stop_bits_ = 1; parity_ = Parity::None;	break; | ||||||
|  | 				case 6:	data_bits_ = 8; stop_bits_ = 1; parity_ = Parity::Even;	break; | ||||||
|  | 				case 7:	data_bits_ = 8; stop_bits_ = 1; parity_ = Parity::Odd;	break; | ||||||
|  | 			} | ||||||
|  | 			switch((value >> 5) & 3) { | ||||||
|  | 				case 0:	request_to_send.write(false); transmit_interrupt_enabled_ = false;	break; | ||||||
|  | 				case 1:	request_to_send.write(false); transmit_interrupt_enabled_ = true;	break; | ||||||
|  | 				case 2:	request_to_send.write(true); transmit_interrupt_enabled_ = false;	break; | ||||||
|  | 				case 3: | ||||||
|  | 					request_to_send.write(false); | ||||||
|  | 					transmit_interrupt_enabled_ = false; | ||||||
|  | 					transmit.reset_writing(); | ||||||
|  | 					transmit.write(false); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 			receive.set_read_delegate(this, Storage::Time(divider_ * 2, int(receive_clock_rate_.as_integral()))); | ||||||
|  | 			receive_interrupt_enabled_ = value & 0x80; | ||||||
|  |  | ||||||
|  | 			update_interrupt_line(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	update_clocking_observer(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ACIA::consider_transmission() { | ||||||
|  | 	if(next_transmission_ != NoValueMask && !transmit.write_data_time_remaining()) { | ||||||
|  | 		// Establish start bit and [7 or 8] data bits. | ||||||
|  | 		if(data_bits_ == 7) next_transmission_ &= 0x7f; | ||||||
|  | 		int transmission = next_transmission_ << 1; | ||||||
|  |  | ||||||
|  | 		// Add a parity bit, if any. | ||||||
|  | 		int mask = 0x2 << data_bits_; | ||||||
|  | 		if(parity_ != Parity::None) { | ||||||
|  | 			transmission |= parity(uint8_t(next_transmission_)) ? mask : 0; | ||||||
|  | 			mask <<= 1; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Add stop bits. | ||||||
|  | 		for(int c = 0; c < stop_bits_; ++c) { | ||||||
|  | 			transmission |= mask; | ||||||
|  | 			mask <<= 1; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Output all that. | ||||||
|  | 		const int total_bits = expected_bits(); | ||||||
|  | 		transmit.write(divider_ * 2, total_bits, transmission); | ||||||
|  |  | ||||||
|  | 		// Mark the transmit register as empty again. | ||||||
|  | 		next_transmission_ = NoValueMask; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ClockingHint::Preference ACIA::preferred_clocking() { | ||||||
|  | 	// Real-time clocking is required if a transmission is ongoing; this is a courtesy for whomever | ||||||
|  | 	// is on the receiving end. | ||||||
|  | 	if(transmit.transmission_data_time_remaining() > 0) return ClockingHint::Preference::RealTime; | ||||||
|  |  | ||||||
|  | 	// If a bit reception is ongoing that might lead to an interrupt, ask for real-time clocking | ||||||
|  | 	// because it's unclear when the interrupt might come. | ||||||
|  | 	if(bits_incoming_ && receive_interrupt_enabled_) return ClockingHint::Preference::RealTime; | ||||||
|  |  | ||||||
|  | 	// No clocking required then. | ||||||
|  | 	return ClockingHint::Preference::None; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool ACIA::get_interrupt_line() const { | ||||||
|  | 	return interrupt_line_; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | int ACIA::expected_bits() { | ||||||
|  | 	return 1 + data_bits_ + stop_bits_ + (parity_ != Parity::None); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t ACIA::parity(uint8_t value) { | ||||||
|  | 	value ^= value >> 4; | ||||||
|  | 	value ^= value >> 2; | ||||||
|  | 	value ^= value >> 1; | ||||||
|  | 	return value ^ (parity_ == Parity::Even); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool ACIA::serial_line_did_produce_bit(Serial::Line *line, int bit) { | ||||||
|  | 	// Shift this bit into the 11-bit input register; this is big enough to hold | ||||||
|  | 	// the largest transmission symbol. | ||||||
|  | 	++bits_received_; | ||||||
|  | 	bits_incoming_ = (bits_incoming_ >> 1) | (bit << 10); | ||||||
|  |  | ||||||
|  | 	// If that's the now-expected number of bits, update. | ||||||
|  | 	const int bit_target = expected_bits(); | ||||||
|  | 	if(bits_received_ >= bit_target) { | ||||||
|  | 		bits_received_ = 0; | ||||||
|  | 		overran_ |= get_status() & 1; | ||||||
|  | 		received_data_ = uint8_t(bits_incoming_ >> (12 - bit_target)); | ||||||
|  | 		update_interrupt_line(); | ||||||
|  | 		update_clocking_observer(); | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// TODO: overrun, and parity. | ||||||
|  |  | ||||||
|  | 	// Keep receiving, and consider a potential clocking change. | ||||||
|  | 	if(bits_received_ == 1) update_clocking_observer(); | ||||||
|  | 	return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ACIA::set_interrupt_delegate(InterruptDelegate *delegate) { | ||||||
|  | 	interrupt_delegate_ = delegate; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ACIA::update_interrupt_line() { | ||||||
|  | 	const bool old_line = interrupt_line_; | ||||||
|  |  | ||||||
|  | 	/* | ||||||
|  | 		"Bit 7 of the control register is the rie bit. When the rie bit is high, the rdrf, ndcd, | ||||||
|  | 		and ovr bits will assert the nirq output. When the rie bit is low, nirq generation is disabled." | ||||||
|  |  | ||||||
|  | 		rie = read interrupt enable | ||||||
|  | 		rdrf = receive data register full (status word bit 0) | ||||||
|  | 		ndcd = data carrier detect (status word bit 2) | ||||||
|  | 		over = receiver overrun (status word bit 5) | ||||||
|  |  | ||||||
|  | 		"Bit 1 of the status register is the tdre bit. When high, the tdre bit indicates that data has been | ||||||
|  | 		transferred from the transmitter data register to the output shift register. At this point, the a6850 | ||||||
|  | 		is ready to accept a new transmit data byte. However, if the ncts signal is high, the tdre bit remains | ||||||
|  | 		low regardless of the status of the transmitter data register. Also, if transmit interrupt is enabled, | ||||||
|  | 		the nirq output is asserted." | ||||||
|  |  | ||||||
|  | 		tdre = transmitter data register empty | ||||||
|  | 		ncts = clear to send | ||||||
|  | 	*/ | ||||||
|  | 	const auto status = get_status(); | ||||||
|  | 	interrupt_line_ = | ||||||
|  | 		(receive_interrupt_enabled_ && (status & 0x25)) || | ||||||
|  | 		(transmit_interrupt_enabled_ && (status & 0x02)); | ||||||
|  |  | ||||||
|  | 	if(interrupt_delegate_ && old_line != interrupt_line_) { | ||||||
|  | 		interrupt_delegate_->acia6850_did_change_interrupt_status(this); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t ACIA::get_status() { | ||||||
|  | 	return | ||||||
|  | 		((received_data_ & NoValueMask) ? 0x00 : 0x01) | | ||||||
|  | 		((next_transmission_ == NoValueMask) ? 0x02 : 0x00) | | ||||||
|  | //		(data_carrier_detect.read() ? 0x04 : 0x00) | | ||||||
|  | //		(clear_to_send.read() ? 0x08 : 0x00) | | ||||||
|  | 		(overran_ ? 0x20 : 0x00) | | ||||||
|  | 		(interrupt_line_ ? 0x80 : 0x00) | ||||||
|  | 	; | ||||||
|  |  | ||||||
|  | 	// b0: receive data full. | ||||||
|  | 	// b1: transmit data empty. | ||||||
|  | 	// b2: DCD. | ||||||
|  | 	// b3: CTS. | ||||||
|  | 	// b4: framing error (i.e. no first stop bit where expected). | ||||||
|  | 	// b5: receiver overran. | ||||||
|  | 	// b6: parity error. | ||||||
|  | 	// b7: IRQ state. | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								Components/6850/6850.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								Components/6850/6850.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | |||||||
|  | // | ||||||
|  | //  6850.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 10/10/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef Motorola_ACIA_6850_hpp | ||||||
|  | #define Motorola_ACIA_6850_hpp | ||||||
|  |  | ||||||
|  | #include <cstdint> | ||||||
|  | #include "../../ClockReceiver/ClockReceiver.hpp" | ||||||
|  | #include "../../ClockReceiver/ForceInline.hpp" | ||||||
|  | #include "../../ClockReceiver/ClockingHintSource.hpp" | ||||||
|  | #include "../Serial/Line.hpp" | ||||||
|  |  | ||||||
|  | namespace Motorola { | ||||||
|  | namespace ACIA { | ||||||
|  |  | ||||||
|  | class ACIA: public ClockingHint::Source, private Serial::Line::ReadDelegate { | ||||||
|  | 	public: | ||||||
|  | 		static constexpr const HalfCycles SameAsTransmit = HalfCycles(0); | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Constructs a new instance of ACIA which will receive a transmission clock at a rate of | ||||||
|  | 			@c transmit_clock_rate, and a receive clock at a rate of @c receive_clock_rate. | ||||||
|  | 		*/ | ||||||
|  | 		ACIA(HalfCycles transmit_clock_rate, HalfCycles receive_clock_rate = SameAsTransmit); | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Reads from the ACIA. | ||||||
|  |  | ||||||
|  | 			Bit 0 of the address is used as the ACIA's register select line — | ||||||
|  | 			so even addresses select control/status registers, odd addresses | ||||||
|  | 			select transmit/receive data registers. | ||||||
|  | 		*/ | ||||||
|  | 		uint8_t read(int address); | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Writes to the ACIA. | ||||||
|  |  | ||||||
|  | 			Bit 0 of the address is used as the ACIA's register select line — | ||||||
|  | 			so even addresses select control/status registers, odd addresses | ||||||
|  | 			select transmit/receive data registers. | ||||||
|  | 		*/ | ||||||
|  | 		void write(int address, uint8_t value); | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Advances @c transmission_cycles in time, which should be | ||||||
|  | 			counted relative to the @c transmit_clock_rate. | ||||||
|  | 		*/ | ||||||
|  | 		forceinline void run_for(HalfCycles transmission_cycles) { | ||||||
|  | 			if(transmit.transmission_data_time_remaining() > HalfCycles(0)) { | ||||||
|  | 				const auto write_data_time_remaining = transmit.write_data_time_remaining(); | ||||||
|  |  | ||||||
|  | 				// There's at most one further byte available to enqueue, so a single 'if' | ||||||
|  | 				// rather than a 'while' is correct here. It's the responsibilit of the caller | ||||||
|  | 				// to ensure run_for lengths are appropriate for longer sequences. | ||||||
|  | 				if(transmission_cycles >= write_data_time_remaining) { | ||||||
|  | 					if(next_transmission_ != NoValueMask) { | ||||||
|  | 						transmit.advance_writer(write_data_time_remaining); | ||||||
|  | 						consider_transmission(); | ||||||
|  | 						transmit.advance_writer(transmission_cycles - write_data_time_remaining); | ||||||
|  | 					} else { | ||||||
|  | 						transmit.advance_writer(transmission_cycles); | ||||||
|  | 						update_clocking_observer(); | ||||||
|  | 						update_interrupt_line(); | ||||||
|  | 					} | ||||||
|  | 				} else { | ||||||
|  | 					transmit.advance_writer(transmission_cycles); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		bool get_interrupt_line() const; | ||||||
|  | 		void reset(); | ||||||
|  |  | ||||||
|  | 		// Input lines. | ||||||
|  | 		Serial::Line receive; | ||||||
|  | 		Serial::Line clear_to_send; | ||||||
|  | 		Serial::Line data_carrier_detect; | ||||||
|  |  | ||||||
|  | 		// Output lines. | ||||||
|  | 		Serial::Line transmit; | ||||||
|  | 		Serial::Line request_to_send; | ||||||
|  |  | ||||||
|  | 		// ClockingHint::Source. | ||||||
|  | 		ClockingHint::Preference preferred_clocking() final; | ||||||
|  |  | ||||||
|  | 		struct InterruptDelegate { | ||||||
|  | 			virtual void acia6850_did_change_interrupt_status(ACIA *acia) = 0; | ||||||
|  | 		}; | ||||||
|  | 		void set_interrupt_delegate(InterruptDelegate *delegate); | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		int divider_ = 1; | ||||||
|  | 		enum class Parity { | ||||||
|  | 			Even, Odd, None | ||||||
|  | 		} parity_ = Parity::None; | ||||||
|  | 		int data_bits_ = 7, stop_bits_ = 2; | ||||||
|  |  | ||||||
|  | 		static constexpr int NoValueMask = 0x100; | ||||||
|  | 		int next_transmission_ = NoValueMask; | ||||||
|  | 		int received_data_ = NoValueMask; | ||||||
|  |  | ||||||
|  | 		int bits_received_ = 0; | ||||||
|  | 		int bits_incoming_ = 0; | ||||||
|  | 		bool overran_ = false; | ||||||
|  |  | ||||||
|  | 		void consider_transmission(); | ||||||
|  | 		int expected_bits(); | ||||||
|  | 		uint8_t parity(uint8_t value); | ||||||
|  |  | ||||||
|  | 		bool receive_interrupt_enabled_ = false; | ||||||
|  | 		bool transmit_interrupt_enabled_ = false; | ||||||
|  |  | ||||||
|  | 		HalfCycles transmit_clock_rate_; | ||||||
|  | 		HalfCycles receive_clock_rate_; | ||||||
|  |  | ||||||
|  | 		bool serial_line_did_produce_bit(Serial::Line *line, int bit) final; | ||||||
|  |  | ||||||
|  | 		bool interrupt_line_ = false; | ||||||
|  | 		void update_interrupt_line(); | ||||||
|  | 		InterruptDelegate *interrupt_delegate_ = nullptr; | ||||||
|  | 		uint8_t get_status(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* Motorola_ACIA_6850_hpp */ | ||||||
							
								
								
									
										374
									
								
								Components/68901/MFP68901.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										374
									
								
								Components/68901/MFP68901.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,374 @@ | |||||||
|  | // | ||||||
|  | //  MFP68901.cpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 06/10/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #include "MFP68901.hpp" | ||||||
|  |  | ||||||
|  | #include <algorithm> | ||||||
|  | #include <cstring> | ||||||
|  |  | ||||||
|  | #ifndef NDEBUG | ||||||
|  | #define NDEBUG | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #define LOG_PREFIX "[MFP] " | ||||||
|  | #include "../../Outputs/Log.hpp" | ||||||
|  |  | ||||||
|  | using namespace Motorola::MFP68901; | ||||||
|  |  | ||||||
|  | ClockingHint::Preference MFP68901::preferred_clocking() { | ||||||
|  | 	// Rule applied: if any timer is actively running and permitted to produce an | ||||||
|  | 	// interrupt, request real-time running. | ||||||
|  | 	return | ||||||
|  | 		(timers_[0].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerA) || | ||||||
|  | 		(timers_[1].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerB) || | ||||||
|  | 		(timers_[2].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerC) || | ||||||
|  | 		(timers_[3].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerD) | ||||||
|  | 			? ClockingHint::Preference::RealTime : ClockingHint::Preference::JustInTime; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t MFP68901::read(int address) { | ||||||
|  | 	address &= 0x1f; | ||||||
|  |  | ||||||
|  | 	// Interrupt block: various bits of state can be read, all passively. | ||||||
|  | 	if(address >= 0x03 && address <= 0x0b) { | ||||||
|  | 		const int shift = (address&1) << 3; | ||||||
|  | 		switch(address) { | ||||||
|  | 			case 0x03:	case 0x04:	return uint8_t(interrupt_enable_ >> shift); | ||||||
|  | 			case 0x05:	case 0x06:	return uint8_t(interrupt_pending_ >> shift); | ||||||
|  | 			case 0x07:	case 0x08:	return uint8_t(interrupt_in_service_ >> shift); | ||||||
|  | 			case 0x09:	case 0x0a:	return uint8_t(interrupt_mask_ >> shift); | ||||||
|  | 			case 0x0b:	return interrupt_vector_; | ||||||
|  |  | ||||||
|  | 			default: break; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch(address) { | ||||||
|  | 		// GPIP block: input, and configured active edge and direction values. | ||||||
|  | 		case 0x00:	return (gpip_input_ & ~gpip_direction_) | (gpip_output_ & gpip_direction_); | ||||||
|  | 		case 0x01:	return gpip_active_edge_; | ||||||
|  | 		case 0x02:	return gpip_direction_; | ||||||
|  |  | ||||||
|  | 		/* Interrupt block dealt with above. */ | ||||||
|  | 		default: break; | ||||||
|  |  | ||||||
|  | 		// Timer block: read back A, B and C/D control, and read current timer values. | ||||||
|  | 		case 0x0c:	case 0x0d:	return timer_ab_control_[address - 0xc]; | ||||||
|  | 		case 0x0e:				return timer_cd_control_; | ||||||
|  | 		case 0x0f:	case 0x10: | ||||||
|  | 		case 0x11:	case 0x12:	return get_timer_data(address - 0xf); | ||||||
|  |  | ||||||
|  | 		// USART block: TODO. | ||||||
|  | 		case 0x13:		LOG("Read: sync character generator");	break; | ||||||
|  | 		case 0x14:		LOG("Read: USART control");				break; | ||||||
|  | 		case 0x15:		LOG("Read: receiver status");			break; | ||||||
|  | 		case 0x16:		LOG("Read: transmitter status");		break; | ||||||
|  | 		case 0x17:		LOG("Read: USART data");				break; | ||||||
|  | 	} | ||||||
|  | 	return 0x00; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MFP68901::write(int address, uint8_t value) { | ||||||
|  | 	address &= 0x1f; | ||||||
|  |  | ||||||
|  | 	// Interrupt block: enabled and masked interrupts can be set; pending and in-service interrupts can be masked. | ||||||
|  | 	if(address >= 0x03 && address <= 0x0b) { | ||||||
|  | 		const int shift = (address&1) << 3; | ||||||
|  | 		const int preserve = 0xff00 >> shift; | ||||||
|  | 		const int word_value = value << shift; | ||||||
|  |  | ||||||
|  | 		switch(address) { | ||||||
|  | 			default: break; | ||||||
|  | 			case 0x03: case 0x04:	// Adjust enabled interrupts; disabled ones also cease to be pending. | ||||||
|  | 				interrupt_enable_ = (interrupt_enable_ & preserve) | word_value; | ||||||
|  | 				interrupt_pending_ &= interrupt_enable_; | ||||||
|  | 			break; | ||||||
|  | 			case 0x05: case 0x06:	// Resolve pending interrupts. | ||||||
|  | 				interrupt_pending_ &= (preserve | word_value); | ||||||
|  | 			break; | ||||||
|  | 			case 0x07: case 0x08:	// Resolve in-service interrupts. | ||||||
|  | 				interrupt_in_service_ &= (preserve | word_value); | ||||||
|  | 			break; | ||||||
|  | 			case 0x09: case 0x0a:	// Adjust interrupt mask. | ||||||
|  | 				interrupt_mask_ = (interrupt_mask_ & preserve) | word_value; | ||||||
|  | 			break; | ||||||
|  | 			case 0x0b:				// Set the interrupt vector, possibly changing end-of-interrupt mode. | ||||||
|  | 				interrupt_vector_ = value; | ||||||
|  |  | ||||||
|  | 				// If automatic end-of-interrupt mode has now been enabled, clear | ||||||
|  | 				// the in-process mask and re-evaluate. | ||||||
|  | 				if(interrupt_vector_ & 0x08) return; | ||||||
|  | 				interrupt_in_service_ = 0; | ||||||
|  | 			break; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Whatever just happened may have affected the state of the interrupt line. | ||||||
|  | 		update_interrupts(); | ||||||
|  | 		update_clocking_observer(); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	constexpr int timer_prescales[] = { | ||||||
|  | 		1, 4, 10, 16, 50, 64, 100, 200 | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	switch(address) { | ||||||
|  | 		// GPIP block: output and configuration of active edge and direction values. | ||||||
|  | 		case 0x00: | ||||||
|  | 			gpip_output_ = value; | ||||||
|  | 		break; | ||||||
|  | 		case 0x01: | ||||||
|  | 			gpip_active_edge_ = value; | ||||||
|  | 			reevaluate_gpip_interrupts(); | ||||||
|  | 		break; | ||||||
|  | 		case 0x02: | ||||||
|  | 			gpip_direction_ = value; | ||||||
|  | 			reevaluate_gpip_interrupts(); | ||||||
|  | 		break; | ||||||
|  |  | ||||||
|  | 		/* Interrupt block dealt with above. */ | ||||||
|  | 		default: break; | ||||||
|  |  | ||||||
|  | 		// Timer block. | ||||||
|  | 		case 0x0c: | ||||||
|  | 		case 0x0d: { | ||||||
|  | 			const auto timer = address - 0xc; | ||||||
|  | 			const bool reset = value & 0x10; | ||||||
|  | 			timer_ab_control_[timer] = value; | ||||||
|  | 			switch(value & 0xf) { | ||||||
|  | 				case 0x0:	set_timer_mode(timer, TimerMode::Stopped, 1, reset);		break; | ||||||
|  | 				case 0x1:	set_timer_mode(timer, TimerMode::Delay, 4, reset);			break; | ||||||
|  | 				case 0x2:	set_timer_mode(timer, TimerMode::Delay, 10, reset);			break; | ||||||
|  | 				case 0x3:	set_timer_mode(timer, TimerMode::Delay, 16, reset);			break; | ||||||
|  | 				case 0x4:	set_timer_mode(timer, TimerMode::Delay, 50, reset);			break; | ||||||
|  | 				case 0x5:	set_timer_mode(timer, TimerMode::Delay, 64, reset);			break; | ||||||
|  | 				case 0x6:	set_timer_mode(timer, TimerMode::Delay, 100, reset);		break; | ||||||
|  | 				case 0x7:	set_timer_mode(timer, TimerMode::Delay, 200, reset);		break; | ||||||
|  | 				case 0x8:	set_timer_mode(timer, TimerMode::EventCount, 1, reset);		break; | ||||||
|  | 				case 0x9:	set_timer_mode(timer, TimerMode::PulseWidth, 4, reset);		break; | ||||||
|  | 				case 0xa:	set_timer_mode(timer, TimerMode::PulseWidth, 10, reset);	break; | ||||||
|  | 				case 0xb:	set_timer_mode(timer, TimerMode::PulseWidth, 16, reset);	break; | ||||||
|  | 				case 0xc:	set_timer_mode(timer, TimerMode::PulseWidth, 50, reset);	break; | ||||||
|  | 				case 0xd:	set_timer_mode(timer, TimerMode::PulseWidth, 64, reset);	break; | ||||||
|  | 				case 0xe:	set_timer_mode(timer, TimerMode::PulseWidth, 100, reset);	break; | ||||||
|  | 				case 0xf:	set_timer_mode(timer, TimerMode::PulseWidth, 200, reset);	break; | ||||||
|  | 			} | ||||||
|  | 		} break; | ||||||
|  | 		case 0x0e: | ||||||
|  | 			timer_cd_control_ = value; | ||||||
|  | 			set_timer_mode(3, (value & 7) ? TimerMode::Delay : TimerMode::Stopped, timer_prescales[value & 7], false); | ||||||
|  | 			set_timer_mode(2, ((value >> 4) & 7) ? TimerMode::Delay : TimerMode::Stopped, timer_prescales[(value >> 4) & 7], false); | ||||||
|  | 		break; | ||||||
|  | 		case 0x0f:	case 0x10:	case 0x11:	case 0x12: | ||||||
|  | 			set_timer_data(address - 0xf, value); | ||||||
|  | 		break; | ||||||
|  |  | ||||||
|  | 		// USART block: TODO. | ||||||
|  | 		case 0x13:		LOG("Write: sync character generator");	break; | ||||||
|  | 		case 0x14:		LOG("Write: USART control");			break; | ||||||
|  | 		case 0x15:		LOG("Write: receiver status");			break; | ||||||
|  | 		case 0x16:		LOG("Write: transmitter status");		break; | ||||||
|  | 		case 0x17:		LOG("Write: USART data");				break; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	update_clocking_observer(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MFP68901::run_for(HalfCycles time) { | ||||||
|  | 	cycles_left_ += time; | ||||||
|  |  | ||||||
|  | 	const int cycles = int(cycles_left_.flush<Cycles>().as_integral()); | ||||||
|  | 	if(!cycles) return; | ||||||
|  |  | ||||||
|  | 	for(int c = 0; c < 4; ++c) { | ||||||
|  | 		if(timers_[c].mode >= TimerMode::Delay) { | ||||||
|  | 			// This code applies the timer prescaling only. prescale_count is used to count | ||||||
|  | 			// upwards rather than downwards for simplicity, but on the real hardware it's | ||||||
|  | 			// pretty safe to assume it actually counted downwards. So the clamp to 0 is | ||||||
|  | 			// because gymnastics may need to occur when the prescale value is altered, e.g. | ||||||
|  | 			// if a prescale of 256 is set and the prescale_count is currently 2 then the | ||||||
|  | 			// counter should roll over in 254 cycles. If the user at that point changes the | ||||||
|  | 			// prescale_count to 1 then the counter will need to be altered to -253 and | ||||||
|  | 			// allowed to keep counting up until it crosses both 0 and 1. | ||||||
|  | 			const int dividend = timers_[c].prescale_count + cycles; | ||||||
|  | 			const int decrements = std::max(dividend / timers_[c].prescale, 0); | ||||||
|  | 			if(decrements) { | ||||||
|  | 				decrement_timer(c, decrements); | ||||||
|  | 				timers_[c].prescale_count = dividend % timers_[c].prescale; | ||||||
|  | 			} else { | ||||||
|  | 				timers_[c].prescale_count += cycles; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | HalfCycles MFP68901::get_next_sequence_point() { | ||||||
|  | 	return HalfCycles(-1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: - Timers | ||||||
|  |  | ||||||
|  | void MFP68901::set_timer_mode(int timer, TimerMode mode, int prescale, bool reset_timer) { | ||||||
|  | 	LOG("Timer " << timer << " mode set: " << int(mode) << "; prescale: " << prescale); | ||||||
|  | 	timers_[timer].mode = mode; | ||||||
|  | 	if(reset_timer) { | ||||||
|  | 		timers_[timer].prescale_count = 0; | ||||||
|  | 		timers_[timer].value = timers_[timer].reload_value; | ||||||
|  | 	} else { | ||||||
|  | 		// This hoop is because the prescale_count here goes upward but I'm assuming it goes downward in | ||||||
|  | 		// real hardware. Therefore this deals with the "switched to a lower prescaling" case whereby the | ||||||
|  | 		// old cycle should be allowed naturally to expire. | ||||||
|  | 		timers_[timer].prescale_count = prescale - (timers_[timer].prescale - timers_[timer].prescale_count); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	timers_[timer].prescale = prescale; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MFP68901::set_timer_data(int timer, uint8_t value) { | ||||||
|  | 	if(timers_[timer].mode == TimerMode::Stopped) { | ||||||
|  | 		timers_[timer].value = value; | ||||||
|  | 	} | ||||||
|  | 	timers_[timer].reload_value = value; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t MFP68901::get_timer_data(int timer) { | ||||||
|  | 	return timers_[timer].value; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MFP68901::set_timer_event_input(int channel, bool value) { | ||||||
|  | 	if(timers_[channel].event_input == value) return; | ||||||
|  |  | ||||||
|  | 	timers_[channel].event_input = value; | ||||||
|  | 	if(timers_[channel].mode == TimerMode::EventCount && (value == !!(gpip_active_edge_ & (0x10 >> channel)))) { | ||||||
|  | 		// "The active state of the signal on TAI or TBI is dependent upon the associated | ||||||
|  | 		// Interrupt Channel’s edge bit (GPIP 4 for TAI and GPIP 3 for TBI [...] ). | ||||||
|  | 		// If the edge bit associated with the TAI or TBI input is a one, it will be active high. | ||||||
|  | 		decrement_timer(channel, 1); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// TODO: | ||||||
|  | 	// | ||||||
|  | 	// Altering the edge bit while the timer is in the event count mode can produce a count pulse. | ||||||
|  | 	// The interrupt channel associated with the input (I3 for I4 for TAI) is allowed to function normally. | ||||||
|  | 	// To count transitions reliably, the input must remain in each state (1/O) for a length of time equal | ||||||
|  | 	// to four periods of the timer clock. | ||||||
|  | 	// | ||||||
|  | 	// (the final bit probably explains 13 cycles of the DE to interrupt latency; not sure about the other ~15) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MFP68901::decrement_timer(int timer, int amount) { | ||||||
|  | 	while(amount--) { | ||||||
|  | 		--timers_[timer].value; | ||||||
|  | 		if(timers_[timer].value < 1) { | ||||||
|  | 			switch(timer) { | ||||||
|  | 				case 0: begin_interrupts(Interrupt::TimerA);	break; | ||||||
|  | 				case 1: begin_interrupts(Interrupt::TimerB);	break; | ||||||
|  | 				case 2: begin_interrupts(Interrupt::TimerC);	break; | ||||||
|  | 				case 3: begin_interrupts(Interrupt::TimerD);	break; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Re: reloading when in event counting mode; I found the data sheet thoroughly unclear on | ||||||
|  | 			// this, but it appears empirically to be correct. See e.g. Pompey Pirates menu 27. | ||||||
|  | 			if(timers_[timer].mode == TimerMode::Delay || timers_[timer].mode == TimerMode::EventCount) { | ||||||
|  | 				timers_[timer].value += timers_[timer].reload_value;	// TODO: properly. | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: - GPIP | ||||||
|  | void MFP68901::set_port_input(uint8_t input) { | ||||||
|  | 	gpip_input_ = input; | ||||||
|  | 	reevaluate_gpip_interrupts(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t MFP68901::get_port_output() { | ||||||
|  | 	return 0xff;	// TODO. | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MFP68901::reevaluate_gpip_interrupts() { | ||||||
|  | 	const uint8_t gpip_state = (gpip_input_ & ~gpip_direction_) ^ gpip_active_edge_; | ||||||
|  |  | ||||||
|  | 	// An interrupt is detected on any falling edge. | ||||||
|  | 	const uint8_t new_interrupt_mask = (gpip_state ^ gpip_interrupt_state_) & gpip_interrupt_state_; | ||||||
|  | 	if(new_interrupt_mask) { | ||||||
|  | 		begin_interrupts( | ||||||
|  | 			(new_interrupt_mask & 0x0f) | | ||||||
|  | 			((new_interrupt_mask & 0x30) << 2) | | ||||||
|  | 			((new_interrupt_mask & 0xc0) << 8) | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | 	gpip_interrupt_state_ = gpip_state; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: - Interrupts | ||||||
|  |  | ||||||
|  | void MFP68901::begin_interrupts(int interrupt) { | ||||||
|  | 	interrupt_pending_ |= interrupt & interrupt_enable_; | ||||||
|  | 	update_interrupts(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MFP68901::end_interrupts(int interrupt) { | ||||||
|  | 	interrupt_pending_ &= ~interrupt; | ||||||
|  | 	update_interrupts(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MFP68901::update_interrupts() { | ||||||
|  | 	const auto old_interrupt_line = interrupt_line_; | ||||||
|  | 	const auto firing_interrupts = interrupt_pending_ & interrupt_mask_; | ||||||
|  |  | ||||||
|  | 	if(!firing_interrupts) { | ||||||
|  | 		interrupt_line_ = false; | ||||||
|  | 	} else { | ||||||
|  | 		if(interrupt_vector_ & 0x8) { | ||||||
|  | 			// Software interrupt mode: permit only if neither this interrupt | ||||||
|  | 			// nor a higher interrupt is currently in service. | ||||||
|  | 			const int highest_bit = msb16(firing_interrupts); | ||||||
|  | 			interrupt_line_ = !(interrupt_in_service_ & ~(highest_bit + highest_bit - 1)); | ||||||
|  | 		} else { | ||||||
|  | 			// Auto-interrupt mode; just signal. | ||||||
|  | 			interrupt_line_ = true; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Update the delegate if necessary. | ||||||
|  | 	if(interrupt_delegate_ && interrupt_line_ != old_interrupt_line) { | ||||||
|  | 		interrupt_delegate_->mfp68901_did_change_interrupt_status(this); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool MFP68901::get_interrupt_line() { | ||||||
|  | 	return interrupt_line_; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | int MFP68901::acknowledge_interrupt() { | ||||||
|  | 	if(!(interrupt_pending_ & interrupt_mask_)) { | ||||||
|  | 		return NoAcknowledgement; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const int mask = msb16(interrupt_pending_ & interrupt_mask_); | ||||||
|  |  | ||||||
|  | 	// Clear the pending bit regardless. | ||||||
|  | 	interrupt_pending_ &= ~mask; | ||||||
|  |  | ||||||
|  | 	// If this is software interrupt mode, set the in-service bit. | ||||||
|  | 	if(interrupt_vector_ & 0x8) { | ||||||
|  | 		interrupt_in_service_ |= mask; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	update_interrupts(); | ||||||
|  |  | ||||||
|  | 	int selected = 0; | ||||||
|  | 	while((1 << selected) != mask) ++selected; | ||||||
|  | //	LOG("Interrupt acknowledged: " << selected); | ||||||
|  | 	return (interrupt_vector_ & 0xf0) | uint8_t(selected); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MFP68901::set_interrupt_delegate(InterruptDelegate *delegate) { | ||||||
|  | 	interrupt_delegate_ = delegate; | ||||||
|  | } | ||||||
							
								
								
									
										187
									
								
								Components/68901/MFP68901.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								Components/68901/MFP68901.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | |||||||
|  | // | ||||||
|  | //  MFP68901.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 06/10/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef MFP68901_hpp | ||||||
|  | #define MFP68901_hpp | ||||||
|  |  | ||||||
|  | #include <cstdint> | ||||||
|  | #include "../../ClockReceiver/ClockReceiver.hpp" | ||||||
|  | #include "../../ClockReceiver/ClockingHintSource.hpp" | ||||||
|  |  | ||||||
|  | namespace Motorola { | ||||||
|  | namespace MFP68901 { | ||||||
|  |  | ||||||
|  | class PortHandler { | ||||||
|  | 	public: | ||||||
|  | 		// TODO: announce changes in output. | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	Models the Motorola 68901 Multi-Function Peripheral ('MFP'). | ||||||
|  | */ | ||||||
|  | class MFP68901: public ClockingHint::Source { | ||||||
|  | 	public: | ||||||
|  | 		/// @returns the result of a read from @c address. | ||||||
|  | 		uint8_t read(int address); | ||||||
|  |  | ||||||
|  | 		/// Performs a write of @c value to @c address. | ||||||
|  | 		void write(int address, uint8_t value); | ||||||
|  |  | ||||||
|  | 		/// Advances the MFP by the supplied number of HalfCycles. | ||||||
|  | 		void run_for(HalfCycles); | ||||||
|  |  | ||||||
|  | 		/// @returns the number of cycles until the next possible sequence point — the next time | ||||||
|  | 		/// at which the interrupt line _might_ change. This object conforms to ClockingHint::Source | ||||||
|  | 		/// so that mechanism can also be used to reduce the quantity of calls into this class. | ||||||
|  | 		/// | ||||||
|  | 		/// @discussion TODO, alas. | ||||||
|  | 		HalfCycles get_next_sequence_point(); | ||||||
|  |  | ||||||
|  | 		/// Sets the current level of either of the timer event inputs — TAI and TBI in datasheet terms. | ||||||
|  | 		void set_timer_event_input(int channel, bool value); | ||||||
|  |  | ||||||
|  | 		/// Sets a port handler, a receiver that will be notified upon any change in GPIP output. | ||||||
|  | 		/// | ||||||
|  | 		/// @discussion TODO. | ||||||
|  | 		void set_port_handler(PortHandler *); | ||||||
|  |  | ||||||
|  | 		/// Sets the current input GPIP values. | ||||||
|  | 		void set_port_input(uint8_t); | ||||||
|  |  | ||||||
|  | 		/// @returns the current GPIP output values. | ||||||
|  | 		/// | ||||||
|  | 		/// @discussion TODO. | ||||||
|  | 		uint8_t get_port_output(); | ||||||
|  |  | ||||||
|  | 		/// @returns @c true if the interrupt output is currently active; @c false otherwise.s | ||||||
|  | 		bool get_interrupt_line(); | ||||||
|  |  | ||||||
|  | 		static constexpr int NoAcknowledgement = 0x100; | ||||||
|  |  | ||||||
|  | 		/// Communicates an interrupt acknowledge cycle. | ||||||
|  | 		/// | ||||||
|  | 		/// @returns the vector placed on the bus if any; @c NoAcknowledgement if nothing is loaded. | ||||||
|  | 		int acknowledge_interrupt(); | ||||||
|  |  | ||||||
|  | 		struct InterruptDelegate { | ||||||
|  | 			/// Informs the delegate of a change in the interrupt line of the nominated MFP. | ||||||
|  | 			virtual void mfp68901_did_change_interrupt_status(MFP68901 *) = 0; | ||||||
|  | 		}; | ||||||
|  | 		/// Sets a delegate that will be notified upon any change in the interrupt line. | ||||||
|  | 		void set_interrupt_delegate(InterruptDelegate *delegate); | ||||||
|  |  | ||||||
|  | 		// ClockingHint::Source. | ||||||
|  | 		ClockingHint::Preference preferred_clocking() final; | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		// MARK: - Timers | ||||||
|  | 		enum class TimerMode { | ||||||
|  | 			Stopped, EventCount, Delay, PulseWidth | ||||||
|  | 		}; | ||||||
|  | 		void set_timer_mode(int timer, TimerMode, int prescale, bool reset_timer); | ||||||
|  | 		void set_timer_data(int timer, uint8_t); | ||||||
|  | 		uint8_t get_timer_data(int timer); | ||||||
|  | 		void decrement_timer(int timer, int amount); | ||||||
|  |  | ||||||
|  | 		struct Timer { | ||||||
|  | 			TimerMode mode = TimerMode::Stopped; | ||||||
|  | 			uint8_t value = 0; | ||||||
|  | 			uint8_t reload_value = 0; | ||||||
|  | 			int prescale = 1; | ||||||
|  | 			int prescale_count = 1; | ||||||
|  | 			bool event_input = false; | ||||||
|  | 		} timers_[4]; | ||||||
|  | 		uint8_t timer_ab_control_[2] = { 0, 0 }; | ||||||
|  | 		uint8_t timer_cd_control_ = 0; | ||||||
|  |  | ||||||
|  | 		HalfCycles cycles_left_; | ||||||
|  |  | ||||||
|  | 		// MARK: - GPIP | ||||||
|  | 		uint8_t gpip_input_ = 0; | ||||||
|  | 		uint8_t gpip_output_ = 0; | ||||||
|  | 		uint8_t gpip_active_edge_ = 0; | ||||||
|  | 		uint8_t gpip_direction_ = 0; | ||||||
|  | 		uint8_t gpip_interrupt_state_ = 0; | ||||||
|  |  | ||||||
|  | 		void reevaluate_gpip_interrupts(); | ||||||
|  |  | ||||||
|  | 		// MARK: - Interrupts | ||||||
|  |  | ||||||
|  | 		InterruptDelegate *interrupt_delegate_ = nullptr; | ||||||
|  |  | ||||||
|  | 		// Ad hoc documentation: | ||||||
|  | 		// | ||||||
|  | 		// An interrupt becomes pending if it is enabled at the time it occurs. | ||||||
|  | 		// | ||||||
|  | 		// If a pending interrupt is enabled in the interrupt mask, a processor | ||||||
|  | 		// interrupt is generated. Otherwise no processor interrupt is generated. | ||||||
|  | 		// | ||||||
|  | 		// (Disabling a bit in the enabled mask also instantaneously clears anything | ||||||
|  | 		// in the pending mask.) | ||||||
|  | 		// | ||||||
|  | 		// The user can write to the pending interrupt register; a write | ||||||
|  | 		// masks whatever is there — so you can disable bits but you cannot set them. | ||||||
|  | 		// | ||||||
|  | 		// If the vector register's 'S' bit is set then software end-of-interrupt mode applies: | ||||||
|  | 		// Acknowledgement of an interrupt clears that interrupt's pending bit, but also sets | ||||||
|  | 		// its in-service bit. That bit will remain set until the user writes a zero to its position. | ||||||
|  | 		// If any bits are set in the in-service register, then they will prevent lower-priority | ||||||
|  | 		// interrupts from being signalled to the CPU. Further interrupts of the same or a higher | ||||||
|  | 		// priority may occur. | ||||||
|  | 		// | ||||||
|  | 		// If the vector register's 'S' bit is clear then automatic end-of-interrupt mode applies: | ||||||
|  | 		// Acknowledgement of an interrupt will automatically clear the corresponding | ||||||
|  | 		// pending bit. | ||||||
|  | 		// | ||||||
|  | 		int interrupt_enable_ = 0; | ||||||
|  | 		int interrupt_pending_ = 0; | ||||||
|  | 		int interrupt_mask_ = 0; | ||||||
|  | 		int interrupt_in_service_ = 0; | ||||||
|  | 		bool interrupt_line_ = false; | ||||||
|  | 		uint8_t interrupt_vector_ = 0; | ||||||
|  |  | ||||||
|  | 		enum Interrupt { | ||||||
|  | 			GPIP0				= (1 << 0), | ||||||
|  | 			GPIP1				= (1 << 1), | ||||||
|  | 			GPIP2				= (1 << 2), | ||||||
|  | 			GPIP3				= (1 << 3), | ||||||
|  | 			TimerD				= (1 << 4), | ||||||
|  | 			TimerC				= (1 << 5), | ||||||
|  | 			GPIP4				= (1 << 6), | ||||||
|  | 			GPIP5				= (1 << 7), | ||||||
|  |  | ||||||
|  | 			TimerB				= (1 << 8), | ||||||
|  | 			TransmitError		= (1 << 9), | ||||||
|  | 			TransmitBufferEmpty	= (1 << 10), | ||||||
|  | 			ReceiveError		= (1 << 11), | ||||||
|  | 			ReceiveBufferFull	= (1 << 12), | ||||||
|  | 			TimerA				= (1 << 13), | ||||||
|  | 			GPIP6				= (1 << 14), | ||||||
|  | 			GPIP7				= (1 << 15), | ||||||
|  | 		}; | ||||||
|  | 		void begin_interrupts(int interrupt); | ||||||
|  | 		void end_interrupts(int interrupt); | ||||||
|  | 		void update_interrupts(); | ||||||
|  |  | ||||||
|  | 		/// @returns the most significant bit set in v, assuming it is one of the least significant 16. | ||||||
|  | 		inline static int msb16(int v) { | ||||||
|  | 			// Saturate all bits below the MSB. | ||||||
|  | 			v |= v >> 1; | ||||||
|  | 			v |= v >> 2; | ||||||
|  | 			v |= v >> 4; | ||||||
|  | 			v |= v >> 8; | ||||||
|  |  | ||||||
|  | 			// Throw away lesser bits. | ||||||
|  | 			return (v+1) >> 1; | ||||||
|  | 		} | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* MFP68901_hpp */ | ||||||
| @@ -27,7 +27,7 @@ template <class T> class i8255 { | |||||||
| 			Stores the value @c value to the register at @c address. If this causes a change in 8255 output | 			Stores the value @c value to the register at @c address. If this causes a change in 8255 output | ||||||
| 			then the PortHandler will be informed. | 			then the PortHandler will be informed. | ||||||
| 		*/ | 		*/ | ||||||
| 		void set_register(int address, uint8_t value) { | 		void write(int address, uint8_t value) { | ||||||
| 			switch(address & 3) { | 			switch(address & 3) { | ||||||
| 				case 0: | 				case 0: | ||||||
| 					if(!(control_ & 0x10)) { | 					if(!(control_ & 0x10)) { | ||||||
| @@ -60,7 +60,7 @@ template <class T> class i8255 { | |||||||
| 			Obtains the current value for the register at @c address. If this provides a reading | 			Obtains the current value for the register at @c address. If this provides a reading | ||||||
| 			of input then the PortHandler will be queried. | 			of input then the PortHandler will be queried. | ||||||
| 		*/ | 		*/ | ||||||
| 		uint8_t get_register(int address) { | 		uint8_t read(int address) { | ||||||
| 			switch(address & 3) { | 			switch(address & 3) { | ||||||
| 				case 0:	return (control_ & 0x10) ? port_handler_.get_value(0) : outputs_[0]; | 				case 0:	return (control_ & 0x10) ? port_handler_.get_value(0) : outputs_[0]; | ||||||
| 				case 1:	return (control_ & 0x02) ? port_handler_.get_value(1) : outputs_[1]; | 				case 1:	return (control_ & 0x02) ? port_handler_.get_value(1) : outputs_[1]; | ||||||
|   | |||||||
| @@ -95,11 +95,11 @@ void i8272::run_for(Cycles cycles) { | |||||||
|  |  | ||||||
| 	// check for an expired timer | 	// check for an expired timer | ||||||
| 	if(delay_time_ > 0) { | 	if(delay_time_ > 0) { | ||||||
| 		if(cycles.as_int() >= delay_time_) { | 		if(cycles.as_integral() >= delay_time_) { | ||||||
| 			delay_time_ = 0; | 			delay_time_ = 0; | ||||||
| 			posit_event(static_cast<int>(Event8272::Timer)); | 			posit_event(static_cast<int>(Event8272::Timer)); | ||||||
| 		} else { | 		} else { | ||||||
| 			delay_time_ -= cycles.as_int(); | 			delay_time_ -= cycles.as_integral(); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -108,8 +108,8 @@ void i8272::run_for(Cycles cycles) { | |||||||
| 		int drives_left = drives_seeking_; | 		int drives_left = drives_seeking_; | ||||||
| 		for(int c = 0; c < 4; c++) { | 		for(int c = 0; c < 4; c++) { | ||||||
| 			if(drives_[c].phase == Drive::Seeking) { | 			if(drives_[c].phase == Drive::Seeking) { | ||||||
| 				drives_[c].step_rate_counter += cycles.as_int(); | 				drives_[c].step_rate_counter += cycles.as_integral(); | ||||||
| 				int steps = drives_[c].step_rate_counter / (8000 * step_rate_time_); | 				auto steps = drives_[c].step_rate_counter / (8000 * step_rate_time_); | ||||||
| 				drives_[c].step_rate_counter %= (8000 * step_rate_time_); | 				drives_[c].step_rate_counter %= (8000 * step_rate_time_); | ||||||
| 				while(steps--) { | 				while(steps--) { | ||||||
| 					// Perform a step. | 					// Perform a step. | ||||||
| @@ -141,12 +141,12 @@ void i8272::run_for(Cycles cycles) { | |||||||
| 			int head = c&1; | 			int head = c&1; | ||||||
|  |  | ||||||
| 			if(drives_[drive].head_unload_delay[head] > 0) { | 			if(drives_[drive].head_unload_delay[head] > 0) { | ||||||
| 				if(cycles.as_int() >= drives_[drive].head_unload_delay[head]) { | 				if(cycles.as_integral() >= drives_[drive].head_unload_delay[head]) { | ||||||
| 					drives_[drive].head_unload_delay[head] = 0; | 					drives_[drive].head_unload_delay[head] = 0; | ||||||
| 					drives_[drive].head_is_loaded[head] = false; | 					drives_[drive].head_is_loaded[head] = false; | ||||||
| 					head_timers_running_--; | 					head_timers_running_--; | ||||||
| 				} else { | 				} else { | ||||||
| 					drives_[drive].head_unload_delay[head] -= cycles.as_int(); | 					drives_[drive].head_unload_delay[head] -= cycles.as_integral(); | ||||||
| 				} | 				} | ||||||
| 				timers_left--; | 				timers_left--; | ||||||
| 				if(!timers_left) break; | 				if(!timers_left) break; | ||||||
| @@ -163,7 +163,7 @@ void i8272::run_for(Cycles cycles) { | |||||||
| 	if(is_sleeping_) update_clocking_observer(); | 	if(is_sleeping_) update_clocking_observer(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void i8272::set_register(int address, uint8_t value) { | void i8272::write(int address, uint8_t value) { | ||||||
| 	// don't consider attempted sets to the status register | 	// don't consider attempted sets to the status register | ||||||
| 	if(!address) return; | 	if(!address) return; | ||||||
|  |  | ||||||
| @@ -181,7 +181,7 @@ void i8272::set_register(int address, uint8_t value) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| uint8_t i8272::get_register(int address) { | uint8_t i8272::read(int address) { | ||||||
| 	if(address) { | 	if(address) { | ||||||
| 		if(result_stack_.empty()) return 0xff; | 		if(result_stack_.empty()) return 0xff; | ||||||
| 		uint8_t result = result_stack_.back(); | 		uint8_t result = result_stack_.back(); | ||||||
| @@ -292,7 +292,7 @@ void i8272::posit_event(int event_type) { | |||||||
| 			WAIT_FOR_EVENT(Event8272::CommandByte) | 			WAIT_FOR_EVENT(Event8272::CommandByte) | ||||||
| 			SetBusy(); | 			SetBusy(); | ||||||
|  |  | ||||||
| 			static const std::size_t required_lengths[32] = { | 			static constexpr std::size_t required_lengths[32] = { | ||||||
| 				0,	0,	9,	3,	2,	9,	9,	2, | 				0,	0,	9,	3,	2,	9,	9,	2, | ||||||
| 				1,	9,	2,	0,	9,	6,	0,	3, | 				1,	9,	2,	0,	9,	6,	0,	3, | ||||||
| 				0,	9,	0,	0,	0,	0,	0,	0, | 				0,	9,	0,	0,	0,	0,	0,	0, | ||||||
| @@ -865,7 +865,7 @@ void i8272::posit_event(int event_type) { | |||||||
| 			SetDataRequest(); | 			SetDataRequest(); | ||||||
| 			SetDataDirectionToProcessor(); | 			SetDataDirectionToProcessor(); | ||||||
|  |  | ||||||
| 			// The actual stuff of unwinding result_stack_ is handled by ::get_register; wait | 			// The actual stuff of unwinding result_stack_ is handled by ::read; wait | ||||||
| 			// until the processor has read all result bytes. | 			// until the processor has read all result bytes. | ||||||
| 			WAIT_FOR_EVENT(Event8272::ResultEmpty); | 			WAIT_FOR_EVENT(Event8272::ResultEmpty); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,13 +33,13 @@ class i8272: public Storage::Disk::MFMController { | |||||||
| 		void set_data_input(uint8_t value); | 		void set_data_input(uint8_t value); | ||||||
| 		uint8_t get_data_output(); | 		uint8_t get_data_output(); | ||||||
|  |  | ||||||
| 		void set_register(int address, uint8_t value); | 		void write(int address, uint8_t value); | ||||||
| 		uint8_t get_register(int address); | 		uint8_t read(int address); | ||||||
|  |  | ||||||
| 		void set_dma_acknowledge(bool dack); | 		void set_dma_acknowledge(bool dack); | ||||||
| 		void set_terminal_count(bool tc); | 		void set_terminal_count(bool tc); | ||||||
|  |  | ||||||
| 		ClockingHint::Preference preferred_clocking() override; | 		ClockingHint::Preference preferred_clocking() final; | ||||||
|  |  | ||||||
| 	protected: | 	protected: | ||||||
| 		virtual void select_drive(int number) = 0; | 		virtual void select_drive(int number) = 0; | ||||||
| @@ -73,7 +73,7 @@ class i8272: public Storage::Disk::MFMController { | |||||||
| 		bool is_access_command_ = false; | 		bool is_access_command_ = false; | ||||||
|  |  | ||||||
| 		// The counter used for ::Timer events. | 		// The counter used for ::Timer events. | ||||||
| 		int delay_time_ = 0; | 		Cycles::IntType delay_time_ = 0; | ||||||
|  |  | ||||||
| 		// The connected drives. | 		// The connected drives. | ||||||
| 		struct Drive { | 		struct Drive { | ||||||
| @@ -89,12 +89,12 @@ class i8272: public Storage::Disk::MFMController { | |||||||
| 			bool seek_failed = false; | 			bool seek_failed = false; | ||||||
|  |  | ||||||
| 			// Seeking: transient state. | 			// Seeking: transient state. | ||||||
| 			int step_rate_counter = 0; | 			Cycles::IntType step_rate_counter = 0; | ||||||
| 			int steps_taken = 0; | 			int steps_taken = 0; | ||||||
| 			int target_head_position = 0;	// either an actual number, or -1 to indicate to step until track zero | 			int target_head_position = 0;	// either an actual number, or -1 to indicate to step until track zero | ||||||
|  |  | ||||||
| 			// Head state. | 			// Head state. | ||||||
| 			int head_unload_delay[2] = {0, 0}; | 			Cycles::IntType head_unload_delay[2] = {0, 0}; | ||||||
| 			bool head_is_loaded[2] = {false, false}; | 			bool head_is_loaded[2] = {false, false}; | ||||||
|  |  | ||||||
| 		} drives_[4]; | 		} drives_[4]; | ||||||
|   | |||||||
							
								
								
									
										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 */ | ||||||
| @@ -11,21 +11,22 @@ | |||||||
| #include <cassert> | #include <cassert> | ||||||
| #include <cstring> | #include <cstring> | ||||||
| #include <cstdlib> | #include <cstdlib> | ||||||
|  | #include "../../Outputs/Log.hpp" | ||||||
|  |  | ||||||
| using namespace TI::TMS; | using namespace TI::TMS; | ||||||
|  |  | ||||||
| namespace { | namespace { | ||||||
|  |  | ||||||
| const uint8_t StatusInterrupt = 0x80; | constexpr uint8_t StatusInterrupt = 0x80; | ||||||
| const uint8_t StatusSpriteOverflow = 0x40; | constexpr uint8_t StatusSpriteOverflow = 0x40; | ||||||
|  |  | ||||||
| const int StatusSpriteCollisionShift = 5; | constexpr int StatusSpriteCollisionShift = 5; | ||||||
| const uint8_t StatusSpriteCollision = 0x20; | constexpr uint8_t StatusSpriteCollision = 0x20; | ||||||
|  |  | ||||||
| // 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole | // 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole | ||||||
| // line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. | // line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. | ||||||
| const unsigned int CRTCyclesPerLine = 1365; | constexpr unsigned int CRTCyclesPerLine = 1365; | ||||||
| const unsigned int CRTCyclesDivider = 4; | constexpr unsigned int CRTCyclesDivider = 4; | ||||||
|  |  | ||||||
| struct ReverseTable { | struct ReverseTable { | ||||||
| 	std::uint8_t map[256]; | 	std::uint8_t map[256]; | ||||||
| @@ -50,7 +51,9 @@ struct ReverseTable { | |||||||
|  |  | ||||||
| Base::Base(Personality p) : | Base::Base(Personality p) : | ||||||
| 	personality_(p), | 	personality_(p), | ||||||
| 	crt_(new Outputs::CRT::CRT(CRTCyclesPerLine, CRTCyclesDivider, Outputs::CRT::DisplayType::NTSC60, 4)) { | 	crt_(CRTCyclesPerLine, CRTCyclesDivider, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red8Green8Blue8) { | ||||||
|  | 	// Unimaginatively, this class just passes RGB through to the shader. Investigation is needed | ||||||
|  | 	// into whether there's a more natural form. It feels unlikely given the diversity of chips modelled. | ||||||
|  |  | ||||||
| 	switch(p) { | 	switch(p) { | ||||||
| 		case TI::TMS::TMS9918A: | 		case TI::TMS::TMS9918A: | ||||||
| @@ -84,21 +87,14 @@ Base::Base(Personality p) : | |||||||
|  |  | ||||||
| TMS9918::TMS9918(Personality p): | TMS9918::TMS9918(Personality p): | ||||||
| 	Base(p) { | 	Base(p) { | ||||||
| 	// Unimaginatively, this class just passes RGB through to the shader. Investigation is needed | 	crt_.set_display_type(Outputs::Display::DisplayType::RGB); | ||||||
| 	// into whether there's a more natural form. | 	crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f)); | ||||||
| 	crt_->set_rgb_sampling_function( |  | ||||||
| 		"vec3 rgb_sample(usampler2D sampler, vec2 coordinate)" |  | ||||||
| 		"{" |  | ||||||
| 			"return texture(sampler, coordinate).rgb / vec3(255.0);" |  | ||||||
| 		"}"); |  | ||||||
| 	crt_->set_video_signal(Outputs::CRT::VideoSignal::RGB); |  | ||||||
| 	crt_->set_visible_area(Outputs::CRT::Rect(0.055f, 0.025f, 0.9f, 0.9f)); |  | ||||||
|  |  | ||||||
| 	// The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement | 	// The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement | ||||||
| 	// intended to produce the correct relationship between the hard edges between pixels and | 	// intended to produce the correct relationship between the hard edges between pixels and | ||||||
| 	// the colour clock. It was eyeballed rather than derived from any knowledge of the TMS | 	// the colour clock. It was eyeballed rather than derived from any knowledge of the TMS | ||||||
| 	// colour burst generator because I've yet to find any. | 	// colour burst generator because I've yet to find any. | ||||||
| 	crt_->set_immediate_default_phase(0.85f); | 	crt_.set_immediate_default_phase(0.85f); | ||||||
| } | } | ||||||
|  |  | ||||||
| void TMS9918::set_tv_standard(TVStandard standard) { | void TMS9918::set_tv_standard(TVStandard standard) { | ||||||
| @@ -107,18 +103,22 @@ void TMS9918::set_tv_standard(TVStandard standard) { | |||||||
| 		case TVStandard::PAL: | 		case TVStandard::PAL: | ||||||
| 			mode_timing_.total_lines = 313; | 			mode_timing_.total_lines = 313; | ||||||
| 			mode_timing_.first_vsync_line = 253; | 			mode_timing_.first_vsync_line = 253; | ||||||
| 			crt_->set_new_display_type(CRTCyclesPerLine, Outputs::CRT::DisplayType::PAL50); | 			crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::PAL50); | ||||||
| 		break; | 		break; | ||||||
| 		default: | 		default: | ||||||
| 			mode_timing_.total_lines = 262; | 			mode_timing_.total_lines = 262; | ||||||
| 			mode_timing_.first_vsync_line = 227; | 			mode_timing_.first_vsync_line = 227; | ||||||
| 			crt_->set_new_display_type(CRTCyclesPerLine, Outputs::CRT::DisplayType::NTSC60); | 			crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::NTSC60); | ||||||
| 		break; | 		break; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| Outputs::CRT::CRT *TMS9918::get_crt() { | void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||||
| 	return crt_.get(); | 	crt_.set_scan_target(scan_target); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) { | ||||||
|  | 	crt_.set_display_type(display_type); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Base::LineBuffer::reset_sprite_collection() { | void Base::LineBuffer::reset_sprite_collection() { | ||||||
| @@ -166,7 +166,7 @@ void TMS9918::run_for(const HalfCycles cycles) { | |||||||
| 	// Convert 456 clocked half cycles per line to 342 internal cycles per line; | 	// Convert 456 clocked half cycles per line to 342 internal cycles per line; | ||||||
| 	// the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised | 	// the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised | ||||||
| 	// for this part. So multiply by three quarters. | 	// for this part. So multiply by three quarters. | ||||||
| 	int int_cycles = (cycles.as_int() * 3) + cycles_error_; | 	int int_cycles = int(cycles.as_integral() * 3) + cycles_error_; | ||||||
| 	cycles_error_ = int_cycles & 3; | 	cycles_error_ = int_cycles & 3; | ||||||
| 	int_cycles >>= 2; | 	int_cycles >>= 2; | ||||||
| 	if(!int_cycles) return; | 	if(!int_cycles) return; | ||||||
| @@ -352,8 +352,7 @@ void TMS9918::run_for(const HalfCycles cycles) { | |||||||
| 				// Output video stream. | 				// Output video stream. | ||||||
| 				// -------------------- | 				// -------------------- | ||||||
|  |  | ||||||
| #define intersect(left, right, code)	\ | #define intersect(left, right, code)	{	\ | ||||||
| 	{	\ |  | ||||||
| 		const int start = std::max(read_pointer_.column, left);	\ | 		const int start = std::max(read_pointer_.column, left);	\ | ||||||
| 		const int end = std::min(end_column, right);	\ | 		const int end = std::min(end_column, right);	\ | ||||||
| 		if(end > start) {\ | 		if(end > start) {\ | ||||||
| @@ -367,7 +366,7 @@ void TMS9918::run_for(const HalfCycles cycles) { | |||||||
| 					if(read_pointer_.row >= mode_timing_.first_vsync_line && read_pointer_.row < mode_timing_.first_vsync_line+4) { | 					if(read_pointer_.row >= mode_timing_.first_vsync_line && read_pointer_.row < mode_timing_.first_vsync_line+4) { | ||||||
| 						// Vertical sync. | 						// Vertical sync. | ||||||
| 						if(end_column == 342) { | 						if(end_column == 342) { | ||||||
| 							crt_->output_sync(342 * 4); | 							crt_.output_sync(342 * 4); | ||||||
| 						} | 						} | ||||||
| 					} else { | 					} else { | ||||||
| 						// Right border. | 						// Right border. | ||||||
| @@ -377,11 +376,11 @@ void TMS9918::run_for(const HalfCycles cycles) { | |||||||
| 						// and 58+15 = 73. So output the lot when the | 						// and 58+15 = 73. So output the lot when the | ||||||
| 						// cursor passes 73. | 						// cursor passes 73. | ||||||
| 						if(read_pointer_.column < 73 && end_column >= 73) { | 						if(read_pointer_.column < 73 && end_column >= 73) { | ||||||
| 							crt_->output_blank(8*4); | 							crt_.output_blank(8*4); | ||||||
| 							crt_->output_sync(26*4); | 							crt_.output_sync(26*4); | ||||||
| 							crt_->output_blank(2*4); | 							crt_.output_blank(2*4); | ||||||
| 							crt_->output_default_colour_burst(14*4); | 							crt_.output_default_colour_burst(14*4); | ||||||
| 							crt_->output_blank(8*4); | 							crt_.output_blank(8*4); | ||||||
| 						} | 						} | ||||||
|  |  | ||||||
| 						// Border colour for the rest of the line. | 						// Border colour for the rest of the line. | ||||||
| @@ -393,11 +392,11 @@ void TMS9918::run_for(const HalfCycles cycles) { | |||||||
|  |  | ||||||
| 					// Blanking region. | 					// Blanking region. | ||||||
| 					if(read_pointer_.column < 73 && end_column >= 73) { | 					if(read_pointer_.column < 73 && end_column >= 73) { | ||||||
| 						crt_->output_blank(8*4); | 						crt_.output_blank(8*4); | ||||||
| 						crt_->output_sync(26*4); | 						crt_.output_sync(26*4); | ||||||
| 						crt_->output_blank(2*4); | 						crt_.output_blank(2*4); | ||||||
| 						crt_->output_default_colour_burst(14*4); | 						crt_.output_default_colour_burst(14*4); | ||||||
| 						crt_->output_blank(8*4); | 						crt_.output_blank(8*4); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					// Left border. | 					// Left border. | ||||||
| @@ -410,7 +409,7 @@ void TMS9918::run_for(const HalfCycles cycles) { | |||||||
| 						if(!asked_for_write_area_) { | 						if(!asked_for_write_area_) { | ||||||
| 							asked_for_write_area_ = true; | 							asked_for_write_area_ = true; | ||||||
| 							pixel_origin_ = pixel_target_ = reinterpret_cast<uint32_t *>( | 							pixel_origin_ = pixel_target_ = reinterpret_cast<uint32_t *>( | ||||||
| 								crt_->allocate_write_area(static_cast<unsigned int>(line_buffer.next_border_column - line_buffer.first_pixel_output_column)) | 								crt_.begin_data(size_t(line_buffer.next_border_column - line_buffer.first_pixel_output_column)) | ||||||
| 							); | 							); | ||||||
| 						} | 						} | ||||||
|  |  | ||||||
| @@ -427,8 +426,8 @@ void TMS9918::run_for(const HalfCycles cycles) { | |||||||
| 						} | 						} | ||||||
|  |  | ||||||
| 						if(end == line_buffer.next_border_column) { | 						if(end == line_buffer.next_border_column) { | ||||||
| 							const unsigned int length = static_cast<unsigned int>(line_buffer.next_border_column - line_buffer.first_pixel_output_column); | 							const int length = line_buffer.next_border_column - line_buffer.first_pixel_output_column; | ||||||
| 							crt_->output_data(length * 4, length); | 							crt_.output_data(length * 4, size_t(length)); | ||||||
| 							pixel_origin_ = pixel_target_ = nullptr; | 							pixel_origin_ = pixel_target_ = nullptr; | ||||||
| 							asked_for_write_area_ = false; | 							asked_for_write_area_ = false; | ||||||
| 						} | 						} | ||||||
| @@ -470,20 +469,30 @@ void Base::output_border(int cycles, uint32_t cram_dot) { | |||||||
| 			palette[background_colour_]; | 			palette[background_colour_]; | ||||||
|  |  | ||||||
| 	if(cram_dot) { | 	if(cram_dot) { | ||||||
| 		uint32_t *const pixel_target = reinterpret_cast<uint32_t *>(crt_->allocate_write_area(1)); | 		uint32_t *const pixel_target = reinterpret_cast<uint32_t *>(crt_.begin_data(1)); | ||||||
|  | 		if(pixel_target) { | ||||||
| 			*pixel_target = border_colour | cram_dot; | 			*pixel_target = border_colour | cram_dot; | ||||||
| 		crt_->output_level(4); | 		} | ||||||
|  | 		crt_.output_level(4); | ||||||
| 		cycles -= 4; | 		cycles -= 4; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if(cycles) { | 	if(cycles) { | ||||||
| 		uint32_t *const pixel_target = reinterpret_cast<uint32_t *>(crt_->allocate_write_area(1)); | 		// If the border colour is 0, that can be communicated | ||||||
|  | 		// more efficiently as an explicit blank. | ||||||
|  | 		if(border_colour) { | ||||||
|  | 			uint32_t *const pixel_target = reinterpret_cast<uint32_t *>(crt_.begin_data(1)); | ||||||
|  | 			if(pixel_target) { | ||||||
| 				*pixel_target = border_colour; | 				*pixel_target = border_colour; | ||||||
| 		crt_->output_level(static_cast<unsigned int>(cycles)); | 			} | ||||||
|  | 			crt_.output_level(cycles); | ||||||
|  | 		} else { | ||||||
|  | 			crt_.output_blank(cycles); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| void TMS9918::set_register(int address, uint8_t value) { | void TMS9918::write(int address, uint8_t value) { | ||||||
| 	// Writes to address 0 are writes to the video RAM. Store | 	// Writes to address 0 are writes to the video RAM. Store | ||||||
| 	// the value and return. | 	// the value and return. | ||||||
| 	if(!(address & 1)) { | 	if(!(address & 1)) { | ||||||
| @@ -582,7 +591,6 @@ void TMS9918::set_register(int address, uint8_t value) { | |||||||
| 			case 8: | 			case 8: | ||||||
| 				if(is_sega_vdp(personality_)) { | 				if(is_sega_vdp(personality_)) { | ||||||
| 					master_system_.horizontal_scroll = low_write_; | 					master_system_.horizontal_scroll = low_write_; | ||||||
| //					printf("Set to %d at %d, %d\n", low_write_, row_, column_); |  | ||||||
| 				} | 				} | ||||||
| 			break; | 			break; | ||||||
|  |  | ||||||
| @@ -599,7 +607,7 @@ void TMS9918::set_register(int address, uint8_t value) { | |||||||
| 			break; | 			break; | ||||||
|  |  | ||||||
| 			default: | 			default: | ||||||
| //				printf("Unknown TMS write: %d to %d\n", low_write_, value); | 				LOG("Unknown TMS write: " << int(low_write_) << " to " << int(value)); | ||||||
| 			break; | 			break; | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| @@ -616,7 +624,7 @@ void TMS9918::set_register(int address, uint8_t value) { | |||||||
|  |  | ||||||
| uint8_t TMS9918::get_current_line() { | uint8_t TMS9918::get_current_line() { | ||||||
| 	// Determine the row to return. | 	// Determine the row to return. | ||||||
| 	static const int row_change_position = 63;	// This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. | 	constexpr int row_change_position = 63;	// This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. | ||||||
| 	int source_row = | 	int source_row = | ||||||
| 		(write_pointer_.column < row_change_position) | 		(write_pointer_.column < row_change_position) | ||||||
| 			? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines | 			? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines | ||||||
| @@ -662,7 +670,7 @@ void TMS9918::latch_horizontal_counter() { | |||||||
| 	latched_column_ = write_pointer_.column; | 	latched_column_ = write_pointer_.column; | ||||||
| } | } | ||||||
|  |  | ||||||
| uint8_t TMS9918::get_register(int address) { | uint8_t TMS9918::read(int address) { | ||||||
| 	write_phase_ = false; | 	write_phase_ = false; | ||||||
|  |  | ||||||
| 	// Reads from address 0 read video RAM, via the read-ahead buffer. | 	// Reads from address 0 read video RAM, via the read-ahead buffer. | ||||||
| @@ -821,8 +829,8 @@ void Base::draw_tms_character(int start, int end) { | |||||||
| 		int sprite_collision = 0; | 		int sprite_collision = 0; | ||||||
| 		memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); | 		memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); | ||||||
|  |  | ||||||
| 		static const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; | 		constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; | ||||||
| 		static const int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; | 		constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; | ||||||
|  |  | ||||||
| 		// Draw all sprites into the sprite buffer. | 		// Draw all sprites into the sprite buffer. | ||||||
| 		const int shifter_target = sprites_16x16_ ? 32 : 16; | 		const int shifter_target = sprites_16x16_ ? 32 : 16; | ||||||
|   | |||||||
| @@ -41,8 +41,11 @@ class TMS9918: public Base { | |||||||
| 		/*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */ | 		/*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */ | ||||||
| 		void set_tv_standard(TVStandard standard); | 		void set_tv_standard(TVStandard standard); | ||||||
|  |  | ||||||
| 		/*! Provides the CRT this TMS is connected to. */ | 		/*! Sets the scan target this TMS will post content to. */ | ||||||
| 		Outputs::CRT::CRT *get_crt(); | 		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 | 			Runs the VCP for the number of cycles indicate; it is an implicit assumption of the code | ||||||
| @@ -51,10 +54,10 @@ class TMS9918: public Base { | |||||||
| 		void run_for(const HalfCycles cycles); | 		void run_for(const HalfCycles cycles); | ||||||
|  |  | ||||||
| 		/*! Sets a register value. */ | 		/*! Sets a register value. */ | ||||||
| 		void set_register(int address, uint8_t value); | 		void write(int address, uint8_t value); | ||||||
|  |  | ||||||
| 		/*! Gets a register value. */ | 		/*! Gets a register value. */ | ||||||
| 		uint8_t get_register(int address); | 		uint8_t read(int address); | ||||||
|  |  | ||||||
| 		/*! Gets the current scan line; provided by the Master System only. */ | 		/*! Gets the current scan line; provided by the Master System only. */ | ||||||
| 		uint8_t get_current_line(); | 		uint8_t get_current_line(); | ||||||
| @@ -66,8 +69,8 @@ class TMS9918: public Base { | |||||||
| 		void latch_horizontal_counter(); | 		void latch_horizontal_counter(); | ||||||
|  |  | ||||||
| 		/*! | 		/*! | ||||||
| 			Returns the amount of time until get_interrupt_line would next return true if | 			Returns the amount of time until @c get_interrupt_line would next return true if | ||||||
| 			there are no interceding calls to set_register or get_register. | 			there are no interceding calls to @c write or to @c read. | ||||||
|  |  | ||||||
| 			If get_interrupt_line is true now, returns zero. If get_interrupt_line would | 			If get_interrupt_line is true now, returns zero. If get_interrupt_line would | ||||||
| 			never return true, returns -1. | 			never return true, returns -1. | ||||||
|   | |||||||
| @@ -78,8 +78,8 @@ class Base { | |||||||
|  |  | ||||||
| 		Base(Personality p); | 		Base(Personality p); | ||||||
|  |  | ||||||
| 		Personality personality_; | 		const Personality personality_; | ||||||
| 		std::unique_ptr<Outputs::CRT::CRT> crt_; | 		Outputs::CRT::CRT crt_; | ||||||
| 		TVStandard tv_standard_ = TVStandard::NTSC; | 		TVStandard tv_standard_ = TVStandard::NTSC; | ||||||
|  |  | ||||||
| 		// Holds the contents of this VDP's connected DRAM. | 		// Holds the contents of this VDP's connected DRAM. | ||||||
| @@ -100,7 +100,7 @@ class Base { | |||||||
| 			// (though, in practice, it won't happen until the next | 			// (though, in practice, it won't happen until the next | ||||||
| 			// external slot after this number of cycles after the | 			// external slot after this number of cycles after the | ||||||
| 			// device has requested the read or write). | 			// device has requested the read or write). | ||||||
| 			return 7; | 			return 6; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Holds the main status register. | 		// Holds the main status register. | ||||||
|   | |||||||
| @@ -12,45 +12,56 @@ | |||||||
|  |  | ||||||
| using namespace GI::AY38910; | using namespace GI::AY38910; | ||||||
|  |  | ||||||
| AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) { | AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) { | ||||||
| 	// set up envelope lookup tables | 	// Don't use the low bit of the envelope position if this is an AY. | ||||||
|  | 	envelope_position_mask_ |= personality == Personality::AY38910; | ||||||
|  |  | ||||||
|  | 	// Set up envelope lookup tables. | ||||||
| 	for(int c = 0; c < 16; c++) { | 	for(int c = 0; c < 16; c++) { | ||||||
| 		for(int p = 0; p < 32; p++) { | 		for(int p = 0; p < 64; p++) { | ||||||
| 			switch(c) { | 			switch(c) { | ||||||
| 				case 0: case 1: case 2: case 3: case 9: | 				case 0: case 1: case 2: case 3: case 9: | ||||||
| 					envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0; | 					/* Envelope: \____ */ | ||||||
| 					envelope_overflow_masks_[c] = 0x1f; | 					envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0; | ||||||
|  | 					envelope_overflow_masks_[c] = 0x3f; | ||||||
| 				break; | 				break; | ||||||
| 				case 4: case 5: case 6: case 7: case 15: | 				case 4: case 5: case 6: case 7: case 15: | ||||||
| 					envelope_shapes_[c][p] = (p < 16) ? p : 0; | 					/* Envelope: /____ */ | ||||||
| 					envelope_overflow_masks_[c] = 0x1f; | 					envelope_shapes_[c][p] = (p < 32) ? p : 0; | ||||||
|  | 					envelope_overflow_masks_[c] = 0x3f; | ||||||
| 				break; | 				break; | ||||||
|  |  | ||||||
| 				case 8: | 				case 8: | ||||||
| 					envelope_shapes_[c][p] = (p & 0xf) ^ 0xf; | 					/* Envelope: \\\\\\\\ */ | ||||||
|  | 					envelope_shapes_[c][p] = (p & 0x1f) ^ 0x1f; | ||||||
| 					envelope_overflow_masks_[c] = 0x00; | 					envelope_overflow_masks_[c] = 0x00; | ||||||
| 				break; | 				break; | ||||||
| 				case 12: | 				case 12: | ||||||
| 					envelope_shapes_[c][p] = (p & 0xf); | 					/* Envelope: //////// */ | ||||||
|  | 					envelope_shapes_[c][p] = (p & 0x1f); | ||||||
| 					envelope_overflow_masks_[c] = 0x00; | 					envelope_overflow_masks_[c] = 0x00; | ||||||
| 				break; | 				break; | ||||||
|  |  | ||||||
| 				case 10: | 				case 10: | ||||||
| 					envelope_shapes_[c][p] = (p & 0xf) ^ ((p < 16) ? 0xf : 0x0); | 					/* Envelope: \/\/\/\/ */ | ||||||
|  | 					envelope_shapes_[c][p] = (p & 0x1f) ^ ((p < 32) ? 0x1f : 0x0); | ||||||
| 					envelope_overflow_masks_[c] = 0x00; | 					envelope_overflow_masks_[c] = 0x00; | ||||||
| 				break; | 				break; | ||||||
| 				case 14: | 				case 14: | ||||||
| 					envelope_shapes_[c][p] = (p & 0xf) ^ ((p < 16) ? 0x0 : 0xf); | 					/* Envelope: /\/\/\/\ */ | ||||||
|  | 					envelope_shapes_[c][p] = (p & 0x1f) ^ ((p < 32) ? 0x0 : 0x1f); | ||||||
| 					envelope_overflow_masks_[c] = 0x00; | 					envelope_overflow_masks_[c] = 0x00; | ||||||
| 				break; | 				break; | ||||||
|  |  | ||||||
| 				case 11: | 				case 11: | ||||||
| 					envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0xf; | 					/* Envelope: \------	(if - is high) */ | ||||||
| 					envelope_overflow_masks_[c] = 0x1f; | 					envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0x1f; | ||||||
|  | 					envelope_overflow_masks_[c] = 0x3f; | ||||||
| 				break; | 				break; | ||||||
| 				case 13: | 				case 13: | ||||||
| 					envelope_shapes_[c][p] = (p < 16) ? p : 0xf; | 					/* Envelope: /------- */ | ||||||
| 					envelope_overflow_masks_[c] = 0x1f; | 					envelope_shapes_[c][p] = (p < 32) ? p : 0x1f; | ||||||
|  | 					envelope_overflow_masks_[c] = 0x3f; | ||||||
| 				break; | 				break; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -61,18 +72,27 @@ AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_ | |||||||
|  |  | ||||||
| void AY38910::set_sample_volume_range(std::int16_t range) { | void AY38910::set_sample_volume_range(std::int16_t range) { | ||||||
| 	// set up volume lookup table | 	// set up volume lookup table | ||||||
| 	const float max_volume = static_cast<float>(range) / 3.0f;	// As there are three channels. | 	const float max_volume = float(range) / 3.0f;	// As there are three channels. | ||||||
| 	const float root_two = sqrtf(2.0f); | 	constexpr float root_two = 1.414213562373095f; | ||||||
| 	for(int v = 0; v < 16; v++) { | 	for(int v = 0; v < 32; v++) { | ||||||
| 		volumes_[v] = static_cast<int>(max_volume / powf(root_two, static_cast<float>(v ^ 0xf))); | 		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(); | 	evaluate_output_volume(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { | 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; | 	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_; | 		target[c] = output_volume_; | ||||||
| 		master_divider_++; | 		master_divider_++; | ||||||
| 		c++; | 		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]--;\ | 	if(tone_counters_[c]) tone_counters_[c]--;\ | ||||||
| 	else {\ | 	else {\ | ||||||
| 		tone_outputs_[c] ^= 1;\ | 		tone_outputs_[c] ^= 1;\ | ||||||
| 		tone_counters_[c] = tone_periods_[c];\ | 		tone_counters_[c] = tone_periods_[c] << 1;\ | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 		// update the tone channels | 		// Update the tone channels. | ||||||
| 		step_channel(0); | 		step_channel(0); | ||||||
| 		step_channel(1); | 		step_channel(1); | ||||||
| 		step_channel(2); | 		step_channel(2); | ||||||
|  |  | ||||||
| #undef step_channel | #undef step_channel | ||||||
|  |  | ||||||
| 		// ... the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting | 		// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting | ||||||
| 		// it into the official 17 upon divider underflow. | 		// it into the official 17 upon divider underflow. | ||||||
| 		if(noise_counter_) noise_counter_--; | 		if(noise_counter_) noise_counter_--; | ||||||
| 		else { | 		else { | ||||||
| 			noise_counter_ = noise_period_; | 			noise_counter_ = noise_period_ << 1;	// To cover the double resolution of envelopes. | ||||||
| 			noise_output_ ^= noise_shift_register_&1; | 			noise_output_ ^= noise_shift_register_&1; | ||||||
| 			noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17; | 			noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17; | ||||||
| 			noise_shift_register_ >>= 1; | 			noise_shift_register_ >>= 1; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// ... and the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of | 		// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of | ||||||
| 		// implementing non-repeating patterns by locking them to table position 0x1f. | 		// implementing non-repeating patterns by locking them to the final table position. | ||||||
| 		if(envelope_divider_) envelope_divider_--; | 		if(envelope_divider_) envelope_divider_--; | ||||||
| 		else { | 		else { | ||||||
| 			envelope_divider_ = envelope_period_; | 			envelope_divider_ = envelope_period_; | ||||||
| 			envelope_position_ ++; | 			envelope_position_ ++; | ||||||
| 			if(envelope_position_ == 32) envelope_position_ = envelope_overflow_masks_[output_registers_[13]]; | 			if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]]; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		evaluate_output_volume(); | 		evaluate_output_volume(); | ||||||
|  |  | ||||||
| 		for(int ic = 0; ic < 8 && c < number_of_samples; ic++) { | 		for(int ic = 0; ic < 4 && c < number_of_samples; ic++) { | ||||||
| 			target[c] = output_volume_; | 			target[c] = output_volume_; | ||||||
| 			c++; | 			c++; | ||||||
| 			master_divider_++; | 			master_divider_++; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	master_divider_ &= 7; | 	master_divider_ &= 3; | ||||||
| } | } | ||||||
|  |  | ||||||
| void AY38910::evaluate_output_volume() { | 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: | 	// The output level for a channel is: | ||||||
| 	//	1 if neither tone nor noise is enabled; | 	//	1 if neither tone nor noise is enabled; | ||||||
| @@ -142,9 +162,20 @@ void AY38910::evaluate_output_volume() { | |||||||
| 	}; | 	}; | ||||||
| #undef level | #undef level | ||||||
|  |  | ||||||
| 		// Channel volume is a simple selection: if the bit at 0x10 is set, use the envelope volume; otherwise use the lower four bits | 	// This remapping table seeks to map 'channel volumes', i.e. the levels produced from the | ||||||
|  | 	// 16-step progammatic volumes set per channel to 'envelope volumes', i.e. the 32-step | ||||||
|  | 	// volumes that are produced by the envelope generators (on a YM at least). My reading of | ||||||
|  | 	// the data sheet is that '0' is still off, but 15 should be as loud as peak envelope. So | ||||||
|  | 	// I've thrown in the discontinuity at the low end, where it'll be very quiet. | ||||||
|  | 	const int channel_volumes[] = { | ||||||
|  | 		0, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31 | ||||||
|  | 	}; | ||||||
|  | 	static_assert(sizeof(channel_volumes) == 16*sizeof(int)); | ||||||
|  |  | ||||||
|  | 		// Channel volume is a simple selection: if the bit at 0x10 is set, use the envelope volume; otherwise use the lower four bits, | ||||||
|  | 		// mapped to the range 1–31 in case this is a YM. | ||||||
| #define channel_volume(c)	\ | #define channel_volume(c)	\ | ||||||
| 	((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * (output_registers_[c]&0xf) | 	((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * channel_volumes[output_registers_[c]&0xf] | ||||||
|  |  | ||||||
| 	const int volumes[3] = { | 	const int volumes[3] = { | ||||||
| 		channel_volume(8), | 		channel_volume(8), | ||||||
| @@ -173,11 +204,15 @@ void AY38910::select_register(uint8_t r) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void AY38910::set_register_value(uint8_t value) { | void AY38910::set_register_value(uint8_t value) { | ||||||
|  | 	// There are only 16 registers. | ||||||
| 	if(selected_register_ > 15) return; | 	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) { | 	if(selected_register_ < 14) { | ||||||
| 		int selected_register = selected_register_; | 		const int selected_register = selected_register_; | ||||||
| 		task_queue_.defer([=] () { | 		task_queue_.defer([=] () { | ||||||
|  | 			// Perform any register-specific mutation to output generation. | ||||||
| 			uint8_t masked_value = value; | 			uint8_t masked_value = value; | ||||||
| 			switch(selected_register) { | 			switch(selected_register) { | ||||||
| 				case 0: case 2: case 4: | 				case 0: case 2: case 4: | ||||||
| @@ -208,12 +243,34 @@ void AY38910::set_register_value(uint8_t value) { | |||||||
| 					envelope_position_ = 0; | 					envelope_position_ = 0; | ||||||
| 				break; | 				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; | 			output_registers_[selected_register] = masked_value; | ||||||
| 			evaluate_output_volume(); | 			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() { | 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) { | void AY38910::set_port_handler(PortHandler *handler) { | ||||||
| 	port_handler_ = handler; | 	port_handler_ = handler; | ||||||
|  | 	set_port_output(true); | ||||||
|  | 	set_port_output(false); | ||||||
| } | } | ||||||
|  |  | ||||||
| void AY38910::set_data_input(uint8_t r) { | void AY38910::set_data_input(uint8_t r) { | ||||||
| @@ -245,6 +304,16 @@ void AY38910::set_data_input(uint8_t r) { | |||||||
| 	update_bus(); | 	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() { | uint8_t AY38910::get_data_output() { | ||||||
| 	if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) { | 	if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) { | ||||||
| 		// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the | 		// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the | ||||||
|   | |||||||
| @@ -35,9 +35,10 @@ class PortHandler { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/*! | 		/*! | ||||||
| 			Requests the current input on an AY port. | 			Sets the current output on an AY port. | ||||||
|  |  | ||||||
| 			@param port_b @c true if the input being queried is Port B. @c false if it is Port A. | 			@param port_b @c true if the output being posted is Port B. @c false if it is Port A. | ||||||
|  | 			@param value the value now being output. | ||||||
| 		*/ | 		*/ | ||||||
| 		virtual void set_port_output(bool port_b, uint8_t value) {} | 		virtual void set_port_output(bool port_b, uint8_t value) {} | ||||||
| }; | }; | ||||||
| @@ -51,6 +52,13 @@ enum ControlLines { | |||||||
| 	BDIR	= (1 << 2) | 	BDIR	= (1 << 2) | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | enum class Personality { | ||||||
|  | 	/// Provides 16 volume levels to envelopes. | ||||||
|  | 	AY38910, | ||||||
|  | 	/// Provides 32 volume levels to envelopes. | ||||||
|  | 	YM2149F | ||||||
|  | }; | ||||||
|  |  | ||||||
| /*! | /*! | ||||||
| 	Provides emulation of an AY-3-8910 / YM2149, which is a three-channel sound chip with a | 	Provides emulation of an AY-3-8910 / YM2149, which is a three-channel sound chip with a | ||||||
| 	noise generator and a volume envelope generator, which also provides two bidirectional | 	noise generator and a volume envelope generator, which also provides two bidirectional | ||||||
| @@ -59,7 +67,7 @@ enum ControlLines { | |||||||
| class AY38910: public ::Outputs::Speaker::SampleSource { | class AY38910: public ::Outputs::Speaker::SampleSource { | ||||||
| 	public: | 	public: | ||||||
| 		/// Creates a new AY38910. | 		/// Creates a new AY38910. | ||||||
| 		AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue); | 		AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &); | ||||||
|  |  | ||||||
| 		/// Sets the value the AY would read from its data lines if it were not outputting. | 		/// Sets the value the AY would read from its data lines if it were not outputting. | ||||||
| 		void set_data_input(uint8_t r); | 		void set_data_input(uint8_t r); | ||||||
| @@ -83,7 +91,7 @@ class AY38910: public ::Outputs::Speaker::SampleSource { | |||||||
| 		*/ | 		*/ | ||||||
| 		void set_port_handler(PortHandler *); | 		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); | 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||||
| 		bool is_zero_level(); | 		bool is_zero_level(); | ||||||
| 		void set_sample_volume_range(std::int16_t range); | 		void set_sample_volume_range(std::int16_t range); | ||||||
| @@ -108,11 +116,11 @@ class AY38910: public ::Outputs::Speaker::SampleSource { | |||||||
|  |  | ||||||
| 		int envelope_period_ = 0; | 		int envelope_period_ = 0; | ||||||
| 		int envelope_divider_ = 0; | 		int envelope_divider_ = 0; | ||||||
| 		int envelope_position_ = 0; | 		int envelope_position_ = 0, envelope_position_mask_ = 0; | ||||||
| 		int envelope_shapes_[16][32]; | 		int envelope_shapes_[16][64]; | ||||||
| 		int envelope_overflow_masks_[16]; | 		int envelope_overflow_masks_[16]; | ||||||
|  |  | ||||||
| 		int volumes_[16]; | 		int volumes_[32]; | ||||||
|  |  | ||||||
| 		enum ControlState { | 		enum ControlState { | ||||||
| 			Inactive, | 			Inactive, | ||||||
| @@ -128,10 +136,11 @@ class AY38910: public ::Outputs::Speaker::SampleSource { | |||||||
| 		uint8_t data_input_, data_output_; | 		uint8_t data_input_, data_output_; | ||||||
|  |  | ||||||
| 		int16_t output_volume_; | 		int16_t output_volume_; | ||||||
| 		inline void evaluate_output_volume(); | 		void evaluate_output_volume(); | ||||||
|  |  | ||||||
| 		inline void update_bus(); | 		void update_bus(); | ||||||
| 		PortHandler *port_handler_ = nullptr; | 		PortHandler *port_handler_ = nullptr; | ||||||
|  | 		void set_port_output(bool port_b); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ namespace  { | |||||||
| DiskII::DiskII(int clock_rate) : | DiskII::DiskII(int clock_rate) : | ||||||
| 	clock_rate_(clock_rate), | 	clock_rate_(clock_rate), | ||||||
| 	inputs_(input_command), | 	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_[0].set_clocking_hint_observer(this); | ||||||
| 	drives_[1].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) { | void DiskII::run_for(const Cycles cycles) { | ||||||
| 	if(preferred_clocking() == ClockingHint::Preference::None) return; | 	if(preferred_clocking() == ClockingHint::Preference::None) return; | ||||||
|  |  | ||||||
| 	int integer_cycles = cycles.as_int(); | 	auto integer_cycles = cycles.as_integral(); | ||||||
| 	while(integer_cycles--) { | 	while(integer_cycles--) { | ||||||
| 		const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6); | 		const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6); | ||||||
| 		if(flux_duration_) { | 		if(flux_duration_) { | ||||||
| @@ -124,7 +124,7 @@ void DiskII::run_for(const Cycles cycles) { | |||||||
| 	// motor switch being flipped and the drive motor actually switching off. | 	// motor switch being flipped and the drive motor actually switching off. | ||||||
| 	// This models that, accepting overrun as a risk. | 	// This models that, accepting overrun as a risk. | ||||||
| 	if(motor_off_time_ >= 0) { | 	if(motor_off_time_ >= 0) { | ||||||
| 		motor_off_time_ -= cycles.as_int(); | 		motor_off_time_ -= cycles.as_integral(); | ||||||
| 		if(motor_off_time_ < 0) { | 		if(motor_off_time_ < 0) { | ||||||
| 			set_control(Control::Motor, false); | 			set_control(Control::Motor, false); | ||||||
| 		} | 		} | ||||||
| @@ -211,7 +211,7 @@ void DiskII::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int driv | |||||||
| 	drives_[drive].set_disk(disk); | 	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) { | 	if(event.type == Storage::Disk::Track::Event::FluxTransition) { | ||||||
| 		inputs_ &= ~input_flux; | 		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. | 		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; | 		break; | ||||||
| 		case 0xf: | 		case 0xf: | ||||||
| 			if(!(inputs_ & input_mode)) | 			if(!(inputs_ & input_mode)) | ||||||
| 				drives_[active_drive_].begin_writing(Storage::Time(1, clock_rate_), false); | 				drives_[active_drive_].begin_writing(Storage::Time(1, int(clock_rate_)), false); | ||||||
| 			inputs_ |= input_mode; | 			inputs_ |= input_mode; | ||||||
| 		break; | 		break; | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ namespace Apple { | |||||||
| /*! | /*! | ||||||
| 	Provides an emulation of the Apple Disk II. | 	Provides an emulation of the Apple Disk II. | ||||||
| */ | */ | ||||||
| class DiskII: | class DiskII final: | ||||||
| 	public Storage::Disk::Drive::EventDelegate, | 	public Storage::Disk::Drive::EventDelegate, | ||||||
| 	public ClockingHint::Source, | 	public ClockingHint::Source, | ||||||
| 	public ClockingHint::Observer { | 	public ClockingHint::Observer { | ||||||
| @@ -48,7 +48,7 @@ class DiskII: | |||||||
| 			The value returned by @c read_address if accessing that address | 			The value returned by @c read_address if accessing that address | ||||||
| 			didn't cause the disk II to place anything onto the bus. | 			didn't cause the disk II to place anything onto the bus. | ||||||
| 		*/ | 		*/ | ||||||
| 		const int DidNotLoad = -1; | 		static constexpr int DidNotLoad = -1; | ||||||
|  |  | ||||||
| 		/// Advances the controller by @c cycles. | 		/// Advances the controller by @c cycles. | ||||||
| 		void run_for(const Cycles cycles); | 		void run_for(const Cycles cycles); | ||||||
| @@ -76,7 +76,7 @@ class DiskII: | |||||||
| 		void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive); | 		void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive); | ||||||
|  |  | ||||||
| 		// As per Sleeper. | 		// As per Sleeper. | ||||||
| 		ClockingHint::Preference preferred_clocking() override; | 		ClockingHint::Preference preferred_clocking() final; | ||||||
|  |  | ||||||
| 		// The Disk II functions as a potential target for @c Activity::Sources. | 		// The Disk II functions as a potential target for @c Activity::Sources. | ||||||
| 		void set_activity_observer(Activity::Observer *observer); | 		void set_activity_observer(Activity::Observer *observer); | ||||||
| @@ -98,10 +98,10 @@ class DiskII: | |||||||
| 		void select_drive(int drive); | 		void select_drive(int drive); | ||||||
|  |  | ||||||
| 		uint8_t trigger_address(int address, uint8_t value); | 		uint8_t trigger_address(int address, uint8_t value); | ||||||
| 		void process_event(const Storage::Disk::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; | 		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 state_ = 0; | ||||||
| 		uint8_t inputs_ = 0; | 		uint8_t inputs_ = 0; | ||||||
| @@ -109,7 +109,7 @@ class DiskII: | |||||||
|  |  | ||||||
| 		int stepper_mask_ = 0; | 		int stepper_mask_ = 0; | ||||||
| 		int stepper_position_ = 0; | 		int stepper_position_ = 0; | ||||||
| 		int motor_off_time_ = -1; | 		Cycles::IntType motor_off_time_ = -1; | ||||||
|  |  | ||||||
| 		bool is_write_protected(); | 		bool is_write_protected(); | ||||||
| 		std::array<uint8_t, 256> state_machine_; | 		std::array<uint8_t, 256> state_machine_; | ||||||
|   | |||||||
							
								
								
									
										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(); | 	evaluate_output_volume(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void SN76489::set_register(uint8_t value) { | void SN76489::write(uint8_t value) { | ||||||
| 	task_queue_.defer([value, this] () { | 	task_queue_.defer([value, this] () { | ||||||
| 		if(value & 0x80) { | 		if(value & 0x80) { | ||||||
| 			active_register_ = value; | 			active_register_ = value; | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ class SN76489: public Outputs::Speaker::SampleSource { | |||||||
| 		SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1); | 		SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1); | ||||||
|  |  | ||||||
| 		/// Writes a new value to the SN76489. | 		/// Writes a new value to the SN76489. | ||||||
| 		void set_register(uint8_t value); | 		void write(uint8_t value); | ||||||
|  |  | ||||||
| 		// As per SampleSource. | 		// As per SampleSource. | ||||||
| 		void get_samples(std::size_t number_of_samples, std::int16_t *target); | 		void get_samples(std::size_t number_of_samples, std::int16_t *target); | ||||||
|   | |||||||
							
								
								
									
										140
									
								
								Components/Serial/Line.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								Components/Serial/Line.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | // | ||||||
|  | //  SerialPort.cpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 12/10/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #include "Line.hpp" | ||||||
|  |  | ||||||
|  | using namespace Serial; | ||||||
|  |  | ||||||
|  | void Line::set_writer_clock_rate(HalfCycles clock_rate) { | ||||||
|  | 	clock_rate_ = clock_rate; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Line::advance_writer(HalfCycles cycles) { | ||||||
|  | 	if(cycles == HalfCycles(0)) return; | ||||||
|  |  | ||||||
|  | 	const auto integral_cycles = cycles.as_integral(); | ||||||
|  | 	remaining_delays_ = std::max(remaining_delays_ - integral_cycles, Cycles::IntType(0)); | ||||||
|  | 	if(events_.empty()) { | ||||||
|  | 		write_cycles_since_delegate_call_ += integral_cycles; | ||||||
|  | 		if(transmission_extra_) { | ||||||
|  | 			transmission_extra_ -= integral_cycles; | ||||||
|  | 			if(transmission_extra_ <= 0) { | ||||||
|  | 				transmission_extra_ = 0; | ||||||
|  | 				update_delegate(level_); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		while(!events_.empty()) { | ||||||
|  | 			if(events_.front().delay <= integral_cycles) { | ||||||
|  | 				cycles -= events_.front().delay; | ||||||
|  | 				write_cycles_since_delegate_call_ += events_.front().delay; | ||||||
|  | 				const auto old_level = level_; | ||||||
|  |  | ||||||
|  | 				auto iterator = events_.begin() + 1; | ||||||
|  | 				while(iterator != events_.end() && iterator->type != Event::Delay) { | ||||||
|  | 					level_ = iterator->type == Event::SetHigh; | ||||||
|  | 					++iterator; | ||||||
|  | 				} | ||||||
|  | 				events_.erase(events_.begin(), iterator); | ||||||
|  |  | ||||||
|  | 				if(old_level != level_) { | ||||||
|  | 					update_delegate(old_level); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// Book enough extra time for the read delegate to be posted | ||||||
|  | 				// the final bit if one is attached. | ||||||
|  | 				if(events_.empty()) { | ||||||
|  | 					transmission_extra_ = minimum_write_cycles_for_read_delegate_bit(); | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				events_.front().delay -= integral_cycles; | ||||||
|  | 				write_cycles_since_delegate_call_ += integral_cycles; | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Line::write(bool level) { | ||||||
|  | 	if(!events_.empty()) { | ||||||
|  | 		events_.emplace_back(); | ||||||
|  | 		events_.back().type = level ? Event::SetHigh : Event::SetLow; | ||||||
|  | 	} else { | ||||||
|  | 		level_ = level; | ||||||
|  | 		transmission_extra_ = minimum_write_cycles_for_read_delegate_bit(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Line::write(HalfCycles cycles, int count, int levels) { | ||||||
|  | 	remaining_delays_ += count * cycles.as_integral(); | ||||||
|  |  | ||||||
|  | 	auto event = events_.size(); | ||||||
|  | 	events_.resize(events_.size() + size_t(count)*2); | ||||||
|  | 	while(count--) { | ||||||
|  | 		events_[event].type = Event::Delay; | ||||||
|  | 		events_[event].delay = int(cycles.as_integral()); | ||||||
|  | 		events_[event+1].type = (levels&1) ? Event::SetHigh : Event::SetLow; | ||||||
|  | 		levels >>= 1; | ||||||
|  | 		event += 2; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Line::reset_writing() { | ||||||
|  | 	remaining_delays_ = 0; | ||||||
|  | 	events_.clear(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Line::read() { | ||||||
|  | 	return level_; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Line::set_read_delegate(ReadDelegate *delegate, Storage::Time bit_length) { | ||||||
|  | 	read_delegate_ = delegate; | ||||||
|  | 	read_delegate_bit_length_ = bit_length; | ||||||
|  | 	read_delegate_bit_length_.simplify(); | ||||||
|  | 	write_cycles_since_delegate_call_ = 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Line::update_delegate(bool level) { | ||||||
|  | 	// Exit early if there's no delegate, or if the delegate is waiting for | ||||||
|  | 	// zero and this isn't zero. | ||||||
|  | 	if(!read_delegate_) return; | ||||||
|  |  | ||||||
|  | 	const int cycles_to_forward = write_cycles_since_delegate_call_; | ||||||
|  | 	write_cycles_since_delegate_call_ = 0; | ||||||
|  | 	if(level && read_delegate_phase_ == ReadDelegatePhase::WaitingForZero) return; | ||||||
|  |  | ||||||
|  | 	// Deal with a transition out of waiting-for-zero mode by seeding time left | ||||||
|  | 	// in bit at half a bit. | ||||||
|  | 	if(read_delegate_phase_ == ReadDelegatePhase::WaitingForZero) { | ||||||
|  | 		time_left_in_bit_ = read_delegate_bit_length_; | ||||||
|  | 		time_left_in_bit_.clock_rate <<= 1; | ||||||
|  | 		read_delegate_phase_ = ReadDelegatePhase::Serialising; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Forward as many bits as occur. | ||||||
|  | 	Storage::Time time_left(cycles_to_forward, int(clock_rate_.as_integral())); | ||||||
|  | 	const int bit = level ? 1 : 0; | ||||||
|  | 	int bits = 0; | ||||||
|  | 	while(time_left >= time_left_in_bit_) { | ||||||
|  | 		++bits; | ||||||
|  | 		if(!read_delegate_->serial_line_did_produce_bit(this, bit)) { | ||||||
|  | 			read_delegate_phase_ = ReadDelegatePhase::WaitingForZero; | ||||||
|  | 			if(bit) return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		time_left -= time_left_in_bit_; | ||||||
|  | 		time_left_in_bit_ = read_delegate_bit_length_; | ||||||
|  | 	} | ||||||
|  | 	time_left_in_bit_ -= time_left; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Cycles::IntType Line::minimum_write_cycles_for_read_delegate_bit() { | ||||||
|  | 	if(!read_delegate_) return 0; | ||||||
|  | 	return 1 + (read_delegate_bit_length_ * static_cast<unsigned int>(clock_rate_.as_integral())).get<int>(); | ||||||
|  | } | ||||||
							
								
								
									
										112
									
								
								Components/Serial/Line.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Components/Serial/Line.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | // | ||||||
|  | //  SerialPort.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 12/10/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef SerialPort_hpp | ||||||
|  | #define SerialPort_hpp | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  | #include "../../Storage/Storage.hpp" | ||||||
|  | #include "../../ClockReceiver/ClockReceiver.hpp" | ||||||
|  | #include "../../ClockReceiver/ForceInline.hpp" | ||||||
|  |  | ||||||
|  | namespace Serial { | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	@c Line connects a single reader and a single writer, allowing timestamped events to be | ||||||
|  | 	published and consumed, potentially with a clock conversion in between. It allows line | ||||||
|  | 	levels to be written and read in larger collections. | ||||||
|  |  | ||||||
|  | 	It is assumed that the owner of the reader and writer will ensure that the reader will never | ||||||
|  | 	get ahead of the writer. If the writer posts events behind the reader they will simply be | ||||||
|  | 	given instanteous effect. | ||||||
|  | */ | ||||||
|  | class Line { | ||||||
|  | 	public: | ||||||
|  | 		void set_writer_clock_rate(HalfCycles clock_rate); | ||||||
|  |  | ||||||
|  | 		/// Advances the read position by @c cycles relative to the writer's | ||||||
|  | 		/// clock rate. | ||||||
|  | 		void advance_writer(HalfCycles cycles); | ||||||
|  |  | ||||||
|  | 		/// Sets the line to @c level. | ||||||
|  | 		void write(bool level); | ||||||
|  |  | ||||||
|  | 		/// Enqueues @c count level changes, the first occurring immediately | ||||||
|  | 		/// after the final event currently posted and each subsequent event | ||||||
|  | 		/// occurring @c cycles after the previous. An additional gap of @c cycles | ||||||
|  | 		/// is scheduled after the final output. The levels to output are | ||||||
|  | 		/// taken from @c levels which is read from lsb to msb. @c cycles is | ||||||
|  | 		/// relative to the writer's clock rate. | ||||||
|  | 		void write(HalfCycles cycles, int count, int levels); | ||||||
|  |  | ||||||
|  | 		/// @returns the number of cycles until currently enqueued write data is exhausted. | ||||||
|  | 		forceinline HalfCycles write_data_time_remaining() const { | ||||||
|  | 			return HalfCycles(remaining_delays_); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// @returns the number of cycles left until it is guaranteed that a passive reader | ||||||
|  | 		/// has received all currently-enqueued bits. | ||||||
|  | 		forceinline HalfCycles transmission_data_time_remaining() const { | ||||||
|  | 			return HalfCycles(remaining_delays_ + transmission_extra_); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// Eliminates all future write states, leaving the output at whatever it is now. | ||||||
|  | 		void reset_writing(); | ||||||
|  |  | ||||||
|  | 		/// @returns The instantaneous level of this line. | ||||||
|  | 		bool read(); | ||||||
|  |  | ||||||
|  | 		struct ReadDelegate { | ||||||
|  | 			virtual bool serial_line_did_produce_bit(Line *line, int bit) = 0; | ||||||
|  | 		}; | ||||||
|  | 		/*! | ||||||
|  | 			Sets a read delegate, which will receive samples of the output level every | ||||||
|  | 			@c bit_lengths of a second apart subject to a state machine: | ||||||
|  |  | ||||||
|  | 				* initially no bits will be delivered; | ||||||
|  | 				* when a zero level is first detected, the line will wait half a bit's length, then start | ||||||
|  | 				sampling at single-bit intervals, passing each bit to the delegate while it returns @c true; | ||||||
|  | 				* as soon as the delegate returns @c false, the line will return to the initial state. | ||||||
|  | 		*/ | ||||||
|  | 		void set_read_delegate(ReadDelegate *delegate, Storage::Time bit_length); | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		struct Event { | ||||||
|  | 			enum Type { | ||||||
|  | 				Delay, SetHigh, SetLow | ||||||
|  | 			} type; | ||||||
|  | 			int delay; | ||||||
|  | 		}; | ||||||
|  | 		std::vector<Event> events_; | ||||||
|  | 		HalfCycles::IntType remaining_delays_ = 0; | ||||||
|  | 		HalfCycles::IntType transmission_extra_ = 0; | ||||||
|  | 		bool level_ = true; | ||||||
|  | 		HalfCycles clock_rate_ = 0; | ||||||
|  |  | ||||||
|  | 		ReadDelegate *read_delegate_ = nullptr; | ||||||
|  | 		Storage::Time read_delegate_bit_length_, time_left_in_bit_; | ||||||
|  | 		int write_cycles_since_delegate_call_ = 0; | ||||||
|  | 		enum class ReadDelegatePhase { | ||||||
|  | 			WaitingForZero, | ||||||
|  | 			Serialising | ||||||
|  | 		} read_delegate_phase_ = ReadDelegatePhase::WaitingForZero; | ||||||
|  |  | ||||||
|  | 		void update_delegate(bool level); | ||||||
|  | 		HalfCycles::IntType minimum_write_cycles_for_read_delegate_bit(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	Defines an RS-232-esque srial port. | ||||||
|  | */ | ||||||
|  | class Port { | ||||||
|  | 	public: | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* SerialPort_hpp */ | ||||||
| @@ -18,7 +18,7 @@ AsyncTaskQueue::AsyncTaskQueue() | |||||||
| #ifdef __APPLE__ | #ifdef __APPLE__ | ||||||
| 	serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL); | 	serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL); | ||||||
| #else | #else | ||||||
| 	thread_.reset(new std::thread([this]() { | 	thread_ = std::make_unique<std::thread>([this]() { | ||||||
| 		while(!should_destruct_) { | 		while(!should_destruct_) { | ||||||
| 			std::function<void(void)> next_function; | 			std::function<void(void)> next_function; | ||||||
|  |  | ||||||
| @@ -39,7 +39,7 @@ AsyncTaskQueue::AsyncTaskQueue() | |||||||
| 				processing_condition_.wait(lock); | 				processing_condition_.wait(lock); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	})); | 	}); | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -70,8 +70,8 @@ void AsyncTaskQueue::flush() { | |||||||
| #ifdef __APPLE__ | #ifdef __APPLE__ | ||||||
| 	dispatch_sync(serial_dispatch_queue_, ^{}); | 	dispatch_sync(serial_dispatch_queue_, ^{}); | ||||||
| #else | #else | ||||||
| 	std::shared_ptr<std::mutex> flush_mutex(new std::mutex); | 	auto flush_mutex = std::make_shared<std::mutex>(); | ||||||
| 	std::shared_ptr<std::condition_variable> flush_condition(new std::condition_variable); | 	auto flush_condition = std::make_shared<std::condition_variable>(); | ||||||
| 	std::unique_lock<std::mutex> lock(*flush_mutex); | 	std::unique_lock<std::mutex> lock(*flush_mutex); | ||||||
| 	enqueue([=] () { | 	enqueue([=] () { | ||||||
| 		std::unique_lock<std::mutex> inner_lock(*flush_mutex); | 		std::unique_lock<std::mutex> inner_lock(*flush_mutex); | ||||||
| @@ -88,7 +88,7 @@ DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() { | |||||||
|  |  | ||||||
| void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) { | void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) { | ||||||
| 	if(!deferred_tasks_) { | 	if(!deferred_tasks_) { | ||||||
| 		deferred_tasks_.reset(new std::list<std::function<void(void)>>); | 		deferred_tasks_ = std::make_shared<std::list<std::function<void(void)>>>(); | ||||||
| 	} | 	} | ||||||
| 	deferred_tasks_->push_back(function); | 	deferred_tasks_->push_back(function); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,16 +14,16 @@ namespace { | |||||||
| 	Appends a Boolean selection of @c selection for option @c name to @c selection_set. | 	Appends a Boolean selection of @c selection for option @c name to @c selection_set. | ||||||
| */ | */ | ||||||
| void append_bool(Configurable::SelectionSet &selection_set, const std::string &name, bool selection) { | void append_bool(Configurable::SelectionSet &selection_set, const std::string &name, bool selection) { | ||||||
| 	selection_set[name] = std::unique_ptr<Configurable::Selection>(new Configurable::BooleanSelection(selection)); | 	selection_set[name] = std::make_unique<Configurable::BooleanSelection>(selection); | ||||||
| } | } | ||||||
|  |  | ||||||
| /*! | /*! | ||||||
| 	Enquires for a Boolean selection for option @c name from @c selections_by_option, storing it to @c result if found. | 	Enquires for a Boolean selection for option @c name from @c selections_by_option, storing it to @c result if found. | ||||||
| */ | */ | ||||||
| bool get_bool(const Configurable::SelectionSet &selections_by_option, const std::string &name, bool &result) { | bool get_bool(const Configurable::SelectionSet &selections_by_option, const std::string &name, bool &result) { | ||||||
| 	auto quickload = Configurable::selection<Configurable::BooleanSelection>(selections_by_option, "quickload"); | 	auto selection = Configurable::selection<Configurable::BooleanSelection>(selections_by_option, name); | ||||||
| 	if(!quickload) return false; | 	if(!selection) return false; | ||||||
| 	result = quickload->value; | 	result = selection->value; | ||||||
| 	return true; | 	return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -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>> Configurable::standard_options(Configurable::StandardOptions mask) { | ||||||
| 	std::vector<std::unique_ptr<Configurable::Option>> options; | 	std::vector<std::unique_ptr<Configurable::Option>> options; | ||||||
| 	if(mask & QuickLoadTape)				options.emplace_back(new Configurable::BooleanOption("Load Tapes Quickly", "quickload")); | 	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; | 		std::vector<std::string> display_options; | ||||||
| 		if(mask & DisplayComposite)	display_options.emplace_back("composite"); | 		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 & DisplaySVideo)				display_options.emplace_back("svideo"); | ||||||
| 		if(mask & DisplayRGB)					display_options.emplace_back("rgb"); | 		if(mask & DisplayRGB)					display_options.emplace_back("rgb"); | ||||||
| 		options.emplace_back(new Configurable::ListOption("Display", "display", display_options)); | 		options.emplace_back(new Configurable::ListOption("Display", "display", display_options)); | ||||||
| 	} | 	} | ||||||
| 	if(mask & AutomaticTapeMotorControl)	options.emplace_back(new Configurable::BooleanOption("Automatic Tape Motor Control", "autotapemotor")); | 	if(mask & AutomaticTapeMotorControl)	options.emplace_back(new Configurable::BooleanOption("Automatic Tape Motor Control", "autotapemotor")); | ||||||
|  | 	if(mask & QuickBoot)					options.emplace_back(new Configurable::BooleanOption("Boot Quickly", "quickboot")); | ||||||
| 	return options; | 	return options; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -59,9 +61,14 @@ void Configurable::append_display_selection(Configurable::SelectionSet &selectio | |||||||
| 		default: | 		default: | ||||||
| 		case Display::RGB:					string_selection = "rgb";				break; | 		case Display::RGB:					string_selection = "rgb";				break; | ||||||
| 		case Display::SVideo:				string_selection = "svideo";			break; | 		case Display::SVideo:				string_selection = "svideo";			break; | ||||||
| 		case Display::Composite:	string_selection = "composite";	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 | // MARK: - Selection parsers | ||||||
| @@ -85,9 +92,17 @@ bool Configurable::get_display(const Configurable::SelectionSet &selections_by_o | |||||||
| 			return true; | 			return true; | ||||||
| 		} | 		} | ||||||
| 		if(display->value == "composite") { | 		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 true; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return false; | 	return false; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | bool Configurable::get_quick_boot(const Configurable::SelectionSet &selections_by_option, bool &result) { | ||||||
|  | 	return get_bool(selections_by_option, "quickboot", result); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -16,15 +16,18 @@ namespace Configurable { | |||||||
| enum StandardOptions { | enum StandardOptions { | ||||||
| 	DisplayRGB					= (1 << 0), | 	DisplayRGB					= (1 << 0), | ||||||
| 	DisplaySVideo				= (1 << 1), | 	DisplaySVideo				= (1 << 1), | ||||||
| 	DisplayComposite			= (1 << 2), | 	DisplayCompositeColour		= (1 << 2), | ||||||
| 	QuickLoadTape				= (1 << 3), | 	DisplayCompositeMonochrome	= (1 << 3), | ||||||
| 	AutomaticTapeMotorControl	= (1 << 4) | 	QuickLoadTape				= (1 << 4), | ||||||
|  | 	AutomaticTapeMotorControl	= (1 << 5), | ||||||
|  | 	QuickBoot					= (1 << 6), | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum class Display { | enum class Display { | ||||||
| 	RGB, | 	RGB, | ||||||
| 	SVideo, | 	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); | void append_display_selection(SelectionSet &selection_set, Display selection); | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	Appends to @c selection_set a selection of @c selection for QuickBoot. | ||||||
|  | */ | ||||||
|  | void append_quick_boot_selection(SelectionSet &selection_set, bool selection); | ||||||
|  |  | ||||||
| /*! | /*! | ||||||
| 	Attempts to discern a QuickLoadTape selection from @c selections_by_option. | 	Attempts to discern a QuickLoadTape selection from @c selections_by_option. | ||||||
|   |   | ||||||
| @@ -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); | bool get_display(const SelectionSet &selections_by_option, Display &result); | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	Attempts to QuickBoot a QuickLoadTape selection from @c selections_by_option. | ||||||
|  |  | ||||||
|  | 	@param selections_by_option The user selections. | ||||||
|  | 	@param result The location to which the selection will be stored if found. | ||||||
|  | 	@returns @c true if a selection is found; @c false otherwise. | ||||||
|  | */ | ||||||
|  | bool get_quick_boot(const SelectionSet &selections_by_option, bool &result); | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #endif /* StandardOptions_hpp */ | #endif /* StandardOptions_hpp */ | ||||||
|   | |||||||
| @@ -10,13 +10,14 @@ | |||||||
|  |  | ||||||
| using namespace Inputs; | using namespace Inputs; | ||||||
|  |  | ||||||
| Keyboard::Keyboard() { | Keyboard::Keyboard(const std::set<Key> &essential_modifiers) : essential_modifiers_(essential_modifiers) { | ||||||
| 	for(int k = 0; k < int(Key::Help); ++k) { | 	for(int k = 0; k < int(Key::Help); ++k) { | ||||||
| 		observed_keys_.insert(Key(k)); | 		observed_keys_.insert(Key(k)); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| Keyboard::Keyboard(const std::set<Key> &observed_keys) : observed_keys_(observed_keys), is_exclusive_(false) {} | Keyboard::Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers) : | ||||||
|  | 	observed_keys_(observed_keys), essential_modifiers_(essential_modifiers), is_exclusive_(false) {} | ||||||
|  |  | ||||||
| void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) { | void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) { | ||||||
| 	std::size_t key_offset = static_cast<std::size_t>(key); | 	std::size_t key_offset = static_cast<std::size_t>(key); | ||||||
| @@ -28,6 +29,10 @@ void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) { | |||||||
| 	if(delegate_) delegate_->keyboard_did_change_key(this, key, is_pressed); | 	if(delegate_) delegate_->keyboard_did_change_key(this, key, is_pressed); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const std::set<Inputs::Keyboard::Key> &Keyboard::get_essential_modifiers() { | ||||||
|  | 	return essential_modifiers_; | ||||||
|  | } | ||||||
|  |  | ||||||
| void Keyboard::reset_all_keys() { | void Keyboard::reset_all_keys() { | ||||||
| 	std::fill(key_states_.begin(), key_states_.end(), false); | 	std::fill(key_states_.begin(), key_states_.end(), false); | ||||||
| 	if(delegate_) delegate_->reset_all_keys(this); | 	if(delegate_) delegate_->reset_all_keys(this); | ||||||
|   | |||||||
| @@ -6,8 +6,8 @@ | |||||||
| //  Copyright 2017 Thomas Harte. All rights reserved. | //  Copyright 2017 Thomas Harte. All rights reserved. | ||||||
| // | // | ||||||
|  |  | ||||||
| #ifndef Keyboard_hpp | #ifndef Inputs_Keyboard_hpp | ||||||
| #define Keyboard_hpp | #define Inputs_Keyboard_hpp | ||||||
|  |  | ||||||
| #include <vector> | #include <vector> | ||||||
| #include <set> | #include <set> | ||||||
| @@ -23,26 +23,26 @@ class Keyboard { | |||||||
| 	public: | 	public: | ||||||
| 		enum class Key { | 		enum class Key { | ||||||
| 			Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, PrintScreen, ScrollLock, Pause, | 			Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, PrintScreen, ScrollLock, Pause, | ||||||
| 			BackTick, k1, k2, k3, k4, k5, k6, k7, k8, k9, k0, Hyphen, Equals, BackSpace, | 			BackTick, k1, k2, k3, k4, k5, k6, k7, k8, k9, k0, Hyphen, Equals, Backspace, | ||||||
| 			Tab, Q, W, E, R, T, Y, U, I, O, P, OpenSquareBracket, CloseSquareBracket, BackSlash, | 			Tab, Q, W, E, R, T, Y, U, I, O, P, OpenSquareBracket, CloseSquareBracket, Backslash, | ||||||
| 			CapsLock, A, S, D, F, G, H, J, K, L, Semicolon, Quote, Hash, Enter, | 			CapsLock, A, S, D, F, G, H, J, K, L, Semicolon, Quote, Hash, Enter, | ||||||
| 			LeftShift, Z, X, C, V, B, N, M, Comma, FullStop, ForwardSlash, RightShift, | 			LeftShift, Z, X, C, V, B, N, M, Comma, FullStop, ForwardSlash, RightShift, | ||||||
| 			LeftControl, LeftOption, LeftMeta, Space, RightMeta, RightOption, RightControl, | 			LeftControl, LeftOption, LeftMeta, Space, RightMeta, RightOption, RightControl, | ||||||
| 			Left, Right, Up, Down, | 			Left, Right, Up, Down, | ||||||
| 			Insert, Home, PageUp, Delete, End, PageDown, | 			Insert, Home, PageUp, Delete, End, PageDown, | ||||||
| 			NumLock, KeyPadSlash, KeyPadAsterisk, KeyPadDelete, | 			NumLock, KeypadSlash, KeypadAsterisk, KeypadDelete, | ||||||
| 			KeyPad7, KeyPad8, KeyPad9, KeyPadPlus, | 			Keypad7, Keypad8, Keypad9, KeypadPlus, | ||||||
| 			KeyPad4, KeyPad5, KeyPad6, KeyPadMinus, | 			Keypad4, Keypad5, Keypad6, KeypadMinus, | ||||||
| 			KeyPad1, KeyPad2, KeyPad3, KeyPadEnter, | 			Keypad1, Keypad2, Keypad3, KeypadEnter, | ||||||
| 			KeyPad0, KeyPadDecimalPoint, KeyPadEquals, | 			Keypad0, KeypadDecimalPoint, KeypadEquals, | ||||||
| 			Help | 			Help | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| 		/// Constructs a Keyboard that declares itself to observe all keys. | 		/// Constructs a Keyboard that declares itself to observe all keys. | ||||||
| 		Keyboard(); | 		Keyboard(const std::set<Key> &essential_modifiers = {}); | ||||||
|  |  | ||||||
| 		/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys. | 		/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys. | ||||||
| 		Keyboard(const std::set<Key> &observed_keys); | 		Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers); | ||||||
|  |  | ||||||
| 		// Host interface. | 		// Host interface. | ||||||
| 		virtual void set_key_pressed(Key key, char value, bool is_pressed); | 		virtual void set_key_pressed(Key key, char value, bool is_pressed); | ||||||
| @@ -51,10 +51,18 @@ class Keyboard { | |||||||
| 		/// @returns a set of all Keys that this keyboard responds to. | 		/// @returns a set of all Keys that this keyboard responds to. | ||||||
| 		virtual const std::set<Key> &observed_keys(); | 		virtual const std::set<Key> &observed_keys(); | ||||||
|  |  | ||||||
| 		/* | 		/// @returns the list of modifiers that this keyboard considers 'essential' (i.e. both mapped and highly used). | ||||||
|  | 		virtual const std::set<Inputs::Keyboard::Key> &get_essential_modifiers(); | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
| 			@returns @c true if this keyboard, on its original machine, looked | 			@returns @c true if this keyboard, on its original machine, looked | ||||||
| 			like a complete keyboard — i.e. if a user would expect this keyboard | 			like a complete keyboard — i.e. if a user would expect this keyboard | ||||||
| 			to be the only thing a real keyboard maps to. | 			to be the only thing a real keyboard maps to. | ||||||
|  |  | ||||||
|  | 			So this would be true of something like the Amstrad CPC, which has a full | ||||||
|  | 			keyboard, but it would be false of something like the Sega Master System | ||||||
|  | 			which has some buttons that you'd expect an emulator to map to its host | ||||||
|  | 			keyboard but which does not offer a full keyboard. | ||||||
| 		*/ | 		*/ | ||||||
| 		virtual bool is_exclusive(); | 		virtual bool is_exclusive(); | ||||||
|  |  | ||||||
| @@ -68,6 +76,7 @@ class Keyboard { | |||||||
|  |  | ||||||
| 	private: | 	private: | ||||||
| 		std::set<Key> observed_keys_; | 		std::set<Key> observed_keys_; | ||||||
|  | 		std::set<Key> essential_modifiers_; | ||||||
| 		std::vector<bool> key_states_; | 		std::vector<bool> key_states_; | ||||||
| 		Delegate *delegate_ = nullptr; | 		Delegate *delegate_ = nullptr; | ||||||
| 		bool is_exclusive_ = true; | 		bool is_exclusive_ = true; | ||||||
| @@ -75,4 +84,4 @@ class Keyboard { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #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 "../../ClockReceiver/ForceInline.hpp" | ||||||
| #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||||
|  | #include "../../Outputs/CRT/CRT.hpp" | ||||||
|  |  | ||||||
| #include "../../Analyser/Static/AmstradCPC/Target.hpp" | #include "../../Analyser/Static/AmstradCPC/Target.hpp" | ||||||
|  |  | ||||||
| @@ -40,7 +41,7 @@ namespace AmstradCPC { | |||||||
|  |  | ||||||
| std::vector<std::unique_ptr<Configurable::Option>> get_options() { | std::vector<std::unique_ptr<Configurable::Option>> get_options() { | ||||||
| 	return Configurable::standard_options( | 	return Configurable::standard_options( | ||||||
| 		static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayComposite) | 		Configurable::StandardOptions(Configurable::DisplayRGB | Configurable::DisplayCompositeColour) | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -123,7 +124,7 @@ class InterruptTimer { | |||||||
| class AYDeferrer { | class AYDeferrer { | ||||||
| 	public: | 	public: | ||||||
| 		/// Constructs a new AY instance and sets its clock rate. | 		/// Constructs a new AY instance and sets its clock rate. | ||||||
| 		AYDeferrer() : ay_(audio_queue_), speaker_(ay_) { | 		AYDeferrer() : ay_(GI::AY38910::Personality::AY38910, audio_queue_), speaker_(ay_) { | ||||||
| 			speaker_.set_input_rate(1000000); | 			speaker_.set_input_rate(1000000); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -170,11 +171,15 @@ class AYDeferrer { | |||||||
| */ | */ | ||||||
| class CRTCBusHandler { | class CRTCBusHandler { | ||||||
| 	public: | 	public: | ||||||
| 		CRTCBusHandler(uint8_t *ram, InterruptTimer &interrupt_timer) : | 		CRTCBusHandler(const uint8_t *ram, InterruptTimer &interrupt_timer) : | ||||||
|  | 			crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2), | ||||||
| 			ram_(ram), | 			ram_(ram), | ||||||
| 			interrupt_timer_(interrupt_timer) { | 			interrupt_timer_(interrupt_timer) { | ||||||
| 				establish_palette_hits(); | 				establish_palette_hits(); | ||||||
| 				build_mode_table(); | 				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_) { | 				if(cycles_) { | ||||||
| 					switch(previous_output_mode_) { | 					switch(previous_output_mode_) { | ||||||
| 						default: | 						default: | ||||||
| 						case OutputMode::Blank:			crt_->output_blank(cycles_ * 16);					break; | 						case OutputMode::Blank:			crt_.output_blank(cycles_ * 16);				break; | ||||||
| 						case OutputMode::Sync:			crt_->output_sync(cycles_ * 16);					break; | 						case OutputMode::Sync:			crt_.output_sync(cycles_ * 16);					break; | ||||||
| 						case OutputMode::Border:		output_border(cycles_);							break; | 						case OutputMode::Border:		output_border(cycles_);							break; | ||||||
| 						case OutputMode::ColourBurst:	crt_->output_default_colour_burst(cycles_ * 16);	break; | 						case OutputMode::ColourBurst:	crt_.output_default_colour_burst(cycles_ * 16);	break; | ||||||
| 						case OutputMode::Pixels: | 						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; | 							pixel_pointer_ = pixel_data_ = nullptr; | ||||||
| 						break; | 						break; | ||||||
| 					} | 					} | ||||||
| @@ -238,52 +243,54 @@ class CRTCBusHandler { | |||||||
| 			// collect some more pixels if output is ongoing | 			// collect some more pixels if output is ongoing | ||||||
| 			if(previous_output_mode_ == OutputMode::Pixels) { | 			if(previous_output_mode_ == OutputMode::Pixels) { | ||||||
| 				if(!pixel_data_) { | 				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_) { | 				if(pixel_pointer_) { | ||||||
| 					// the CPC shuffles output lines as: | 					// the CPC shuffles output lines as: | ||||||
| 					//	MA13 MA12	RA2 RA1 RA0		MA9 MA8 MA7 MA6 MA5 MA4 MA3 MA2 MA1 MA0		CCLK | 					//	MA13 MA12	RA2 RA1 RA0		MA9 MA8 MA7 MA6 MA5 MA4 MA3 MA2 MA1 MA0		CCLK | ||||||
| 					// ... so form the real access address. | 					// ... so form the real access address. | ||||||
| 					uint16_t address = | 					const uint16_t address = | ||||||
| 						static_cast<uint16_t>( | 						uint16_t( | ||||||
| 							((state.refresh_address & 0x3ff) << 1) | | 							((state.refresh_address & 0x3ff) << 1) | | ||||||
| 							((state.row_address & 0x7) << 11) | | 							((state.row_address & 0x7) << 11) | | ||||||
| 							((state.refresh_address & 0x3000) << 2) | 							((state.refresh_address & 0x3000) << 2) | ||||||
| 						); | 						); | ||||||
|  |  | ||||||
| 					// fetch two bytes and translate into pixels | 					// Fetch two bytes and translate into pixels. Guaranteed: the mode can change only at | ||||||
|  | 					// hsync, so there's no risk of pixel_pointer_ overrunning 320 output pixels without | ||||||
|  | 					// exactly reaching 320 output pixels. | ||||||
| 					switch(mode_) { | 					switch(mode_) { | ||||||
| 						case 0: | 						case 0: | ||||||
| 							reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode0_output_[ram_[address]]; | 							reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode0_output_[ram_[address]]; | ||||||
| 							reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode0_output_[ram_[address+1]]; | 							reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode0_output_[ram_[address+1]]; | ||||||
| 							pixel_pointer_ += 4; | 							pixel_pointer_ += 2 * sizeof(uint16_t); | ||||||
| 						break; | 						break; | ||||||
|  |  | ||||||
| 						case 1: | 						case 1: | ||||||
| 							reinterpret_cast<uint32_t *>(pixel_pointer_)[0] = mode1_output_[ram_[address]]; | 							reinterpret_cast<uint32_t *>(pixel_pointer_)[0] = mode1_output_[ram_[address]]; | ||||||
| 							reinterpret_cast<uint32_t *>(pixel_pointer_)[1] = mode1_output_[ram_[address+1]]; | 							reinterpret_cast<uint32_t *>(pixel_pointer_)[1] = mode1_output_[ram_[address+1]]; | ||||||
| 							pixel_pointer_ += 8; | 							pixel_pointer_ += 2 * sizeof(uint32_t); | ||||||
| 						break; | 						break; | ||||||
|  |  | ||||||
| 						case 2: | 						case 2: | ||||||
| 							reinterpret_cast<uint64_t *>(pixel_pointer_)[0] = mode2_output_[ram_[address]]; | 							reinterpret_cast<uint64_t *>(pixel_pointer_)[0] = mode2_output_[ram_[address]]; | ||||||
| 							reinterpret_cast<uint64_t *>(pixel_pointer_)[1] = mode2_output_[ram_[address+1]]; | 							reinterpret_cast<uint64_t *>(pixel_pointer_)[1] = mode2_output_[ram_[address+1]]; | ||||||
| 							pixel_pointer_ += 16; | 							pixel_pointer_ += 2 * sizeof(uint64_t); | ||||||
| 						break; | 						break; | ||||||
|  |  | ||||||
| 						case 3: | 						case 3: | ||||||
| 							reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode3_output_[ram_[address]]; | 							reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode3_output_[ram_[address]]; | ||||||
| 							reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode3_output_[ram_[address+1]]; | 							reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode3_output_[ram_[address+1]]; | ||||||
| 							pixel_pointer_ += 4; | 							pixel_pointer_ += 2 * sizeof(uint16_t); | ||||||
| 						break; | 						break; | ||||||
|  |  | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					// flush the current buffer pixel if full; the CRTC allows many different display | 					// Flush the current buffer pixel if full; the CRTC allows many different display | ||||||
| 					// widths so it's not necessarily possible to predict the correct number in advance | 					// widths so it's not necessarily possible to predict the correct number in advance | ||||||
| 					// and using the upper bound could lead to inefficient behaviour | 					// and using the upper bound could lead to inefficient behaviour. | ||||||
| 					if(pixel_pointer_ == pixel_data_ + 320) { | 					if(pixel_pointer_ == pixel_data_ + 320) { | ||||||
| 						crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_); | 						crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_)); | ||||||
| 						pixel_pointer_ = pixel_data_ = nullptr; | 						pixel_pointer_ = pixel_data_ = nullptr; | ||||||
| 						cycles_ = 0; | 						cycles_ = 0; | ||||||
| 					} | 					} | ||||||
| @@ -323,27 +330,14 @@ class CRTCBusHandler { | |||||||
| 			was_hsync_ = state.hsync; | 			was_hsync_ = state.hsync; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/// Constructs an appropriate CRT for video output. | 		/// Sets the destination for output. | ||||||
| 		void setup_output(float aspect_ratio) { | 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||||
| 			crt_.reset(new Outputs::CRT::CRT(1024, 16, Outputs::CRT::DisplayType::PAL50, 1)); | 			crt_.set_scan_target(scan_target); | ||||||
| 			crt_->set_rgb_sampling_function( |  | ||||||
| 				"vec3 rgb_sample(usampler2D sampler, vec2 coordinate)" |  | ||||||
| 				"{" |  | ||||||
| 					"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); |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/// Destructs the CRT. | 		/// Sets the type of display. | ||||||
| 		void close_output() { | 		void set_display_type(Outputs::Display::DisplayType display_type) { | ||||||
| 			crt_.reset(); | 			crt_.set_display_type(display_type); | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		/// @returns the CRT. |  | ||||||
| 		Outputs::CRT::CRT *get_crt() { |  | ||||||
| 			return crt_.get(); |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/*! | 		/*! | ||||||
| @@ -376,10 +370,18 @@ class CRTCBusHandler { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	private: | 	private: | ||||||
| 		void output_border(unsigned int length) { | 		void output_border(int length) { | ||||||
| 			uint8_t *colour_pointer = static_cast<uint8_t *>(crt_->allocate_write_area(1)); | 			assert(length >= 0); | ||||||
|  |  | ||||||
|  | 			// A black border can be output via crt_.output_blank for a minor performance | ||||||
|  | 			// win; otherwise paint whatever the border colour really is. | ||||||
|  | 			if(border_) { | ||||||
|  | 				uint8_t *const colour_pointer = static_cast<uint8_t *>(crt_.begin_data(1)); | ||||||
| 				if(colour_pointer) *colour_pointer = border_; | 				if(colour_pointer) *colour_pointer = border_; | ||||||
| 			crt_->output_level(length * 16); | 				crt_.output_level(length * 16); | ||||||
|  | 			} else { | ||||||
|  | 				crt_.output_blank(length * 16); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| #define Mode0Colour0(c) ((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2) | #define Mode0Colour0(c) ((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2) | ||||||
| @@ -395,16 +397,16 @@ class CRTCBusHandler { | |||||||
|  |  | ||||||
| 		void establish_palette_hits() { | 		void establish_palette_hits() { | ||||||
| 			for(int c = 0; c < 256; c++) { | 			for(int c = 0; c < 256; c++) { | ||||||
| 				mode0_palette_hits_[Mode0Colour0(c)].push_back(static_cast<uint8_t>(c)); | 				mode0_palette_hits_[Mode0Colour0(c)].push_back(uint8_t(c)); | ||||||
| 				mode0_palette_hits_[Mode0Colour1(c)].push_back(static_cast<uint8_t>(c)); | 				mode0_palette_hits_[Mode0Colour1(c)].push_back(uint8_t(c)); | ||||||
|  |  | ||||||
| 				mode1_palette_hits_[Mode1Colour0(c)].push_back(static_cast<uint8_t>(c)); | 				mode1_palette_hits_[Mode1Colour0(c)].push_back(uint8_t(c)); | ||||||
| 				mode1_palette_hits_[Mode1Colour1(c)].push_back(static_cast<uint8_t>(c)); | 				mode1_palette_hits_[Mode1Colour1(c)].push_back(uint8_t(c)); | ||||||
| 				mode1_palette_hits_[Mode1Colour2(c)].push_back(static_cast<uint8_t>(c)); | 				mode1_palette_hits_[Mode1Colour2(c)].push_back(uint8_t(c)); | ||||||
| 				mode1_palette_hits_[Mode1Colour3(c)].push_back(static_cast<uint8_t>(c)); | 				mode1_palette_hits_[Mode1Colour3(c)].push_back(uint8_t(c)); | ||||||
|  |  | ||||||
| 				mode3_palette_hits_[Mode3Colour0(c)].push_back(static_cast<uint8_t>(c)); | 				mode3_palette_hits_[Mode3Colour0(c)].push_back(uint8_t(c)); | ||||||
| 				mode3_palette_hits_[Mode3Colour1(c)].push_back(static_cast<uint8_t>(c)); | 				mode3_palette_hits_[Mode3Colour1(c)].push_back(uint8_t(c)); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -414,7 +416,7 @@ class CRTCBusHandler { | |||||||
| 					// Mode 0: abcdefgh -> [gcea] [hdfb] | 					// Mode 0: abcdefgh -> [gcea] [hdfb] | ||||||
| 					for(int c = 0; c < 256; c++) { | 					for(int c = 0; c < 256; c++) { | ||||||
| 						// prepare mode 0 | 						// prepare mode 0 | ||||||
| 						uint8_t *mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]); | 						uint8_t *const mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]); | ||||||
| 						mode0_pixels[0] = palette_[Mode0Colour0(c)]; | 						mode0_pixels[0] = palette_[Mode0Colour0(c)]; | ||||||
| 						mode0_pixels[1] = palette_[Mode0Colour1(c)]; | 						mode0_pixels[1] = palette_[Mode0Colour1(c)]; | ||||||
| 					} | 					} | ||||||
| @@ -423,7 +425,7 @@ class CRTCBusHandler { | |||||||
| 				case 1: | 				case 1: | ||||||
| 					for(int c = 0; c < 256; c++) { | 					for(int c = 0; c < 256; c++) { | ||||||
| 						// prepare mode 1 | 						// prepare mode 1 | ||||||
| 						uint8_t *mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]); | 						uint8_t *const mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]); | ||||||
| 						mode1_pixels[0] = palette_[Mode1Colour0(c)]; | 						mode1_pixels[0] = palette_[Mode1Colour0(c)]; | ||||||
| 						mode1_pixels[1] = palette_[Mode1Colour1(c)]; | 						mode1_pixels[1] = palette_[Mode1Colour1(c)]; | ||||||
| 						mode1_pixels[2] = palette_[Mode1Colour2(c)]; | 						mode1_pixels[2] = palette_[Mode1Colour2(c)]; | ||||||
| @@ -434,7 +436,7 @@ class CRTCBusHandler { | |||||||
| 				case 2: | 				case 2: | ||||||
| 					for(int c = 0; c < 256; c++) { | 					for(int c = 0; c < 256; c++) { | ||||||
| 						// prepare mode 2 | 						// prepare mode 2 | ||||||
| 						uint8_t *mode2_pixels = reinterpret_cast<uint8_t *>(&mode2_output_[c]); | 						uint8_t *const mode2_pixels = reinterpret_cast<uint8_t *>(&mode2_output_[c]); | ||||||
| 						mode2_pixels[0] = palette_[((c & 0x80) >> 7)]; | 						mode2_pixels[0] = palette_[((c & 0x80) >> 7)]; | ||||||
| 						mode2_pixels[1] = palette_[((c & 0x40) >> 6)]; | 						mode2_pixels[1] = palette_[((c & 0x40) >> 6)]; | ||||||
| 						mode2_pixels[2] = palette_[((c & 0x20) >> 5)]; | 						mode2_pixels[2] = palette_[((c & 0x20) >> 5)]; | ||||||
| @@ -449,7 +451,7 @@ class CRTCBusHandler { | |||||||
| 				case 3: | 				case 3: | ||||||
| 					for(int c = 0; c < 256; c++) { | 					for(int c = 0; c < 256; c++) { | ||||||
| 						// prepare mode 3 | 						// prepare mode 3 | ||||||
| 						uint8_t *mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]); | 						uint8_t *const mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]); | ||||||
| 						mode3_pixels[0] = palette_[Mode3Colour0(c)]; | 						mode3_pixels[0] = palette_[Mode3Colour0(c)]; | ||||||
| 						mode3_pixels[1] = palette_[Mode3Colour1(c)]; | 						mode3_pixels[1] = palette_[Mode3Colour1(c)]; | ||||||
| 					} | 					} | ||||||
| @@ -461,7 +463,7 @@ class CRTCBusHandler { | |||||||
| 			switch(mode_) { | 			switch(mode_) { | ||||||
| 				case 0: { | 				case 0: { | ||||||
| 					for(uint8_t c : mode0_palette_hits_[pen]) { | 					for(uint8_t c : mode0_palette_hits_[pen]) { | ||||||
| 						uint8_t *mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]); | 						uint8_t *const mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]); | ||||||
| 						mode0_pixels[0] = palette_[Mode0Colour0(c)]; | 						mode0_pixels[0] = palette_[Mode0Colour0(c)]; | ||||||
| 						mode0_pixels[1] = palette_[Mode0Colour1(c)]; | 						mode0_pixels[1] = palette_[Mode0Colour1(c)]; | ||||||
| 					} | 					} | ||||||
| @@ -469,7 +471,7 @@ class CRTCBusHandler { | |||||||
| 				case 1: | 				case 1: | ||||||
| 					if(pen > 3) return; | 					if(pen > 3) return; | ||||||
| 					for(uint8_t c : mode1_palette_hits_[pen]) { | 					for(uint8_t c : mode1_palette_hits_[pen]) { | ||||||
| 						uint8_t *mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]); | 						uint8_t *const mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]); | ||||||
| 						mode1_pixels[0] = palette_[Mode1Colour0(c)]; | 						mode1_pixels[0] = palette_[Mode1Colour0(c)]; | ||||||
| 						mode1_pixels[1] = palette_[Mode1Colour1(c)]; | 						mode1_pixels[1] = palette_[Mode1Colour1(c)]; | ||||||
| 						mode1_pixels[2] = palette_[Mode1Colour2(c)]; | 						mode1_pixels[2] = palette_[Mode1Colour2(c)]; | ||||||
| @@ -486,7 +488,7 @@ class CRTCBusHandler { | |||||||
| 					if(pen > 3) return; | 					if(pen > 3) return; | ||||||
| 					// Same argument applies here as to case 1, as the unused bits aren't masked out. | 					// Same argument applies here as to case 1, as the unused bits aren't masked out. | ||||||
| 					for(uint8_t c : mode3_palette_hits_[pen]) { | 					for(uint8_t c : mode3_palette_hits_[pen]) { | ||||||
| 						uint8_t *mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]); | 						uint8_t *const mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]); | ||||||
| 						mode3_pixels[0] = palette_[Mode3Colour0(c)]; | 						mode3_pixels[0] = palette_[Mode3Colour0(c)]; | ||||||
| 						mode3_pixels[1] = palette_[Mode3Colour1(c)]; | 						mode3_pixels[1] = palette_[Mode3Colour1(c)]; | ||||||
| 					} | 					} | ||||||
| @@ -507,7 +509,7 @@ class CRTCBusHandler { | |||||||
|  |  | ||||||
| 		uint8_t mapped_palette_value(uint8_t colour) { | 		uint8_t mapped_palette_value(uint8_t colour) { | ||||||
| #define COL(r, g, b) (r << 4) | (g << 2) | b | #define COL(r, g, b) (r << 4) | (g << 2) | b | ||||||
| 			static const uint8_t mapping[32] = { | 			constexpr uint8_t mapping[32] = { | ||||||
| 				COL(1, 1, 1),	COL(1, 1, 1),	COL(0, 2, 1),	COL(2, 2, 1), | 				COL(1, 1, 1),	COL(1, 1, 1),	COL(0, 2, 1),	COL(2, 2, 1), | ||||||
| 				COL(0, 0, 1),	COL(2, 0, 1),	COL(0, 1, 1),	COL(2, 1, 1), | 				COL(0, 0, 1),	COL(2, 0, 1),	COL(0, 1, 1),	COL(2, 1, 1), | ||||||
| 				COL(2, 0, 1),	COL(2, 2, 1),	COL(2, 2, 0),	COL(2, 2, 2), | 				COL(2, 0, 1),	COL(2, 2, 1),	COL(2, 2, 0),	COL(2, 2, 2), | ||||||
| @@ -528,19 +530,19 @@ class CRTCBusHandler { | |||||||
| 			Border, | 			Border, | ||||||
| 			Pixels | 			Pixels | ||||||
| 		} previous_output_mode_ = OutputMode::Sync; | 		} previous_output_mode_ = OutputMode::Sync; | ||||||
| 		unsigned int cycles_ = 0; | 		int cycles_ = 0; | ||||||
|  |  | ||||||
| 		bool was_hsync_ = false, was_vsync_ = false; | 		bool was_hsync_ = false, was_vsync_ = false; | ||||||
| 		int cycles_into_hsync_ = 0; | 		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 *pixel_data_ = nullptr, *pixel_pointer_ = nullptr; | ||||||
|  |  | ||||||
| 		uint8_t *ram_ = nullptr; | 		const uint8_t *const ram_ = nullptr; | ||||||
|  |  | ||||||
| 		int next_mode_ = 2, mode_ = 2; | 		int next_mode_ = 2, mode_ = 2; | ||||||
|  |  | ||||||
| 		unsigned int pixel_divider_ = 1; | 		int pixel_divider_ = 1; | ||||||
| 		uint16_t mode0_output_[256]; | 		uint16_t mode0_output_[256]; | ||||||
| 		uint32_t mode1_output_[256]; | 		uint32_t mode1_output_[256]; | ||||||
| 		uint64_t mode2_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. | 			Sets the row currently being reported to the AY. | ||||||
| 		*/ | 		*/ | ||||||
| 		void set_row(int row) { | 		void set_row(int row) { | ||||||
| 			row_ = static_cast<size_t>(row); | 			row_ = size_t(row); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/*! | 		/*! | ||||||
| @@ -591,7 +593,7 @@ class KeyboardState: public GI::AY38910::PortHandler { | |||||||
| 		*/ | 		*/ | ||||||
| 		void set_is_pressed(bool is_pressed, int line, int key) { | 		void set_is_pressed(bool is_pressed, int line, int key) { | ||||||
| 			int mask = 1 << key; | 			int mask = 1 << key; | ||||||
| 			assert(static_cast<size_t>(line) < sizeof(rows_)); | 			assert(size_t(line) < sizeof(rows_)); | ||||||
| 			if(is_pressed) rows_[line] &= ~mask; else rows_[line] |= mask; | 			if(is_pressed) rows_[line] &= ~mask; else rows_[line] |= mask; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -602,7 +604,7 @@ class KeyboardState: public GI::AY38910::PortHandler { | |||||||
| 			memset(rows_, 0xff, sizeof(rows_)); | 			memset(rows_, 0xff, sizeof(rows_)); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() { | 		const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() { | ||||||
| 			return joysticks_; | 			return joysticks_; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -789,33 +791,43 @@ template <bool has_fdc> class ConcreteMachine: | |||||||
| 			ay_.ay().set_port_handler(&key_state_); | 			ay_.ay().set_port_handler(&key_state_); | ||||||
|  |  | ||||||
| 			// construct the list of necessary ROMs | 			// 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; | 			std::string model_number; | ||||||
|  | 			uint32_t crcs[2]; | ||||||
| 			switch(target.model) { | 			switch(target.model) { | ||||||
| 				default: | 				default: | ||||||
| 					model_number = "6128"; | 					model_number = "6128"; | ||||||
| 					has_128k_ = true; | 					has_128k_ = true; | ||||||
|  | 					crcs[0] = 0x0219bb74; | ||||||
|  | 					crcs[1] = 0xca6af63d; | ||||||
| 				break; | 				break; | ||||||
| 				case Analyser::Static::AmstradCPC::Target::Model::CPC464: | 				case Analyser::Static::AmstradCPC::Target::Model::CPC464: | ||||||
| 					model_number = "464"; | 					model_number = "464"; | ||||||
| 					has_128k_ = false; | 					has_128k_ = false; | ||||||
|  | 					crcs[0] = 0x815752df; | ||||||
|  | 					crcs[1] = 0x7d9a3bac; | ||||||
| 				break; | 				break; | ||||||
| 				case Analyser::Static::AmstradCPC::Target::Model::CPC664: | 				case Analyser::Static::AmstradCPC::Target::Model::CPC664: | ||||||
| 					model_number = "664"; | 					model_number = "664"; | ||||||
| 					has_128k_ = false; | 					has_128k_ = false; | ||||||
|  | 					crcs[0] = 0x3f5a6dc4; | ||||||
|  | 					crcs[1] = 0x32fee492; | ||||||
| 				break; | 				break; | ||||||
| 			} | 			} | ||||||
| 			required_roms.push_back("os" + model_number + ".rom"); | 			required_roms.emplace_back(machine_name, "the CPC " + model_number + " firmware", "os" + model_number + ".rom", 16*1024, crcs[0]); | ||||||
| 			required_roms.push_back("basic" + model_number + ".rom"); | 			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 | 			// 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) { | 			for(std::size_t index = 0; index < roms.size(); ++index) { | ||||||
| 				auto &data = roms[index]; | 				auto &data = roms[index]; | ||||||
| 				if(!data) throw ROMMachine::Error::MissingROMs; | 				if(!data) throw ROMMachine::Error::MissingROMs; | ||||||
| 				roms_[static_cast<int>(index)] = std::move(*data); | 				roms_[int(index)] = std::move(*data); | ||||||
| 				roms_[static_cast<int>(index)].resize(16384); | 				roms_[int(index)].resize(16384); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// Establish default memory map | 			// Establish default memory map | ||||||
| @@ -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 | 			// TODO (in the player, not here): adapt it to accept an input clock rate and | ||||||
| 			// run_for as HalfCycles | 			// run_for as HalfCycles | ||||||
| 			if(!tape_player_is_sleeping_) tape_player_.run_for(cycle.length.as_int()); | 			if(!tape_player_is_sleeping_) tape_player_.run_for(cycle.length.as_integral()); | ||||||
|  |  | ||||||
| 			// Pump the AY | 			// Pump the AY | ||||||
| 			ay_.run_for(cycle.length); | 			ay_.run_for(cycle.length); | ||||||
|  |  | ||||||
|  | 			if constexpr (has_fdc) { | ||||||
| 				// Clock the FDC, if connected, using a lazy scale by two | 				// Clock the FDC, if connected, using a lazy scale by two | ||||||
| 				time_since_fdc_update_ += cycle.length; | 				time_since_fdc_update_ += cycle.length; | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			// Update typing activity | 			// Update typing activity | ||||||
| 			if(typer_) typer_->run_for(cycle.length); | 			if(typer_) typer_->run_for(cycle.length); | ||||||
| @@ -892,10 +906,12 @@ template <bool has_fdc> class ConcreteMachine: | |||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					// Check for an upper ROM selection | 					// Check for an upper ROM selection | ||||||
| 					if(has_fdc && !(address&0x2000)) { | 					if constexpr (has_fdc) { | ||||||
|  | 						if(!(address&0x2000)) { | ||||||
| 							upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC; | 							upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : ROMType::BASIC; | ||||||
| 							if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data(); | 							if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data(); | ||||||
| 						} | 						} | ||||||
|  | 					} | ||||||
|  |  | ||||||
| 					// Check for a CRTC access | 					// Check for a CRTC access | ||||||
| 					if(!(address & 0x4000)) { | 					if(!(address & 0x4000)) { | ||||||
| @@ -908,20 +924,22 @@ template <bool has_fdc> class ConcreteMachine: | |||||||
|  |  | ||||||
| 					// Check for an 8255 PIO access | 					// Check for an 8255 PIO access | ||||||
| 					if(!(address & 0x800)) { | 					if(!(address & 0x800)) { | ||||||
| 						i8255_.set_register((address >> 8) & 3, *cycle.value); | 						i8255_.write((address >> 8) & 3, *cycle.value); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
|  | 					if constexpr (has_fdc) { | ||||||
| 						// Check for an FDC access | 						// Check for an FDC access | ||||||
| 					if(has_fdc && (address & 0x580) == 0x100) { | 						if((address & 0x580) == 0x100) { | ||||||
| 							flush_fdc(); | 							flush_fdc(); | ||||||
| 						fdc_.set_register(address & 1, *cycle.value); | 							fdc_.write(address & 1, *cycle.value); | ||||||
| 						} | 						} | ||||||
|  |  | ||||||
| 						// Check for a disk motor access | 						// Check for a disk motor access | ||||||
| 					if(has_fdc && !(address & 0x580)) { | 						if(!(address & 0x580)) { | ||||||
| 							flush_fdc(); | 							flush_fdc(); | ||||||
| 							fdc_.set_motor_on(!!(*cycle.value)); | 							fdc_.set_motor_on(!!(*cycle.value)); | ||||||
| 						} | 						} | ||||||
|  | 					} | ||||||
| 				break; | 				break; | ||||||
| 				case CPU::Z80::PartialMachineCycle::Input: | 				case CPU::Z80::PartialMachineCycle::Input: | ||||||
| 					// Default to nothing answering | 					// Default to nothing answering | ||||||
| @@ -929,13 +947,15 @@ template <bool has_fdc> class ConcreteMachine: | |||||||
|  |  | ||||||
| 					// Check for a PIO access | 					// Check for a PIO access | ||||||
| 					if(!(address & 0x800)) { | 					if(!(address & 0x800)) { | ||||||
| 						*cycle.value &= i8255_.get_register((address >> 8) & 3); | 						*cycle.value &= i8255_.read((address >> 8) & 3); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					// Check for an FDC access | 					// Check for an FDC access | ||||||
| 					if(has_fdc && (address & 0x580) == 0x100) { | 					if constexpr (has_fdc) { | ||||||
|  | 						if((address & 0x580) == 0x100) { | ||||||
| 							flush_fdc(); | 							flush_fdc(); | ||||||
| 						*cycle.value &= fdc_.get_register(address & 1); | 							*cycle.value &= fdc_.read(address & 1); | ||||||
|  | 						} | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					// Check for a CRTC access; the below is not a typo, the CRTC can be selected | 					// Check for a CRTC access; the below is not a typo, the CRTC can be selected | ||||||
| @@ -981,19 +1001,14 @@ template <bool has_fdc> class ConcreteMachine: | |||||||
| 			flush_fdc(); | 			flush_fdc(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/// A CRTMachine function; indicates that outputs should be created now. | 		/// A CRTMachine function; sets the destination for video. | ||||||
| 		void setup_output(float aspect_ratio) override final { | 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final { | ||||||
| 			crtc_bus_handler_.setup_output(aspect_ratio); | 			crtc_bus_handler_.set_scan_target(scan_target); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/// A CRTMachine function; indicates that outputs should be destroyed now. | 		/// A CRTMachine function; sets the output display type. | ||||||
| 		void close_output() override final { | 		void set_display_type(Outputs::Display::DisplayType display_type) override final { | ||||||
| 			crtc_bus_handler_.close_output(); | 			crtc_bus_handler_.set_display_type(display_type); | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		/// @returns the CRT in use. |  | ||||||
| 		Outputs::CRT::CRT *get_crt() override final { |  | ||||||
| 			return crtc_bus_handler_.get_crt(); |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		/// @returns the speaker in use. | 		/// @returns the speaker in use. | ||||||
| @@ -1058,7 +1073,7 @@ template <bool has_fdc> class ConcreteMachine: | |||||||
|  |  | ||||||
| 		// MARK: - Activity Source | 		// MARK: - Activity Source | ||||||
| 		void set_activity_observer(Activity::Observer *observer) override { | 		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. | 		// MARK: - Configuration options. | ||||||
| @@ -1086,7 +1101,7 @@ template <bool has_fdc> class ConcreteMachine: | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// MARK: - Joysticks | 		// 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(); | 			return key_state_.get_joysticks(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -1148,12 +1163,14 @@ template <bool has_fdc> class ConcreteMachine: | |||||||
| 		FDC fdc_; | 		FDC fdc_; | ||||||
| 		HalfCycles time_since_fdc_update_; | 		HalfCycles time_since_fdc_update_; | ||||||
| 		void flush_fdc() { | 		void flush_fdc() { | ||||||
|  | 			if constexpr (has_fdc) { | ||||||
| 				// Clock the FDC, if connected, using a lazy scale by two | 				// Clock the FDC, if connected, using a lazy scale by two | ||||||
| 			if(has_fdc && !fdc_is_sleeping_) { | 				if(!fdc_is_sleeping_) { | ||||||
| 				fdc_.run_for(Cycles(time_since_fdc_update_.as_int())); | 					fdc_.run_for(Cycles(time_since_fdc_update_.as_integral())); | ||||||
| 				} | 				} | ||||||
| 				time_since_fdc_update_ = HalfCycles(0); | 				time_since_fdc_update_ = HalfCycles(0); | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		InterruptTimer interrupt_timer_; | 		InterruptTimer interrupt_timer_; | ||||||
| 		Storage::Tape::BinaryTapePlayer tape_player_; | 		Storage::Tape::BinaryTapePlayer tape_player_; | ||||||
|   | |||||||
| @@ -31,12 +31,12 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) { | |||||||
| 		BIND(F11, KeyRightSquareBracket); | 		BIND(F11, KeyRightSquareBracket); | ||||||
| 		BIND(F12, KeyClear); | 		BIND(F12, KeyClear); | ||||||
|  |  | ||||||
| 		BIND(Hyphen, KeyMinus);		BIND(Equals, KeyCaret);		BIND(BackSpace, KeyDelete); | 		BIND(Hyphen, KeyMinus);		BIND(Equals, KeyCaret);		BIND(Backspace, KeyDelete); | ||||||
| 		BIND(Tab, KeyTab); | 		BIND(Tab, KeyTab); | ||||||
|  |  | ||||||
| 		BIND(OpenSquareBracket, KeyAt); | 		BIND(OpenSquareBracket, KeyAt); | ||||||
| 		BIND(CloseSquareBracket, KeyLeftSquareBracket); | 		BIND(CloseSquareBracket, KeyLeftSquareBracket); | ||||||
| 		BIND(BackSlash, KeyBackSlash); | 		BIND(Backslash, KeyBackSlash); | ||||||
|  |  | ||||||
| 		BIND(CapsLock, KeyCapsLock); | 		BIND(CapsLock, KeyCapsLock); | ||||||
| 		BIND(Semicolon, KeyColon); | 		BIND(Semicolon, KeyColon); | ||||||
| @@ -57,19 +57,19 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) { | |||||||
| 		BIND(Left, KeyLeft);	BIND(Right, KeyRight); | 		BIND(Left, KeyLeft);	BIND(Right, KeyRight); | ||||||
| 		BIND(Up, KeyUp);		BIND(Down, KeyDown); | 		BIND(Up, KeyUp);		BIND(Down, KeyDown); | ||||||
|  |  | ||||||
| 		BIND(KeyPad0, KeyF0); | 		BIND(Keypad0, KeyF0); | ||||||
| 		BIND(KeyPad1, KeyF1);		BIND(KeyPad2, KeyF2);		BIND(KeyPad3, KeyF3); | 		BIND(Keypad1, KeyF1);		BIND(Keypad2, KeyF2);		BIND(Keypad3, KeyF3); | ||||||
| 		BIND(KeyPad4, KeyF4);		BIND(KeyPad5, KeyF5);		BIND(KeyPad6, KeyF6); | 		BIND(Keypad4, KeyF4);		BIND(Keypad5, KeyF5);		BIND(Keypad6, KeyF6); | ||||||
| 		BIND(KeyPad7, KeyF7);		BIND(KeyPad8, KeyF8);		BIND(KeyPad9, KeyF9); | 		BIND(Keypad7, KeyF7);		BIND(Keypad8, KeyF8);		BIND(Keypad9, KeyF9); | ||||||
| 		BIND(KeyPadPlus, KeySemicolon); | 		BIND(KeypadPlus, KeySemicolon); | ||||||
| 		BIND(KeyPadMinus, KeyMinus); | 		BIND(KeypadMinus, KeyMinus); | ||||||
|  |  | ||||||
| 		BIND(KeyPadEnter, KeyEnter); | 		BIND(KeypadEnter, KeyEnter); | ||||||
| 		BIND(KeyPadDecimalPoint, KeyFullStop); | 		BIND(KeypadDecimalPoint, KeyFullStop); | ||||||
| 		BIND(KeyPadEquals, KeyMinus); | 		BIND(KeypadEquals, KeyMinus); | ||||||
| 		BIND(KeyPadSlash, KeyForwardSlash); | 		BIND(KeypadSlash, KeyForwardSlash); | ||||||
| 		BIND(KeyPadAsterisk, KeyColon); | 		BIND(KeypadAsterisk, KeyColon); | ||||||
| 		BIND(KeyPadDelete, KeyDelete); | 		BIND(KeypadDelete, KeyDelete); | ||||||
| 	} | 	} | ||||||
| #undef BIND | #undef BIND | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,32 +8,40 @@ | |||||||
| 
 | 
 | ||||||
| #include "AppleII.hpp" | #include "AppleII.hpp" | ||||||
| 
 | 
 | ||||||
| #include "../../Activity/Source.hpp" | #include "../../../Activity/Source.hpp" | ||||||
| #include "../MediaTarget.hpp" | #include "../../MediaTarget.hpp" | ||||||
| #include "../CRTMachine.hpp" | #include "../../CRTMachine.hpp" | ||||||
| #include "../JoystickMachine.hpp" | #include "../../JoystickMachine.hpp" | ||||||
| #include "../KeyboardMachine.hpp" | #include "../../KeyboardMachine.hpp" | ||||||
| #include "../Utility/MemoryFuzzer.hpp" | #include "../../Utility/MemoryFuzzer.hpp" | ||||||
| #include "../Utility/StringSerialiser.hpp" | #include "../../Utility/StringSerialiser.hpp" | ||||||
| 
 | 
 | ||||||
| #include "../../Processors/6502/6502.hpp" | #include "../../../Processors/6502/6502.hpp" | ||||||
| #include "../../Components/AudioToggle/AudioToggle.hpp" | #include "../../../Components/AudioToggle/AudioToggle.hpp" | ||||||
| 
 | 
 | ||||||
| #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||||
| #include "../../Outputs/Log.hpp" | #include "../../../Outputs/Log.hpp" | ||||||
| 
 | 
 | ||||||
| #include "Card.hpp" | #include "Card.hpp" | ||||||
| #include "DiskIICard.hpp" | #include "DiskIICard.hpp" | ||||||
| #include "Video.hpp" | #include "Video.hpp" | ||||||
| 
 | 
 | ||||||
| #include "../../Analyser/Static/AppleII/Target.hpp" | #include "../../../Analyser/Static/AppleII/Target.hpp" | ||||||
| #include "../../ClockReceiver/ForceInline.hpp" | #include "../../../ClockReceiver/ForceInline.hpp" | ||||||
|  | #include "../../../Configurable/StandardOptions.hpp" | ||||||
| 
 | 
 | ||||||
| #include <algorithm> | #include <algorithm> | ||||||
| #include <array> | #include <array> | ||||||
| #include <memory> | #include <memory> | ||||||
| 
 | 
 | ||||||
| namespace { | namespace Apple { | ||||||
|  | namespace II { | ||||||
|  | 
 | ||||||
|  | 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)) | #define is_iie() ((model == Analyser::Static::AppleII::Target::Model::IIe) || (model == Analyser::Static::AppleII::Target::Model::EnhancedIIe)) | ||||||
| 
 | 
 | ||||||
| @@ -43,12 +51,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 	public KeyboardMachine::MappedMachine, | 	public KeyboardMachine::MappedMachine, | ||||||
| 	public CPU::MOS6502::BusHandler, | 	public CPU::MOS6502::BusHandler, | ||||||
| 	public Inputs::Keyboard, | 	public Inputs::Keyboard, | ||||||
| 	public AppleII::Machine, | 	public Configurable::Device, | ||||||
|  | 	public Apple::II::Machine, | ||||||
| 	public Activity::Source, | 	public Activity::Source, | ||||||
| 	public JoystickMachine::Machine, | 	public JoystickMachine::Machine, | ||||||
| 	public AppleII::Card::Delegate { | 	public Apple::II::Card::Delegate { | ||||||
| 	private: | 	private: | ||||||
| 		struct VideoBusHandler : public AppleII::Video::BusHandler { | 		struct VideoBusHandler : public Apple::II::Video::BusHandler { | ||||||
| 			public: | 			public: | ||||||
| 				VideoBusHandler(uint8_t *ram, uint8_t *aux_ram) : ram_(ram), aux_ram_(aux_ram) {} | 				VideoBusHandler(uint8_t *ram, uint8_t *aux_ram) : ram_(ram), aux_ram_(aux_ram) {} | ||||||
| 
 | 
 | ||||||
| @@ -63,28 +72,29 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 
 | 
 | ||||||
| 		CPU::MOS6502::Processor<(model == Analyser::Static::AppleII::Target::Model::EnhancedIIe) ? CPU::MOS6502::Personality::PSynertek65C02 : CPU::MOS6502::Personality::P6502, 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_; | 		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; | 		int cycles_into_current_line_ = 0; | ||||||
| 		Cycles cycles_since_video_update_; | 		Cycles cycles_since_video_update_; | ||||||
| 
 | 
 | ||||||
| 		void update_video() { | 		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() { | 		void update_audio() { | ||||||
| 			speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider))); | 			speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider))); | ||||||
| 		} | 		} | ||||||
| 		void update_just_in_time_cards() { | 		void update_just_in_time_cards() { | ||||||
|  | 			if(cycles_since_card_update_ > Cycles(0)) { | ||||||
| 				for(const auto &card : just_in_time_cards_) { | 				for(const auto &card : just_in_time_cards_) { | ||||||
| 					card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_); | 					card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_); | ||||||
| 				} | 				} | ||||||
|  | 			} | ||||||
| 			cycles_since_card_update_ = 0; | 			cycles_since_card_update_ = 0; | ||||||
| 			stretched_cycles_since_card_update_ = 0; | 			stretched_cycles_since_card_update_ = 0; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		uint8_t ram_[65536], aux_ram_[65536]; | 		uint8_t ram_[65536], aux_ram_[65536]; | ||||||
| 		std::vector<uint8_t> rom_; | 		std::vector<uint8_t> rom_; | ||||||
| 		std::vector<uint8_t> character_rom_; |  | ||||||
| 		uint8_t keyboard_input_ = 0x00; | 		uint8_t keyboard_input_ = 0x00; | ||||||
| 		bool key_is_down_ = false; | 		bool key_is_down_ = false; | ||||||
| 
 | 
 | ||||||
| @@ -102,41 +112,47 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 		Cycles cycles_since_audio_update_; | 		Cycles cycles_since_audio_update_; | ||||||
| 
 | 
 | ||||||
| 		// MARK: - Cards
 | 		// 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_; | 		Cycles cycles_since_card_update_; | ||||||
| 		std::vector<AppleII::Card *> every_cycle_cards_; | 		std::vector<Apple::II::Card *> every_cycle_cards_; | ||||||
| 		std::vector<AppleII::Card *> just_in_time_cards_; | 		std::vector<Apple::II::Card *> just_in_time_cards_; | ||||||
| 
 | 
 | ||||||
| 		int stretched_cycles_since_card_update_ = 0; | 		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); | 			assert(slot >= 1 && slot < 8); | ||||||
| 			cards_[slot - 1].reset(card); | 			cards_[slot - 1].reset(card); | ||||||
| 			card->set_delegate(this); | 			card->set_delegate(this); | ||||||
| 			pick_card_messaging_group(card); | 			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(); | 			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); | 			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<Apple::II::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_; |  | ||||||
| 
 | 
 | ||||||
|  | 			// If the card is already in the proper group, stop.
 | ||||||
| 			if(std::find(intended.begin(), intended.end(), card) != intended.end()) return; | 			if(std::find(intended.begin(), intended.end(), card) != intended.end()) return; | ||||||
| 			auto old_membership = std::find(undesired.begin(), undesired.end(), card); | 
 | ||||||
| 			if(old_membership != undesired.end()) undesired.erase(old_membership); | 			// Otherwise, mark the sets as dirty. It isn't safe to transition the card here,
 | ||||||
| 			intended.push_back(card); | 			// as the main loop may be part way through iterating the two lists.
 | ||||||
|  | 			card_lists_are_dirty_ = true; | ||||||
|  | 			card_became_just_in_time_ |= !is_every_cycle; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		void card_did_change_select_constraints(AppleII::Card *card) override { | 		void card_did_change_select_constraints(Apple::II::Card *card) override { | ||||||
| 			pick_card_messaging_group(card); | 			pick_card_messaging_group(card); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		AppleII::DiskIICard *diskii_card() { | 		Apple::II::DiskIICard *diskii_card() { | ||||||
| 			return dynamic_cast<AppleII::DiskIICard *>(cards_[5].get()); | 			return dynamic_cast<Apple::II::DiskIICard *>(cards_[5].get()); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// MARK: - Memory Map.
 | 		// MARK: - Memory Map.
 | ||||||
| @@ -234,13 +250,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 				read_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200], | 				read_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200], | ||||||
| 				write_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200]); | 				write_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200]); | ||||||
| 
 | 
 | ||||||
| 			if(video_ && video_->get_80_store()) { | 			if(video_.get_80_store()) { | ||||||
| 				bool use_aux_ram = video_->get_page2(); | 				bool use_aux_ram = video_.get_page2(); | ||||||
| 				page(0x04, 0x08, | 				page(0x04, 0x08, | ||||||
| 					use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400], | 					use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400], | ||||||
| 					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, | 					page(0x20, 0x40, | ||||||
| 						use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000], | 						use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000], | ||||||
| 						use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000]); | 						use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000]); | ||||||
| @@ -309,10 +325,11 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 		ConcreteMachine(const Analyser::Static::AppleII::Target &target, const ROMMachine::ROMFetcher &rom_fetcher): | 		ConcreteMachine(const Analyser::Static::AppleII::Target &target, const ROMMachine::ROMFetcher &rom_fetcher): | ||||||
| 			m6502_(*this), | 			m6502_(*this), | ||||||
| 			video_bus_handler_(ram_, aux_ram_), | 			video_bus_handler_(ram_, aux_ram_), | ||||||
|  | 			video_(video_bus_handler_), | ||||||
| 			audio_toggle_(audio_queue_), | 			audio_toggle_(audio_queue_), | ||||||
| 			speaker_(audio_toggle_) { | 			speaker_(audio_toggle_) { | ||||||
| 			// The system's master clock rate.
 | 			// The system's master clock rate.
 | ||||||
| 		 	const float master_clock = 14318180.0; | 			constexpr float master_clock = 14318180.0; | ||||||
| 
 | 
 | ||||||
| 			// This is where things get slightly convoluted: establish the machine as having a clock rate
 | 			// This is where things get slightly convoluted: establish the machine as having a clock rate
 | ||||||
| 			// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
 | 			// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
 | ||||||
| @@ -338,30 +355,39 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 
 | 
 | ||||||
| 			// Pick the required ROMs.
 | 			// Pick the required ROMs.
 | ||||||
| 			using Target = Analyser::Static::AppleII::Target; | 			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; | 			size_t rom_size = 12*1024; | ||||||
| 			switch(target.model) { | 			switch(target.model) { | ||||||
| 				default: | 				default: | ||||||
| 					rom_names.push_back("apple2-character.rom"); | 					rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6); | ||||||
| 					rom_names.push_back("apple2o.rom"); | 					rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588); | ||||||
| 				break; | 				break; | ||||||
| 				case Target::Model::IIplus: | 				case Target::Model::IIplus: | ||||||
| 					rom_names.push_back("apple2-character.rom"); | 					rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6); | ||||||
| 					rom_names.push_back("apple2.rom"); | 					rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26); | ||||||
| 				break; | 				break; | ||||||
| 				case Target::Model::IIe: | 				case Target::Model::IIe: | ||||||
| 					rom_size += 3840; | 					rom_size += 3840; | ||||||
| 					rom_names.push_back("apple2eu-character.rom"); | 					rom_descriptions.emplace_back(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1); | ||||||
| 					rom_names.push_back("apple2eu.rom"); | 					rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d); | ||||||
| 				break; | 				break; | ||||||
| 				case Target::Model::EnhancedIIe: | 				case Target::Model::EnhancedIIe: | ||||||
| 					rom_size += 3840; | 					rom_size += 3840; | ||||||
| 					rom_names.push_back("apple2e-character.rom"); | 					rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d); | ||||||
| 					rom_names.push_back("apple2e.rom"); | 					rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942); | ||||||
| 				break; | 				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]) { | 			if(!roms[0] || !roms[1]) { | ||||||
| 				throw ROMMachine::Error::MissingROMs; | 				throw ROMMachine::Error::MissingROMs; | ||||||
| 			} | 			} | ||||||
| @@ -371,12 +397,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 				rom_.erase(rom_.begin(), rom_.end() - static_cast<off_t>(rom_size)); | 				rom_.erase(rom_.begin(), rom_.end() - static_cast<off_t>(rom_size)); | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			character_rom_ = std::move(*roms[0]); | 			video_.set_character_rom(*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)); |  | ||||||
| 			} |  | ||||||
| 
 | 
 | ||||||
| 			// Set up the default memory blocks. On a II or II+ these values will never change.
 | 			// 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.
 | 			// On a IIe they'll be affected by selection of auxiliary RAM.
 | ||||||
| @@ -396,17 +417,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 			audio_queue_.flush(); | 			audio_queue_.flush(); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		void setup_output(float aspect_ratio) override { | 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { | ||||||
| 			video_.reset(new AppleII::Video::Video<VideoBusHandler, is_iie()>(video_bus_handler_)); | 			video_.set_scan_target(scan_target); | ||||||
| 			video_->set_character_rom(character_rom_); |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		void close_output() override { | 		/// Sets the type of display.
 | ||||||
| 			video_.reset(); | 		void set_display_type(Outputs::Display::DisplayType display_type) override { | ||||||
| 		} | 			video_.set_display_type(display_type); | ||||||
| 
 |  | ||||||
| 		Outputs::CRT::CRT *get_crt() override { |  | ||||||
| 			return video_->get_crt(); |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		Outputs::Speaker::Speaker *get_speaker() override { | 		Outputs::Speaker::Speaker *get_speaker() override { | ||||||
| @@ -463,7 +480,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 				// actor, but this will actually be the result most of the time so it's not
 | 				// actor, but this will actually be the result most of the time so it's not
 | ||||||
| 				// too terrible.
 | 				// too terrible.
 | ||||||
| 				if(isReadOperation(operation) && address != 0xc000) { | 				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) { | 				switch(address) { | ||||||
| @@ -523,18 +540,18 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 								case 0xc015:	IIeSwitchRead(internal_CX_rom_);											break; | 								case 0xc015:	IIeSwitchRead(internal_CX_rom_);											break; | ||||||
| 								case 0xc016:	IIeSwitchRead(alternative_zero_page_);										break; | 								case 0xc016:	IIeSwitchRead(alternative_zero_page_);										break; | ||||||
| 								case 0xc017:	IIeSwitchRead(slot_C3_rom_);												break; | 								case 0xc017:	IIeSwitchRead(slot_C3_rom_);												break; | ||||||
| 								case 0xc018:	IIeSwitchRead(video_->get_80_store());										break; | 								case 0xc018:	IIeSwitchRead(video_.get_80_store());										break; | ||||||
| 								case 0xc019:	IIeSwitchRead(video_->get_is_vertical_blank(cycles_since_video_update_));	break; | 								case 0xc019:	IIeSwitchRead(video_.get_is_vertical_blank(cycles_since_video_update_));	break; | ||||||
| 								case 0xc01a:	IIeSwitchRead(video_->get_text());											break; | 								case 0xc01a:	IIeSwitchRead(video_.get_text());											break; | ||||||
| 								case 0xc01b:	IIeSwitchRead(video_->get_mixed());											break; | 								case 0xc01b:	IIeSwitchRead(video_.get_mixed());											break; | ||||||
| 								case 0xc01c:	IIeSwitchRead(video_->get_page2());											break; | 								case 0xc01c:	IIeSwitchRead(video_.get_page2());											break; | ||||||
| 								case 0xc01d:	IIeSwitchRead(video_->get_high_resolution());								break; | 								case 0xc01d:	IIeSwitchRead(video_.get_high_resolution());								break; | ||||||
| 								case 0xc01e:	IIeSwitchRead(video_->get_alternative_character_set());						break; | 								case 0xc01e:	IIeSwitchRead(video_.get_alternative_character_set());						break; | ||||||
| 								case 0xc01f:	IIeSwitchRead(video_->get_80_columns());									break; | 								case 0xc01f:	IIeSwitchRead(video_.get_80_columns());										break; | ||||||
| #undef IIeSwitchRead | #undef IIeSwitchRead | ||||||
| 
 | 
 | ||||||
| 								case 0xc07f: | 								case 0xc07f: | ||||||
| 									if(is_iie()) *value = (*value & 0x7f) | (video_->get_annunciator_3() ? 0x80 : 0x00); | 									if(is_iie()) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00); | ||||||
| 								break; | 								break; | ||||||
| 							} | 							} | ||||||
| 						} else { | 						} else { | ||||||
| @@ -546,7 +563,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 									case 0xc000: | 									case 0xc000: | ||||||
| 									case 0xc001: | 									case 0xc001: | ||||||
| 										update_video(); | 										update_video(); | ||||||
| 										video_->set_80_store(!!(address&1)); | 										video_.set_80_store(!!(address&1)); | ||||||
| 										set_main_paging(); | 										set_main_paging(); | ||||||
| 									break; | 									break; | ||||||
| 
 | 
 | ||||||
| @@ -586,13 +603,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 									case 0xc00c: | 									case 0xc00c: | ||||||
| 									case 0xc00d: | 									case 0xc00d: | ||||||
| 										update_video(); | 										update_video(); | ||||||
| 										video_->set_80_columns(!!(address&1)); | 										video_.set_80_columns(!!(address&1)); | ||||||
| 									break; | 									break; | ||||||
| 
 | 
 | ||||||
| 									case 0xc00e: | 									case 0xc00e: | ||||||
| 									case 0xc00f: | 									case 0xc00f: | ||||||
| 										update_video(); | 										update_video(); | ||||||
| 										video_->set_alternative_character_set(!!(address&1)); | 										video_.set_alternative_character_set(!!(address&1)); | ||||||
| 									break; | 									break; | ||||||
| 								} | 								} | ||||||
| 							} | 							} | ||||||
| @@ -615,20 +632,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 					case 0xc050: | 					case 0xc050: | ||||||
| 					case 0xc051: | 					case 0xc051: | ||||||
| 						update_video(); | 						update_video(); | ||||||
| 						video_->set_text(!!(address&1)); | 						video_.set_text(!!(address&1)); | ||||||
| 					break; | 					break; | ||||||
| 					case 0xc052:	update_video();		video_->set_mixed(false);			break; | 					case 0xc052:	update_video();		video_.set_mixed(false);		break; | ||||||
| 					case 0xc053:	update_video();		video_->set_mixed(true);			break; | 					case 0xc053:	update_video();		video_.set_mixed(true);			break; | ||||||
| 					case 0xc054: | 					case 0xc054: | ||||||
| 					case 0xc055: | 					case 0xc055: | ||||||
| 						update_video(); | 						update_video(); | ||||||
| 						video_->set_page2(!!(address&1)); | 						video_.set_page2(!!(address&1)); | ||||||
| 						set_main_paging(); | 						set_main_paging(); | ||||||
| 					break; | 					break; | ||||||
| 					case 0xc056: | 					case 0xc056: | ||||||
| 					case 0xc057: | 					case 0xc057: | ||||||
| 						update_video(); | 						update_video(); | ||||||
| 						video_->set_high_resolution(!!(address&1)); | 						video_.set_high_resolution(!!(address&1)); | ||||||
| 						set_main_paging(); | 						set_main_paging(); | ||||||
| 					break; | 					break; | ||||||
| 
 | 
 | ||||||
| @@ -636,7 +653,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 					case 0xc05f: | 					case 0xc05f: | ||||||
| 						if(is_iie()) { | 						if(is_iie()) { | ||||||
| 							update_video(); | 							update_video(); | ||||||
| 							video_->set_annunciator_3(!(address&1)); | 							video_.set_annunciator_3(!(address&1)); | ||||||
| 						} | 						} | ||||||
| 					break; | 					break; | ||||||
| 
 | 
 | ||||||
| @@ -696,7 +713,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 					// If this is a card access, figure out which card is at play before determining
 | 					// If this is a card access, figure out which card is at play before determining
 | ||||||
| 					// the totality of who needs messaging.
 | 					// the totality of who needs messaging.
 | ||||||
| 					size_t card_number = 0; | 					size_t card_number = 0; | ||||||
| 					AppleII::Card::Select select = AppleII::Card::None; | 					Apple::II::Card::Select select = Apple::II::Card::None; | ||||||
| 
 | 
 | ||||||
| 					if(address >= 0xc100) { | 					if(address >= 0xc100) { | ||||||
| 						/*
 | 						/*
 | ||||||
| @@ -704,20 +721,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 								0xCn00 to 0xCnff: card n. | 								0xCn00 to 0xCnff: card n. | ||||||
| 						*/ | 						*/ | ||||||
| 						card_number = (address - 0xc100) >> 8; | 						card_number = (address - 0xc100) >> 8; | ||||||
| 						select = AppleII::Card::Device; | 						select = Apple::II::Card::Device; | ||||||
| 					} else { | 					} else { | ||||||
| 						/*
 | 						/*
 | ||||||
| 							Decode the area conventionally used by cards for registers: | 							Decode the area conventionally used by cards for registers: | ||||||
| 								C0n0 to C0nF: card n - 8. | 								C0n0 to C0nF: card n - 8. | ||||||
| 						*/ | 						*/ | ||||||
| 						card_number = (address - 0xc090) >> 4; | 						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,
 | 					// If the selected card is a just-in-time card, update the just-in-time cards,
 | ||||||
| 					// and then message it specifically.
 | 					// and then message it specifically.
 | ||||||
| 					const bool is_read = isReadOperation(operation); | 					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)) { | 					if(target && !is_every_cycle_card(target)) { | ||||||
| 						update_just_in_time_cards(); | 						update_just_in_time_cards(); | ||||||
| 						target->perform_bus_operation(select, is_read, address, value); | 						target->perform_bus_operation(select, is_read, address, value); | ||||||
| @@ -728,7 +745,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 					for(const auto &card: every_cycle_cards_) { | 					for(const auto &card: every_cycle_cards_) { | ||||||
| 						card->run_for(Cycles(1), is_stretched_cycle); | 						card->run_for(Cycles(1), is_stretched_cycle); | ||||||
| 						card->perform_bus_operation( | 						card->perform_bus_operation( | ||||||
| 							(card == target) ? select : AppleII::Card::None, | 							(card == target) ? select : Apple::II::Card::None, | ||||||
| 							is_read, address, value); | 							is_read, address, value); | ||||||
| 					} | 					} | ||||||
| 					has_updated_cards = true; | 					has_updated_cards = true; | ||||||
| @@ -740,7 +757,32 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 				const bool is_read = isReadOperation(operation); | 				const bool is_read = isReadOperation(operation); | ||||||
| 				for(const auto &card: every_cycle_cards_) { | 				for(const auto &card: every_cycle_cards_) { | ||||||
| 					card->run_for(Cycles(1), is_stretched_cycle); | 					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()); | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| @@ -786,7 +828,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 					case Key::Right:		value = 0x15;	break; | 					case Key::Right:		value = 0x15;	break; | ||||||
| 					case Key::Down:			value = 0x0a;	break; | 					case Key::Down:			value = 0x0a;	break; | ||||||
| 					case Key::Up:			value = 0x0b;	break; | 					case Key::Up:			value = 0x0b;	break; | ||||||
| 					case Key::BackSpace:	value = 0x7f;	break; | 					case Key::Backspace:	value = 0x7f;	break; | ||||||
| 					default: return; | 					default: return; | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @@ -809,7 +851,29 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		void type_string(const std::string &string) override { | 		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
 | 		// MARK: MediaTarget
 | ||||||
| @@ -829,14 +893,15 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// MARK: JoystickMachine
 | 		// 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_; | 			return joysticks_; | ||||||
| 		} | 		} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| using namespace AppleII; | using namespace Apple::II; | ||||||
| 
 | 
 | ||||||
| Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { | Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { | ||||||
| 	using Target = Analyser::Static::AppleII::Target; | 	using Target = Analyser::Static::AppleII::Target; | ||||||
| @@ -9,14 +9,18 @@ | |||||||
| #ifndef AppleII_hpp | #ifndef AppleII_hpp | ||||||
| #define AppleII_hpp | #define AppleII_hpp | ||||||
| 
 | 
 | ||||||
| #include "../../Configurable/Configurable.hpp" | #include "../../../Configurable/Configurable.hpp" | ||||||
| #include "../../Analyser/Static/StaticAnalyser.hpp" | #include "../../../Analyser/Static/StaticAnalyser.hpp" | ||||||
| #include "../ROMMachine.hpp" | #include "../../ROMMachine.hpp" | ||||||
| 
 | 
 | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <vector> | #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 { | class Machine { | ||||||
| 	public: | 	public: | ||||||
| @@ -26,6 +30,7 @@ class Machine { | |||||||
| 		static Machine *AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | 		static Machine *AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| }; | } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| #endif /* AppleII_hpp */ | #endif /* AppleII_hpp */ | ||||||
| @@ -9,11 +9,12 @@ | |||||||
| #ifndef Card_h | #ifndef Card_h | ||||||
| #define Card_h | #define Card_h | ||||||
| 
 | 
 | ||||||
| #include "../../Processors/6502/6502.hpp" | #include "../../../Processors/6502/6502.hpp" | ||||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||||
| #include "../../Activity/Observer.hpp" | #include "../../../Activity/Observer.hpp" | ||||||
| 
 | 
 | ||||||
| namespace AppleII { | namespace Apple { | ||||||
|  | namespace II { | ||||||
| 
 | 
 | ||||||
| /*!
 | /*!
 | ||||||
| 	This provides a small subset of the interface offered to cards installed in | 	This provides a small subset of the interface offered to cards installed in | ||||||
| @@ -39,6 +40,7 @@ namespace AppleII { | |||||||
| */ | */ | ||||||
| class Card { | class Card { | ||||||
| 	public: | 	public: | ||||||
|  | 		virtual ~Card() {} | ||||||
| 		enum Select: int { | 		enum Select: int { | ||||||
| 			None	= 0,		// No select line is active
 | 			None	= 0,		// No select line is active
 | ||||||
| 			IO		= 1 << 0,	// IO select 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 | 			will receive a perform_bus_operation every cycle. To reduce the number of | ||||||
| 			virtual method calls, they **will not** receive run_for. run_for will propagate | 			virtual method calls, they **will not** receive run_for. run_for will propagate | ||||||
| 			only to cards that register for IO and/or Device accesses only. | 			only to cards that register for IO and/or Device accesses only. | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 		*/ | 		*/ | ||||||
| 		int get_select_constraints() { | 		int get_select_constraints() const { | ||||||
| 			return select_constraints_; | 			return select_constraints_; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| @@ -108,6 +108,7 @@ class Card { | |||||||
| 		} | 		} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #endif /* Card_h */ | #endif /* Card_h */ | ||||||
| @@ -8,15 +8,25 @@ | |||||||
| 
 | 
 | ||||||
| #include "DiskIICard.hpp" | #include "DiskIICard.hpp" | ||||||
| 
 | 
 | ||||||
| using namespace AppleII; | using namespace Apple::II; | ||||||
| 
 | 
 | ||||||
| DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector) : diskii_(2045454) { | DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector) : diskii_(2045454) { | ||||||
| 	const auto roms = rom_fetcher( | 	std::vector<std::unique_ptr<std::vector<uint8_t>>> roms; | ||||||
| 		"DiskII", | 	if(is_16_sector) { | ||||||
| 		{ | 		roms = rom_fetcher({ | ||||||
| 			is_16_sector ? "boot-16.rom" : "boot-13.rom", | 			{"DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6}, | ||||||
| 			is_16_sector ? "state-machine-16.rom" : "state-machine-13.rom" | 			{"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]); | 	boot_ = std::move(*roms[0]); | ||||||
| 	diskii_.set_state_machine(*roms[1]); | 	diskii_.set_state_machine(*roms[1]); | ||||||
| 	set_select_constraints(None); | 	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) { | void DiskIICard::run_for(Cycles cycles, int stretches) { | ||||||
| 	if(diskii_clocking_preference_ == ClockingHint::Preference::None) return; | 	if(diskii_clocking_preference_ == ClockingHint::Preference::None) return; | ||||||
| 	diskii_.run_for(Cycles(cycles.as_int() * 2)); | 	diskii_.run_for(Cycles(cycles.as_integral() * 2)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void DiskIICard::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive) { | void DiskIICard::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive) { | ||||||
| @@ -55,7 +65,7 @@ void DiskIICard::set_activity_observer(Activity::Observer *observer) { | |||||||
| 
 | 
 | ||||||
| void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) { | void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) { | ||||||
| 	diskii_clocking_preference_ = preference; | 	diskii_clocking_preference_ = preference; | ||||||
| 	set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : 0); | 	set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : None); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Storage::Disk::Drive &DiskIICard::get_drive(int drive) { | Storage::Disk::Drive &DiskIICard::get_drive(int drive) { | ||||||
| @@ -10,17 +10,18 @@ | |||||||
| #define DiskIICard_hpp | #define DiskIICard_hpp | ||||||
| 
 | 
 | ||||||
| #include "Card.hpp" | #include "Card.hpp" | ||||||
| #include "../ROMMachine.hpp" | #include "../../ROMMachine.hpp" | ||||||
| 
 | 
 | ||||||
| #include "../../Components/DiskII/DiskII.hpp" | #include "../../../Components/DiskII/DiskII.hpp" | ||||||
| #include "../../Storage/Disk/Disk.hpp" | #include "../../../Storage/Disk/Disk.hpp" | ||||||
| #include "../../ClockReceiver/ClockingHintSource.hpp" | #include "../../../ClockReceiver/ClockingHintSource.hpp" | ||||||
| 
 | 
 | ||||||
| #include <cstdint> | #include <cstdint> | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <vector> | #include <vector> | ||||||
| 
 | 
 | ||||||
| namespace AppleII { | namespace Apple { | ||||||
|  | namespace II { | ||||||
| 
 | 
 | ||||||
| class DiskIICard: public Card, public ClockingHint::Observer { | class DiskIICard: public Card, public ClockingHint::Observer { | ||||||
| 	public: | 	public: | ||||||
| @@ -41,6 +42,7 @@ class DiskIICard: public Card, public ClockingHint::Observer { | |||||||
| 		ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime; | 		ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #endif /* DiskIICard_hpp */ | #endif /* DiskIICard_hpp */ | ||||||
| @@ -8,25 +8,22 @@ | |||||||
| 
 | 
 | ||||||
| #include "Video.hpp" | #include "Video.hpp" | ||||||
| 
 | 
 | ||||||
| using namespace AppleII::Video; | using namespace Apple::II::Video; | ||||||
| 
 | 
 | ||||||
| VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) : | VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) : | ||||||
| 	crt_(new Outputs::CRT::CRT(910, 1, Outputs::CRT::DisplayType::NTSC60, 1)), | 	crt_(910, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance1), | ||||||
| 	is_iie_(is_iie), | 	is_iie_(is_iie), | ||||||
| 	deferrer_(std::move(target)) { | 	deferrer_(std::move(target)) { | ||||||
| 
 | 
 | ||||||
| 	// Set a composite sampling function that assumes one byte per pixel input, and
 |  | ||||||
| 	// accepts any non-zero value as being fully on, zero being fully off.
 |  | ||||||
| 	crt_->set_composite_sampling_function( |  | ||||||
| 		"float composite_sample(usampler2D sampler, vec2 coordinate, float phase, float amplitude)" |  | ||||||
| 		"{" |  | ||||||
| 			"return clamp(texture(sampler, coordinate).r, 0.0, 0.66);" |  | ||||||
| 		"}"); |  | ||||||
| 
 |  | ||||||
| 	// Show only the centre 75% of the TV frame.
 | 	// Show only the centre 75% of the TV frame.
 | ||||||
| 	crt_->set_video_signal(Outputs::CRT::VideoSignal::Composite); | 	crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour); | ||||||
| 	crt_->set_visible_area(Outputs::CRT::Rect(0.118f, 0.122f, 0.77f, 0.77f)); | 	crt_.set_visible_area(Outputs::Display::Rect(0.118f, 0.122f, 0.77f, 0.77f)); | ||||||
| 	crt_->set_immediate_default_phase(0.0f); | 
 | ||||||
|  | 	// 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].xor_mask = 0; | ||||||
| 	character_zones[0].address_mask = 0x3f; | 	character_zones[0].address_mask = 0x3f; | ||||||
| @@ -46,8 +43,12 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) : | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Outputs::CRT::CRT *VideoBase::get_crt() { | void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||||
| 	return crt_.get(); | 	crt_.set_scan_target(scan_target); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) { | ||||||
|  | 	crt_.set_display_type(display_type); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /*
 | /*
 | ||||||
| @@ -9,14 +9,15 @@ | |||||||
| #ifndef Video_hpp | #ifndef Video_hpp | ||||||
| #define Video_hpp | #define Video_hpp | ||||||
| 
 | 
 | ||||||
| #include "../../Outputs/CRT/CRT.hpp" | #include "../../../Outputs/CRT/CRT.hpp" | ||||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||||
| #include "../../ClockReceiver/ClockDeferrer.hpp" | #include "../../../ClockReceiver/DeferredQueue.hpp" | ||||||
| 
 | 
 | ||||||
| #include <array> | #include <array> | ||||||
| #include <vector> | #include <vector> | ||||||
| 
 | 
 | ||||||
| namespace AppleII { | namespace Apple { | ||||||
|  | namespace II { | ||||||
| namespace Video { | namespace Video { | ||||||
| 
 | 
 | ||||||
| class BusHandler { | class BusHandler { | ||||||
| @@ -36,8 +37,11 @@ class VideoBase { | |||||||
| 	public: | 	public: | ||||||
| 		VideoBase(bool is_iie, std::function<void(Cycles)> &&target); | 		VideoBase(bool is_iie, std::function<void(Cycles)> &&target); | ||||||
| 
 | 
 | ||||||
| 		/// @returns The CRT this video feed is feeding.
 | 		/// Sets the scan target.
 | ||||||
| 		Outputs::CRT::CRT *get_crt(); | 		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 | 			Descriptions for the setters below are taken verbatim from | ||||||
| @@ -146,7 +150,7 @@ class VideoBase { | |||||||
| 		void set_character_rom(const std::vector<uint8_t> &); | 		void set_character_rom(const std::vector<uint8_t> &); | ||||||
| 
 | 
 | ||||||
| 	protected: | 	protected: | ||||||
| 		std::unique_ptr<Outputs::CRT::CRT> crt_; | 		Outputs::CRT::CRT crt_; | ||||||
| 
 | 
 | ||||||
| 		// State affecting output video stream generation.
 | 		// State affecting output video stream generation.
 | ||||||
| 		uint8_t *pixel_pointer_ = nullptr; | 		uint8_t *pixel_pointer_ = nullptr; | ||||||
| @@ -199,7 +203,7 @@ class VideoBase { | |||||||
| 		std::array<uint8_t, 40> auxiliary_stream_; | 		std::array<uint8_t, 40> auxiliary_stream_; | ||||||
| 
 | 
 | ||||||
| 		bool is_iie_ = false; | 		bool is_iie_ = false; | ||||||
| 		static const int flash_length = 8406; | 		static constexpr int flash_length = 8406; | ||||||
| 
 | 
 | ||||||
| 		// Describes the current text mode mapping from in-memory character index
 | 		// Describes the current text mode mapping from in-memory character index
 | ||||||
| 		// to output character.
 | 		// to output character.
 | ||||||
| @@ -247,8 +251,8 @@ class VideoBase { | |||||||
| 		*/ | 		*/ | ||||||
| 		void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const; | 		void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const; | ||||||
| 
 | 
 | ||||||
| 		// Maintain a ClockDeferrer for delayed mode switches.
 | 		// Maintain a DeferredQueue for delayed mode switches.
 | ||||||
| 		ClockDeferrer<Cycles> deferrer_; | 		DeferredQueue<Cycles> deferrer_; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| template <class BusHandler, bool is_iie> class Video: public VideoBase { | template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||||
| @@ -280,7 +284,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 			// Source: Have an Apple Split by Bob Bishop; http://rich12345.tripod.com/aiivideo/softalk.html
 | 			// Source: Have an Apple Split by Bob Bishop; http://rich12345.tripod.com/aiivideo/softalk.html
 | ||||||
| 
 | 
 | ||||||
| 			// Determine column at offset.
 | 			// Determine column at offset.
 | ||||||
| 			int mapped_column = column_ + offset.as_int(); | 			int mapped_column = column_ + int(offset.as_integral()); | ||||||
| 
 | 
 | ||||||
| 			// Map that backwards from the internal pixels-at-start generation to pixels-at-end
 | 			// Map that backwards from the internal pixels-at-start generation to pixels-at-end
 | ||||||
| 			// (so what was column 0 is now column 25).
 | 			// (so what was column 0 is now column 25).
 | ||||||
| @@ -311,7 +315,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 		bool get_is_vertical_blank(Cycles offset) { | 		bool get_is_vertical_blank(Cycles offset) { | ||||||
| 			// Map that backwards from the internal pixels-at-start generation to pixels-at-end
 | 			// Map that backwards from the internal pixels-at-start generation to pixels-at-end
 | ||||||
| 			// (so what was column 0 is now column 25).
 | 			// (so what was column 0 is now column 25).
 | ||||||
| 			int mapped_column = column_ + offset.as_int(); | 			int mapped_column = column_ + int(offset.as_integral()); | ||||||
| 
 | 
 | ||||||
| 			// Map that backwards from the internal pixels-at-start generation to pixels-at-end
 | 			// Map that backwards from the internal pixels-at-start generation to pixels-at-end
 | ||||||
| 			// (so what was column 0 is now column 25).
 | 			// (so what was column 0 is now column 25).
 | ||||||
| @@ -335,30 +339,31 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 
 | 
 | ||||||
| 				A frame is oriented around 65 cycles across, 262 lines down. | 				A frame is oriented around 65 cycles across, 262 lines down. | ||||||
| 			*/ | 			*/ | ||||||
| 			static const int first_sync_line = 220;		// A complete guess. Information needed.
 | 			constexpr int first_sync_line = 220;		// A complete guess. Information needed.
 | ||||||
| 			static const int first_sync_column = 49;	// Also a guess.
 | 			constexpr int first_sync_column = 49;	// Also a guess.
 | ||||||
| 			static const int sync_length = 4;			// One of the two likely candidates.
 | 			constexpr int sync_length = 4;			// One of the two likely candidates.
 | ||||||
| 
 | 
 | ||||||
| 			int int_cycles = cycles.as_int(); | 			int int_cycles = int(cycles.as_integral()); | ||||||
| 			while(int_cycles) { | 			while(int_cycles) { | ||||||
| 				const int cycles_this_line = std::min(65 - column_, int_cycles); | 				const int cycles_this_line = std::min(65 - column_, int_cycles); | ||||||
| 				const int ending_column = column_ + cycles_this_line; | 				const int ending_column = column_ + cycles_this_line; | ||||||
|  | 				const bool is_vertical_sync_line = (row_ >= first_sync_line && row_ < first_sync_line + 3); | ||||||
| 
 | 
 | ||||||
| 				if(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
 | 					// 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).
 | 					// 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_start = std::max(first_sync_column - sync_length, column_); | ||||||
| 					const int blank_end = std::min(first_sync_column, ending_column); | 					const int blank_end = std::min(first_sync_column, ending_column); | ||||||
| 					if(blank_end > blank_start) { | 					if(blank_end > blank_start) { | ||||||
| 						if(blank_start > column_) { | 						if(blank_start > column_) { | ||||||
| 							crt_->output_sync(static_cast<unsigned int>(blank_start - column_) * 14); | 							crt_.output_sync((blank_start - column_) * 14); | ||||||
| 						} | 						} | ||||||
| 						crt_->output_blank(static_cast<unsigned int>(blank_end - blank_start) * 14); | 						crt_.output_blank((blank_end - blank_start) * 14); | ||||||
| 						if(blank_end < ending_column) { | 						if(blank_end < ending_column) { | ||||||
| 							crt_->output_sync(static_cast<unsigned int>(ending_column - blank_end) * 14); | 							crt_.output_sync((ending_column - blank_end) * 14); | ||||||
| 						} | 						} | ||||||
| 					} else { | 					} else { | ||||||
| 						crt_->output_sync(static_cast<unsigned int>(cycles_this_line) * 14); | 						crt_.output_sync(cycles_this_line * 14); | ||||||
| 					} | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					const GraphicsMode line_mode = graphics_mode(row_); | 					const GraphicsMode line_mode = graphics_mode(row_); | ||||||
| @@ -394,7 +399,6 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 							static_cast<size_t>(fetch_end - column_), | 							static_cast<size_t>(fetch_end - column_), | ||||||
| 							&base_stream_[static_cast<size_t>(column_)], | 							&base_stream_[static_cast<size_t>(column_)], | ||||||
| 							&auxiliary_stream_[static_cast<size_t>(column_)]); | 							&auxiliary_stream_[static_cast<size_t>(column_)]); | ||||||
| 						// TODO: should character modes be mapped to character pixel outputs here?
 |  | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					if(row_ < 192) { | 					if(row_ < 192) { | ||||||
| @@ -402,7 +406,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 						// remain where they would naturally be but auxiliary
 | 						// remain where they would naturally be but auxiliary
 | ||||||
| 						// graphics appear to the left of that.
 | 						// graphics appear to the left of that.
 | ||||||
| 						if(!column_) { | 						if(!column_) { | ||||||
| 							pixel_pointer_ = crt_->allocate_write_area(568); | 							pixel_pointer_ = crt_.begin_data(568); | ||||||
| 							graphics_carry_ = 0; | 							graphics_carry_ = 0; | ||||||
| 							was_double_ = true; | 							was_double_ = true; | ||||||
| 						} | 						} | ||||||
| @@ -413,7 +417,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 							const int pixel_row = row_ & 7; | 							const int pixel_row = row_ & 7; | ||||||
| 
 | 
 | ||||||
| 							const bool is_double = Video::is_double_mode(line_mode); | 							const bool is_double = Video::is_double_mode(line_mode); | ||||||
| 							if(!is_double && was_double_) { | 							if(!is_double && was_double_ && pixel_pointer_) { | ||||||
| 								pixel_pointer_[pixel_start*14 + 0] = | 								pixel_pointer_[pixel_start*14 + 0] = | ||||||
| 								pixel_pointer_[pixel_start*14 + 1] = | 								pixel_pointer_[pixel_start*14 + 1] = | ||||||
| 								pixel_pointer_[pixel_start*14 + 2] = | 								pixel_pointer_[pixel_start*14 + 2] = | ||||||
| @@ -424,6 +428,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 							} | 							} | ||||||
| 							was_double_ = is_double; | 							was_double_ = is_double; | ||||||
| 
 | 
 | ||||||
|  | 							if(pixel_pointer_) { | ||||||
| 								switch(line_mode) { | 								switch(line_mode) { | ||||||
| 									case GraphicsMode::Text: | 									case GraphicsMode::Text: | ||||||
| 										output_text( | 										output_text( | ||||||
| @@ -487,8 +492,10 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 
 | 
 | ||||||
| 									default: break; | 									default: break; | ||||||
| 								} | 								} | ||||||
|  | 							} | ||||||
| 
 | 
 | ||||||
| 							if(pixel_end == 40) { | 							if(pixel_end == 40) { | ||||||
|  | 								if(pixel_pointer_) { | ||||||
| 									if(was_double_) { | 									if(was_double_) { | ||||||
| 										pixel_pointer_[560] = pixel_pointer_[561] = pixel_pointer_[562] = pixel_pointer_[563] = | 										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; | 										pixel_pointer_[564] = pixel_pointer_[565] = pixel_pointer_[566] = pixel_pointer_[567] = 0; | ||||||
| @@ -498,14 +505,15 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 										else | 										else | ||||||
| 											pixel_pointer_[567] = 0; | 											pixel_pointer_[567] = 0; | ||||||
| 									} | 									} | ||||||
|  | 								} | ||||||
| 
 | 
 | ||||||
| 								crt_->output_data(568, 568); | 								crt_.output_data(568, 568); | ||||||
| 								pixel_pointer_ = nullptr; | 								pixel_pointer_ = nullptr; | ||||||
| 							} | 							} | ||||||
| 						} | 						} | ||||||
| 					} else { | 					} else { | ||||||
| 						if(column_ < 40 && ending_column >= 40) { | 						if(column_ < 40 && ending_column >= 40) { | ||||||
| 							crt_->output_blank(568); | 							crt_.output_blank(568); | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| @@ -515,11 +523,11 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 					*/ | 					*/ | ||||||
| 
 | 
 | ||||||
| 					if(column_ < first_sync_column && ending_column >= first_sync_column) { | 					if(column_ < first_sync_column && ending_column >= first_sync_column) { | ||||||
| 						crt_->output_blank((first_sync_column - 41)*14 - 1); | 						crt_.output_blank(first_sync_column*14 - 568); | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					if(column_ < (first_sync_column + sync_length) && ending_column >= (first_sync_column + sync_length)) { | 					if(column_ < (first_sync_column + sync_length) && ending_column >= (first_sync_column + sync_length)) { | ||||||
| 						crt_->output_sync(sync_length*14); | 						crt_.output_sync(sync_length*14); | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					int second_blank_start; | 					int second_blank_start; | ||||||
| @@ -527,7 +535,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 						const int colour_burst_start = std::max(first_sync_column + sync_length + 1, column_); | 						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); | 						const int colour_burst_end = std::min(first_sync_column + sync_length + 4, ending_column); | ||||||
| 						if(colour_burst_end > colour_burst_start) { | 						if(colour_burst_end > colour_burst_start) { | ||||||
| 							crt_->output_colour_burst(static_cast<unsigned int>(colour_burst_end - colour_burst_start) * 14, 192); | 							crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, 0); | ||||||
| 						} | 						} | ||||||
| 
 | 
 | ||||||
| 						second_blank_start = std::max(first_sync_column + sync_length + 3, column_); | 						second_blank_start = std::max(first_sync_column + sync_length + 3, column_); | ||||||
| @@ -536,7 +544,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					if(ending_column > second_blank_start) { | 					if(ending_column > second_blank_start) { | ||||||
| 						crt_->output_blank(static_cast<unsigned int>(ending_column - second_blank_start) * 14); | 						crt_.output_blank((ending_column - second_blank_start) * 14); | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| @@ -550,8 +558,13 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					// Add an extra half a colour cycle of blank; this isn't counted in the run_for
 | 					// Add an extra half a colour cycle of blank; this isn't counted in the run_for
 | ||||||
| 					// count explicitly but is promised.
 | 					// count explicitly but is promised. If this is a vertical sync line, output sync
 | ||||||
| 					crt_->output_blank(2); | 					// instead of blank, taking that to be the default level.
 | ||||||
|  | 					if(is_vertical_sync_line) { | ||||||
|  | 						crt_.output_sync(2); | ||||||
|  | 					} else { | ||||||
|  | 						crt_.output_blank(2); | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -588,6 +601,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | |||||||
| 		BusHandler &bus_handler_; | 		BusHandler &bus_handler_; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | } | ||||||
| } | } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
							
								
								
									
										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; | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										88
									
								
								Machines/Apple/Macintosh/Audio.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								Machines/Apple/Macintosh/Audio.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | // | ||||||
|  | //  Audio.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 31/05/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef Audio_hpp | ||||||
|  | #define Audio_hpp | ||||||
|  |  | ||||||
|  | #include "../../../Concurrency/AsyncTaskQueue.hpp" | ||||||
|  | #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||||
|  | #include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||||
|  |  | ||||||
|  | #include <array> | ||||||
|  | #include <atomic> | ||||||
|  |  | ||||||
|  | namespace Apple { | ||||||
|  | namespace Macintosh { | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	Implements the Macintosh's audio output hardware. | ||||||
|  |  | ||||||
|  | 	Designed to be clocked at half the rate of the real hardware — i.e. | ||||||
|  | 	a shade less than 4Mhz. | ||||||
|  | */ | ||||||
|  | class Audio: public ::Outputs::Speaker::SampleSource { | ||||||
|  | 	public: | ||||||
|  | 		Audio(Concurrency::DeferringAsyncTaskQueue &task_queue); | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Macintosh audio is (partly) sourced by the same scanning | ||||||
|  | 			hardware as the video; each line it collects an additional | ||||||
|  | 			word of memory, half of which is used for audio output. | ||||||
|  |  | ||||||
|  | 			Use this method to add a newly-collected sample to the queue. | ||||||
|  | 		*/ | ||||||
|  | 		void post_sample(uint8_t sample); | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Macintosh audio also separately receives an output volume | ||||||
|  | 			level, in the range 0 to 7. | ||||||
|  |  | ||||||
|  | 			Use this method to set the current output volume. | ||||||
|  | 		*/ | ||||||
|  | 		void set_volume(int volume); | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			A further factor in audio output is the on-off toggle. | ||||||
|  | 		*/ | ||||||
|  | 		void set_enabled(bool on); | ||||||
|  |  | ||||||
|  | 		// 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); | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		Concurrency::DeferringAsyncTaskQueue &task_queue_; | ||||||
|  |  | ||||||
|  | 		// A queue of fetched samples; read from by one thread, | ||||||
|  | 		// written to by another. | ||||||
|  | 		struct { | ||||||
|  | 			std::array<uint8_t, 740> buffer; | ||||||
|  | 			size_t read_pointer = 0, write_pointer = 0; | ||||||
|  | 		} sample_queue_; | ||||||
|  |  | ||||||
|  | 		// Emulator-thread stateful variables, to avoid work posting | ||||||
|  | 		// deferral updates if possible. | ||||||
|  | 		int posted_volume_ = 0; | ||||||
|  | 		int posted_enable_mask_ = 0; | ||||||
|  |  | ||||||
|  | 		// Stateful variables, modified from the audio generation | ||||||
|  | 		// thread only. | ||||||
|  | 		int volume_ = 0; | ||||||
|  | 		int enabled_mask_ = 0; | ||||||
|  | 		std::int16_t output_volume_ = 0; | ||||||
|  |  | ||||||
|  | 		std::int16_t volume_multiplier_ = 0; | ||||||
|  | 		std::size_t subcycle_offset_ = 0; | ||||||
|  | 		void set_volume_multiplier(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* Audio_hpp */ | ||||||
							
								
								
									
										34
									
								
								Machines/Apple/Macintosh/DeferredAudio.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								Machines/Apple/Macintosh/DeferredAudio.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | // | ||||||
|  | //  DeferredAudio.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 01/06/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef DeferredAudio_h | ||||||
|  | #define DeferredAudio_h | ||||||
|  |  | ||||||
|  | #include "Audio.hpp" | ||||||
|  | #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||||
|  |  | ||||||
|  | namespace Apple { | ||||||
|  | namespace Macintosh { | ||||||
|  |  | ||||||
|  | struct DeferredAudio { | ||||||
|  | 	Concurrency::DeferringAsyncTaskQueue queue; | ||||||
|  | 	Audio audio; | ||||||
|  | 	Outputs::Speaker::LowpassSpeaker<Audio> speaker; | ||||||
|  | 	HalfCycles time_since_update; | ||||||
|  |  | ||||||
|  | 	DeferredAudio() : audio(queue), speaker(audio) {} | ||||||
|  |  | ||||||
|  | 	void flush() { | ||||||
|  | 		speaker.run_for(queue, time_since_update.flush<Cycles>()); | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* DeferredAudio_h */ | ||||||
							
								
								
									
										56
									
								
								Machines/Apple/Macintosh/DriveSpeedAccumulator.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								Machines/Apple/Macintosh/DriveSpeedAccumulator.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | // | ||||||
|  | //  DriveSpeedAccumulator.cpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 01/06/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #include "DriveSpeedAccumulator.hpp" | ||||||
|  |  | ||||||
|  | using namespace Apple::Macintosh; | ||||||
|  |  | ||||||
|  | void DriveSpeedAccumulator::post_sample(uint8_t sample) { | ||||||
|  | 	if(!delegate_) return; | ||||||
|  |  | ||||||
|  | 	// An Euler-esque approximation is used here: just collect all | ||||||
|  | 	// the samples until there is a certain small quantity of them, | ||||||
|  | 	// then produce a new estimate of rotation speed and start the | ||||||
|  | 	// buffer afresh. | ||||||
|  | 	samples_[sample_pointer_] = sample; | ||||||
|  | 	++sample_pointer_; | ||||||
|  |  | ||||||
|  | 	if(sample_pointer_ == samples_.size()) { | ||||||
|  | 		sample_pointer_ = 0; | ||||||
|  |  | ||||||
|  | 		// The below fits for a function like `a + bc`; it encapsultes the following | ||||||
|  | 		// beliefs: | ||||||
|  | 		// | ||||||
|  | 		//	(i) motor speed is proportional to voltage supplied; | ||||||
|  | 		//	(ii) with pulse-width modulation it's therefore proportional to the duty cycle; | ||||||
|  | 		//	(iii) the Mac pulse-width modulates whatever it reads from the disk speed buffer; | ||||||
|  | 		//	(iv) ... subject to software pulse-width modulation of that pulse-width modulation. | ||||||
|  | 		// | ||||||
|  | 		// So, I believe current motor speed is proportional to a low-pass filtering of | ||||||
|  | 		// the speed buffer. Which I've implemented very coarsely via 'large' bucketed-averages, | ||||||
|  | 		// noting also that exact disk motor speed is always a little approximate. | ||||||
|  |  | ||||||
|  | 		// Sum all samples. | ||||||
|  | 		// TODO: if the above is the correct test, do it on sample receipt rather than | ||||||
|  | 		// bothering with an intermediate buffer. | ||||||
|  | 		int sum = 0; | ||||||
|  | 		for(auto s: samples_) { | ||||||
|  | 			sum += s; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// The formula below was derived from observing values the Mac wrote into its | ||||||
|  | 		// disk-speed buffer. Given that it runs a calibration loop before doing so, | ||||||
|  | 		// I cannot guarantee the accuracy of these numbers beyond being within the | ||||||
|  | 		// range that the computer would accept. | ||||||
|  | 		const float normalised_sum = float(sum) / float(samples_.size()); | ||||||
|  | 		const float rotation_speed = (normalised_sum * 27.08f) - 259.0f; | ||||||
|  |  | ||||||
|  | 		delegate_->drive_speed_accumulator_set_drive_speed(this, rotation_speed); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | // | ||||||
|  | //  DriveSpeedAccumulator.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 01/06/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef DriveSpeedAccumulator_hpp | ||||||
|  | #define DriveSpeedAccumulator_hpp | ||||||
|  |  | ||||||
|  | #include <array> | ||||||
|  | #include <cstddef> | ||||||
|  | #include <cstdint> | ||||||
|  |  | ||||||
|  | namespace Apple { | ||||||
|  | namespace Macintosh { | ||||||
|  |  | ||||||
|  | class DriveSpeedAccumulator { | ||||||
|  | 	public: | ||||||
|  | 		/*! | ||||||
|  | 			Accepts fetched motor control values. | ||||||
|  | 		*/ | ||||||
|  | 		void post_sample(uint8_t sample); | ||||||
|  |  | ||||||
|  | 		struct Delegate { | ||||||
|  | 			virtual void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) = 0; | ||||||
|  | 		}; | ||||||
|  | 		/*! | ||||||
|  | 			Sets the delegate to receive drive speed changes. | ||||||
|  | 		*/ | ||||||
|  | 		void set_delegate(Delegate *delegate) { | ||||||
|  | 			delegate_ = delegate;; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		std::array<uint8_t, 20> samples_; | ||||||
|  | 		std::size_t sample_pointer_ = 0; | ||||||
|  | 		Delegate *delegate_ = nullptr; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* DriveSpeedAccumulator_hpp */ | ||||||
							
								
								
									
										88
									
								
								Machines/Apple/Macintosh/Keyboard.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								Machines/Apple/Macintosh/Keyboard.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | // | ||||||
|  | //  Keyboard.cpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 02/08/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #include "Keyboard.hpp" | ||||||
|  |  | ||||||
|  | using namespace Apple::Macintosh; | ||||||
|  |  | ||||||
|  | uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) { | ||||||
|  | 	using Key = Inputs::Keyboard::Key; | ||||||
|  | 	using MacKey = Apple::Macintosh::Key; | ||||||
|  | 	switch(key) { | ||||||
|  | 		default: return KeyboardMachine::MappedMachine::KeyNotMapped; | ||||||
|  |  | ||||||
|  | #define Bind(x, y) case Key::x: return uint16_t(y) | ||||||
|  |  | ||||||
|  | 		Bind(BackTick, MacKey::BackTick); | ||||||
|  | 		Bind(k1, MacKey::k1);	Bind(k2, MacKey::k2);	Bind(k3, MacKey::k3); | ||||||
|  | 		Bind(k4, MacKey::k4);	Bind(k5, MacKey::k5);	Bind(k6, MacKey::k6); | ||||||
|  | 		Bind(k7, MacKey::k7);	Bind(k8, MacKey::k8);	Bind(k9, MacKey::k9); | ||||||
|  | 		Bind(k0, MacKey::k0); | ||||||
|  | 		Bind(Hyphen, MacKey::Hyphen); | ||||||
|  | 		Bind(Equals, MacKey::Equals); | ||||||
|  | 		Bind(Backspace, MacKey::Backspace); | ||||||
|  |  | ||||||
|  | 		Bind(Tab, MacKey::Tab); | ||||||
|  | 		Bind(Q, MacKey::Q);		Bind(W, MacKey::W);		Bind(E, MacKey::E);		Bind(R, MacKey::R); | ||||||
|  | 		Bind(T, MacKey::T);		Bind(Y, MacKey::Y);		Bind(U, MacKey::U);		Bind(I, MacKey::I); | ||||||
|  | 		Bind(O, MacKey::O);		Bind(P, MacKey::P); | ||||||
|  | 		Bind(OpenSquareBracket, MacKey::OpenSquareBracket); | ||||||
|  | 		Bind(CloseSquareBracket, MacKey::CloseSquareBracket); | ||||||
|  |  | ||||||
|  | 		Bind(CapsLock, MacKey::CapsLock); | ||||||
|  | 		Bind(A, MacKey::A);		Bind(S, MacKey::S);		Bind(D, MacKey::D);		Bind(F, MacKey::F); | ||||||
|  | 		Bind(G, MacKey::G);		Bind(H, MacKey::H);		Bind(J, MacKey::J);		Bind(K, MacKey::K); | ||||||
|  | 		Bind(L, MacKey::L); | ||||||
|  | 		Bind(Semicolon, MacKey::Semicolon); | ||||||
|  | 		Bind(Quote, MacKey::Quote); | ||||||
|  | 		Bind(Enter, MacKey::Return); | ||||||
|  |  | ||||||
|  | 		Bind(LeftShift, MacKey::Shift); | ||||||
|  | 		Bind(Z, MacKey::Z);		Bind(X, MacKey::X);		Bind(C, MacKey::C);		Bind(V, MacKey::V); | ||||||
|  | 		Bind(B, MacKey::B);		Bind(N, MacKey::N);		Bind(M, MacKey::M); | ||||||
|  | 		Bind(Comma, MacKey::Comma); | ||||||
|  | 		Bind(FullStop, MacKey::FullStop); | ||||||
|  | 		Bind(ForwardSlash, MacKey::ForwardSlash); | ||||||
|  | 		Bind(RightShift, MacKey::Shift); | ||||||
|  |  | ||||||
|  | 		Bind(Left, MacKey::Left); | ||||||
|  | 		Bind(Right, MacKey::Right); | ||||||
|  | 		Bind(Up, MacKey::Up); | ||||||
|  | 		Bind(Down, MacKey::Down); | ||||||
|  |  | ||||||
|  | 		Bind(LeftOption, MacKey::Option); | ||||||
|  | 		Bind(RightOption, MacKey::Option); | ||||||
|  | 		Bind(LeftMeta, MacKey::Command); | ||||||
|  | 		Bind(RightMeta, MacKey::Command); | ||||||
|  |  | ||||||
|  | 		Bind(Space, MacKey::Space); | ||||||
|  | 		Bind(Backslash, MacKey::Backslash); | ||||||
|  |  | ||||||
|  | 		Bind(KeypadDelete, MacKey::KeypadDelete); | ||||||
|  | 		Bind(KeypadEquals, MacKey::KeypadEquals); | ||||||
|  | 		Bind(KeypadSlash, MacKey::KeypadSlash); | ||||||
|  | 		Bind(KeypadAsterisk, MacKey::KeypadAsterisk); | ||||||
|  | 		Bind(KeypadMinus, MacKey::KeypadMinus); | ||||||
|  | 		Bind(KeypadPlus, MacKey::KeypadPlus); | ||||||
|  | 		Bind(KeypadEnter, MacKey::KeypadEnter); | ||||||
|  | 		Bind(KeypadDecimalPoint, MacKey::KeypadDecimalPoint); | ||||||
|  |  | ||||||
|  | 		Bind(Keypad9, MacKey::Keypad9); | ||||||
|  | 		Bind(Keypad8, MacKey::Keypad8); | ||||||
|  | 		Bind(Keypad7, MacKey::Keypad7); | ||||||
|  | 		Bind(Keypad6, MacKey::Keypad6); | ||||||
|  | 		Bind(Keypad5, MacKey::Keypad5); | ||||||
|  | 		Bind(Keypad4, MacKey::Keypad4); | ||||||
|  | 		Bind(Keypad3, MacKey::Keypad3); | ||||||
|  | 		Bind(Keypad2, MacKey::Keypad2); | ||||||
|  | 		Bind(Keypad1, MacKey::Keypad1); | ||||||
|  | 		Bind(Keypad0, MacKey::Keypad0); | ||||||
|  |  | ||||||
|  | #undef Bind | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										300
									
								
								Machines/Apple/Macintosh/Keyboard.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								Machines/Apple/Macintosh/Keyboard.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,300 @@ | |||||||
|  | // | ||||||
|  | //  Keyboard.h | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 08/05/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef Apple_Macintosh_Keyboard_hpp | ||||||
|  | #define Apple_Macintosh_Keyboard_hpp | ||||||
|  |  | ||||||
|  | #include "../../KeyboardMachine.hpp" | ||||||
|  | #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||||
|  |  | ||||||
|  | #include <mutex> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
|  | namespace Apple { | ||||||
|  | namespace Macintosh { | ||||||
|  |  | ||||||
|  | constexpr uint16_t KeypadMask = 0x100; | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	Defines the keycodes that could be passed directly to a Macintosh via set_key_pressed. | ||||||
|  | */ | ||||||
|  | enum class Key: uint16_t { | ||||||
|  | 	/* | ||||||
|  | 		See p284 of the Apple Guide to the Macintosh Family Hardware | ||||||
|  | 		for documentation of the mapping below. | ||||||
|  | 	*/ | ||||||
|  | 	BackTick = 0x65, | ||||||
|  | 	k1 = 0x25,	k2 = 0x27,	k3 = 0x29,	k4 = 0x2b,	k5 = 0x2f, | ||||||
|  | 	k6 = 0x2d,	k7 = 0x35,	k8 = 0x39,	k9 = 0x33,	k0 = 0x3b, | ||||||
|  |  | ||||||
|  | 	Hyphen = 0x37, | ||||||
|  | 	Equals = 0x31, | ||||||
|  | 	Backspace = 0x67, | ||||||
|  | 	Tab = 0x61, | ||||||
|  |  | ||||||
|  | 	Q = 0x19, W = 0x1b, E = 0x1d, R = 0x1f, T = 0x23, Y = 0x21, U = 0x41, I = 0x45, O = 0x3f, P = 0x47, | ||||||
|  | 	A = 0x01, S = 0x03, D = 0x05, F = 0x07, G = 0x0b, H = 0x09, J = 0x4d, K = 0x51, L = 0x4b, | ||||||
|  | 	Z = 0x0d, X = 0x0f, C = 0x11, V = 0x13, B = 0x17, N = 0x5b, M = 0x5d, | ||||||
|  |  | ||||||
|  | 	OpenSquareBracket = 0x43, | ||||||
|  | 	CloseSquareBracket = 0x3d, | ||||||
|  | 	Semicolon = 0x53, | ||||||
|  | 	Quote = 0x4f, | ||||||
|  | 	Comma = 0x57, | ||||||
|  | 	FullStop = 0x5f, | ||||||
|  | 	ForwardSlash = 0x59, | ||||||
|  |  | ||||||
|  | 	CapsLock = 0x73, | ||||||
|  | 	Shift = 0x71, | ||||||
|  | 	Option = 0x75, | ||||||
|  | 	Command = 0x6f, | ||||||
|  |  | ||||||
|  | 	Space = 0x63, | ||||||
|  | 	Backslash = 0x55, | ||||||
|  | 	Return = 0x49, | ||||||
|  |  | ||||||
|  | 	Left = KeypadMask | 0x0d, | ||||||
|  | 	Right = KeypadMask | 0x05, | ||||||
|  | 	Up = KeypadMask | 0x1b, | ||||||
|  | 	Down = KeypadMask | 0x11, | ||||||
|  |  | ||||||
|  | 	KeypadDelete = KeypadMask | 0x0f, | ||||||
|  | 	KeypadEquals = KeypadMask | 0x11, | ||||||
|  | 	KeypadSlash = KeypadMask | 0x1b, | ||||||
|  | 	KeypadAsterisk = KeypadMask | 0x05, | ||||||
|  | 	KeypadMinus = KeypadMask | 0x1d, | ||||||
|  | 	KeypadPlus = KeypadMask | 0x0d, | ||||||
|  | 	KeypadEnter = KeypadMask | 0x19, | ||||||
|  | 	KeypadDecimalPoint = KeypadMask | 0x03, | ||||||
|  |  | ||||||
|  | 	Keypad9 = KeypadMask | 0x39, | ||||||
|  | 	Keypad8 = KeypadMask | 0x37, | ||||||
|  | 	Keypad7 = KeypadMask | 0x33, | ||||||
|  | 	Keypad6 = KeypadMask | 0x31, | ||||||
|  | 	Keypad5 = KeypadMask | 0x2f, | ||||||
|  | 	Keypad4 = KeypadMask | 0x2d, | ||||||
|  | 	Keypad3 = KeypadMask | 0x2b, | ||||||
|  | 	Keypad2 = KeypadMask | 0x29, | ||||||
|  | 	Keypad1 = KeypadMask | 0x27, | ||||||
|  | 	Keypad0 = KeypadMask | 0x25 | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class Keyboard { | ||||||
|  | 	public: | ||||||
|  | 		void set_input(bool data) { | ||||||
|  | 			switch(mode_) { | ||||||
|  | 				case Mode::Waiting: | ||||||
|  | 					/* | ||||||
|  | 						"Only the computer can initiate communication over the keyboard lines. When the computer and keyboard | ||||||
|  | 						are turned on, the computer is in charge of the keyboard interface and the keyboard is passive. The | ||||||
|  | 						computer signals that it is ready to begin communication by pulling the Keyboard Data line low." | ||||||
|  | 					*/ | ||||||
|  | 					if(!data) { | ||||||
|  | 						mode_ = Mode::AcceptingCommand; | ||||||
|  | 						phase_ = 0; | ||||||
|  | 						command_ = 0; | ||||||
|  | 					} | ||||||
|  | 				break; | ||||||
|  |  | ||||||
|  | 				case Mode::AcceptingCommand: | ||||||
|  | 					/* Note value, so that it can be latched upon a clock transition. */ | ||||||
|  | 					data_input_ = data; | ||||||
|  | 				break; | ||||||
|  |  | ||||||
|  | 				case Mode::AwaitingEndOfCommand: | ||||||
|  | 					/* | ||||||
|  | 						The last bit of the command leaves the Keyboard Data line low; the computer then indicates that it is ready | ||||||
|  | 						to receive the keyboard's response by setting the Keyboard Data line high. | ||||||
|  | 					*/ | ||||||
|  | 					if(data) { | ||||||
|  | 						mode_ = Mode::PerformingCommand; | ||||||
|  | 						phase_ = 0; | ||||||
|  | 					} | ||||||
|  | 				break; | ||||||
|  |  | ||||||
|  | 				default: | ||||||
|  | 				case Mode::SendingResponse: | ||||||
|  | 					/* This line isn't currently an input; do nothing. */ | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		bool get_clock() { | ||||||
|  | 			return clock_output_; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		bool get_data() { | ||||||
|  | 			return !!(response_ & 0x80); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			The keyboard expects ~10 µs-frequency ticks, i.e. a clock rate of just around 100 kHz. | ||||||
|  | 		*/ | ||||||
|  | 		void run_for(HalfCycles cycle) { | ||||||
|  | 			switch(mode_) { | ||||||
|  | 				default: | ||||||
|  | 				case Mode::Waiting: return; | ||||||
|  |  | ||||||
|  | 				case Mode::AcceptingCommand: { | ||||||
|  | 					/* | ||||||
|  | 						"When the computer is sending data to the keyboard, the keyboard transmits eight cycles of 400 µS each (180 µS low, | ||||||
|  | 						220 µS high) on the Keyboard Clock line. On the falling edge of each keyboard clock cycle, the Macintosh Plus places | ||||||
|  | 						a data bit on the data line and holds it there for 400 µS. The keyboard reads the data bit 80 µS after the rising edge | ||||||
|  | 						of the Keyboard Clock signal." | ||||||
|  | 					*/ | ||||||
|  | 					const auto offset = phase_ % 40; | ||||||
|  | 					clock_output_ = offset >= 18; | ||||||
|  |  | ||||||
|  | 					if(offset == 26) { | ||||||
|  | 						command_ = (command_ << 1) | (data_input_ ? 1 : 0); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					++phase_; | ||||||
|  | 					if(phase_ == 8*40) { | ||||||
|  | 						mode_ = Mode::AwaitingEndOfCommand; | ||||||
|  | 						phase_ = 0; | ||||||
|  | 						clock_output_ = false; | ||||||
|  | 					} | ||||||
|  | 				} break; | ||||||
|  |  | ||||||
|  | 				case Mode::AwaitingEndOfCommand: | ||||||
|  | 					// Time out if the end-of-command seems not to be forthcoming. | ||||||
|  | 					// This is an elaboration on my part; a guess. | ||||||
|  | 					++phase_; | ||||||
|  | 					if(phase_ == 1000) { | ||||||
|  | 						clock_output_ = false; | ||||||
|  | 						mode_ = Mode::Waiting; | ||||||
|  | 						phase_ = 0; | ||||||
|  | 					} | ||||||
|  | 				return; | ||||||
|  |  | ||||||
|  | 				case Mode::PerformingCommand: { | ||||||
|  | 					response_ = perform_command(command_); | ||||||
|  |  | ||||||
|  | 					// Inquiry has a 0.25-second timeout; everything else is instant. | ||||||
|  | 					++phase_; | ||||||
|  | 					if(phase_ == 25000 || command_ != 0x10 || response_ != 0x7b) { | ||||||
|  | 						mode_ = Mode::SendingResponse; | ||||||
|  | 						phase_ = 0; | ||||||
|  | 					} | ||||||
|  | 				} break; | ||||||
|  |  | ||||||
|  | 				case Mode::SendingResponse: { | ||||||
|  | 					/* | ||||||
|  | 						"When sending data to the computer, the keyboard transmits eight cycles of 330 µS each (160 µS low, 170 µS high) | ||||||
|  | 						on the normally high Keyboard Clock line. It places a data bit on the data line 40 µS before the falling edge of each | ||||||
|  | 						clock cycle and maintains it for 330 µS. The VIA in the computer latches the data bit into its shift register on the | ||||||
|  | 						rising edge of the Keyboard Clock signal." | ||||||
|  | 					*/ | ||||||
|  | 					const auto offset = phase_ % 33; | ||||||
|  | 					clock_output_ = offset >= 16; | ||||||
|  |  | ||||||
|  | 					if(offset == 29) { | ||||||
|  | 						response_ <<= 1; | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					++phase_; | ||||||
|  | 					if(phase_ == 8*33) { | ||||||
|  | 						clock_output_ = false; | ||||||
|  | 						mode_ = Mode::Waiting; | ||||||
|  | 						phase_ = 0; | ||||||
|  | 					} | ||||||
|  | 				} break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void enqueue_key_state(uint16_t key, bool is_pressed) { | ||||||
|  | 			// Front insert; messages will be pop_back'd. | ||||||
|  | 			std::lock_guard<decltype(key_queue_mutex_)> lock(key_queue_mutex_); | ||||||
|  |  | ||||||
|  | 			// Keys on the keypad are preceded by a $79 keycode; in the internal naming scheme | ||||||
|  | 			// they are indicated by having bit 8 set. So add the $79 prefix if required. | ||||||
|  | 			if(key & KeypadMask) { | ||||||
|  | 				key_queue_.insert(key_queue_.begin(), 0x79); | ||||||
|  | 			} | ||||||
|  | 			key_queue_.insert(key_queue_.begin(), (is_pressed ? 0x00 : 0x80) | uint8_t(key)); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		/// Performs the pre-ADB Apple keyboard protocol command @c command, returning | ||||||
|  | 		/// the proper result if the command were to terminate now. So, it treats inquiry | ||||||
|  | 		/// and instant as the same command. | ||||||
|  | 		int perform_command(int command) { | ||||||
|  | 			switch(command) { | ||||||
|  | 				case 0x10:		// Inquiry. | ||||||
|  | 				case 0x14: {	// Instant. | ||||||
|  | 					std::lock_guard<decltype(key_queue_mutex_)> lock(key_queue_mutex_); | ||||||
|  | 					if(!key_queue_.empty()) { | ||||||
|  | 						const auto new_message = key_queue_.back(); | ||||||
|  | 						key_queue_.pop_back(); | ||||||
|  | 						return new_message; | ||||||
|  | 					} | ||||||
|  | 				} break; | ||||||
|  |  | ||||||
|  | 				case 0x16:	// Model number. | ||||||
|  | 				return | ||||||
|  | 					0x01 |			// b0: always 1 | ||||||
|  | 					(1 << 1) |		// keyboard model number | ||||||
|  | 					(1 << 4);		// next device number | ||||||
|  | 									// (b7 not set => no next device) | ||||||
|  |  | ||||||
|  | 				case 0x36:	// Test | ||||||
|  | 				return 0x7d;		// 0x7d = ACK, 0x77 = not ACK. | ||||||
|  | 			} | ||||||
|  | 			return 0x7b;	// No key transition. | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// Maintains the current operating mode — a record of what the | ||||||
|  | 		/// keyboard is doing now. | ||||||
|  | 		enum class Mode { | ||||||
|  | 			/// The keyboard is waiting to begin a transaction. | ||||||
|  | 			Waiting, | ||||||
|  | 			/// The keyboard is currently clocking in a new command. | ||||||
|  | 			AcceptingCommand, | ||||||
|  | 			/// The keyboard is waiting for the computer to indicate that it is ready for a response. | ||||||
|  | 			AwaitingEndOfCommand, | ||||||
|  | 			/// The keyboard is in the process of performing the command it most-recently received. | ||||||
|  | 			/// If the command was an 'inquiry', this state may persist for a non-neglibible period of time. | ||||||
|  | 			PerformingCommand, | ||||||
|  | 			/// The keyboard is currently shifting a response back to the computer. | ||||||
|  | 			SendingResponse, | ||||||
|  | 		} mode_ = Mode::Waiting; | ||||||
|  |  | ||||||
|  | 		/// Holds a count of progress through the current @c Mode. Exact meaning depends on mode. | ||||||
|  | 		int phase_ = 0; | ||||||
|  | 		/// Holds the most-recently-received command; the command is shifted into here as it is received | ||||||
|  | 		/// so this may not be valid prior to Mode::PerformingCommand. | ||||||
|  | 		int command_ = 0; | ||||||
|  | 		/// Populated during PerformingCommand as the response to the most-recently-received command, this | ||||||
|  | 		/// is then shifted out to teh host computer. So it is guaranteed valid at the beginning of Mode::SendingResponse, | ||||||
|  | 		/// but not afterwards. | ||||||
|  | 		int response_ = 0; | ||||||
|  |  | ||||||
|  | 		/// The current state of the serial connection's data input. | ||||||
|  | 		bool data_input_ = false; | ||||||
|  | 		/// The current clock output from this keyboard. | ||||||
|  | 		bool clock_output_ = false; | ||||||
|  |  | ||||||
|  | 		/// Guards multithread access to key_queue_. | ||||||
|  | 		std::mutex key_queue_mutex_; | ||||||
|  | 		/// A FIFO queue for key events, in the form they'd be communicated to the Macintosh, | ||||||
|  | 		/// with the newest events towards the front. | ||||||
|  | 		std::vector<uint8_t> key_queue_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	Provides a mapping from idiomatic PC keys to Macintosh keys. | ||||||
|  | */ | ||||||
|  | class KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper { | ||||||
|  | 	uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) final; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* Apple_Macintosh_Keyboard_hpp */ | ||||||
							
								
								
									
										877
									
								
								Machines/Apple/Macintosh/Macintosh.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										877
									
								
								Machines/Apple/Macintosh/Macintosh.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,877 @@ | |||||||
|  | // | ||||||
|  | //  Macintosh.cpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 03/05/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #include "Macintosh.hpp" | ||||||
|  |  | ||||||
|  | #include <array> | ||||||
|  |  | ||||||
|  | #include "DeferredAudio.hpp" | ||||||
|  | #include "DriveSpeedAccumulator.hpp" | ||||||
|  | #include "Keyboard.hpp" | ||||||
|  | #include "RealTimeClock.hpp" | ||||||
|  | #include "Video.hpp" | ||||||
|  |  | ||||||
|  | #include "../../../Activity/Source.hpp" | ||||||
|  | #include "../../CRTMachine.hpp" | ||||||
|  | #include "../../KeyboardMachine.hpp" | ||||||
|  | #include "../../MediaTarget.hpp" | ||||||
|  | #include "../../MouseMachine.hpp" | ||||||
|  |  | ||||||
|  | #include "../../../Inputs/QuadratureMouse/QuadratureMouse.hpp" | ||||||
|  | #include "../../../Outputs/Log.hpp" | ||||||
|  |  | ||||||
|  | #include "../../../ClockReceiver/JustInTime.hpp" | ||||||
|  | #include "../../../ClockReceiver/ClockingHintSource.hpp" | ||||||
|  | #include "../../../Configurable/StandardOptions.hpp" | ||||||
|  |  | ||||||
|  | //#define LOG_TRACE | ||||||
|  |  | ||||||
|  | #include "../../../Components/5380/ncr5380.hpp" | ||||||
|  | #include "../../../Components/6522/6522.hpp" | ||||||
|  | #include "../../../Components/8530/z8530.hpp" | ||||||
|  | #include "../../../Components/DiskII/IWM.hpp" | ||||||
|  | #include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp" | ||||||
|  | #include "../../../Processors/68000/68000.hpp" | ||||||
|  |  | ||||||
|  | #include "../../../Storage/MassStorage/SCSI/SCSI.hpp" | ||||||
|  | #include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp" | ||||||
|  | #include "../../../Storage/MassStorage/Encodings/MacintoshVolume.hpp" | ||||||
|  |  | ||||||
|  | #include "../../../Analyser/Static/Macintosh/Target.hpp" | ||||||
|  |  | ||||||
|  | #include "../../Utility/MemoryPacker.hpp" | ||||||
|  | #include "../../Utility/MemoryFuzzer.hpp" | ||||||
|  |  | ||||||
|  | namespace { | ||||||
|  |  | ||||||
|  | constexpr int CLOCK_RATE = 7833600; | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | namespace Apple { | ||||||
|  | namespace Macintosh { | ||||||
|  |  | ||||||
|  | std::vector<std::unique_ptr<Configurable::Option>> get_options() { | ||||||
|  | 	return Configurable::standard_options( | ||||||
|  | 		static_cast<Configurable::StandardOptions>(Configurable::QuickBoot) | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachine: | ||||||
|  | 	public Machine, | ||||||
|  | 	public CRTMachine::Machine, | ||||||
|  | 	public MediaTarget::Machine, | ||||||
|  | 	public MouseMachine::Machine, | ||||||
|  | 	public CPU::MC68000::BusHandler, | ||||||
|  | 	public KeyboardMachine::MappedMachine, | ||||||
|  | 	public Zilog::SCC::z8530::Delegate, | ||||||
|  | 	public Activity::Source, | ||||||
|  | 	public Configurable::Device, | ||||||
|  | 	public DriveSpeedAccumulator::Delegate, | ||||||
|  | 	public ClockingHint::Observer { | ||||||
|  | 	public: | ||||||
|  | 		using Target = Analyser::Static::Macintosh::Target; | ||||||
|  |  | ||||||
|  | 		ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : | ||||||
|  | 			KeyboardMachine::MappedMachine({ | ||||||
|  | 				Inputs::Keyboard::Key::LeftShift, Inputs::Keyboard::Key::RightShift, | ||||||
|  | 				Inputs::Keyboard::Key::LeftOption, Inputs::Keyboard::Key::RightOption, | ||||||
|  | 				Inputs::Keyboard::Key::LeftMeta, Inputs::Keyboard::Key::RightMeta, | ||||||
|  | 			}), | ||||||
|  | 		 	mc68000_(*this), | ||||||
|  | 		 	iwm_(CLOCK_RATE), | ||||||
|  | 		 	video_(audio_, drive_speed_accumulator_), | ||||||
|  | 		 	via_(via_port_handler_), | ||||||
|  | 		 	via_port_handler_(*this, clock_, keyboard_, audio_, iwm_, mouse_), | ||||||
|  | 		 	scsi_bus_(CLOCK_RATE * 2), | ||||||
|  | 		 	scsi_(scsi_bus_, CLOCK_RATE * 2), | ||||||
|  | 		 	hard_drive_(scsi_bus_, 6 /* SCSI ID */), | ||||||
|  | 		 	drives_{ | ||||||
|  | 		 		{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke}, | ||||||
|  | 		 		{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke} | ||||||
|  | 			}, | ||||||
|  | 			mouse_(1) { | ||||||
|  |  | ||||||
|  | 			// Select a ROM name and determine the proper ROM and RAM sizes | ||||||
|  | 			// based on the machine model. | ||||||
|  | 			using Model = Analyser::Static::Macintosh::Target::Model; | ||||||
|  | 			const std::string machine_name = "Macintosh"; | ||||||
|  | 			uint32_t ram_size, rom_size; | ||||||
|  | 			std::vector<ROMMachine::ROM> rom_descriptions; | ||||||
|  | 			switch(model) { | ||||||
|  | 				default: | ||||||
|  | 				case Model::Mac128k: | ||||||
|  | 					ram_size = 128*1024; | ||||||
|  | 					rom_size = 64*1024; | ||||||
|  | 					rom_descriptions.emplace_back(machine_name, "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28); | ||||||
|  | 				break; | ||||||
|  | 				case Model::Mac512k: | ||||||
|  | 					ram_size = 512*1024; | ||||||
|  | 					rom_size = 64*1024; | ||||||
|  | 					rom_descriptions.emplace_back(machine_name, "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d); | ||||||
|  | 				break; | ||||||
|  | 				case Model::Mac512ke: | ||||||
|  | 				case Model::MacPlus: { | ||||||
|  | 					ram_size = ((model == Model::MacPlus) ? 4096 : 512)*1024; | ||||||
|  | 					rom_size = 128*1024; | ||||||
|  | 					const std::initializer_list<uint32_t> crc32s = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e }; | ||||||
|  | 					rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s); | ||||||
|  | 				} break; | ||||||
|  | 			} | ||||||
|  | 			ram_mask_ = (ram_size >> 1) - 1; | ||||||
|  | 			rom_mask_ = (rom_size >> 1) - 1; | ||||||
|  | 			ram_.resize(ram_size >> 1); | ||||||
|  | 			video_.set_ram(ram_.data(), ram_mask_); | ||||||
|  |  | ||||||
|  | 			// Grab a copy of the ROM and convert it into big-endian data. | ||||||
|  | 			const auto roms = rom_fetcher(rom_descriptions); | ||||||
|  | 			if(!roms[0]) { | ||||||
|  | 				throw ROMMachine::Error::MissingROMs; | ||||||
|  | 			} | ||||||
|  | 			roms[0]->resize(rom_size); | ||||||
|  | 			Memory::PackBigEndian16(*roms[0], rom_); | ||||||
|  |  | ||||||
|  | 			// Randomise memory contents. | ||||||
|  | 			Memory::Fuzz(ram_); | ||||||
|  |  | ||||||
|  | 			// Attach the drives to the IWM. | ||||||
|  | 			iwm_->set_drive(0, &drives_[0]); | ||||||
|  | 			iwm_->set_drive(1, &drives_[1]); | ||||||
|  |  | ||||||
|  | 			// If they are 400kb drives, also attach them to the drive-speed accumulator. | ||||||
|  | 			if(!drives_[0].is_800k() || !drives_[1].is_800k()) { | ||||||
|  | 				drive_speed_accumulator_.set_delegate(this); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Make sure interrupt changes from the SCC are observed. | ||||||
|  | 			scc_.set_delegate(this); | ||||||
|  |  | ||||||
|  | 			// Also watch for changes in clocking requirement from the SCSI chip. | ||||||
|  | 			if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) { | ||||||
|  | 				scsi_bus_.set_clocking_hint_observer(this); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// The Mac runs at 7.8336mHz. | ||||||
|  | 			set_clock_rate(double(CLOCK_RATE)); | ||||||
|  | 			audio_.speaker.set_input_rate(float(CLOCK_RATE) / 2.0f); | ||||||
|  |  | ||||||
|  | 			// Insert any supplied media. | ||||||
|  | 			insert_media(target.media); | ||||||
|  |  | ||||||
|  | 			// Set the immutables of the memory map. | ||||||
|  | 			setup_memory_map(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		~ConcreteMachine() { | ||||||
|  | 			audio_.queue.flush(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { | ||||||
|  | 			video_.set_scan_target(scan_target); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		Outputs::Speaker::Speaker *get_speaker() override { | ||||||
|  | 			return &audio_.speaker; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void run_for(const Cycles cycles) override { | ||||||
|  | 			mc68000_.run_for(cycles); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		using Microcycle = CPU::MC68000::Microcycle; | ||||||
|  |  | ||||||
|  | 		forceinline HalfCycles perform_bus_operation(const Microcycle &cycle, int is_supervisor) { | ||||||
|  | 			// Advance time. | ||||||
|  | 			advance_time(cycle.length); | ||||||
|  |  | ||||||
|  | 			// A null cycle leaves nothing else to do. | ||||||
|  | 			if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return HalfCycles(0); | ||||||
|  |  | ||||||
|  | 			// Grab the value on the address bus, at word precision. | ||||||
|  | 			uint32_t word_address = cycle.active_operation_word_address(); | ||||||
|  |  | ||||||
|  | 			// Everything above E0 0000 is signalled as being on the peripheral bus. | ||||||
|  | 			mc68000_.set_is_peripheral_address(word_address >= 0x700000); | ||||||
|  |  | ||||||
|  | 			// All code below deals only with reads and writes — cycles in which a | ||||||
|  | 			// data select is active. So quit now if this is not the active part of | ||||||
|  | 			// a read or write. | ||||||
|  | 			// | ||||||
|  | 			// The 68000 uses 6800-style autovectored interrupts, so the mere act of | ||||||
|  | 			// having set VPA above deals with those given that the generated address | ||||||
|  | 			// for interrupt acknowledge cycles always has all bits set except the | ||||||
|  | 			// lowest explicit address lines. | ||||||
|  | 			if(!cycle.data_select_active() || (cycle.operation & Microcycle::InterruptAcknowledge)) return HalfCycles(0); | ||||||
|  |  | ||||||
|  | 			// Grab the word-precision address being accessed. | ||||||
|  | 			uint16_t *memory_base = nullptr; | ||||||
|  | 			HalfCycles delay; | ||||||
|  | 			switch(memory_map_[word_address >> 16]) { | ||||||
|  | 				default: assert(false); | ||||||
|  |  | ||||||
|  | 				case BusDevice::Unassigned: | ||||||
|  | 					fill_unmapped(cycle); | ||||||
|  | 				return delay; | ||||||
|  |  | ||||||
|  | 				case BusDevice::VIA: { | ||||||
|  | 					if(*cycle.address & 1) { | ||||||
|  | 						fill_unmapped(cycle); | ||||||
|  | 					} else { | ||||||
|  | 						const int register_address = word_address >> 8; | ||||||
|  |  | ||||||
|  | 						// VIA accesses are via address 0xefe1fe + register*512, | ||||||
|  | 						// which at word precision is 0x77f0ff + register*256. | ||||||
|  | 						if(cycle.operation & Microcycle::Read) { | ||||||
|  | 							cycle.value->halves.low = via_.read(register_address); | ||||||
|  | 						} else { | ||||||
|  | 							via_.write(register_address, cycle.value->halves.low); | ||||||
|  | 						} | ||||||
|  |  | ||||||
|  | 						if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff; | ||||||
|  | 					} | ||||||
|  | 				} return delay; | ||||||
|  |  | ||||||
|  | 				case BusDevice::PhaseRead: { | ||||||
|  | 					if(cycle.operation & Microcycle::Read) { | ||||||
|  | 						cycle.value->halves.low = phase_ & 7; | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff; | ||||||
|  | 				} return delay; | ||||||
|  |  | ||||||
|  | 				case BusDevice::IWM: { | ||||||
|  | 					if(*cycle.address & 1) { | ||||||
|  | 						const int register_address = word_address >> 8; | ||||||
|  |  | ||||||
|  | 						// The IWM; this is a purely polled device, so can be run on demand. | ||||||
|  | 						if(cycle.operation & Microcycle::Read) { | ||||||
|  | 							cycle.value->halves.low = iwm_->read(register_address); | ||||||
|  | 						} else { | ||||||
|  | 							iwm_->write(register_address, cycle.value->halves.low); | ||||||
|  | 						} | ||||||
|  |  | ||||||
|  | 						if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff; | ||||||
|  | 					} else { | ||||||
|  | 						fill_unmapped(cycle); | ||||||
|  | 					} | ||||||
|  | 				} return delay; | ||||||
|  |  | ||||||
|  | 				case BusDevice::SCSI: { | ||||||
|  | 					const int register_address = word_address >> 3; | ||||||
|  | 					const bool dma_acknowledge = word_address & 0x100; | ||||||
|  |  | ||||||
|  | 					// Even accesses = read; odd = write. | ||||||
|  | 					if(*cycle.address & 1) { | ||||||
|  | 						// Odd access => this is a write. Data will be in the upper byte. | ||||||
|  | 						if(cycle.operation & Microcycle::Read) { | ||||||
|  | 							scsi_.write(register_address, 0xff, dma_acknowledge); | ||||||
|  | 						} else { | ||||||
|  | 							if(cycle.operation & Microcycle::SelectWord) { | ||||||
|  | 								scsi_.write(register_address, cycle.value->halves.high, dma_acknowledge); | ||||||
|  | 							} else { | ||||||
|  | 								scsi_.write(register_address, cycle.value->halves.low, dma_acknowledge); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} else { | ||||||
|  | 						// Even access => this is a read. | ||||||
|  | 						if(cycle.operation & Microcycle::Read) { | ||||||
|  | 							const auto result = scsi_.read(register_address, dma_acknowledge); | ||||||
|  | 							if(cycle.operation & Microcycle::SelectWord) { | ||||||
|  | 								// Data is loaded on the top part of the bus only. | ||||||
|  | 								cycle.value->full = uint16_t((result << 8) | 0xff); | ||||||
|  | 							} else { | ||||||
|  | 								cycle.value->halves.low = result; | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} return delay; | ||||||
|  |  | ||||||
|  | 				case BusDevice::SCCReadResetPhase: { | ||||||
|  | 					// Any word access here adjusts phase. | ||||||
|  | 					if(cycle.operation & Microcycle::SelectWord) { | ||||||
|  | 						adjust_phase(); | ||||||
|  | 					} else { | ||||||
|  | 						// A0 = 1 => reset; A0 = 0 => read. | ||||||
|  | 						if(*cycle.address & 1) { | ||||||
|  | 							scc_.reset(); | ||||||
|  |  | ||||||
|  | 							if(cycle.operation & Microcycle::Read) { | ||||||
|  | 								cycle.value->halves.low = 0xff; | ||||||
|  | 							} | ||||||
|  | 						} else { | ||||||
|  | 							const auto read = scc_.read(int(word_address)); | ||||||
|  | 							if(cycle.operation & Microcycle::Read) { | ||||||
|  | 								cycle.value->halves.low = read; | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} return delay; | ||||||
|  |  | ||||||
|  | 				case BusDevice::SCCWrite: { | ||||||
|  | 					// Any word access here adjusts phase. | ||||||
|  | 					if(cycle.operation & Microcycle::SelectWord) { | ||||||
|  | 						adjust_phase(); | ||||||
|  | 					} else { | ||||||
|  | 						if(*cycle.address & 1) { | ||||||
|  | 							if(cycle.operation & Microcycle::Read) { | ||||||
|  | 								scc_.write(int(word_address), 0xff); | ||||||
|  | 								cycle.value->halves.low = 0xff; | ||||||
|  | 							} else { | ||||||
|  | 								scc_.write(int(word_address), cycle.value->halves.low); | ||||||
|  | 							} | ||||||
|  | 						} else { | ||||||
|  | 							fill_unmapped(cycle); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} return delay; | ||||||
|  |  | ||||||
|  | 				case BusDevice::RAM: { | ||||||
|  | 					// This is coupled with the Macintosh implementation of video; the magic | ||||||
|  | 					// constant should probably be factored into the Video class. | ||||||
|  | 					// It embodies knowledge of the fact that video (and audio) will always | ||||||
|  | 					// be fetched from the final $d900 bytes (i.e. $6c80 words) of memory. | ||||||
|  | 					// (And that ram_mask_ = ram size - 1). | ||||||
|  | 					if(word_address > ram_mask_ - 0x6c80) | ||||||
|  | 						update_video(); | ||||||
|  |  | ||||||
|  | 					memory_base = ram_.data(); | ||||||
|  | 					word_address &= ram_mask_; | ||||||
|  |  | ||||||
|  | 					// Apply a delay due to video contention if applicable; scheme applied: | ||||||
|  | 					// only every other access slot is available during the period of video | ||||||
|  | 					// output. I believe this to be correct for the 128k, 512k and Plus. | ||||||
|  | 					// More research to do on other models. | ||||||
|  | 					if(video_is_outputting() && ram_subcycle_ < 8) { | ||||||
|  | 						delay = HalfCycles(8 - ram_subcycle_); | ||||||
|  | 						advance_time(delay); | ||||||
|  | 					} | ||||||
|  | 				} break; | ||||||
|  |  | ||||||
|  | 				case BusDevice::ROM: { | ||||||
|  | 					if(!(cycle.operation & Microcycle::Read)) return delay; | ||||||
|  | 					memory_base = rom_; | ||||||
|  | 					word_address &= rom_mask_; | ||||||
|  | 				} break; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// If control has fallen through to here, the access is either a read from ROM, or a read or write to RAM. | ||||||
|  | 			switch(cycle.operation & (Microcycle::SelectWord | Microcycle::SelectByte | Microcycle::Read)) { | ||||||
|  | 				default: | ||||||
|  | 				break; | ||||||
|  |  | ||||||
|  | 				case Microcycle::SelectWord | Microcycle::Read: | ||||||
|  | 					cycle.value->full = memory_base[word_address]; | ||||||
|  | 				break; | ||||||
|  | 				case Microcycle::SelectByte | Microcycle::Read: | ||||||
|  | 					cycle.value->halves.low = uint8_t(memory_base[word_address] >> cycle.byte_shift()); | ||||||
|  | 				break; | ||||||
|  | 				case Microcycle::SelectWord: | ||||||
|  | 					memory_base[word_address] = cycle.value->full; | ||||||
|  | 				break; | ||||||
|  | 				case Microcycle::SelectByte: | ||||||
|  | 					memory_base[word_address] = uint16_t( | ||||||
|  | 						(cycle.value->halves.low << cycle.byte_shift()) | | ||||||
|  | 						(memory_base[word_address] & cycle.untouched_byte_mask()) | ||||||
|  | 					); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			return delay; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void flush() { | ||||||
|  | 			// Flush the video before the audio queue; in a Mac the | ||||||
|  | 			// video is responsible for providing part of the | ||||||
|  | 			// audio signal, so the two aren't as distinct as in | ||||||
|  | 			// most machines. | ||||||
|  | 			update_video(); | ||||||
|  |  | ||||||
|  | 			// As above: flush audio after video. | ||||||
|  | 			via_.flush(); | ||||||
|  | 			audio_.queue.perform(); | ||||||
|  |  | ||||||
|  | 			// Experimental? | ||||||
|  | 			iwm_.flush(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void set_rom_is_overlay(bool rom_is_overlay) { | ||||||
|  | 			ROM_is_overlay_ = rom_is_overlay; | ||||||
|  |  | ||||||
|  | 			using Model = Analyser::Static::Macintosh::Target::Model; | ||||||
|  | 			switch(model) { | ||||||
|  | 				case Model::Mac128k: | ||||||
|  | 				case Model::Mac512k: | ||||||
|  | 				case Model::Mac512ke: | ||||||
|  | 					populate_memory_map(0, [rom_is_overlay] (std::function<void(int target, BusDevice device)> map_to) { | ||||||
|  | 						// Addresses up to $80 0000 aren't affected by this bit. | ||||||
|  | 						if(rom_is_overlay) { | ||||||
|  | 							// Up to $60 0000 mirrors of the ROM alternate with unassigned areas every $10 0000 byes. | ||||||
|  | 							for(int c = 0; c < 0x600000; c += 0x100000) { | ||||||
|  | 								map_to(c + 0x100000, (c & 0x100000) ? BusDevice::Unassigned : BusDevice::ROM); | ||||||
|  | 							} | ||||||
|  | 							map_to(0x800000, BusDevice::RAM); | ||||||
|  | 						} else { | ||||||
|  | 							map_to(0x400000, BusDevice::RAM); | ||||||
|  | 							map_to(0x500000, BusDevice::ROM); | ||||||
|  | 							map_to(0x800000, BusDevice::Unassigned); | ||||||
|  | 						} | ||||||
|  | 					}); | ||||||
|  | 				break; | ||||||
|  |  | ||||||
|  | 				case Model::MacPlus: | ||||||
|  | 					populate_memory_map(0, [rom_is_overlay] (std::function<void(int target, BusDevice device)> map_to) { | ||||||
|  | 						// Addresses up to $80 0000 aren't affected by this bit. | ||||||
|  | 						if(rom_is_overlay) { | ||||||
|  | 							for(int c = 0; c < 0x580000; c += 0x20000) { | ||||||
|  | 								map_to(c + 0x20000, ((c & 0x100000) || (c & 0x20000)) ? BusDevice::Unassigned : BusDevice::ROM); | ||||||
|  | 							} | ||||||
|  | 							map_to(0x600000, BusDevice::SCSI); | ||||||
|  | 							map_to(0x800000, BusDevice::RAM); | ||||||
|  | 						} else { | ||||||
|  | 							map_to(0x400000, BusDevice::RAM); | ||||||
|  | 							for(int c = 0x400000; c < 0x580000; c += 0x20000) { | ||||||
|  | 								map_to(c + 0x20000, ((c & 0x100000) || (c & 0x20000)) ? BusDevice::Unassigned : BusDevice::ROM); | ||||||
|  | 							} | ||||||
|  | 							map_to(0x600000, BusDevice::SCSI); | ||||||
|  | 							map_to(0x800000, BusDevice::Unassigned); | ||||||
|  | 						} | ||||||
|  | 					}); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		bool video_is_outputting() { | ||||||
|  | 			return video_.is_outputting(time_since_video_update_); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) { | ||||||
|  | 			update_video(); | ||||||
|  | 			video_.set_use_alternate_buffers(use_alternate_screen_buffer, use_alternate_audio_buffer); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		bool insert_media(const Analyser::Static::Media &media) override { | ||||||
|  | 			if(media.disks.empty() && media.mass_storage_devices.empty()) | ||||||
|  | 				return false; | ||||||
|  |  | ||||||
|  | 			// TODO: shouldn't allow disks to be replaced like this, as the Mac | ||||||
|  | 			// uses software eject. Will need to expand messaging ability of | ||||||
|  | 			// insert_media. | ||||||
|  | 			if(!media.disks.empty()) { | ||||||
|  | 				if(drives_[0].has_disk()) | ||||||
|  | 					drives_[1].set_disk(media.disks[0]); | ||||||
|  | 				else | ||||||
|  | 					drives_[0].set_disk(media.disks[0]); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// TODO: allow this only at machine startup. | ||||||
|  | 			if(!media.mass_storage_devices.empty()) { | ||||||
|  | 				const auto volume = dynamic_cast<Storage::MassStorage::Encodings::Macintosh::Volume *>(media.mass_storage_devices.front().get()); | ||||||
|  | 				if(volume) { | ||||||
|  | 					volume->set_drive_type(Storage::MassStorage::Encodings::Macintosh::DriveType::SCSI); | ||||||
|  | 				} | ||||||
|  | 				hard_drive_->set_storage(media.mass_storage_devices.front()); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			return true; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// MARK: Keyboard input. | ||||||
|  |  | ||||||
|  | 		KeyboardMapper *get_keyboard_mapper() override { | ||||||
|  | 			return &keyboard_mapper_; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void set_key_state(uint16_t key, bool is_pressed) override { | ||||||
|  | 			keyboard_.enqueue_key_state(key, is_pressed); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// TODO: clear all keys. | ||||||
|  |  | ||||||
|  | 		// MARK: Interrupt updates. | ||||||
|  |  | ||||||
|  | 		void did_change_interrupt_status(Zilog::SCC::z8530 *sender, bool new_status) override { | ||||||
|  | 			update_interrupt_input(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void update_interrupt_input() { | ||||||
|  | 			// Update interrupt input. | ||||||
|  | 			// TODO: does this really cascade like this? | ||||||
|  | 			if(scc_.get_interrupt_line()) { | ||||||
|  | 				mc68000_.set_interrupt_level(2); | ||||||
|  | 			} else if(via_.get_interrupt_line()) { | ||||||
|  | 				mc68000_.set_interrupt_level(1); | ||||||
|  | 			} else { | ||||||
|  | 				mc68000_.set_interrupt_level(0); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// MARK: - Activity Source | ||||||
|  | 		void set_activity_observer(Activity::Observer *observer) override { | ||||||
|  | 			iwm_->set_activity_observer(observer); | ||||||
|  |  | ||||||
|  | 			if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) { | ||||||
|  | 				scsi_bus_.set_activity_observer(observer); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// MARK: - Configuration options. | ||||||
|  | 		std::vector<std::unique_ptr<Configurable::Option>> get_options() override { | ||||||
|  | 			return Apple::Macintosh::get_options(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void set_selections(const Configurable::SelectionSet &selections_by_option) override { | ||||||
|  | 			bool quick_boot; | ||||||
|  | 			if(Configurable::get_quick_boot(selections_by_option, quick_boot)) { | ||||||
|  | 				if(quick_boot) { | ||||||
|  | 					// Cf. Big Mess o' Wires' disassembly of the Mac Plus ROM, and the | ||||||
|  | 					// test at $E00. TODO: adapt as(/if?) necessary for other Macs. | ||||||
|  | 					ram_[0x02ae >> 1] = 0x40; | ||||||
|  | 					ram_[0x02b0 >> 1] = 0x00; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		Configurable::SelectionSet get_accurate_selections() override { | ||||||
|  | 			Configurable::SelectionSet selection_set; | ||||||
|  | 			Configurable::append_quick_boot_selection(selection_set, false); | ||||||
|  | 			return selection_set; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		Configurable::SelectionSet get_user_friendly_selections() override { | ||||||
|  | 			Configurable::SelectionSet selection_set; | ||||||
|  | 			Configurable::append_quick_boot_selection(selection_set, true); | ||||||
|  | 			return selection_set; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override { | ||||||
|  | 			scsi_bus_is_clocked_ = scsi_bus_.preferred_clocking() != ClockingHint::Preference::None; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) override { | ||||||
|  | 			iwm_.flush(); | ||||||
|  | 			drives_[0].set_rotation_speed(speed); | ||||||
|  | 			drives_[1].set_rotation_speed(speed); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		forceinline void adjust_phase() { | ||||||
|  | 			++phase_; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		forceinline void fill_unmapped(const Microcycle &cycle) { | ||||||
|  | 			if(!(cycle.operation & Microcycle::Read)) return; | ||||||
|  | 			if(cycle.operation & Microcycle::SelectWord) { | ||||||
|  | 				cycle.value->full = 0xffff; | ||||||
|  | 			} else { | ||||||
|  | 				cycle.value->halves.low = 0xff; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// Advances all non-CPU components by @c duration half cycles. | ||||||
|  | 		forceinline void advance_time(HalfCycles duration) { | ||||||
|  | 			time_since_video_update_ += duration; | ||||||
|  | 			iwm_ += duration; | ||||||
|  | 			ram_subcycle_ = (ram_subcycle_ + duration.as_integral()) & 15; | ||||||
|  |  | ||||||
|  | 			// The VIA runs at one-tenth of the 68000's clock speed, in sync with the E clock. | ||||||
|  | 			// See: Guide to the Macintosh Hardware Family p149 (PDF p188). Some extra division | ||||||
|  | 			// may occur here in order to provide VSYNC at a proper moment. | ||||||
|  | 			// Possibly route vsync. | ||||||
|  | 			if(time_since_video_update_ < time_until_video_event_) { | ||||||
|  | 				via_clock_ += duration; | ||||||
|  | 				via_.run_for(via_clock_.divide(HalfCycles(10))); | ||||||
|  | 			} else { | ||||||
|  | 				auto via_time_base = time_since_video_update_ - duration; | ||||||
|  | 				auto via_cycles_outstanding = duration; | ||||||
|  | 				while(time_until_video_event_ < time_since_video_update_) { | ||||||
|  | 					const auto via_cycles = time_until_video_event_ - via_time_base; | ||||||
|  | 					via_time_base = HalfCycles(0); | ||||||
|  | 					via_cycles_outstanding -= via_cycles; | ||||||
|  |  | ||||||
|  | 					via_clock_ += via_cycles; | ||||||
|  | 					via_.run_for(via_clock_.divide(HalfCycles(10))); | ||||||
|  |  | ||||||
|  | 					video_.run_for(time_until_video_event_); | ||||||
|  | 					time_since_video_update_ -= time_until_video_event_; | ||||||
|  | 					time_until_video_event_ = video_.get_next_sequence_point(); | ||||||
|  |  | ||||||
|  | 					via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !video_.vsync()); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				via_clock_ += via_cycles_outstanding; | ||||||
|  | 				via_.run_for(via_clock_.divide(HalfCycles(10))); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// The keyboard also has a clock, albeit a very slow one — 100,000 cycles/second. | ||||||
|  | 			// Its clock and data lines are connected to the VIA. | ||||||
|  | 			keyboard_clock_ += duration; | ||||||
|  | 			const auto keyboard_ticks = keyboard_clock_.divide(HalfCycles(CLOCK_RATE / 100000)); | ||||||
|  | 			if(keyboard_ticks > HalfCycles(0)) { | ||||||
|  | 				keyboard_.run_for(keyboard_ticks); | ||||||
|  | 				via_.set_control_line_input(MOS::MOS6522::Port::B, MOS::MOS6522::Line::Two, keyboard_.get_data()); | ||||||
|  | 				via_.set_control_line_input(MOS::MOS6522::Port::B, MOS::MOS6522::Line::One, keyboard_.get_clock()); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Feed mouse inputs within at most 1250 cycles of each other. | ||||||
|  | 			if(mouse_.has_steps()) { | ||||||
|  | 				time_since_mouse_update_ += duration; | ||||||
|  | 				const auto mouse_ticks = time_since_mouse_update_.divide(HalfCycles(2500)); | ||||||
|  | 				if(mouse_ticks > HalfCycles(0)) { | ||||||
|  | 					mouse_.prepare_step(); | ||||||
|  | 					scc_.set_dcd(0, mouse_.get_channel(1) & 1); | ||||||
|  | 					scc_.set_dcd(1, mouse_.get_channel(0) & 1); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// TODO: SCC should be clocked at a divide-by-two, if and when it actually has | ||||||
|  | 			// anything connected. | ||||||
|  |  | ||||||
|  | 			// Consider updating the real-time clock. | ||||||
|  | 			real_time_clock_ += duration; | ||||||
|  | 			auto ticks = real_time_clock_.divide_cycles(Cycles(CLOCK_RATE)).as_integral(); | ||||||
|  | 			while(ticks--) { | ||||||
|  | 				clock_.update(); | ||||||
|  | 				// TODO: leave a delay between toggling the input rather than using this coupled hack. | ||||||
|  | 				via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two, true); | ||||||
|  | 				via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two, false); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Update the SCSI if currently active. | ||||||
|  | 			if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) { | ||||||
|  | 				if(scsi_bus_is_clocked_) scsi_bus_.run_for(duration); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		forceinline void update_video() { | ||||||
|  | 			video_.run_for(time_since_video_update_.flush<HalfCycles>()); | ||||||
|  | 			time_until_video_event_ = video_.get_next_sequence_point(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		Inputs::Mouse &get_mouse() override { | ||||||
|  | 			return mouse_; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		using IWMActor = JustInTimeActor<IWM, 1, 1, HalfCycles, Cycles>; | ||||||
|  |  | ||||||
|  | 		class VIAPortHandler: public MOS::MOS6522::PortHandler { | ||||||
|  | 			public: | ||||||
|  | 				VIAPortHandler(ConcreteMachine &machine, RealTimeClock &clock, Keyboard &keyboard, DeferredAudio &audio, IWMActor &iwm, Inputs::QuadratureMouse &mouse) : | ||||||
|  | 					machine_(machine), clock_(clock), keyboard_(keyboard), audio_(audio), iwm_(iwm), mouse_(mouse) {} | ||||||
|  |  | ||||||
|  | 				using Port = MOS::MOS6522::Port; | ||||||
|  | 				using Line = MOS::MOS6522::Line; | ||||||
|  |  | ||||||
|  | 				void set_port_output(Port port, uint8_t value, uint8_t direction_mask) { | ||||||
|  | 					/* | ||||||
|  | 						Peripheral lines: keyboard data, interrupt configuration. | ||||||
|  | 						(See p176 [/215]) | ||||||
|  | 					*/ | ||||||
|  | 					switch(port) { | ||||||
|  | 						case Port::A: | ||||||
|  | 							/* | ||||||
|  | 								Port A: | ||||||
|  | 									b7:	[input] SCC wait/request (/W/REQA and /W/REQB wired together for a logical OR) | ||||||
|  | 									b6:	0 = alternate screen buffer, 1 = main screen buffer | ||||||
|  | 									b5:	floppy disk SEL state control (upper/lower head "among other things") | ||||||
|  | 									b4:	1 = use ROM overlay memory map, 0 = use ordinary memory map | ||||||
|  | 									b3:	0 = use alternate sound buffer, 1 = use ordinary sound buffer | ||||||
|  | 									b2–b0:	audio output volume | ||||||
|  | 							*/ | ||||||
|  | 							iwm_->set_select(!!(value & 0x20)); | ||||||
|  |  | ||||||
|  | 							machine_.set_use_alternate_buffers(!(value & 0x40), !(value&0x08)); | ||||||
|  | 							machine_.set_rom_is_overlay(!!(value & 0x10)); | ||||||
|  |  | ||||||
|  | 							audio_.flush(); | ||||||
|  | 							audio_.audio.set_volume(value & 7); | ||||||
|  | 						break; | ||||||
|  |  | ||||||
|  | 						case Port::B: | ||||||
|  | 							/* | ||||||
|  | 								Port B: | ||||||
|  | 									b7:	0 = sound enabled, 1 = sound disabled | ||||||
|  | 									b6:	[input] 0 = video beam in visible portion of line, 1 = outside | ||||||
|  | 									b5:	[input] mouse y2 | ||||||
|  | 									b4:	[input] mouse x2 | ||||||
|  | 									b3:	[input] 0 = mouse button down, 1 = up | ||||||
|  | 									b2:	0 = real-time clock enabled, 1 = disabled | ||||||
|  | 									b1:	clock's data-clock line | ||||||
|  | 									b0:	clock's serial data line | ||||||
|  | 							*/ | ||||||
|  | 							if(value & 0x4) clock_.abort(); | ||||||
|  | 							else clock_.set_input(!!(value & 0x2), !!(value & 0x1)); | ||||||
|  |  | ||||||
|  | 							audio_.flush(); | ||||||
|  | 							audio_.audio.set_enabled(!(value & 0x80)); | ||||||
|  | 						break; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				uint8_t get_port_input(Port port) { | ||||||
|  | 					switch(port) { | ||||||
|  | 						case Port::A: | ||||||
|  | //							printf("6522 r A\n"); | ||||||
|  | 						return 0x00;	// TODO: b7 = SCC wait/request | ||||||
|  |  | ||||||
|  | 						case Port::B: | ||||||
|  | 						return uint8_t( | ||||||
|  | 							((mouse_.get_button_mask() & 1) ? 0x00 : 0x08) | | ||||||
|  | 							((mouse_.get_channel(0) & 2) << 3) | | ||||||
|  | 							((mouse_.get_channel(1) & 2) << 4) | | ||||||
|  | 							(clock_.get_data() ? 0x02 : 0x00) | | ||||||
|  | 							(machine_.video_is_outputting() ? 0x00 : 0x40) | ||||||
|  | 						); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					// Should be unreachable. | ||||||
|  | 					return 0xff; | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				void set_control_line_output(Port port, Line line, bool value) { | ||||||
|  | 					/* | ||||||
|  | 						Keyboard wiring (I believe): | ||||||
|  | 						CB2 = data		(input/output) | ||||||
|  | 						CB1 = clock		(input) | ||||||
|  |  | ||||||
|  | 						CA2 is used for receiving RTC interrupts. | ||||||
|  | 						CA1 is used for receiving vsync. | ||||||
|  | 					*/ | ||||||
|  | 					if(port == Port::B && line == Line::Two) { | ||||||
|  | 						keyboard_.set_input(value); | ||||||
|  | 					} | ||||||
|  | 					else LOG("Unhandled control line output: " << (port ? 'B' : 'A') << int(line)); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				void run_for(HalfCycles duration) { | ||||||
|  | 					// The 6522 enjoys a divide-by-ten, so multiply back up here to make the | ||||||
|  | 					// divided-by-two clock the audio works on. | ||||||
|  | 					audio_.time_since_update += HalfCycles(duration.as_integral() * 5); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				void flush() { | ||||||
|  | 					audio_.flush(); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				void set_interrupt_status(bool status) { | ||||||
|  | 					machine_.update_interrupt_input(); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 			private: | ||||||
|  | 				ConcreteMachine &machine_; | ||||||
|  | 				RealTimeClock &clock_; | ||||||
|  | 				Keyboard &keyboard_; | ||||||
|  | 				DeferredAudio &audio_; | ||||||
|  | 				IWMActor &iwm_; | ||||||
|  | 				Inputs::QuadratureMouse &mouse_; | ||||||
|  | 		}; | ||||||
|  |  | ||||||
|  | 		CPU::MC68000::Processor<ConcreteMachine, true> mc68000_; | ||||||
|  |  | ||||||
|  | 		DriveSpeedAccumulator drive_speed_accumulator_; | ||||||
|  | 		IWMActor iwm_; | ||||||
|  |  | ||||||
|  | 		DeferredAudio audio_; | ||||||
|  | 		Video video_; | ||||||
|  |  | ||||||
|  | 		RealTimeClock clock_; | ||||||
|  | 		Keyboard keyboard_; | ||||||
|  |  | ||||||
|  | 		MOS::MOS6522::MOS6522<VIAPortHandler> via_; | ||||||
|  |  		VIAPortHandler via_port_handler_; | ||||||
|  |  | ||||||
|  |  		Zilog::SCC::z8530 scc_; | ||||||
|  | 		SCSI::Bus scsi_bus_; | ||||||
|  |  		NCR::NCR5380::NCR5380 scsi_; | ||||||
|  | 		SCSI::Target::Target<SCSI::DirectAccessDevice> hard_drive_; | ||||||
|  |  		bool scsi_bus_is_clocked_ = false; | ||||||
|  |  | ||||||
|  |  		HalfCycles via_clock_; | ||||||
|  |  		HalfCycles real_time_clock_; | ||||||
|  |  		HalfCycles keyboard_clock_; | ||||||
|  |  		HalfCycles time_since_video_update_; | ||||||
|  |  		HalfCycles time_until_video_event_; | ||||||
|  |  		HalfCycles time_since_mouse_update_; | ||||||
|  |  | ||||||
|  | 		bool ROM_is_overlay_ = true; | ||||||
|  | 		int phase_ = 1; | ||||||
|  | 		int ram_subcycle_ = 0; | ||||||
|  |  | ||||||
|  | 		DoubleDensityDrive drives_[2]; | ||||||
|  | 		Inputs::QuadratureMouse mouse_; | ||||||
|  |  | ||||||
|  | 		Apple::Macintosh::KeyboardMapper keyboard_mapper_; | ||||||
|  |  | ||||||
|  | 		enum class BusDevice { | ||||||
|  | 			RAM, ROM, VIA, IWM, SCCWrite, SCCReadResetPhase, SCSI, PhaseRead, Unassigned | ||||||
|  | 		}; | ||||||
|  |  | ||||||
|  | 		/// Divides the 24-bit address space up into $20000 (i.e. 128kb) segments, recording | ||||||
|  | 		/// which device is current mapped in each area. Keeping it in a table is a bit faster | ||||||
|  | 		/// than the multi-level address inspection that is otherwise required, as well as | ||||||
|  | 		/// simplifying slightly the handling of different models. | ||||||
|  | 		/// | ||||||
|  | 		/// So: index with the top 7 bits of the 24-bit address. | ||||||
|  | 		BusDevice memory_map_[128]; | ||||||
|  |  | ||||||
|  | 		void setup_memory_map() { | ||||||
|  | 			// Apply the power-up memory map, i.e. assume that ROM_is_overlay_ = true; | ||||||
|  | 			// start by calling into set_rom_is_overlay to seed everything up to $800000. | ||||||
|  | 			set_rom_is_overlay(true); | ||||||
|  |  | ||||||
|  | 			populate_memory_map(0x800000, [] (std::function<void(int target, BusDevice device)> map_to) { | ||||||
|  | 				map_to(0x900000, BusDevice::Unassigned); | ||||||
|  | 				map_to(0xa00000, BusDevice::SCCReadResetPhase); | ||||||
|  | 				map_to(0xb00000, BusDevice::Unassigned); | ||||||
|  | 				map_to(0xc00000, BusDevice::SCCWrite); | ||||||
|  | 				map_to(0xd00000, BusDevice::Unassigned); | ||||||
|  | 				map_to(0xe00000, BusDevice::IWM); | ||||||
|  | 				map_to(0xe80000, BusDevice::Unassigned); | ||||||
|  | 				map_to(0xf00000, BusDevice::VIA); | ||||||
|  | 				map_to(0xf80000, BusDevice::PhaseRead); | ||||||
|  | 				map_to(0x1000000, BusDevice::Unassigned); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		void populate_memory_map(int start_address, std::function<void(std::function<void(int, BusDevice)>)> populator) { | ||||||
|  | 			// Define semantics for below; map_to will write from the current cursor position | ||||||
|  | 			// to the supplied 24-bit address, setting a particular mapped device. | ||||||
|  | 			int segment = start_address >> 17; | ||||||
|  | 			auto map_to = [&segment, this](int address, BusDevice device) { | ||||||
|  | 				for(; segment < address >> 17; ++segment) { | ||||||
|  | 					this->memory_map_[segment] = device; | ||||||
|  | 				} | ||||||
|  | 			}; | ||||||
|  |  | ||||||
|  | 			populator(map_to); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		uint32_t ram_mask_ = 0; | ||||||
|  | 		uint32_t rom_mask_ = 0; | ||||||
|  | 		uint16_t rom_[64*1024];	// i.e. up to 128kb in size. | ||||||
|  | 		std::vector<uint16_t> ram_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | using namespace Apple::Macintosh; | ||||||
|  |  | ||||||
|  | Machine *Machine::Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { | ||||||
|  | 	auto *const mac_target = dynamic_cast<const Analyser::Static::Macintosh::Target *>(target); | ||||||
|  |  | ||||||
|  | 	using Model = Analyser::Static::Macintosh::Target::Model; | ||||||
|  | 	switch(mac_target->model) { | ||||||
|  | 		default: | ||||||
|  | 		case Model::Mac128k:	return new ConcreteMachine<Model::Mac128k>(*mac_target, rom_fetcher); | ||||||
|  | 		case Model::Mac512k:	return new ConcreteMachine<Model::Mac512k>(*mac_target, rom_fetcher); | ||||||
|  | 		case Model::Mac512ke:	return new ConcreteMachine<Model::Mac512ke>(*mac_target, rom_fetcher); | ||||||
|  | 		case Model::MacPlus:	return new ConcreteMachine<Model::MacPlus>(*mac_target, rom_fetcher); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Machine::~Machine() {} | ||||||
							
								
								
									
										33
									
								
								Machines/Apple/Macintosh/Macintosh.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								Machines/Apple/Macintosh/Macintosh.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | // | ||||||
|  | //  Macintosh.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 03/05/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef Macintosh_hpp | ||||||
|  | #define Macintosh_hpp | ||||||
|  |  | ||||||
|  | #include "../../../Configurable/Configurable.hpp" | ||||||
|  | #include "../../../Analyser/Static/StaticAnalyser.hpp" | ||||||
|  | #include "../../ROMMachine.hpp" | ||||||
|  |  | ||||||
|  | namespace Apple { | ||||||
|  | namespace Macintosh { | ||||||
|  |  | ||||||
|  | std::vector<std::unique_ptr<Configurable::Option>> get_options(); | ||||||
|  |  | ||||||
|  | class Machine { | ||||||
|  | 	public: | ||||||
|  | 		virtual ~Machine(); | ||||||
|  |  | ||||||
|  | 		/// Creates and returns a Macintosh. | ||||||
|  | 		static Machine *Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* Macintosh_hpp */ | ||||||
							
								
								
									
										173
									
								
								Machines/Apple/Macintosh/RealTimeClock.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								Machines/Apple/Macintosh/RealTimeClock.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | |||||||
|  | // | ||||||
|  | //  RealTimeClock.hpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 07/05/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #ifndef RealTimeClock_hpp | ||||||
|  | #define RealTimeClock_hpp | ||||||
|  |  | ||||||
|  | #include "../../Utility/MemoryFuzzer.hpp" | ||||||
|  |  | ||||||
|  | namespace Apple { | ||||||
|  | namespace Macintosh { | ||||||
|  |  | ||||||
|  | /*! | ||||||
|  | 	Models the storage component of Apple's real-time clock. | ||||||
|  |  | ||||||
|  | 	Since tracking of time is pushed to this class, it is assumed | ||||||
|  | 	that whomever is translating real time into emulated time | ||||||
|  | 	will notify the VIA of a potential interrupt. | ||||||
|  | */ | ||||||
|  | class RealTimeClock { | ||||||
|  | 	public: | ||||||
|  | 		RealTimeClock() { | ||||||
|  | 			// TODO: this should persist, if possible, rather than | ||||||
|  | 			// being default initialised. | ||||||
|  | 			const uint8_t default_data[] = { | ||||||
|  | 				0xa8, 0x00, 0x00, 0x00, | ||||||
|  | 				0xcc, 0x0a, 0xcc, 0x0a, | ||||||
|  | 				0x00, 0x00, 0x00, 0x00, | ||||||
|  | 				0x00, 0x02, 0x63, 0x00, | ||||||
|  | 				0x03, 0x88, 0x00, 0x4c | ||||||
|  | 			}; | ||||||
|  | 			memcpy(data_, default_data, sizeof(data_)); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Advances the clock by 1 second. | ||||||
|  |  | ||||||
|  | 			The caller should also notify the VIA. | ||||||
|  | 		*/ | ||||||
|  | 		void update() { | ||||||
|  | 			for(int c = 0; c < 4; ++c) { | ||||||
|  | 				++seconds_[c]; | ||||||
|  | 				if(seconds_[c]) break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Sets the current clock and data inputs to the clock. | ||||||
|  | 		*/ | ||||||
|  | 		void set_input(bool clock, bool data) { | ||||||
|  | 			/* | ||||||
|  | 				Documented commands: | ||||||
|  |  | ||||||
|  | 					z0000001		Seconds register 0 (lowest order byte) | ||||||
|  | 					z0000101		Seconds register 1 | ||||||
|  | 					z0001001		Seconds register 2 | ||||||
|  | 					z0001101		Seconds register 3 | ||||||
|  | 					00110001		Test register (write only) | ||||||
|  | 					00110101		Write-protect register (write only) | ||||||
|  | 					z010aa01		RAM addresses 0x10 - 0x13 | ||||||
|  | 					z1aaaa01		RAM addresses 0x00 – 0x0f | ||||||
|  |  | ||||||
|  | 					z = 1 => a read; z = 0 => a write. | ||||||
|  |  | ||||||
|  | 				The top bit of the write-protect register enables (0) or disables (1) | ||||||
|  | 				writes to other locations. | ||||||
|  |  | ||||||
|  | 				All the documentation says about the test register is to set the top | ||||||
|  | 				two bits to 0 for normal operation. Abnormal operation is undefined. | ||||||
|  |  | ||||||
|  | 				The data line is valid when the clock transitions to level 0. | ||||||
|  | 			*/ | ||||||
|  |  | ||||||
|  | 			if(clock && !previous_clock_) { | ||||||
|  | 				// Shift into the command_ register, no matter what. | ||||||
|  | 				command_ = uint16_t((command_ << 1) | (data ? 1 : 0)); | ||||||
|  | 				result_ <<= 1; | ||||||
|  |  | ||||||
|  | 				// Increment phase. | ||||||
|  | 				++phase_; | ||||||
|  |  | ||||||
|  | 				// When phase hits 8, inspect the command. | ||||||
|  | 				// If it's a read, prepare a result. | ||||||
|  | 				if(phase_ == 8) { | ||||||
|  | 					if(command_ & 0x80) { | ||||||
|  | 						// A read. | ||||||
|  | 						const auto address = (command_ >> 2) & 0x1f; | ||||||
|  |  | ||||||
|  | 						// Begin pessimistically. | ||||||
|  | 						result_ = 0xff; | ||||||
|  |  | ||||||
|  | 						if(address < 4) { | ||||||
|  | 							result_ = seconds_[address]; | ||||||
|  | 						} else if(address >= 0x10) { | ||||||
|  | 							result_ = data_[address & 0xf]; | ||||||
|  | 						} else if(address >= 0x8 && address <= 0xb) { | ||||||
|  | 							result_ = data_[0x10 + (address & 0x3)]; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// If phase hits 16 and this was a read command, | ||||||
|  | 				// just stop. If it was a write command, do the | ||||||
|  | 				// actual write. | ||||||
|  | 				if(phase_ == 16) { | ||||||
|  | 					if(!(command_ & 0x8000)) { | ||||||
|  | 						// A write. | ||||||
|  |  | ||||||
|  | 						const auto address = (command_ >> 10) & 0x1f; | ||||||
|  | 						const uint8_t value = uint8_t(command_ & 0xff); | ||||||
|  |  | ||||||
|  | 						// First test: is this to the write-protect register? | ||||||
|  | 						if(address == 0xd) { | ||||||
|  | 							write_protect_ = value; | ||||||
|  | 						} | ||||||
|  |  | ||||||
|  | 						// No other writing is permitted if the write protect | ||||||
|  | 						// register won't allow it. | ||||||
|  | 						if(!(write_protect_ & 0x80)) { | ||||||
|  | 							if(address < 4) { | ||||||
|  | 								seconds_[address] = value; | ||||||
|  | 							} else if(address >= 0x10) { | ||||||
|  | 								data_[address & 0xf] = value; | ||||||
|  | 							} else if(address >= 0x8 && address <= 0xb) { | ||||||
|  | 								data_[0x10 + (address & 0x3)] = value; | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					// A phase of 16 always ends the command, so reset here. | ||||||
|  | 					abort(); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			previous_clock_ = clock; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Reads the current data output level from the clock. | ||||||
|  | 		*/ | ||||||
|  | 		bool get_data() { | ||||||
|  | 			return !!(result_ & 0x80); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 			Announces that a serial command has been aborted. | ||||||
|  | 		*/ | ||||||
|  | 		void abort() { | ||||||
|  | 			result_ = 0; | ||||||
|  | 			phase_ = 0; | ||||||
|  | 			command_ = 0; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		uint8_t data_[0x14]; | ||||||
|  | 		uint8_t seconds_[4]; | ||||||
|  | 		uint8_t write_protect_; | ||||||
|  |  | ||||||
|  | 		int phase_ = 0; | ||||||
|  | 		uint16_t command_; | ||||||
|  | 		uint8_t result_ = 0; | ||||||
|  |  | ||||||
|  | 		bool previous_clock_ = false; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif /* RealTimeClock_hpp */ | ||||||
							
								
								
									
										184
									
								
								Machines/Apple/Macintosh/Video.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								Machines/Apple/Macintosh/Video.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | |||||||
|  | // | ||||||
|  | //  Video.cpp | ||||||
|  | //  Clock Signal | ||||||
|  | // | ||||||
|  | //  Created by Thomas Harte on 03/05/2019. | ||||||
|  | //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #include "Video.hpp" | ||||||
|  |  | ||||||
|  | #include <algorithm> | ||||||
|  |  | ||||||
|  | using namespace Apple::Macintosh; | ||||||
|  |  | ||||||
|  | // Re: CRT timings, see the Apple Guide to the Macintosh Hardware Family, | ||||||
|  | // bottom of page 400: | ||||||
|  | // | ||||||
|  | //	"For each scan line, 512 pixels are drawn on the screen ... | ||||||
|  | //	The horizontal blanking interval takes the time of an additional 192 pixels" | ||||||
|  | // | ||||||
|  | // And, at the top of 401: | ||||||
|  | // | ||||||
|  | //	"The visible portion of a full-screen display consists of 342 horizontal scan lines... | ||||||
|  | //	During the vertical blanking interval, the turned-off beam ... traces out an additional 28 scan lines," | ||||||
|  | // | ||||||
|  | Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) : | ||||||
|  | 	audio_(audio), | ||||||
|  | 	drive_speed_accumulator_(drive_speed_accumulator), | ||||||
|  |  	crt_(704, 1, 370, Outputs::Display::ColourSpace::YIQ, 1, 1, 6, false, Outputs::Display::InputDataType::Luminance1) { | ||||||
|  |  | ||||||
|  |  	crt_.set_display_type(Outputs::Display::DisplayType::RGB); | ||||||
|  | 	crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f)); | ||||||
|  | 	crt_.set_aspect_ratio(1.73f);	// The Mac uses a non-standard scanning area. | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||||
|  | 	crt_.set_scan_target(scan_target); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Video::run_for(HalfCycles duration) { | ||||||
|  | 	// Determine the current video and audio bases. These values don't appear to be latched, they apply immediately. | ||||||
|  | 	const size_t video_base = (use_alternate_screen_buffer_ ? (0xffff2700 >> 1) : (0xffffa700 >> 1)) & ram_mask_; | ||||||
|  | 	const size_t audio_base = (use_alternate_audio_buffer_ ? (0xffffa100 >> 1) : (0xfffffd00 >> 1)) & ram_mask_; | ||||||
|  |  | ||||||
|  | 	// The number of HalfCycles is literally the number of pixel clocks to move through, | ||||||
|  | 	// since pixel output occurs at twice the processor clock. So divide by 16 to get | ||||||
|  | 	// the number of fetches. | ||||||
|  | 	while(duration > HalfCycles(0)) { | ||||||
|  | 		const auto pixel_start = frame_position_ % line_length; | ||||||
|  | 		const int line = int((frame_position_ / line_length).as_integral()); | ||||||
|  |  | ||||||
|  | 		const auto cycles_left_in_line = std::min(line_length - pixel_start, duration); | ||||||
|  |  | ||||||
|  | 		// Line timing, entirely invented as I can find exactly zero words of documentation: | ||||||
|  | 		// | ||||||
|  | 		//	First 342 lines: | ||||||
|  | 		// | ||||||
|  | 		//	First 32 words = pixels; | ||||||
|  | 		//	next 5 words = right border; | ||||||
|  | 		//	next 2 words = sync level; | ||||||
|  | 		//	final 5 words = left border. | ||||||
|  | 		// | ||||||
|  | 		//	Then 12 lines of border, 3 of sync, 11 more of border. | ||||||
|  |  | ||||||
|  | 		const int first_word = int(pixel_start.as_integral()) >> 4; | ||||||
|  | 		const int final_word = int((pixel_start + cycles_left_in_line).as_integral()) >> 4; | ||||||
|  |  | ||||||
|  | 		if(first_word != final_word) { | ||||||
|  | 			if(line < 342) { | ||||||
|  | 				// If there are any pixels left to output, do so. | ||||||
|  | 				if(first_word < 32) { | ||||||
|  | 					const int final_pixel_word = std::min(final_word, 32); | ||||||
|  |  | ||||||
|  | 					if(!first_word) { | ||||||
|  | 						pixel_buffer_ = crt_.begin_data(512); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					if(pixel_buffer_) { | ||||||
|  | 						for(int c = first_word; c < final_pixel_word; ++c) { | ||||||
|  | 							uint16_t pixels = ram_[video_base + video_address_] ^ 0xffff; | ||||||
|  | 							++video_address_; | ||||||
|  |  | ||||||
|  | 							pixel_buffer_[15] = pixels & 0x01; | ||||||
|  | 							pixel_buffer_[14] = pixels & 0x02; | ||||||
|  | 							pixel_buffer_[13] = pixels & 0x04; | ||||||
|  | 							pixel_buffer_[12] = pixels & 0x08; | ||||||
|  | 							pixel_buffer_[11] = pixels & 0x10; | ||||||
|  | 							pixel_buffer_[10] = pixels & 0x20; | ||||||
|  | 							pixel_buffer_[9] = pixels & 0x40; | ||||||
|  | 							pixel_buffer_[8] = pixels & 0x80; | ||||||
|  |  | ||||||
|  | 							pixels >>= 8; | ||||||
|  | 							pixel_buffer_[7] = pixels & 0x01; | ||||||
|  | 							pixel_buffer_[6] = pixels & 0x02; | ||||||
|  | 							pixel_buffer_[5] = pixels & 0x04; | ||||||
|  | 							pixel_buffer_[4] = pixels & 0x08; | ||||||
|  | 							pixel_buffer_[3] = pixels & 0x10; | ||||||
|  | 							pixel_buffer_[2] = pixels & 0x20; | ||||||
|  | 							pixel_buffer_[1] = pixels & 0x40; | ||||||
|  | 							pixel_buffer_[0] = pixels & 0x80; | ||||||
|  |  | ||||||
|  | 							pixel_buffer_ += 16; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					if(final_pixel_word == 32) { | ||||||
|  | 						crt_.output_data(512); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if(first_word < sync_start && final_word >= sync_start)	crt_.output_blank((sync_start - 32) * 16); | ||||||
|  | 				if(first_word < sync_end && final_word >= sync_end)		crt_.output_sync((sync_end - sync_start) * 16); | ||||||
|  | 				if(final_word == 44)									crt_.output_blank((44 - sync_end) * 16); | ||||||
|  | 			} else if(final_word == 44) { | ||||||
|  | 				if(line >= 353 && line < 356) { | ||||||
|  | 					/* Output a sync line. */ | ||||||
|  | 					crt_.output_sync(sync_start * 16); | ||||||
|  | 					crt_.output_blank((sync_end - sync_start) * 16); | ||||||
|  | 					crt_.output_sync((44 - sync_end) * 16); | ||||||
|  | 				} else { | ||||||
|  | 					/* Output a blank line. */ | ||||||
|  | 					crt_.output_blank(sync_start * 16); | ||||||
|  | 					crt_.output_sync((sync_end - sync_start) * 16); | ||||||
|  | 					crt_.output_blank((44 - sync_end) * 16); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Audio and disk fetches occur "just before video data". | ||||||
|  | 			if(final_word == 44) { | ||||||
|  | 				const uint16_t audio_word = ram_[audio_address_ + audio_base]; | ||||||
|  | 				++audio_address_; | ||||||
|  | 				audio_.audio.post_sample(audio_word >> 8); | ||||||
|  | 				drive_speed_accumulator_.post_sample(audio_word & 0xff); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		duration -= cycles_left_in_line; | ||||||
|  | 		frame_position_ = frame_position_ + cycles_left_in_line; | ||||||
|  | 		if(frame_position_ == frame_length) { | ||||||
|  | 			frame_position_ = HalfCycles(0); | ||||||
|  | 			/* | ||||||
|  | 				Video: $1A700 and the alternate buffer starts at $12700; for a 512K Macintosh, add $60000 to these numbers. | ||||||
|  | 			*/ | ||||||
|  | 			video_address_ = 0; | ||||||
|  |  | ||||||
|  | 			/* | ||||||
|  | 				"The main sound buffer is at $1FD00 in a 128K Macintosh, and the alternate buffer is at $1A100; | ||||||
|  | 				for a 512K Macintosh, add $60000 to these values." | ||||||
|  | 			*/ | ||||||
|  | 			audio_address_ = 0; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Video::vsync() { | ||||||
|  | 	const auto line = (frame_position_ / line_length).as_integral(); | ||||||
|  | 	return line >= 353 && line < 356; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | HalfCycles Video::get_next_sequence_point() { | ||||||
|  | 	const auto line = (frame_position_ / line_length).as_integral(); | ||||||
|  | 	if(line >= 353 && line < 356) { | ||||||
|  | 		// Currently in vsync, so get time until start of line 357, | ||||||
|  | 		// when vsync will end. | ||||||
|  | 		return HalfCycles(356) * line_length - frame_position_; | ||||||
|  | 	} else { | ||||||
|  | 		// Not currently in vsync, so get time until start of line 353. | ||||||
|  | 		const auto start_of_vsync = HalfCycles(353) * line_length; | ||||||
|  | 		if(frame_position_ < start_of_vsync) | ||||||
|  | 			return start_of_vsync - frame_position_; | ||||||
|  | 		else | ||||||
|  | 			return start_of_vsync + HalfCycles(number_of_lines) * line_length - frame_position_; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Video::set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) { | ||||||
|  | 	use_alternate_screen_buffer_ = use_alternate_screen_buffer; | ||||||
|  | 	use_alternate_audio_buffer_ = use_alternate_audio_buffer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Video::set_ram(uint16_t *ram, uint32_t mask) { | ||||||
|  | 	ram_ = ram; | ||||||
|  | 	ram_mask_ = mask; | ||||||
|  | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user