mirror of
				https://github.com/TomHarte/CLK.git
				synced 2025-10-30 14:16:04 +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; | ||||
|  | ||||
| MultiCRTMachine::MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::mutex &machines_mutex) : | ||||
| MultiCRTMachine::MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::recursive_mutex &machines_mutex) : | ||||
| 	machines_(machines), machines_mutex_(machines_mutex), queues_(machines.size()) { | ||||
| 	speaker_ = MultiSpeaker::create(machines); | ||||
| } | ||||
| @@ -25,7 +25,7 @@ void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Ma | ||||
| 	std::condition_variable condition; | ||||
| 	std::mutex mutex; | ||||
| 	{ | ||||
| 		std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| 		std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| 		std::lock_guard<std::mutex> lock(mutex); | ||||
| 		outstanding_machines = machines_.size(); | ||||
|  | ||||
| @@ -46,29 +46,18 @@ void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Ma | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::perform_serial(const std::function<void (::CRTMachine::Machine *)> &function) { | ||||
| 	std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| 	std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		CRTMachine::Machine *crt_machine = machine->crt_machine(); | ||||
| 		CRTMachine::Machine *const crt_machine = machine->crt_machine(); | ||||
| 		if(crt_machine) function(crt_machine); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::setup_output(float aspect_ratio) { | ||||
| 	perform_serial([=](::CRTMachine::Machine *machine) { | ||||
| 		machine->setup_output(aspect_ratio); | ||||
| 	}); | ||||
| } | ||||
| void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||
| 	scan_target_ = scan_target; | ||||
|  | ||||
| void MultiCRTMachine::close_output() { | ||||
| 	perform_serial([=](::CRTMachine::Machine *machine) { | ||||
| 		machine->close_output(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| Outputs::CRT::CRT *MultiCRTMachine::get_crt() { | ||||
| 	std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| 	CRTMachine::Machine *crt_machine = machines_.front()->crt_machine(); | ||||
| 	return crt_machine ? crt_machine->get_crt() : nullptr; | ||||
| 	CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine(); | ||||
| 	if(crt_machine) crt_machine->set_scan_target(scan_target); | ||||
| } | ||||
|  | ||||
| Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() { | ||||
| @@ -84,6 +73,14 @@ void MultiCRTMachine::run_for(Time::Seconds duration) { | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::did_change_machine_order() { | ||||
| 	if(scan_target_) scan_target_->will_change_owner(); | ||||
|  | ||||
| 	perform_serial([=](::CRTMachine::Machine *machine) { | ||||
| 		machine->set_scan_target(nullptr); | ||||
| 	}); | ||||
| 	CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine(); | ||||
| 	if(crt_machine) crt_machine->set_scan_target(scan_target_); | ||||
|  | ||||
| 	if(speaker_) { | ||||
| 		speaker_->set_new_front_machine(machines_.front().get()); | ||||
| 	} | ||||
|   | ||||
| @@ -31,7 +31,7 @@ namespace Dynamic { | ||||
| */ | ||||
| class MultiCRTMachine: public CRTMachine::Machine { | ||||
| 	public: | ||||
| 		MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::mutex &machines_mutex); | ||||
| 		MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::recursive_mutex &machines_mutex); | ||||
|  | ||||
| 		/*! | ||||
| 			Informs the MultiCRTMachine that the order of machines has changed; the MultiCRTMachine | ||||
| @@ -53,19 +53,18 @@ class MultiCRTMachine: public CRTMachine::Machine { | ||||
| 		} | ||||
|  | ||||
| 		// Below is the standard CRTMachine::Machine interface; see there for documentation. | ||||
| 		void setup_output(float aspect_ratio) override; | ||||
| 		void close_output() override; | ||||
| 		Outputs::CRT::CRT *get_crt() override; | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override; | ||||
| 		Outputs::Speaker::Speaker *get_speaker() override; | ||||
| 		void run_for(Time::Seconds duration) override; | ||||
|  | ||||
| 	private: | ||||
| 		void run_for(const Cycles cycles) override {} | ||||
| 		const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_; | ||||
| 		std::mutex &machines_mutex_; | ||||
| 		std::recursive_mutex &machines_mutex_; | ||||
| 		std::vector<Concurrency::AsyncTaskQueue> queues_; | ||||
| 		MultiSpeaker *speaker_ = nullptr; | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| 		Outputs::Display::ScanTarget *scan_target_ = nullptr; | ||||
|  | ||||
| 		/*! | ||||
| 			Performs a parallel for operation across all machines, performing the supplied | ||||
|   | ||||
| @@ -81,6 +81,6 @@ MultiJoystickMachine::MultiJoystickMachine(const std::vector<std::unique_ptr<::M | ||||
| 	} | ||||
| } | ||||
|  | ||||
| std::vector<std::unique_ptr<Inputs::Joystick>> &MultiJoystickMachine::get_joysticks() { | ||||
| const std::vector<std::unique_ptr<Inputs::Joystick>> &MultiJoystickMachine::get_joysticks() { | ||||
| 	return joysticks_; | ||||
| } | ||||
|   | ||||
| @@ -28,7 +28,7 @@ class MultiJoystickMachine: public JoystickMachine::Machine { | ||||
| 		MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		// Below is the standard JoystickMachine::Machine interface; see there for documentation. | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override; | ||||
| 		const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_; | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| // | ||||
|  | ||||
| #include "MultiMachine.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| @@ -58,6 +59,11 @@ KeyboardMachine::Machine *MultiMachine::keyboard_machine() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| MouseMachine::Machine *MultiMachine::mouse_machine() { | ||||
| 	// TODO. | ||||
| 	return nullptr; | ||||
| } | ||||
|  | ||||
| Configurable::Device *MultiMachine::configurable_device() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->configurable_device(); | ||||
| @@ -73,15 +79,13 @@ bool MultiMachine::would_collapse(const std::vector<std::unique_ptr<DynamicMachi | ||||
| } | ||||
|  | ||||
| void MultiMachine::multi_crt_did_run_machines() { | ||||
| 	std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| #ifdef DEBUG | ||||
| 	std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| #ifndef NDEBUG | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		CRTMachine::Machine *crt = machine->crt_machine(); | ||||
| 		printf("%0.2f ", crt->get_confidence()); | ||||
| 		crt->print_type(); | ||||
| 		printf("; "); | ||||
| 		LOGNBR(PADHEX(2) << crt->get_confidence() << " " << crt->debug_type() << "; "); | ||||
| 	} | ||||
| 	printf("\n"); | ||||
| 	LOGNBR(std::endl); | ||||
| #endif | ||||
|  | ||||
| 	DynamicMachine *front = machines_.front().get(); | ||||
|   | ||||
| @@ -54,6 +54,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De | ||||
| 		Configurable::Device *configurable_device() override; | ||||
| 		CRTMachine::Machine *crt_machine() override; | ||||
| 		JoystickMachine::Machine *joystick_machine() override; | ||||
| 		MouseMachine::Machine *mouse_machine() override; | ||||
| 		KeyboardMachine::Machine *keyboard_machine() override; | ||||
| 		MediaTarget::Machine *media_target() override; | ||||
| 		void *raw_pointer() override; | ||||
| @@ -62,7 +63,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De | ||||
| 		void multi_crt_did_run_machines() override; | ||||
|  | ||||
| 		std::vector<std::unique_ptr<DynamicMachine>> machines_; | ||||
| 		std::mutex machines_mutex_; | ||||
| 		std::recursive_mutex machines_mutex_; | ||||
|  | ||||
| 		MultiConfigurable configurable_; | ||||
| 		MultiCRTMachine crt_machine_; | ||||
|   | ||||
| @@ -15,8 +15,10 @@ enum class Machine { | ||||
| 	AmstradCPC, | ||||
| 	AppleII, | ||||
| 	Atari2600, | ||||
| 	AtariST, | ||||
| 	ColecoVision, | ||||
| 	Electron, | ||||
| 	Macintosh, | ||||
| 	MasterSystem, | ||||
| 	MSX, | ||||
| 	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) { | ||||
| 	// c.f. http://beebwiki.mdfs.net/Acorn_DFS_disc_format | ||||
| 	std::unique_ptr<Catalogue> catalogue(new Catalogue); | ||||
| 	auto catalogue = std::make_unique<Catalogue>(); | ||||
| 	Storage::Encodings::MFM::Parser parser(false, disk); | ||||
|  | ||||
| 	Storage::Encodings::MFM::Sector *names = parser.get_sector(0, 0, 0); | ||||
| @@ -75,7 +75,7 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s | ||||
| 	return catalogue; | ||||
| } | ||||
| std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) { | ||||
| 	std::unique_ptr<Catalogue> catalogue(new Catalogue); | ||||
| 	auto catalogue = std::make_unique<Catalogue>(); | ||||
| 	Storage::Encodings::MFM::Parser parser(true, disk); | ||||
|  | ||||
| 	Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.get_sector(0, 0, 1); | ||||
|   | ||||
| @@ -58,7 +58,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| } | ||||
|  | ||||
| Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->machine = Machine::Electron; | ||||
| 	target->confidence = 0.5; // TODO: a proper estimation | ||||
| 	target->has_dfs = false; | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
| using namespace Analyser::Static::Acorn; | ||||
|  | ||||
| static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::Tape::Tape> &tape, Storage::Tape::Acorn::Parser &parser) { | ||||
| 	std::unique_ptr<File::Chunk> new_chunk(new File::Chunk); | ||||
| 	auto new_chunk = std::make_unique<File::Chunk>(); | ||||
| 	int shift_register = 0; | ||||
|  | ||||
| // TODO: move this into the parser | ||||
| @@ -90,7 +90,7 @@ static std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &chunks) { | ||||
| 	if(!chunks.size()) return nullptr; | ||||
|  | ||||
| 	// accumulate chunks for as long as block number is sequential and the end-of-file bit isn't set | ||||
| 	std::unique_ptr<File> file(new File); | ||||
| 	auto file = std::make_unique<File>(); | ||||
|  | ||||
| 	uint16_t block_number = 0; | ||||
|  | ||||
|   | ||||
| @@ -181,7 +181,7 @@ static bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, co | ||||
|  | ||||
| Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	TargetList destination; | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->machine = Machine::AmstradCPC; | ||||
| 	target->confidence = 0.5; | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
| #include "Target.hpp" | ||||
|  | ||||
| Analyser::Static::TargetList Analyser::Static::AppleII::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	auto target = std::unique_ptr<Target>(new Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->machine = Machine::AppleII; | ||||
| 	target->media = media; | ||||
|  | ||||
|   | ||||
| @@ -12,9 +12,10 @@ | ||||
| 
 | ||||
| #include "../Disassembler/6502.hpp" | ||||
| 
 | ||||
| using namespace Analyser::Static::Atari; | ||||
| using namespace Analyser::Static::Atari2600; | ||||
| using Target = Analyser::Static::Atari2600::Target; | ||||
| 
 | ||||
| static void DeterminePagingFor2kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||
| static void DeterminePagingFor2kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||
| 	// if this is a 2kb cartridge then it's definitely either unpaged or a CommaVid
 | ||||
| 	uint16_t entry_address, break_address; | ||||
| 
 | ||||
| @@ -48,10 +49,10 @@ static void DeterminePagingFor2kCartridge(Analyser::Static::Atari::Target &targe | ||||
| 	// caveat: false positives aren't likely to be problematic; a false positive is a 2KB ROM that always addresses
 | ||||
| 	// itself so as to land in ROM even if mapped as a CommaVid and this code is on the fence as to whether it
 | ||||
| 	// attempts to modify itself but it probably doesn't
 | ||||
| 	if(has_wide_area_store) target.paging_model = Analyser::Static::Atari::Target::PagingModel::CommaVid; | ||||
| 	if(has_wide_area_store) target.paging_model = Target::PagingModel::CommaVid; | ||||
| } | ||||
| 
 | ||||
| static void DeterminePagingFor8kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| static void DeterminePagingFor8kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	// Activision stack titles have their vectors at the top of the low 4k, not the top, and
 | ||||
| 	// always list 0xf000 as both vectors; they do not repeat them, and, inexplicably, they all
 | ||||
| 	// issue an SEI as their first instruction (maybe some sort of relic of the development environment?)
 | ||||
| @@ -60,12 +61,12 @@ static void DeterminePagingFor8kCartridge(Analyser::Static::Atari::Target &targe | ||||
| 		(segment.data[8191] != 0xf0 || segment.data[8189] != 0xf0 || segment.data[8190] != 0x00 || segment.data[8188] != 0x00) && | ||||
| 		segment.data[0] == 0x78 | ||||
| 	) { | ||||
| 		target.paging_model = Analyser::Static::Atari::Target::PagingModel::ActivisionStack; | ||||
| 		target.paging_model = Target::PagingModel::ActivisionStack; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	// make an assumption that this is the Atari paging model
 | ||||
| 	target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari8k; | ||||
| 	target.paging_model = Target::PagingModel::Atari8k; | ||||
| 
 | ||||
| 	std::set<uint16_t> internal_accesses; | ||||
| 	internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end()); | ||||
| @@ -85,13 +86,13 @@ static void DeterminePagingFor8kCartridge(Analyser::Static::Atari::Target &targe | ||||
| 		tigervision_access_count += masked_address == 0x3f; | ||||
| 	} | ||||
| 
 | ||||
| 	if(parker_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::ParkerBros; | ||||
| 	else if(tigervision_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::Tigervision; | ||||
| 	if(parker_access_count > atari_access_count) target.paging_model = Target::PagingModel::ParkerBros; | ||||
| 	else if(tigervision_access_count > atari_access_count) target.paging_model = Target::PagingModel::Tigervision; | ||||
| } | ||||
| 
 | ||||
| static void DeterminePagingFor16kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| static void DeterminePagingFor16kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	// make an assumption that this is the Atari paging model
 | ||||
| 	target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari16k; | ||||
| 	target.paging_model = Target::PagingModel::Atari16k; | ||||
| 
 | ||||
| 	std::set<uint16_t> internal_accesses; | ||||
| 	internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end()); | ||||
| @@ -106,17 +107,17 @@ static void DeterminePagingFor16kCartridge(Analyser::Static::Atari::Target &targ | ||||
| 		mnetwork_access_count += masked_address >= 0x1fe0 && masked_address < 0x1ffb; | ||||
| 	} | ||||
| 
 | ||||
| 	if(mnetwork_access_count > atari_access_count) target.paging_model = Analyser::Static::Atari::Target::PagingModel::MNetwork; | ||||
| 	if(mnetwork_access_count > atari_access_count) target.paging_model = Target::PagingModel::MNetwork; | ||||
| } | ||||
| 
 | ||||
| static void DeterminePagingFor64kCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| static void DeterminePagingFor64kCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	// make an assumption that this is a Tigervision if there is a write to 3F
 | ||||
| 	target.paging_model = | ||||
| 		(disassembly.external_stores.find(0x3f) != disassembly.external_stores.end()) ? | ||||
| 			Analyser::Static::Atari::Target::PagingModel::Tigervision : Analyser::Static::Atari::Target::PagingModel::MegaBoy; | ||||
| 			Target::PagingModel::Tigervision : Target::PagingModel::MegaBoy; | ||||
| } | ||||
| 
 | ||||
| static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||
| static void DeterminePagingForCartridge(Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||
| 	if(segment.data.size() == 2048) { | ||||
| 		DeterminePagingFor2kCartridge(target, segment); | ||||
| 		return; | ||||
| @@ -140,16 +141,16 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, | ||||
| 			DeterminePagingFor8kCartridge(target, segment, disassembly); | ||||
| 		break; | ||||
| 		case 10495: | ||||
| 			target.paging_model = Analyser::Static::Atari::Target::PagingModel::Pitfall2; | ||||
| 			target.paging_model = Target::PagingModel::Pitfall2; | ||||
| 		break; | ||||
| 		case 12288: | ||||
| 			target.paging_model = Analyser::Static::Atari::Target::PagingModel::CBSRamPlus; | ||||
| 			target.paging_model = Target::PagingModel::CBSRamPlus; | ||||
| 		break; | ||||
| 		case 16384: | ||||
| 			DeterminePagingFor16kCartridge(target, segment, disassembly); | ||||
| 		break; | ||||
| 		case 32768: | ||||
| 			target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari32k; | ||||
| 			target.paging_model = Target::PagingModel::Atari32k; | ||||
| 		break; | ||||
| 		case 65536: | ||||
| 			DeterminePagingFor64kCartridge(target, segment, disassembly); | ||||
| @@ -161,8 +162,8 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, | ||||
| 	// check for a Super Chip. Atari ROM images [almost] always have the same value stored over RAM
 | ||||
| 	// regions; when they don't they at least seem to have the first 128 bytes be the same as the
 | ||||
| 	// next 128 bytes. So check for that.
 | ||||
| 	if(	target.paging_model != Analyser::Static::Atari::Target::PagingModel::CBSRamPlus && | ||||
| 		target.paging_model != Analyser::Static::Atari::Target::PagingModel::MNetwork) { | ||||
| 	if(	target.paging_model != Target::PagingModel::CBSRamPlus && | ||||
| 		target.paging_model != Target::PagingModel::MNetwork) { | ||||
| 		bool has_superchip = true; | ||||
| 		for(std::size_t address = 0; address < 128; address++) { | ||||
| 			if(segment.data[address] != segment.data[address+128]) { | ||||
| @@ -174,19 +175,19 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, | ||||
| 	} | ||||
| 
 | ||||
| 	// check for a Tigervision or Tigervision-esque scheme
 | ||||
| 	if(target.paging_model == Analyser::Static::Atari::Target::PagingModel::None && segment.data.size() > 4096) { | ||||
| 	if(target.paging_model == Target::PagingModel::None && segment.data.size() > 4096) { | ||||
| 		bool looks_like_tigervision = disassembly.external_stores.find(0x3f) != disassembly.external_stores.end(); | ||||
| 		if(looks_like_tigervision) target.paging_model = Analyser::Static::Atari::Target::PagingModel::Tigervision; | ||||
| 		if(looks_like_tigervision) target.paging_model = Target::PagingModel::Tigervision; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| Analyser::Static::TargetList Analyser::Static::Atari::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| Analyser::Static::TargetList Analyser::Static::Atari2600::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	// TODO: sanity checking; is this image really for an Atari 2600?
 | ||||
| 	std::unique_ptr<Analyser::Static::Atari::Target> target(new Analyser::Static::Atari::Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->machine = Machine::Atari2600; | ||||
| 	target->confidence = 0.5; | ||||
| 	target->media.cartridges = media.cartridges; | ||||
| 	target->paging_model = Analyser::Static::Atari::Target::PagingModel::None; | ||||
| 	target->paging_model = Target::PagingModel::None; | ||||
| 	target->uses_superchip = false; | ||||
| 
 | ||||
| 	// try to figure out the paging scheme
 | ||||
| @@ -15,7 +15,7 @@ | ||||
| 
 | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Atari { | ||||
| namespace Atari2600 { | ||||
| 
 | ||||
| TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); | ||||
| 
 | ||||
| @@ -6,14 +6,14 @@ | ||||
| //  Copyright 2018 Thomas Harte. All rights reserved.
 | ||||
| //
 | ||||
| 
 | ||||
| #ifndef Analyser_Static_Atari_Target_h | ||||
| #define Analyser_Static_Atari_Target_h | ||||
| #ifndef Analyser_Static_Atari2600_Target_h | ||||
| #define Analyser_Static_Atari2600_Target_h | ||||
| 
 | ||||
| #include "../StaticAnalyser.hpp" | ||||
| 
 | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Atari { | ||||
| namespace Atari2600 { | ||||
| 
 | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| 	enum class PagingModel { | ||||
							
								
								
									
										26
									
								
								Analyser/Static/AtariST/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Analyser/Static/AtariST/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| // | ||||
| //  StaticAnalyser.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/10/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "StaticAnalyser.hpp" | ||||
| #include "Target.hpp" | ||||
|  | ||||
| Analyser::Static::TargetList Analyser::Static::AtariST::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	// This analyser can comprehend disks and mass-storage devices only. | ||||
| 	if(media.disks.empty()) return {}; | ||||
|  | ||||
| 	// As there is at least one usable media image, wave it through. | ||||
| 	Analyser::Static::TargetList targets; | ||||
|  | ||||
| 	using Target = Analyser::Static::Target; | ||||
| 	auto *target = new Target; | ||||
| 	target->machine = Analyser::Machine::AtariST; | ||||
| 	target->media = media; | ||||
| 	targets.push_back(std::unique_ptr<Analyser::Static::Target>(target)); | ||||
|  | ||||
| 	return targets; | ||||
| } | ||||
							
								
								
									
										27
									
								
								Analyser/Static/AtariST/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Analyser/Static/AtariST/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  StaticAnalyser.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/10/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Analyser_Static_AtariST_StaticAnalyser_hpp | ||||
| #define Analyser_Static_AtariST_StaticAnalyser_hpp | ||||
|  | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include "../../../Storage/TargetPlatforms.hpp" | ||||
| #include <string> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace AtariST { | ||||
|  | ||||
| TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| #endif /* Analyser_Static_AtariST_StaticAnalyser_hpp */ | ||||
							
								
								
									
										23
									
								
								Analyser/Static/AtariST/Target.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								Analyser/Static/AtariST/Target.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| // | ||||
| //  Target.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Analyser_Static_AtariST_Target_h | ||||
| #define Analyser_Static_AtariST_Target_h | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace AtariST { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Analyser_Static_AtariST_Target_h */ | ||||
| @@ -54,7 +54,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
|  | ||||
| Analyser::Static::TargetList Analyser::Static::Coleco::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	TargetList targets; | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->machine = Machine::ColecoVision; | ||||
| 	target->confidence = 1.0f - 1.0f / 32768.0f; | ||||
| 	target->media.cartridges = ColecoCartridgesFrom(media.cartridges); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 		std::shared_ptr<Storage::Disk::Drive> drive; | ||||
|  | ||||
| 		CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) { | ||||
| 			drive.reset(new Storage::Disk::Drive(4000000, 300, 2)); | ||||
| 			drive = std::make_shared<Storage::Disk::Drive>(4000000, 300, 2); | ||||
| 			set_drive(drive); | ||||
| 			drive->set_motor_on(true); | ||||
| 		} | ||||
| @@ -125,7 +125,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 		} | ||||
|  | ||||
| 		std::shared_ptr<Sector> get_next_sector() { | ||||
| 			std::shared_ptr<Sector> sector(new Sector); | ||||
| 			auto sector = std::make_shared<Sector>(); | ||||
| 			const int max_index_count = index_count_ + 2; | ||||
|  | ||||
| 			while(index_count_ < max_index_count) { | ||||
|   | ||||
| @@ -13,8 +13,10 @@ | ||||
| #include "Tape.hpp" | ||||
| #include "Target.hpp" | ||||
| #include "../../../Storage/Cartridge/Encodings/CommodoreROM.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cstring> | ||||
| #include <sstream> | ||||
|  | ||||
| using namespace Analyser::Static::Commodore; | ||||
| @@ -43,7 +45,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	TargetList destination; | ||||
|  | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->machine = Machine::Vic20;	// TODO: machine estimation | ||||
| 	target->confidence = 0.5; // TODO: a proper estimation | ||||
|  | ||||
| @@ -77,7 +79,7 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media | ||||
| 	} | ||||
|  | ||||
| 	if(!files.empty()) { | ||||
| 		target->memory_model = Target::MemoryModel::Unexpanded; | ||||
| 		auto memory_model = Target::MemoryModel::Unexpanded; | ||||
| 		std::ostringstream string_stream; | ||||
| 		string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ","; | ||||
| 		if(files.front().is_basic()) { | ||||
| @@ -91,18 +93,20 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media | ||||
| 		// make a first guess based on loading address | ||||
| 		switch(files.front().starting_address) { | ||||
| 			default: | ||||
| 				printf("Starting address %04x?\n", files.front().starting_address); | ||||
| 				LOG("Unrecognised loading address for Commodore program: " << PADHEX(4) <<  files.front().starting_address); | ||||
| 			case 0x1001: | ||||
| 				target->memory_model = Target::MemoryModel::Unexpanded; | ||||
| 				memory_model = Target::MemoryModel::Unexpanded; | ||||
| 			break; | ||||
| 			case 0x1201: | ||||
| 				target->memory_model = Target::MemoryModel::ThirtyTwoKB; | ||||
| 				memory_model = Target::MemoryModel::ThirtyTwoKB; | ||||
| 			break; | ||||
| 			case 0x0401: | ||||
| 				target->memory_model = Target::MemoryModel::EightKB; | ||||
| 				memory_model = Target::MemoryModel::EightKB; | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		target->set_memory_model(memory_model); | ||||
|  | ||||
| 		// General approach: increase memory size conservatively such that the largest file found will fit. | ||||
| //		for(File &file : files) { | ||||
| //			std::size_t file_size = file.data.size(); | ||||
| @@ -144,13 +148,52 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media | ||||
| 	} | ||||
|  | ||||
| 	if(!target->media.empty()) { | ||||
| 		// Inspect filename for a region hint. | ||||
| 		// Inspect filename for configuration hints. | ||||
| 		std::string lowercase_name = file_name; | ||||
| 		std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower); | ||||
|  | ||||
| 		// Hint 1: 'ntsc' anywhere in the name implies America. | ||||
| 		if(lowercase_name.find("ntsc") != std::string::npos) { | ||||
| 			target->region = Analyser::Static::Commodore::Target::Region::American; | ||||
| 		} | ||||
|  | ||||
| 		// Potential additional hints: check for TheC64 tags. | ||||
| 		auto final_underscore = lowercase_name.find_last_of('_'); | ||||
| 		if(final_underscore != std::string::npos) { | ||||
| 			auto iterator = lowercase_name.begin() + ssize_t(final_underscore) + 1; | ||||
|  | ||||
| 			while(iterator != lowercase_name.end()) { | ||||
| 				// Grab the next tag. | ||||
| 				char next_tag[3] = {0, 0, 0}; | ||||
| 				next_tag[0] = *iterator++; | ||||
| 				if(iterator == lowercase_name.end()) break; | ||||
| 				next_tag[1] = *iterator++; | ||||
|  | ||||
| 				// Exit early if attempting to read another tag has run over the file extension. | ||||
| 				if(next_tag[0] == '.' || next_tag[1] == '.') break; | ||||
|  | ||||
| 				// Check whether it's anything. | ||||
| 				target->enabled_ram.bank0 |= !strcmp(next_tag, "b0"); | ||||
| 				target->enabled_ram.bank1 |= !strcmp(next_tag, "b1"); | ||||
| 				target->enabled_ram.bank2 |= !strcmp(next_tag, "b2"); | ||||
| 				target->enabled_ram.bank3 |= !strcmp(next_tag, "b3"); | ||||
| 				target->enabled_ram.bank5 |= !strcmp(next_tag, "b5"); | ||||
| 				if(!strcmp(next_tag, "tn")) {	// i.e. NTSC. | ||||
| 					target->region = Analyser::Static::Commodore::Target::Region::American; | ||||
| 				} | ||||
| 				if(!strcmp(next_tag, "tp")) {	// i.e. PAL. | ||||
| 					target->region = Analyser::Static::Commodore::Target::Region::European; | ||||
| 				} | ||||
|  | ||||
| 				// Unhandled: | ||||
| 				// | ||||
| 				//	M6: 	this is a C64 file. | ||||
| 				//	MV: 	this is a Vic-20 file. | ||||
| 				//	J1/J2:	this C64 file should have the primary joystick in slot 1/2. | ||||
| 				//	RO:		this disk image should be treated as read-only. | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Attach a 1540 if there are any disks here. | ||||
| 		target->has_c1540 = !target->media.disks.empty(); | ||||
|  | ||||
|   | ||||
| @@ -31,7 +31,26 @@ struct Target: public ::Analyser::Static::Target { | ||||
| 		Swedish | ||||
| 	}; | ||||
|  | ||||
| 	MemoryModel memory_model = MemoryModel::Unexpanded; | ||||
| 	/// Maps from a named memory model to a bank enabled/disabled set. | ||||
| 	void set_memory_model(MemoryModel memory_model) { | ||||
| 		// This is correct for unexpanded and 32kb memory models. | ||||
| 		enabled_ram.bank0 = enabled_ram.bank1 = | ||||
| 		enabled_ram.bank2 = enabled_ram.bank3 = | ||||
| 		enabled_ram.bank5 = memory_model == MemoryModel::ThirtyTwoKB; | ||||
|  | ||||
| 		// Bank 0 will need to be enabled if this is an 8kb machine. | ||||
| 		enabled_ram.bank0 |= memory_model == MemoryModel::EightKB; | ||||
| 	} | ||||
| 	struct { | ||||
| 		bool bank0 = false; | ||||
| 		bool bank1 = false; | ||||
| 		bool bank2 = false; | ||||
| 		bool bank3 = false; | ||||
| 		bool bank5 = false; | ||||
| 					// Sic. There is no bank 4; this is because the area that logically would be | ||||
| 					// bank 4 is occupied by the character ROM, colour RAM, hardware registers, etc. | ||||
| 	} enabled_ram; | ||||
|  | ||||
| 	Region region = Region::European; | ||||
| 	bool has_c1540 = false; | ||||
| 	std::string loading_command; | ||||
|   | ||||
| @@ -34,7 +34,7 @@ static std::unique_ptr<Analyser::Static::Target> CartridgeTarget( | ||||
| 		output_segments.emplace_back(start_address, segment.data); | ||||
| 	} | ||||
|  | ||||
| 	std::unique_ptr<Analyser::Static::MSX::Target> target(new Analyser::Static::MSX::Target); | ||||
| 	auto target = std::make_unique<Analyser::Static::MSX::Target>(); | ||||
| 	target->machine = Analyser::Machine::MSX; | ||||
| 	target->confidence = confidence; | ||||
|  | ||||
| @@ -269,7 +269,7 @@ Analyser::Static::TargetList Analyser::Static::MSX::GetTargets(const Media &medi | ||||
| 	std::move(cartridge_targets.begin(), cartridge_targets.end(), std::back_inserter(destination)); | ||||
|  | ||||
| 	// Consider building a target for disks and/or tapes. | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
|  | ||||
| 	// Check tapes for loadable files. | ||||
| 	for(auto &tape : media.tapes) { | ||||
| @@ -285,7 +285,12 @@ Analyser::Static::TargetList Analyser::Static::MSX::GetTargets(const Media &medi | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Region selection: for now, this as simple as: | ||||
| 	// "If a tape is involved, be European. Otherwise be American (i.e. English, but 60Hz)". | ||||
| 	target->region = target->media.tapes.empty() ? Target::Region::USA : Target::Region::Europe; | ||||
|  | ||||
| 	// Blindly accept disks for now. | ||||
| 	// TODO: how to spot an MSX disk? | ||||
| 	target->media.disks = media.disks; | ||||
| 	target->has_disk_drive = !media.disks.empty(); | ||||
|  | ||||
|   | ||||
| @@ -19,6 +19,12 @@ namespace MSX { | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| 	bool has_disk_drive = false; | ||||
| 	std::string loading_command; | ||||
|  | ||||
| 	enum class Region { | ||||
| 		Japan, | ||||
| 		USA, | ||||
| 		Europe | ||||
| 	} region = Region::USA; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										26
									
								
								Analyser/Static/Macintosh/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Analyser/Static/Macintosh/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| // | ||||
| //  StaticAnalyser.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 02/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "StaticAnalyser.hpp" | ||||
| #include "Target.hpp" | ||||
|  | ||||
| Analyser::Static::TargetList Analyser::Static::Macintosh::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	// This analyser can comprehend disks and mass-storage devices only. | ||||
| 	if(media.disks.empty() && media.mass_storage_devices.empty()) return {}; | ||||
|  | ||||
| 	// As there is at least one usable media image, wave it through. | ||||
| 	Analyser::Static::TargetList targets; | ||||
|  | ||||
| 	using Target = Analyser::Static::Macintosh::Target; | ||||
| 	auto *target = new Target; | ||||
| 	target->machine = Analyser::Machine::Macintosh; | ||||
| 	target->media = media; | ||||
| 	targets.push_back(std::unique_ptr<Analyser::Static::Target>(target)); | ||||
|  | ||||
| 	return targets; | ||||
| } | ||||
							
								
								
									
										27
									
								
								Analyser/Static/Macintosh/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Analyser/Static/Macintosh/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  StaticAnalyser.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 02/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Analyser_Static_Macintosh_StaticAnalyser_hpp | ||||
| #define Analyser_Static_Macintosh_StaticAnalyser_hpp | ||||
|  | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include "../../../Storage/TargetPlatforms.hpp" | ||||
| #include <string> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Macintosh { | ||||
|  | ||||
| TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| #endif /* Analyser_Static_Macintosh_StaticAnalyser_hpp */ | ||||
							
								
								
									
										31
									
								
								Analyser/Static/Macintosh/Target.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								Analyser/Static/Macintosh/Target.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| // | ||||
| //  Target.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Analyser_Static_Macintosh_Target_h | ||||
| #define Analyser_Static_Macintosh_Target_h | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Macintosh { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| 	enum class Model { | ||||
| 		Mac128k, | ||||
| 		Mac512k, | ||||
| 		Mac512ke, | ||||
| 		MacPlus | ||||
| 	}; | ||||
|  | ||||
| 	Model model = Model::MacPlus; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Analyser_Static_Macintosh_Target_h */ | ||||
| @@ -20,7 +20,9 @@ | ||||
|  | ||||
| using namespace Analyser::Static::Oric; | ||||
|  | ||||
| static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) { | ||||
| namespace { | ||||
|  | ||||
| int score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) { | ||||
| 	int score = 0; | ||||
|  | ||||
| 	for(const auto address : disassembly.outward_calls)		score += (rom_functions.find(address) != rom_functions.end()) ? 1 : -1; | ||||
| @@ -30,7 +32,7 @@ static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, cons | ||||
| 	return score; | ||||
| } | ||||
|  | ||||
| static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| int basic10_score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	const std::set<uint16_t> rom_functions = { | ||||
| 		0x0228,	0x022b, | ||||
| 		0xc3ca,	0xc3f8,	0xc448,	0xc47c,	0xc4b5,	0xc4e3,	0xc4e0,	0xc524,	0xc56f,	0xc5a2,	0xc5f8,	0xc60a,	0xc6a5,	0xc6de,	0xc719,	0xc738, | ||||
| @@ -51,10 +53,10 @@ static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembl | ||||
| 		0x0228, 0x0229, 0x022a, 0x022b, 0x022c, 0x022d, 0x0230 | ||||
| 	}; | ||||
|  | ||||
| 	return Score(disassembly, rom_functions, variable_locations); | ||||
| 	return score(disassembly, rom_functions, variable_locations); | ||||
| } | ||||
|  | ||||
| static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| int basic11_score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	const std::set<uint16_t> rom_functions = { | ||||
| 		0x0238,	0x023b,	0x023e,	0x0241,	0x0244,	0x0247, | ||||
| 		0xc3c6,	0xc3f4,	0xc444,	0xc47c,	0xc4a8,	0xc4d3,	0xc4e0,	0xc524,	0xc55f,	0xc592,	0xc5e8,	0xc5fa,	0xc692,	0xc6b3,	0xc6ee,	0xc70d, | ||||
| @@ -76,10 +78,10 @@ static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembl | ||||
| 		0x0244, 0x0245, 0x0246, 0x0247, 0x0248, 0x0249, 0x024a, 0x024b, 0x024c | ||||
| 	}; | ||||
|  | ||||
| 	return Score(disassembly, rom_functions, variable_locations); | ||||
| 	return score(disassembly, rom_functions, variable_locations); | ||||
| } | ||||
|  | ||||
| static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) { | ||||
| bool is_microdisc(Storage::Encodings::MFM::Parser &parser) { | ||||
| 	/* | ||||
| 		The Microdisc boot sector is sector 2 of track 0 and contains a 23-byte signature. | ||||
| 	*/ | ||||
| @@ -100,8 +102,51 @@ static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) { | ||||
| 	return !std::memcmp(signature, first_sample.data(), sizeof(signature)); | ||||
| } | ||||
|  | ||||
| bool is_400_loader(Storage::Encodings::MFM::Parser &parser, uint16_t range_start, uint16_t range_end) { | ||||
| 	/* | ||||
| 		Both the Jasmin and BD-DOS boot sectors are sector 1 of track 0 and are loaded at $400; | ||||
| 		use disassembly to test for likely matches. | ||||
| 	*/ | ||||
|  | ||||
| 	Storage::Encodings::MFM::Sector *sector = parser.get_sector(0, 0, 1); | ||||
| 	if(!sector) return false; | ||||
| 	if(sector->samples.empty()) return false; | ||||
|  | ||||
| 	// Take a copy of the first sampling, and keep only the final 256 bytes (assuming at least that many were found). | ||||
| 	std::vector<uint8_t> first_sample = sector->samples[0]; | ||||
| 	if(first_sample.size() < 256) return false; | ||||
| 	if(first_sample.size() > 256) { | ||||
| 		first_sample.erase(first_sample.end() - 256, first_sample.end()); | ||||
| 	} | ||||
|  | ||||
| 	// Grab a disassembly. | ||||
| 	const auto disassembly = | ||||
| 		Analyser::Static::MOS6502::Disassemble(first_sample, Analyser::Static::Disassembler::OffsetMapper(0x400), {0x400}); | ||||
|  | ||||
| 	// Check for references to the Jasmin registers. | ||||
| 	int register_hits = 0; | ||||
| 	for(auto list : {disassembly.external_stores, disassembly.external_loads, disassembly.external_modifies}) { | ||||
| 		for(auto address : list) { | ||||
| 			register_hits += (address >= range_start && address <= range_end); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Arbitrary, sure, but as long as at least two accesses to the requested register range are found, accept this. | ||||
| 	return register_hits >= 2; | ||||
| } | ||||
|  | ||||
| bool is_jasmin(Storage::Encodings::MFM::Parser &parser) { | ||||
| 	return is_400_loader(parser, 0x3f4, 0x3ff); | ||||
| } | ||||
|  | ||||
| bool is_bd500(Storage::Encodings::MFM::Parser &parser) { | ||||
| 	return is_400_loader(parser, 0x310, 0x323); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->machine = Machine::Oric; | ||||
| 	target->confidence = 0.5; | ||||
|  | ||||
| @@ -115,12 +160,10 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med | ||||
| 			for(const auto &file : tape_files) { | ||||
| 				if(file.data_type == File::MachineCode) { | ||||
| 					std::vector<uint16_t> entry_points = {file.starting_address}; | ||||
| 					Analyser::Static::MOS6502::Disassembly disassembly = | ||||
| 					const Analyser::Static::MOS6502::Disassembly disassembly = | ||||
| 						Analyser::Static::MOS6502::Disassemble(file.data, Analyser::Static::Disassembler::OffsetMapper(file.starting_address), entry_points); | ||||
|  | ||||
| 					int basic10_score = Basic10Score(disassembly); | ||||
| 					int basic11_score = Basic11Score(disassembly); | ||||
| 					if(basic10_score > basic11_score) basic10_votes++; else basic11_votes++; | ||||
| 					if(basic10_score(disassembly) > basic11_score(disassembly)) ++basic10_votes; else ++basic11_votes; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| @@ -130,12 +173,22 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med | ||||
| 	} | ||||
|  | ||||
| 	if(!media.disks.empty()) { | ||||
| 		// Only the Microdisc is emulated right now, so accept only disks that it can boot from. | ||||
| 		// 8-DOS is recognised by a dedicated Disk II analyser, so check only for Microdisc, | ||||
| 		// Jasmin and BD-DOS formats here. | ||||
| 		for(auto &disk: media.disks) { | ||||
| 			Storage::Encodings::MFM::Parser parser(true, disk); | ||||
| 			if(IsMicrodisc(parser)) { | ||||
|  | ||||
| 			if(is_microdisc(parser)) { | ||||
| 				target->disk_interface = Target::DiskInterface::Microdisc; | ||||
| 				target->media.disks.push_back(disk); | ||||
| 			} else if(is_jasmin(parser)) { | ||||
| 				target->disk_interface = Target::DiskInterface::Jasmin; | ||||
| 				target->should_start_jasmin = true; | ||||
| 				target->media.disks.push_back(disk); | ||||
| 			} else if(is_bd500(parser)) { | ||||
| 				target->disk_interface = Target::DiskInterface::BD500; | ||||
| 				target->media.disks.push_back(disk); | ||||
| 				target->rom = Target::ROM::BASIC10; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -26,12 +26,15 @@ struct Target: public ::Analyser::Static::Target { | ||||
| 	enum class DiskInterface { | ||||
| 		Microdisc, | ||||
| 		Pravetz, | ||||
| 		Jasmin, | ||||
| 		BD500, | ||||
| 		None | ||||
| 	}; | ||||
|  | ||||
| 	ROM rom = ROM::BASIC11; | ||||
| 	DiskInterface disk_interface = DiskInterface::None; | ||||
| 	std::string loading_command; | ||||
| 	bool should_start_jasmin = false; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &med | ||||
| 		return {}; | ||||
|  | ||||
| 	TargetList targets; | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
|  | ||||
| 	target->machine = Machine::MasterSystem; | ||||
|  | ||||
|   | ||||
| @@ -17,10 +17,12 @@ | ||||
| #include "Acorn/StaticAnalyser.hpp" | ||||
| #include "AmstradCPC/StaticAnalyser.hpp" | ||||
| #include "AppleII/StaticAnalyser.hpp" | ||||
| #include "Atari/StaticAnalyser.hpp" | ||||
| #include "Atari2600/StaticAnalyser.hpp" | ||||
| #include "AtariST/StaticAnalyser.hpp" | ||||
| #include "Coleco/StaticAnalyser.hpp" | ||||
| #include "Commodore/StaticAnalyser.hpp" | ||||
| #include "DiskII/StaticAnalyser.hpp" | ||||
| #include "Macintosh/StaticAnalyser.hpp" | ||||
| #include "MSX/StaticAnalyser.hpp" | ||||
| #include "Oric/StaticAnalyser.hpp" | ||||
| #include "Sega/StaticAnalyser.hpp" | ||||
| @@ -35,15 +37,22 @@ | ||||
| #include "../../Storage/Disk/DiskImage/Formats/AppleDSK.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/D64.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/G64.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/DMK.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/HFE.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/MSA.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/MSXDSK.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/NIB.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/SSD.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/ST.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/STX.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp" | ||||
|  | ||||
| // Mass Storage Devices (i.e. usually, hard disks) | ||||
| #include "../../Storage/MassStorage/Formats/HFV.hpp" | ||||
|  | ||||
| // Tapes | ||||
| #include "../../Storage/Tape/Formats/CAS.hpp" | ||||
| #include "../../Storage/Tape/Formats/CommodoreTAP.hpp" | ||||
| @@ -89,7 +98,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | ||||
| 	Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// 81 | ||||
| 	Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600)							// A26 | ||||
| 	Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn)			// ADF | ||||
| 	Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge)					// BIN | ||||
| 	Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge)						// BIN (cartridge dump) | ||||
| 	Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX)													// CAS | ||||
| 	Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC)											// CDT | ||||
| 	Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision)						// COL | ||||
| @@ -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("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)				// DSD | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC)		// DSK (Amstrad CPC) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)		// DSK (Apple) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)			// DSK (Apple II) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// DSK (Macintosh, floppy disk) | ||||
| 	Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh)						// DSK (Macintosh, hard disk) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX)				// DSK (MSX) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric)			// DSK (Oric) | ||||
| 	Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore)			// G64 | ||||
| @@ -108,6 +119,9 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | ||||
| 			Disk::DiskImageHolder<Storage::Disk::HFE>, | ||||
| 			TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric) | ||||
| 			// HFE (TODO: switch to AllDisk once the MSX stops being so greedy) | ||||
| 	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)		// IMG (DiskCopy 4.2) | ||||
| 	Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// IMG (DiskCopy 4.2) | ||||
| 	Format("msa", result.disks, Disk::DiskImageHolder<Storage::Disk::MSA>, TargetPlatform::AtariST)				// MSA | ||||
| 	Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII)				// NIB | ||||
| 	Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// O | ||||
| 	Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// P | ||||
| @@ -133,6 +147,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | ||||
| 	Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega)								// SG | ||||
| 	Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega)								// SMS | ||||
| 	Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)				// SSD | ||||
| 	Format("st", result.disks, Disk::DiskImageHolder<Storage::Disk::ST>, TargetPlatform::AtariST)				// ST | ||||
| 	Format("stx", result.disks, Disk::DiskImageHolder<Storage::Disk::STX>, TargetPlatform::AtariST)				// STX | ||||
| 	Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore)									// TAP (Commodore) | ||||
| 	Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric)											// TAP (Oric) | ||||
| 	Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX)													// TSX | ||||
| @@ -155,7 +171,7 @@ Media Analyser::Static::GetMedia(const std::string &file_name) { | ||||
| TargetList Analyser::Static::GetTargets(const std::string &file_name) { | ||||
| 	TargetList targets; | ||||
|  | ||||
| 	// Collect all disks, tapes and ROMs as can be extrapolated from this file, forming the | ||||
| 	// Collect all disks, tapes ROMs, etc as can be extrapolated from this file, forming the | ||||
| 	// union of all platforms this file might be a target for. | ||||
| 	TargetPlatform::IntType potential_platforms = 0; | ||||
| 	Media media = GetMediaAndPlatforms(file_name, potential_platforms); | ||||
| @@ -169,13 +185,15 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) { | ||||
| 	if(potential_platforms & TargetPlatform::Acorn)			Append(Acorn); | ||||
| 	if(potential_platforms & TargetPlatform::AmstradCPC)	Append(AmstradCPC); | ||||
| 	if(potential_platforms & TargetPlatform::AppleII)		Append(AppleII); | ||||
| 	if(potential_platforms & TargetPlatform::Atari2600)		Append(Atari); | ||||
| 	if(potential_platforms & TargetPlatform::Atari2600)		Append(Atari2600); | ||||
| 	if(potential_platforms & TargetPlatform::AtariST)		Append(AtariST); | ||||
| 	if(potential_platforms & TargetPlatform::ColecoVision)	Append(Coleco); | ||||
| 	if(potential_platforms & TargetPlatform::Commodore)		Append(Commodore); | ||||
| 	if(potential_platforms & TargetPlatform::DiskII)		Append(DiskII); | ||||
| 	if(potential_platforms & TargetPlatform::Sega)			Append(Sega); | ||||
| 	if(potential_platforms & TargetPlatform::Macintosh)		Append(Macintosh); | ||||
| 	if(potential_platforms & TargetPlatform::MSX)			Append(MSX); | ||||
| 	if(potential_platforms & TargetPlatform::Oric)			Append(Oric); | ||||
| 	if(potential_platforms & TargetPlatform::Sega)			Append(Sega); | ||||
| 	if(potential_platforms & TargetPlatform::ZX8081)		Append(ZX8081); | ||||
| 	#undef Append | ||||
|  | ||||
|   | ||||
| @@ -11,9 +11,10 @@ | ||||
|  | ||||
| #include "../Machines.hpp" | ||||
|  | ||||
| #include "../../Storage/Tape/Tape.hpp" | ||||
| #include "../../Storage/Disk/Disk.hpp" | ||||
| #include "../../Storage/Cartridge/Cartridge.hpp" | ||||
| #include "../../Storage/Disk/Disk.hpp" | ||||
| #include "../../Storage/MassStorage/MassStorageDevice.hpp" | ||||
| #include "../../Storage/Tape/Tape.hpp" | ||||
|  | ||||
| #include <memory> | ||||
| #include <string> | ||||
| @@ -29,9 +30,10 @@ struct Media { | ||||
| 	std::vector<std::shared_ptr<Storage::Disk::Disk>> disks; | ||||
| 	std::vector<std::shared_ptr<Storage::Tape::Tape>> tapes; | ||||
| 	std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges; | ||||
| 	std::vector<std::shared_ptr<Storage::MassStorage::MassStorageDevice>> mass_storage_devices; | ||||
|  | ||||
| 	bool empty() const { | ||||
| 		return disks.empty() && tapes.empty() && cartridges.empty(); | ||||
| 		return disks.empty() && tapes.empty() && cartridges.empty() && mass_storage_devices.empty(); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,9 @@ | ||||
| #ifndef ClockReceiver_hpp | ||||
| #define ClockReceiver_hpp | ||||
|  | ||||
| #include "ForceInline.hpp" | ||||
| #include <cstdint> | ||||
|  | ||||
| /* | ||||
| 	Informal pattern for all classes that run from a clock cycle: | ||||
|  | ||||
| @@ -52,149 +55,193 @@ | ||||
| */ | ||||
| template <class T> class WrappedInt { | ||||
| 	public: | ||||
| 		constexpr WrappedInt(int l) : length_(l) {} | ||||
| 		constexpr WrappedInt() : length_(0) {} | ||||
| 		using IntType = int64_t; | ||||
|  | ||||
| 		T &operator =(const T &rhs) { | ||||
| 		forceinline constexpr WrappedInt(IntType l) noexcept : length_(l) {} | ||||
| 		forceinline constexpr WrappedInt() noexcept : length_(0) {} | ||||
|  | ||||
| 		forceinline T &operator =(const T &rhs) { | ||||
| 			length_ = rhs.length_; | ||||
| 			return *this; | ||||
| 		} | ||||
|  | ||||
| 		T &operator +=(const T &rhs) { | ||||
| 		forceinline T &operator +=(const T &rhs) { | ||||
| 			length_ += rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		T &operator -=(const T &rhs) { | ||||
| 		forceinline T &operator -=(const T &rhs) { | ||||
| 			length_ -= rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		T &operator ++() { | ||||
| 		forceinline T &operator ++() { | ||||
| 			++ length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		T &operator ++(int) { | ||||
| 		forceinline T &operator ++(int) { | ||||
| 			length_ ++; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		T &operator --() { | ||||
| 		forceinline T &operator --() { | ||||
| 			-- length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		T &operator --(int) { | ||||
| 		forceinline T &operator --(int) { | ||||
| 			length_ --; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		T &operator %=(const T &rhs) { | ||||
| 		forceinline T &operator *=(const T &rhs) { | ||||
| 			length_ *= rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		forceinline T &operator /=(const T &rhs) { | ||||
| 			length_ /= rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		forceinline T &operator %=(const T &rhs) { | ||||
| 			length_ %= rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		T &operator &=(const T &rhs) { | ||||
| 		forceinline T &operator &=(const T &rhs) { | ||||
| 			length_ &= rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		constexpr T operator +(const T &rhs) const			{	return T(length_ + rhs.length_);	} | ||||
| 		constexpr T operator -(const T &rhs) const			{	return T(length_ - rhs.length_);	} | ||||
| 		forceinline constexpr T operator +(const T &rhs) const			{	return T(length_ + rhs.length_);	} | ||||
| 		forceinline constexpr T operator -(const T &rhs) const			{	return T(length_ - rhs.length_);	} | ||||
|  | ||||
| 		constexpr T operator %(const T &rhs) const			{	return T(length_ % rhs.length_);	} | ||||
| 		constexpr T operator &(const T &rhs) const			{	return T(length_ & rhs.length_);	} | ||||
| 		forceinline constexpr T operator *(const T &rhs) const			{	return T(length_ * rhs.length_);	} | ||||
| 		forceinline constexpr T operator /(const T &rhs) const			{	return T(length_ / rhs.length_);	} | ||||
|  | ||||
| 		constexpr T operator -() const						{	return T(- length_);				} | ||||
| 		forceinline constexpr T operator %(const T &rhs) const			{	return T(length_ % rhs.length_);	} | ||||
| 		forceinline constexpr T operator &(const T &rhs) const			{	return T(length_ & rhs.length_);	} | ||||
|  | ||||
| 		constexpr bool operator <(const T &rhs) const		{	return length_ < rhs.length_;		} | ||||
| 		constexpr bool operator >(const T &rhs) const		{	return length_ > rhs.length_;		} | ||||
| 		constexpr bool operator <=(const T &rhs) const		{	return length_ <= rhs.length_;		} | ||||
| 		constexpr bool operator >=(const T &rhs) const		{	return length_ >= rhs.length_;		} | ||||
| 		constexpr bool operator ==(const T &rhs) const		{	return length_ == rhs.length_;		} | ||||
| 		constexpr bool operator !=(const T &rhs) const		{	return length_ != rhs.length_;		} | ||||
| 		forceinline constexpr T operator -() const						{	return T(- length_);				} | ||||
|  | ||||
| 		constexpr bool operator !() const					{	return !length_;					} | ||||
| 		forceinline constexpr bool operator <(const T &rhs) const		{	return length_ < rhs.length_;		} | ||||
| 		forceinline constexpr bool operator >(const T &rhs) const		{	return length_ > rhs.length_;		} | ||||
| 		forceinline constexpr bool operator <=(const T &rhs) const		{	return length_ <= rhs.length_;		} | ||||
| 		forceinline constexpr bool operator >=(const T &rhs) const		{	return length_ >= rhs.length_;		} | ||||
| 		forceinline constexpr bool operator ==(const T &rhs) const		{	return length_ == rhs.length_;		} | ||||
| 		forceinline constexpr bool operator !=(const T &rhs) const		{	return length_ != rhs.length_;		} | ||||
|  | ||||
| 		forceinline constexpr bool operator !() const					{	return !length_;					} | ||||
| 		// bool operator () is not supported because it offers an implicit cast to int, which is prone silently to permit misuse | ||||
|  | ||||
| 		constexpr int as_int() const { return length_; } | ||||
| 		/// @returns The underlying int, cast to an integral type of your choosing. | ||||
| 		template<typename Type = IntType> forceinline constexpr Type as() const { return Type(length_); } | ||||
|  | ||||
| 		/// @returns The underlying int, in its native form. | ||||
| 		forceinline constexpr IntType as_integral() const { return length_; } | ||||
|  | ||||
| 		/*! | ||||
| 			Severs from @c this the effect of dividing by @c divisor; @c this will end up with | ||||
| 			the value of @c this modulo @c divisor and @c divided by @c divisor is returned. | ||||
| 		*/ | ||||
| 		T divide(const T &divisor) { | ||||
| 			T result(length_ / divisor.length_); | ||||
| 			length_ %= divisor.length_; | ||||
| 			return result; | ||||
| 		template <typename Result = T> forceinline Result divide(const T &divisor) { | ||||
| 			Result r; | ||||
| 			static_cast<T *>(this)->fill(r, divisor); | ||||
| 			return r; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Flushes the value in @c this. The current value is returned, and the internal value | ||||
| 			is reset to zero. | ||||
| 		*/ | ||||
| 		T flush() { | ||||
| 			T result(length_); | ||||
| 			length_ = 0; | ||||
| 			return result; | ||||
| 		template <typename Result> Result flush() { | ||||
| 			// Jiggery pokery here; switching to function overloading avoids | ||||
| 			// the namespace-level requirement for template specialisation. | ||||
| 			Result r; | ||||
| 			static_cast<T *>(this)->fill(r); | ||||
| 			return r; | ||||
| 		} | ||||
|  | ||||
| 		// operator int() is deliberately not provided, to avoid accidental subtitution of | ||||
| 		// classes that use this template. | ||||
|  | ||||
| 	protected: | ||||
| 		int length_; | ||||
| 		IntType length_; | ||||
| }; | ||||
|  | ||||
| /// Describes an integer number of whole cycles: pairs of clock signal transitions. | ||||
| class Cycles: public WrappedInt<Cycles> { | ||||
| 	public: | ||||
| 		constexpr Cycles(int l) : WrappedInt<Cycles>(l) {} | ||||
| 		constexpr Cycles() : WrappedInt<Cycles>() {} | ||||
| 		constexpr Cycles(const Cycles &cycles) : WrappedInt<Cycles>(cycles.length_) {} | ||||
| 		forceinline constexpr Cycles(IntType l) noexcept : WrappedInt<Cycles>(l) {} | ||||
| 		forceinline constexpr Cycles() noexcept : WrappedInt<Cycles>() {} | ||||
| 		forceinline constexpr Cycles(const Cycles &cycles) noexcept : WrappedInt<Cycles>(cycles.length_) {} | ||||
|  | ||||
| 	private: | ||||
| 		friend WrappedInt; | ||||
| 		void fill(Cycles &result) { | ||||
| 			result.length_ = length_; | ||||
| 			length_ = 0; | ||||
| 		} | ||||
|  | ||||
| 		void fill(Cycles &result, const Cycles &divisor) { | ||||
| 			result.length_ = length_ / divisor.length_; | ||||
| 			length_ %= divisor.length_; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| /// Describes an integer number of half cycles: single clock signal transitions. | ||||
| class HalfCycles: public WrappedInt<HalfCycles> { | ||||
| 	public: | ||||
| 		constexpr HalfCycles(int l) : WrappedInt<HalfCycles>(l) {} | ||||
| 		constexpr HalfCycles() : WrappedInt<HalfCycles>() {} | ||||
| 		forceinline constexpr HalfCycles(IntType l) noexcept : WrappedInt<HalfCycles>(l) {} | ||||
| 		forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {} | ||||
|  | ||||
| 		constexpr HalfCycles(const Cycles cycles) : WrappedInt<HalfCycles>(cycles.as_int() * 2) {} | ||||
| 		constexpr HalfCycles(const HalfCycles &half_cycles) : WrappedInt<HalfCycles>(half_cycles.length_) {} | ||||
| 		forceinline constexpr HalfCycles(const Cycles &cycles) noexcept : WrappedInt<HalfCycles>(cycles.as_integral() * 2) {} | ||||
| 		forceinline constexpr HalfCycles(const HalfCycles &half_cycles) noexcept : WrappedInt<HalfCycles>(half_cycles.length_) {} | ||||
|  | ||||
| 		/// @returns The number of whole cycles completely covered by this span of half cycles. | ||||
| 		constexpr Cycles cycles() const { | ||||
| 		forceinline constexpr Cycles cycles() const { | ||||
| 			return Cycles(length_ >> 1); | ||||
| 		} | ||||
|  | ||||
| 		/// Flushes the whole cycles in @c this, subtracting that many from the total stored here. | ||||
| 		Cycles flush_cycles() { | ||||
| 			Cycles result(length_ >> 1); | ||||
| 			length_ &= 1; | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		/// Flushes the half cycles in @c this, returning the number stored and setting this total to zero. | ||||
| 		HalfCycles flush() { | ||||
| 			HalfCycles result(length_); | ||||
| 			length_ = 0; | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Severs from @c this the effect of dividing by @c divisor; @c this will end up with | ||||
| 			the value of @c this modulo @c divisor and @c divided by @c divisor is returned. | ||||
| 		*/ | ||||
| 		Cycles divide_cycles(const Cycles &divisor) { | ||||
| 			HalfCycles half_divisor = HalfCycles(divisor); | ||||
| 			Cycles result(length_ / half_divisor.length_); | ||||
| 		forceinline Cycles divide_cycles(const Cycles &divisor) { | ||||
| 			const HalfCycles half_divisor = HalfCycles(divisor); | ||||
| 			const Cycles result(length_ / half_divisor.length_); | ||||
| 			length_ %= half_divisor.length_; | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		friend WrappedInt; | ||||
| 		void fill(Cycles &result) { | ||||
| 			result = Cycles(length_ >> 1); | ||||
| 			length_ &= 1; | ||||
| 		} | ||||
|  | ||||
| 		void fill(HalfCycles &result) { | ||||
| 			result.length_ = length_; | ||||
| 			length_ = 0; | ||||
| 		} | ||||
|  | ||||
| 		void fill(Cycles &result, const HalfCycles &divisor) { | ||||
| 			result = Cycles(length_ / (divisor.length_ << 1)); | ||||
| 			length_ %= (divisor.length_ << 1); | ||||
| 		} | ||||
|  | ||||
| 		void fill(HalfCycles &result, const HalfCycles &divisor) { | ||||
| 			result.length_ = length_ / divisor.length_; | ||||
| 			length_ %= divisor.length_; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| // Create a specialisation of WrappedInt::flush for converting HalfCycles to Cycles | ||||
| // without losing the fractional part. | ||||
|  | ||||
| /*! | ||||
| 	If a component implements only run_for(Cycles), an owner can wrap it in HalfClockReceiver | ||||
| 	automatically to gain run_for(HalfCycles). | ||||
| @@ -203,9 +250,9 @@ template <class T> class HalfClockReceiver: public T { | ||||
| 	public: | ||||
| 		using T::T; | ||||
|  | ||||
| 		inline void run_for(const HalfCycles half_cycles) { | ||||
| 		forceinline void run_for(const HalfCycles half_cycles) { | ||||
| 			half_cycles_ += half_cycles; | ||||
| 			T::run_for(half_cycles_.flush_cycles()); | ||||
| 			T::run_for(half_cycles_.flush<Cycles>()); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
|   | ||||
| @@ -1,25 +1,26 @@ | ||||
| //
 | ||||
| //  ClockDeferrer.hpp
 | ||||
| //  DeferredQueue.hpp
 | ||||
| //  Clock Signal
 | ||||
| //
 | ||||
| //  Created by Thomas Harte on 23/08/2018.
 | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved.
 | ||||
| //
 | ||||
| 
 | ||||
| #ifndef ClockDeferrer_h | ||||
| #define ClockDeferrer_h | ||||
| #ifndef DeferredQueue_h | ||||
| #define DeferredQueue_h | ||||
| 
 | ||||
| #include <functional> | ||||
| #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 | ||||
| 	that occur between those actions, triggering each action when it is reached. | ||||
| */ | ||||
| template <typename TimeUnit> class ClockDeferrer { | ||||
| template <typename TimeUnit> class DeferredQueue { | ||||
| 	public: | ||||
| 		/// Constructs a ClockDeferrer that will call target(period) in between deferred actions.
 | ||||
| 		ClockDeferrer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {} | ||||
| 		/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
 | ||||
| 		DeferredQueue(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {} | ||||
| 
 | ||||
| 		/*!
 | ||||
| 			Schedules @c action to occur in @c delay units of time. | ||||
| @@ -78,4 +79,4 @@ template <typename TimeUnit> class ClockDeferrer { | ||||
| 		std::vector<DeferredAction> pending_actions_; | ||||
| }; | ||||
| 
 | ||||
| #endif /* ClockDeferrer_h */ | ||||
| #endif /* DeferredQueue_h */ | ||||
| @@ -9,9 +9,9 @@ | ||||
| #ifndef ForceInline_hpp | ||||
| #define ForceInline_hpp | ||||
|  | ||||
| #ifdef DEBUG | ||||
| #ifndef NDEBUG | ||||
|  | ||||
| #define forceinline | ||||
| #define forceinline inline | ||||
|  | ||||
| #else | ||||
|  | ||||
|   | ||||
							
								
								
									
										123
									
								
								ClockReceiver/JustInTime.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								ClockReceiver/JustInTime.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| // | ||||
| //  JustInTime.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 28/07/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef JustInTime_h | ||||
| #define JustInTime_h | ||||
|  | ||||
| #include "../Concurrency/AsyncTaskQueue.hpp" | ||||
| #include "ForceInline.hpp" | ||||
|  | ||||
| /*! | ||||
| 	A JustInTimeActor holds (i) an embedded object with a run_for method; and (ii) an amount | ||||
| 	of time since run_for was last called. | ||||
|  | ||||
| 	Time can be added using the += operator. The -> operator can be used to access the | ||||
| 	embedded object. All time accumulated will be pushed to object before the pointer is returned. | ||||
|  | ||||
| 	Machines that accumulate HalfCycle time but supply to a Cycle-counted device may supply a | ||||
| 	separate @c TargetTimeScale at template declaration. | ||||
| */ | ||||
| template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class JustInTimeActor { | ||||
| 	public: | ||||
| 		/// Constructs a new JustInTimeActor using the same construction arguments as the included object. | ||||
| 		template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {} | ||||
|  | ||||
| 		/// Adds time to the actor. | ||||
| 		forceinline void operator += (const LocalTimeScale &rhs) { | ||||
| 			if constexpr (multiplier != 1) { | ||||
| 				time_since_update_ += rhs * multiplier; | ||||
| 			} else { | ||||
| 				time_since_update_ += rhs; | ||||
| 			} | ||||
| 			is_flushed_ = false; | ||||
| 		} | ||||
|  | ||||
| 		/// Flushes all accumulated time and returns a pointer to the included object. | ||||
| 		forceinline T *operator->() { | ||||
| 			flush(); | ||||
| 			return &object_; | ||||
| 		} | ||||
|  | ||||
| 		/// Returns a pointer to the included object without flushing time. | ||||
| 		forceinline T *last_valid() { | ||||
| 			return &object_; | ||||
| 		} | ||||
|  | ||||
| 		/// Flushes all accumulated time. | ||||
| 		forceinline void flush() { | ||||
| 			if(!is_flushed_) { | ||||
| 				is_flushed_ = true; | ||||
| 				if constexpr (divider == 1) { | ||||
| 					object_.run_for(time_since_update_.template flush<TargetTimeScale>()); | ||||
| 				} else { | ||||
| 					const auto duration = time_since_update_.template divide<TargetTimeScale>(LocalTimeScale(divider)); | ||||
| 					if(duration > TargetTimeScale(0)) | ||||
| 						object_.run_for(duration); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		T object_; | ||||
| 		LocalTimeScale time_since_update_; | ||||
| 		bool is_flushed_ = true; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue. | ||||
| 	Any time the amount of accumulated time crosses a threshold provided at construction time, | ||||
| 	the object will be updated on the AsyncTaskQueue. | ||||
| */ | ||||
| template <class T, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class AsyncJustInTimeActor { | ||||
| 	public: | ||||
| 		/// Constructs a new AsyncJustInTimeActor using the same construction arguments as the included object. | ||||
| 		template<typename... Args> AsyncJustInTimeActor(TargetTimeScale threshold, Args&&... args) : | ||||
| 			object_(std::forward<Args>(args)...), | ||||
| 		 	threshold_(threshold) {} | ||||
|  | ||||
| 		/// Adds time to the actor. | ||||
| 		inline void operator += (const LocalTimeScale &rhs) { | ||||
| 			time_since_update_ += rhs; | ||||
| 			if(time_since_update_ >= threshold_) { | ||||
| 				time_since_update_ -= threshold_; | ||||
| 				task_queue_.enqueue([this] () { | ||||
| 					object_.run_for(threshold_); | ||||
| 				}); | ||||
| 			} | ||||
| 			is_flushed_ = false; | ||||
| 		} | ||||
|  | ||||
| 		/// Flushes all accumulated time and returns a pointer to the included object. | ||||
| 		inline T *operator->() { | ||||
| 			flush(); | ||||
| 			return &object_; | ||||
| 		} | ||||
|  | ||||
| 		/// Returns a pointer to the included object without flushing time. | ||||
| 		inline T *last_valid() { | ||||
| 			return &object_; | ||||
| 		} | ||||
|  | ||||
| 		/// Flushes all accumulated time. | ||||
| 		inline void flush() { | ||||
| 			if(!is_flushed_) { | ||||
| 				task_queue_.flush(); | ||||
| 				object_.run_for(time_since_update_.template flush<TargetTimeScale>()); | ||||
| 				is_flushed_ = true; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		T object_; | ||||
| 		LocalTimeScale time_since_update_; | ||||
| 		TargetTimeScale threshold_; | ||||
| 		bool is_flushed_ = true; | ||||
| 		Concurrency::AsyncTaskQueue task_queue_; | ||||
| }; | ||||
|  | ||||
| #endif /* JustInTime_h */ | ||||
| @@ -9,6 +9,8 @@ | ||||
| #include "1770.hpp" | ||||
|  | ||||
| #include "../../Storage/Disk/Encodings/MFM/Constants.hpp" | ||||
|  | ||||
| #define LOG_PREFIX "[WD FDC] " | ||||
| #include "../../Outputs/Log.hpp" | ||||
|  | ||||
| using namespace WD; | ||||
| @@ -16,19 +18,19 @@ using namespace WD; | ||||
| WD1770::WD1770(Personality p) : | ||||
| 		Storage::Disk::MFMController(8000000), | ||||
| 		personality_(p), | ||||
| 		interesting_event_mask_(static_cast<int>(Event1770::Command)) { | ||||
| 		interesting_event_mask_(int(Event1770::Command)) { | ||||
| 	set_is_double_density(false); | ||||
| 	posit_event(static_cast<int>(Event1770::Command)); | ||||
| 	posit_event(int(Event1770::Command)); | ||||
| } | ||||
|  | ||||
| void WD1770::set_register(int address, uint8_t value) { | ||||
| void WD1770::write(int address, uint8_t value) { | ||||
| 	switch(address&3) { | ||||
| 		case 0: { | ||||
| 			if((value&0xf0) == 0xd0) { | ||||
| 				if(value == 0xd0) { | ||||
| 					// Force interrupt **immediately**. | ||||
| 					LOG("Force interrupt immediately"); | ||||
| 					posit_event(static_cast<int>(Event1770::ForceInterrupt)); | ||||
| 					posit_event(int(Event1770::ForceInterrupt)); | ||||
| 				} else { | ||||
| 					ERROR("!!!TODO: force interrupt!!!"); | ||||
| 					update_status([] (Status &status) { | ||||
| @@ -37,7 +39,7 @@ void WD1770::set_register(int address, uint8_t value) { | ||||
| 				} | ||||
| 			} else { | ||||
| 				command_ = value; | ||||
| 				posit_event(static_cast<int>(Event1770::Command)); | ||||
| 				posit_event(int(Event1770::Command)); | ||||
| 			} | ||||
| 		} | ||||
| 		break; | ||||
| @@ -52,27 +54,35 @@ void WD1770::set_register(int address, uint8_t value) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t WD1770::get_register(int address) { | ||||
| uint8_t WD1770::read(int address) { | ||||
| 	switch(address&3) { | ||||
| 		default: { | ||||
| 			update_status([] (Status &status) { | ||||
| 				status.interrupt_request = false; | ||||
| 			}); | ||||
| 			uint8_t status = | ||||
| 					(status_.write_protect ? Flag::WriteProtect : 0) | | ||||
| 				(status_.crc_error ? Flag::CRCError : 0) | | ||||
| 				(status_.busy ? Flag::Busy : 0); | ||||
|  | ||||
| 			// Per Jean Louis-Guérin's documentation: | ||||
| 			// | ||||
| 			//	* 	the write-protect bit is locked into place by a type 2 or type 3 command, but is | ||||
| 			//		read live after a type 1. | ||||
| 			//	*	the track 0 bit is captured during a type 1 instruction and lost upon any other type, | ||||
| 			//		it is not live sampled. | ||||
| 			switch(status_.type) { | ||||
| 				case Status::One: | ||||
| 					status |= | ||||
| 						(get_drive().get_is_track_zero() ? Flag::TrackZero : 0) | | ||||
| 						(status_.seek_error ? Flag::SeekError : 0); | ||||
| 						// TODO: index hole | ||||
| 						(status_.track_zero ? Flag::TrackZero : 0) | | ||||
| 						(status_.seek_error ? Flag::SeekError : 0) | | ||||
| 						(get_drive().get_is_read_only() ? Flag::WriteProtect : 0) | | ||||
| 						(get_drive().get_index_pulse() ? Flag::Index : 0); | ||||
| 				break; | ||||
|  | ||||
| 				case Status::Two: | ||||
| 				case Status::Three: | ||||
| 					status |= | ||||
| 						(status_.write_protect ? Flag::WriteProtect : 0) | | ||||
| 						(status_.record_type ? Flag::RecordType : 0) | | ||||
| 						(status_.lost_data ? Flag::LostData : 0) | | ||||
| 						(status_.data_request ? Flag::DataRequest : 0) | | ||||
| @@ -89,10 +99,15 @@ uint8_t WD1770::get_register(int address) { | ||||
| 				if(status_.type == Status::One) | ||||
| 					status |= (status_.spin_up ? Flag::SpinUp : 0); | ||||
| 			} | ||||
| //			LOG("Returned status " << PADHEX(2) << int(status) << " of type " << 1+int(status_.type)); | ||||
| 			return status; | ||||
| 		} | ||||
| 		case 1:		return track_; | ||||
| 		case 2:		return sector_; | ||||
| 		case 1: | ||||
| 			LOG("Returned track " << int(track_)); | ||||
| 			return track_; | ||||
| 		case 2: | ||||
| 			LOG("Returned sector " << int(sector_)); | ||||
| 			return sector_; | ||||
| 		case 3: | ||||
| 			update_status([] (Status &status) { | ||||
| 				status.data_request = false; | ||||
| @@ -105,28 +120,30 @@ void WD1770::run_for(const Cycles cycles) { | ||||
| 	Storage::Disk::Controller::run_for(cycles); | ||||
|  | ||||
| 	if(delay_time_) { | ||||
| 		unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_int()); | ||||
| 		const auto number_of_cycles = cycles.as_integral(); | ||||
| 		if(delay_time_ <= number_of_cycles) { | ||||
| 			delay_time_ = 0; | ||||
| 			posit_event(static_cast<int>(Event1770::Timer)); | ||||
| 			posit_event(int(Event1770::Timer)); | ||||
| 		} else { | ||||
| 			delay_time_ -= number_of_cycles; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(mask); return; case __LINE__: | ||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = int(mask); return; case __LINE__: | ||||
| #define WAIT_FOR_TIME(ms)		resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer); | ||||
| #define WAIT_FOR_BYTES(count)	resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = static_cast<int>(Event::Token); return; } | ||||
| #define WAIT_FOR_BYTES(count)	resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = int(Event::Token); return; } | ||||
| #define BEGIN_SECTION()	switch(resume_point_) { default: | ||||
| #define END_SECTION()	(void)0; } | ||||
|  | ||||
| #define READ_ID()	\ | ||||
| 		if(new_event_type == static_cast<int>(Event::Token)) {	\ | ||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; }	\ | ||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {	\ | ||||
| 		if(new_event_type == int(Event::Token)) {	\ | ||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {\ | ||||
| 				set_data_mode(DataMode::Reading);	\ | ||||
| 				++distance_into_section_;	\ | ||||
| 			} else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {	\ | ||||
| 				header_[distance_into_section_ - 1] = get_latest_token().byte_value;	\ | ||||
| 				distance_into_section_++;	\ | ||||
| 				++distance_into_section_;	\ | ||||
| 			}	\ | ||||
| 		} | ||||
|  | ||||
| @@ -159,10 +176,10 @@ void WD1770::run_for(const Cycles cycles) { | ||||
| // +--------+----------+-------------------------+ | ||||
|  | ||||
| void WD1770::posit_event(int new_event_type) { | ||||
| 	if(new_event_type == static_cast<int>(Event::IndexHole)) { | ||||
| 	if(new_event_type == int(Event::IndexHole)) { | ||||
| 		index_hole_count_++; | ||||
| 		if(index_hole_count_target_ == index_hole_count_) { | ||||
| 			posit_event(static_cast<int>(Event1770::IndexHoleTarget)); | ||||
| 			posit_event(int(Event1770::IndexHoleTarget)); | ||||
| 			index_hole_count_target_ = -1; | ||||
| 		} | ||||
|  | ||||
| @@ -177,19 +194,19 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if(new_event_type == static_cast<int>(Event1770::ForceInterrupt)) { | ||||
| 	if(new_event_type == int(Event1770::ForceInterrupt)) { | ||||
| 		interesting_event_mask_ = 0; | ||||
| 		resume_point_ = 0; | ||||
| 		update_status([] (Status &status) { | ||||
| 			status.type = Status::One; | ||||
| 			status.data_request = false; | ||||
| 			status.spin_up = false; | ||||
| 		}); | ||||
| 	} else { | ||||
| 		if(!(interesting_event_mask_ & static_cast<int>(new_event_type))) return; | ||||
| 		if(!(interesting_event_mask_ & int(new_event_type))) return; | ||||
| 		interesting_event_mask_ &= ~new_event_type; | ||||
| 	} | ||||
|  | ||||
| 	Status new_status; | ||||
| 	BEGIN_SECTION() | ||||
|  | ||||
| 	// Wait for a new command, branch to the appropriate handler. | ||||
| @@ -209,9 +226,10 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		update_status([] (Status &status) { | ||||
| 			status.busy = true; | ||||
| 			status.interrupt_request = false; | ||||
| 			status.track_zero = false;	// Always reset by a non-type 1; so reset regardless and set properly later. | ||||
| 		}); | ||||
|  | ||||
| 		LOG("Starting " << std::hex << command_ << std::endl); | ||||
| 		LOG("Starting " << PADHEX(2) << int(command_)); | ||||
|  | ||||
| 		if(!(command_ & 0x80)) goto begin_type_1; | ||||
| 		if(!(command_ & 0x40)) goto begin_type_2; | ||||
| @@ -241,6 +259,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			status.data_request = false; | ||||
| 		}); | ||||
|  | ||||
| 		LOG("Step/Seek/Restore with track " << int(track_) << " data " << int(data_)); | ||||
| 		if(!has_motor_on_line() && !has_head_load_line()) goto test_type1_type; | ||||
|  | ||||
| 		if(has_motor_on_line()) goto begin_type1_spin_up; | ||||
| @@ -273,19 +292,19 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		} | ||||
|  | ||||
| 	perform_seek_or_restore_command: | ||||
| 		if(track_ == data_) goto verify; | ||||
| 		if(track_ == data_) goto verify_seek; | ||||
| 		step_direction_ = (data_ > track_); | ||||
|  | ||||
| 	adjust_track: | ||||
| 		if(step_direction_) track_++; else track_--; | ||||
| 		if(step_direction_) ++track_; else --track_; | ||||
|  | ||||
| 	perform_step: | ||||
| 		if(!step_direction_ && get_drive().get_is_track_zero()) { | ||||
| 			track_ = 0; | ||||
| 			goto verify; | ||||
| 			goto verify_seek; | ||||
| 		} | ||||
| 		get_drive().step(Storage::Disk::HeadPosition(step_direction_ ? 1 : -1)); | ||||
| 		unsigned int time_to_wait; | ||||
| 		Cycles::IntType time_to_wait; | ||||
| 		switch(command_ & 3) { | ||||
| 			default: | ||||
| 			case 0: time_to_wait = 6;	break; | ||||
| @@ -294,14 +313,17 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			case 3: time_to_wait = (personality_ == P1772) ? 3 : 30;	break; | ||||
| 		} | ||||
| 		WAIT_FOR_TIME(time_to_wait); | ||||
| 		if(command_ >> 5) goto verify; | ||||
| 		if(command_ >> 5) goto verify_seek; | ||||
| 		goto perform_seek_or_restore_command; | ||||
|  | ||||
| 	perform_step_command: | ||||
| 		if(command_ & 0x10) goto adjust_track; | ||||
| 		goto perform_step; | ||||
|  | ||||
| 	verify: | ||||
| 	verify_seek: | ||||
| 		update_status([this] (Status &status) { | ||||
| 			status.track_zero = get_drive().get_is_track_zero(); | ||||
| 		}); | ||||
| 		if(!(command_ & 0x04)) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| @@ -310,17 +332,20 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		distance_into_section_ = 0; | ||||
|  | ||||
| 	verify_read_data: | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | ||||
| 		WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token)); | ||||
| 		READ_ID(); | ||||
|  | ||||
| 		if(index_hole_count_ == 6) { | ||||
| 			LOG("Nothing found to verify"); | ||||
| 			update_status([] (Status &status) { | ||||
| 				status.seek_error = true; | ||||
| 			}); | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		if(distance_into_section_ == 7) { | ||||
| 			distance_into_section_ = 0; | ||||
| 			set_data_mode(DataMode::Scanning); | ||||
|  | ||||
| 			if(get_crc_generator().get_value()) { | ||||
| 				update_status([] (Status &status) { | ||||
| 					status.crc_error = true; | ||||
| @@ -329,14 +354,12 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			} | ||||
|  | ||||
| 			if(header_[0] == track_) { | ||||
| 				LOG("Reached track " << std::dec << track_); | ||||
| 				LOG("Reached track " << std::dec << int(track_)); | ||||
| 				update_status([] (Status &status) { | ||||
| 					status.crc_error = false; | ||||
| 				}); | ||||
| 				goto wait_for_command; | ||||
| 			} | ||||
|  | ||||
| 			distance_into_section_ = 0; | ||||
| 		} | ||||
| 		goto verify_read_data; | ||||
|  | ||||
| @@ -393,23 +416,28 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
|  | ||||
| 		distance_into_section_ = 0; | ||||
| 		set_data_mode(DataMode::Scanning); | ||||
|  | ||||
| 	type2_get_header: | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | ||||
| 		WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token)); | ||||
| 		READ_ID(); | ||||
|  | ||||
| 		if(index_hole_count_ == 5) { | ||||
| 			LOG("Failed to find sector " << std::dec << sector_); | ||||
| 			LOG("Failed to find sector " << std::dec << int(sector_)); | ||||
| 			update_status([] (Status &status) { | ||||
| 				status.record_not_found = true; | ||||
| 			}); | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		if(distance_into_section_ == 7) { | ||||
| 			LOG("Considering " << std::dec << header_[0] << "/" << header_[2]); | ||||
| 			distance_into_section_ = 0; | ||||
| 			set_data_mode(DataMode::Scanning); | ||||
|  | ||||
| 			LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2])); | ||||
| 			if(		header_[0] == track_ && header_[2] == sector_ && | ||||
| 					(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) { | ||||
| 				LOG("Found " << std::dec << header_[0] << "/" << header_[2]); | ||||
| 				LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2])); | ||||
| 				if(get_crc_generator().get_value()) { | ||||
| 					LOG("CRC error; back to searching"); | ||||
| 					update_status([] (Status &status) { | ||||
| @@ -423,7 +451,6 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 				}); | ||||
| 				goto type2_read_or_write_data; | ||||
| 			} | ||||
| 			distance_into_section_ = 0; | ||||
| 		} | ||||
| 		goto type2_get_header; | ||||
|  | ||||
| @@ -466,6 +493,9 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		header_[distance_into_section_] = get_latest_token().byte_value; | ||||
| 		distance_into_section_++; | ||||
| 		if(distance_into_section_ == 2) { | ||||
| 			distance_into_section_ = 0; | ||||
| 			set_data_mode(DataMode::Scanning); | ||||
|  | ||||
| 			if(get_crc_generator().get_value()) { | ||||
| 				LOG("CRC error; terminating"); | ||||
| 				update_status([this] (Status &status) { | ||||
| @@ -474,11 +504,13 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 				goto wait_for_command; | ||||
| 			} | ||||
|  | ||||
| 			LOG("Finished reading sector " << std::dec << int(sector_)); | ||||
|  | ||||
| 			if(command_ & 0x10) { | ||||
| 				sector_++; | ||||
| 				LOG("Advancing to search for sector " << std::dec << int(sector_)); | ||||
| 				goto test_type2_write_protection; | ||||
| 			} | ||||
| 			LOG("Finished reading sector " << std::dec << sector_); | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		goto type2_check_crc; | ||||
| @@ -560,7 +592,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			sector_++; | ||||
| 			goto test_type2_write_protection; | ||||
| 		} | ||||
| 		LOG("Wrote sector " << std::dec << sector_); | ||||
| 		LOG("Wrote sector " << std::dec << int(sector_)); | ||||
| 		goto wait_for_command; | ||||
|  | ||||
|  | ||||
| @@ -611,8 +643,8 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		distance_into_section_ = 0; | ||||
|  | ||||
| 	read_address_get_header: | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | ||||
| 		if(new_event_type == static_cast<int>(Event::Token)) { | ||||
| 		WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token)); | ||||
| 		if(new_event_type == int(Event::Token)) { | ||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; } | ||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { | ||||
| 				if(status_.data_request) { | ||||
| @@ -626,9 +658,11 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 				update_status([] (Status &status) { | ||||
| 					status.data_request = true; | ||||
| 				}); | ||||
| 				distance_into_section_++; | ||||
| 				++distance_into_section_; | ||||
|  | ||||
| 				if(distance_into_section_ == 7) { | ||||
| 					distance_into_section_ = 0; | ||||
|  | ||||
| 					if(get_crc_generator().get_value()) { | ||||
| 						update_status([] (Status &status) { | ||||
| 							status.crc_error = true; | ||||
| @@ -652,7 +686,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		index_hole_count_ = 0; | ||||
|  | ||||
| 	read_track_read_byte: | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); | ||||
| 		WAIT_FOR_EVENT(int(Event::Token) | int(Event::IndexHole)); | ||||
| 		if(index_hole_count_) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| @@ -719,7 +753,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 				case 0xfd: case 0xfe: | ||||
| 					// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022 | ||||
| 					write_raw_short( | ||||
| 						static_cast<uint16_t>( | ||||
| 						uint16_t( | ||||
| 							0xa022 | | ||||
| 							((data_ & 0x80) << 7) | | ||||
| 							((data_ & 0x40) << 6) | | ||||
| @@ -768,15 +802,18 @@ void WD1770::posit_event(int new_event_type) { | ||||
| } | ||||
|  | ||||
| void WD1770::update_status(std::function<void(Status &)> updater) { | ||||
| 	const Status old_status = status_; | ||||
|  | ||||
| 	if(delegate_) { | ||||
| 		Status old_status = status_; | ||||
| 		updater(status_); | ||||
| 		bool did_change = | ||||
| 		const bool did_change = | ||||
| 			(status_.busy != old_status.busy) || | ||||
| 			(status_.data_request != old_status.data_request); | ||||
| 			(status_.data_request != old_status.data_request) || | ||||
| 			(status_.interrupt_request != old_status.interrupt_request); | ||||
| 		if(did_change) delegate_->wd1770_did_change_output(this); | ||||
| 	} | ||||
| 	else updater(status_); | ||||
| 	} else updater(status_); | ||||
|  | ||||
| 	if(status_.busy != old_status.busy) update_clocking_observer(); | ||||
| } | ||||
|  | ||||
| void WD1770::set_head_load_request(bool head_load) {} | ||||
| @@ -784,5 +821,14 @@ void WD1770::set_motor_on(bool motor_on) {} | ||||
|  | ||||
| void WD1770::set_head_loaded(bool head_loaded) { | ||||
| 	head_is_loaded_ = head_loaded; | ||||
| 	if(head_loaded) posit_event(static_cast<int>(Event1770::HeadLoad)); | ||||
| 	if(head_loaded) posit_event(int(Event1770::HeadLoad)); | ||||
| } | ||||
|  | ||||
| bool WD1770::get_head_loaded() { | ||||
| 	return head_is_loaded_; | ||||
| } | ||||
|  | ||||
| ClockingHint::Preference WD1770::preferred_clocking() { | ||||
| 	if(status_.busy) return ClockingHint::Preference::RealTime; | ||||
| 	return Storage::Disk::MFMController::preferred_clocking(); | ||||
| } | ||||
|   | ||||
| @@ -36,10 +36,10 @@ class WD1770: public Storage::Disk::MFMController { | ||||
| 		using Storage::Disk::MFMController::set_is_double_density; | ||||
|  | ||||
| 		/// Writes @c value to the register at @c address. Only the low two bits of the address are decoded. | ||||
| 		void set_register(int address, uint8_t value); | ||||
| 		void write(int address, uint8_t value); | ||||
|  | ||||
| 		/// Fetches the value of the register @c address. Only the low two bits of the address are decoded. | ||||
| 		uint8_t get_register(int address); | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		/// Runs the controller for @c number_of_cycles cycles. | ||||
| 		void run_for(const Cycles cycles); | ||||
| @@ -73,11 +73,16 @@ class WD1770: public Storage::Disk::MFMController { | ||||
| 		}; | ||||
| 		inline void set_delegate(Delegate *delegate)	{	delegate_ = delegate;			} | ||||
|  | ||||
| 		ClockingHint::Preference preferred_clocking() final; | ||||
|  | ||||
| 	protected: | ||||
| 		virtual void set_head_load_request(bool head_load); | ||||
| 		virtual void set_motor_on(bool motor_on); | ||||
| 		void set_head_loaded(bool head_loaded); | ||||
|  | ||||
| 		/// @returns The last value posted to @c set_head_loaded. | ||||
| 		bool get_head_loaded(); | ||||
|  | ||||
| 	private: | ||||
| 		Personality personality_; | ||||
| 		inline bool has_motor_on_line() { return (personality_ != P1793 ) && (personality_ != P1773); } | ||||
| @@ -94,6 +99,7 @@ class WD1770: public Storage::Disk::MFMController { | ||||
| 			bool data_request = false; | ||||
| 			bool interrupt_request = false; | ||||
| 			bool busy = false; | ||||
| 			bool track_zero = false; | ||||
| 			enum { | ||||
| 				One, Two, Three | ||||
| 			} type = One; | ||||
| @@ -121,7 +127,7 @@ class WD1770: public Storage::Disk::MFMController { | ||||
| 		void posit_event(int type); | ||||
| 		int interesting_event_mask_; | ||||
| 		int resume_point_ = 0; | ||||
| 		unsigned int delay_time_ = 0; | ||||
| 		Cycles::IntType delay_time_ = 0; | ||||
|  | ||||
| 		// ID buffer | ||||
| 		uint8_t header_[6]; | ||||
|   | ||||
							
								
								
									
										312
									
								
								Components/5380/ncr5380.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								Components/5380/ncr5380.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,312 @@ | ||||
| // | ||||
| //  ncr5380.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/08/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "ncr5380.hpp" | ||||
|  | ||||
| #include "../../Outputs/Log.hpp" | ||||
|  | ||||
| using namespace NCR::NCR5380; | ||||
| using SCSI::Line; | ||||
|  | ||||
| NCR5380::NCR5380(SCSI::Bus &bus, int clock_rate) : | ||||
| 	bus_(bus), | ||||
| 	clock_rate_(clock_rate) { | ||||
| 	device_id_ = bus_.add_device(); | ||||
| 	bus_.add_observer(this); | ||||
| } | ||||
|  | ||||
| void NCR5380::write(int address, uint8_t value, bool dma_acknowledge) { | ||||
| 	switch(address & 7) { | ||||
| 		case 0: | ||||
| //			LOG("[SCSI 0] Set current SCSI bus state to " << PADHEX(2) << int(value)); | ||||
| 			data_bus_ = value; | ||||
|  | ||||
| 			if(dma_request_ && dma_operation_ == DMAOperation::Send) { | ||||
| //				printf("w %02x\n", value); | ||||
| 				dma_acknowledge_ = true; | ||||
| 				dma_request_ = false; | ||||
| 				update_control_output(); | ||||
| 				bus_.set_device_output(device_id_, bus_output_); | ||||
| 			} | ||||
| 		break; | ||||
|  | ||||
| 		case 1: { | ||||
| //			LOG("[SCSI 1] Initiator command register set: " << PADHEX(2) << int(value)); | ||||
| 			initiator_command_ = value; | ||||
|  | ||||
| 			bus_output_ &= ~(Line::Reset | Line::Acknowledge | Line::Busy | Line::SelectTarget | Line::Attention); | ||||
| 			if(value & 0x80) bus_output_ |= Line::Reset; | ||||
| 			if(value & 0x08) bus_output_ |= Line::Busy; | ||||
| 			if(value & 0x04) bus_output_ |= Line::SelectTarget; | ||||
|  | ||||
| 			/* bit 5 = differential enable if this were a 5381 */ | ||||
|  | ||||
| 			test_mode_ = value & 0x40; | ||||
| 			assert_data_bus_ = value & 0x01; | ||||
| 			update_control_output(); | ||||
| 		} break; | ||||
|  | ||||
| 		case 2: | ||||
| //			LOG("[SCSI 2] Set mode: " << PADHEX(2) << int(value)); | ||||
| 			mode_ = value; | ||||
|  | ||||
| 			// bit 7: 1 = use block mode DMA mode (if DMA mode is also enabled) | ||||
| 			// bit 6: 1 = be a SCSI target; 0 = be an initiator | ||||
| 			// bit 5: 1 = check parity | ||||
| 			// bit 4: 1 = generate an interrupt if parity checking is enabled and an error is found | ||||
| 			// bit 3: 1 = generate an interrupt when an EOP is received from the DMA controller | ||||
| 			// bit 2: 1 = generate an interrupt and reset low 6 bits of register 1 if an unexpected loss of Line::Busy occurs | ||||
| 			// bit 1: 1 = use DMA mode | ||||
| 			// bit 0: 1 = begin arbitration mode (device ID should be in register 0) | ||||
| 			arbitration_in_progress_ = false; | ||||
| 			switch(mode_ & 0x3) { | ||||
| 				case 0x0: | ||||
| 					bus_output_ &= ~SCSI::Line::Busy; | ||||
| 					dma_request_ = false; | ||||
| 					set_execution_state(ExecutionState::None); | ||||
| 				break; | ||||
|  | ||||
| 				case 0x1: | ||||
| 					arbitration_in_progress_ = true; | ||||
| 					set_execution_state(ExecutionState::WaitingForBusy); | ||||
| 					lost_arbitration_ = false; | ||||
| 				break; | ||||
|  | ||||
| 				default: | ||||
| 					assert_data_bus_ = false; | ||||
| 					set_execution_state(ExecutionState::PerformingDMA); | ||||
| 					bus_.update_observers(); | ||||
| 				break; | ||||
| 			} | ||||
| 			update_control_output(); | ||||
| 		break; | ||||
|  | ||||
| 		case 3: { | ||||
| //			LOG("[SCSI 3] Set target command: " << PADHEX(2) << int(value)); | ||||
| 			target_command_ = value; | ||||
| 			update_control_output(); | ||||
| 		} break; | ||||
|  | ||||
| 		case 4: | ||||
| //			LOG("[SCSI 4] Set select enabled: " << PADHEX(2) << int(value)); | ||||
| 		break; | ||||
|  | ||||
| 		case 5: | ||||
| //			LOG("[SCSI 5] Start DMA send: " << PADHEX(2) << int(value)); | ||||
| 			dma_operation_ = DMAOperation::Send; | ||||
| 		break; | ||||
|  | ||||
| 		case 6: | ||||
| //			LOG("[SCSI 6] Start DMA target receive: " << PADHEX(2) << int(value)); | ||||
| 			dma_operation_ = DMAOperation::TargetReceive; | ||||
| 		break; | ||||
|  | ||||
| 		case 7: | ||||
| //			LOG("[SCSI 7] Start DMA initiator receive: " << PADHEX(2) << int(value)); | ||||
| 			dma_operation_ = DMAOperation::InitiatorReceive; | ||||
| 		break; | ||||
| 	} | ||||
|  | ||||
| 	// Data is output only if the data bus is asserted. | ||||
| 	if(assert_data_bus_) { | ||||
| 		bus_output_ = (bus_output_ & ~SCSI::Line::Data) | data_bus_; | ||||
| 	} else { | ||||
| 		bus_output_ &= ~SCSI::Line::Data; | ||||
| 	} | ||||
|  | ||||
| 	// In test mode, still nothing is output. Otherwise throw out | ||||
| 	// the current value of bus_output_. | ||||
| 	if(test_mode_) { | ||||
| 		bus_.set_device_output(device_id_, SCSI::DefaultBusState); | ||||
| 	} else { | ||||
| 		bus_.set_device_output(device_id_, bus_output_); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t NCR5380::read(int address, bool dma_acknowledge) { | ||||
| 	switch(address & 7) { | ||||
| 		case 0: | ||||
| //			LOG("[SCSI 0] Get current SCSI bus state: " << PADHEX(2) << (bus_.get_state() & 0xff)); | ||||
|  | ||||
| 			if(dma_request_ && dma_operation_ == DMAOperation::InitiatorReceive) { | ||||
| 				dma_acknowledge_ = true; | ||||
| 				dma_request_ = false; | ||||
| 				update_control_output(); | ||||
| 				bus_.set_device_output(device_id_, bus_output_); | ||||
| 			} | ||||
| 		return uint8_t(bus_.get_state()); | ||||
|  | ||||
| 		case 1: | ||||
| //			LOG("[SCSI 1] Initiator command register get: " << (arbitration_in_progress_ ? 'p' : '-') <<  (lost_arbitration_ ? 'l' : '-')); | ||||
| 		return | ||||
| 			// Bits repeated as they were set. | ||||
| 			(initiator_command_ & ~0x60) | | ||||
|  | ||||
| 			// Arbitration in progress. | ||||
| 			(arbitration_in_progress_ ? 0x40 : 0x00) | | ||||
|  | ||||
| 			// Lost arbitration. | ||||
| 			(lost_arbitration_ ? 0x20 : 0x00); | ||||
|  | ||||
| 		case 2: | ||||
| //			LOG("[SCSI 2] Get mode"); | ||||
| 		return mode_; | ||||
|  | ||||
| 		case 3: | ||||
| //			LOG("[SCSI 3] Get target command"); | ||||
| 		return target_command_; | ||||
|  | ||||
| 		case 4: { | ||||
| 			const auto bus_state = bus_.get_state(); | ||||
| 			const uint8_t result = | ||||
| 				((bus_state & Line::Reset)			? 0x80 : 0x00) | | ||||
| 				((bus_state & Line::Busy)			? 0x40 : 0x00) | | ||||
| 				((bus_state & Line::Request)		? 0x20 : 0x00) | | ||||
| 				((bus_state & Line::Message)		? 0x10 : 0x00) | | ||||
| 				((bus_state & Line::Control)		? 0x08 : 0x00) | | ||||
| 				((bus_state & Line::Input)			? 0x04 : 0x00) | | ||||
| 				((bus_state & Line::SelectTarget)	? 0x02 : 0x00) | | ||||
| 				((bus_state & Line::Parity)			? 0x01 : 0x00); | ||||
| //			LOG("[SCSI 4] Get current bus state: " << PADHEX(2) << int(result)); | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		case 5: { | ||||
| 			const auto bus_state = bus_.get_state(); | ||||
| 			const bool phase_matches = | ||||
| 				(target_output() & (Line::Message | Line::Control | Line::Input)) == | ||||
| 				(bus_state & (Line::Message | Line::Control | Line::Input)); | ||||
|  | ||||
| 			const uint8_t result = | ||||
| 				/* b7 = end of DMA */ | ||||
| 				((dma_request_ && state_ == ExecutionState::PerformingDMA) ? 0x40 : 0x00)	| | ||||
| 				/* b5 = parity error */ | ||||
| 				/* b4 = IRQ active */ | ||||
| 				(phase_matches ? 0x08 : 0x00)	| | ||||
| 				/* b2 = busy error */ | ||||
| 				((bus_state & Line::Attention) ? 0x02 : 0x00) | | ||||
| 				((bus_state & Line::Acknowledge) ? 0x01 : 0x00); | ||||
| //			LOG("[SCSI 5] Get bus and status: " << PADHEX(2) << int(result)); | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		case 6: | ||||
| //			LOG("[SCSI 6] Get input data"); | ||||
| 		return 0xff; | ||||
|  | ||||
| 		case 7: | ||||
| //			LOG("[SCSI 7] Reset parity/interrupt"); | ||||
| 		return 0xff; | ||||
| 	} | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| SCSI::BusState NCR5380::target_output() { | ||||
| 	SCSI::BusState output = SCSI::DefaultBusState; | ||||
| 	if(target_command_ & 0x08) output |= Line::Request; | ||||
| 	if(target_command_ & 0x04) output |= Line::Message; | ||||
| 	if(target_command_ & 0x02) output |= Line::Control; | ||||
| 	if(target_command_ & 0x01) output |= Line::Input; | ||||
| 	return output; | ||||
| } | ||||
|  | ||||
| void NCR5380::update_control_output() { | ||||
| 	bus_output_ &= ~(Line::Request | Line::Message | Line::Control | Line::Input | Line::Acknowledge | Line::Attention); | ||||
| 	if(mode_ & 0x40) { | ||||
| 		// This is a target; C/D, I/O, /MSG and /REQ are signalled on the bus. | ||||
| 		bus_output_ |= target_output(); | ||||
| 	} else { | ||||
| 		// This is an initiator; /ATN and /ACK are signalled on the bus. | ||||
| 		if( | ||||
| 			(initiator_command_ & 0x10) || | ||||
| 			(state_ == ExecutionState::PerformingDMA && dma_acknowledge_) | ||||
| 		) bus_output_ |= Line::Acknowledge; | ||||
| 		if(initiator_command_ & 0x02) bus_output_ |= Line::Attention; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void NCR5380::scsi_bus_did_change(SCSI::Bus *, SCSI::BusState new_state, double time_since_change) { | ||||
| 	switch(state_) { | ||||
| 		default: break; | ||||
|  | ||||
| 		/* | ||||
| 			Official documentation: | ||||
|  | ||||
| 				Arbitration is accomplished using a bus-free filter to continuously monitor BSY. | ||||
| 				If BSY remains inactive for at least 400 nsec then the SCSI bus is considered free | ||||
| 				and arbitration may begin. Arbitration will begin if the bus is free, SEL is inactive | ||||
| 				and the ARBITRATION bit (port 2, bit 0) is active. Once arbitration has begun | ||||
| 				(BSY asserted), an arbitration delay of 2.2 /Lsec must elapse before the data bus | ||||
| 				can be examined to deter- mine if arbitration has been won. This delay must be | ||||
| 				implemented in the controlling software driver. | ||||
|  | ||||
| 			Personal notes: | ||||
|  | ||||
| 				I'm discounting that "arbitratation is accomplished" opening, and assuming that what needs | ||||
| 				to happen is: | ||||
|  | ||||
| 					(i) wait for BSY to be inactive; | ||||
| 					(ii) count 400 nsec; | ||||
| 					(iii) check that BSY and SEL are inactive. | ||||
| 		*/ | ||||
|  | ||||
| 		case ExecutionState::WaitingForBusy: | ||||
| 			if(!(new_state & SCSI::Line::Busy) || time_since_change < SCSI::DeskewDelay) return; | ||||
| 			state_ = ExecutionState::WatchingBusy; | ||||
|  | ||||
| 		case ExecutionState::WatchingBusy: | ||||
| 			if(!(new_state & SCSI::Line::Busy)) { | ||||
| 				lost_arbitration_ = true; | ||||
| 				set_execution_state(ExecutionState::None); | ||||
| 			} | ||||
|  | ||||
| 			// Check for having hit 400ns (more or less) since BSY was inactive. | ||||
| 			if(time_since_change >= SCSI::BusSettleDelay) { | ||||
| //				arbitration_in_progress_ = false; | ||||
| 				if(new_state & SCSI::Line::SelectTarget) { | ||||
| 					lost_arbitration_ = true; | ||||
| 					set_execution_state(ExecutionState::None); | ||||
| 				} else { | ||||
| 					bus_output_ &= ~SCSI::Line::Busy; | ||||
| 					set_execution_state(ExecutionState::None); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			/* TODO: there's a bug here, given that the dropping of Busy isn't communicated onward. */ | ||||
| 		break; | ||||
|  | ||||
| 		case ExecutionState::PerformingDMA: | ||||
| 			if(time_since_change < SCSI::DeskewDelay) return; | ||||
|  | ||||
| 			// Signal a DMA request if the request line is active, i.e. meaningful data is | ||||
| 			// on the bus, and this device hasn't yet acknowledged it. | ||||
| 			switch(new_state & (SCSI::Line::Request | SCSI::Line::Acknowledge)) { | ||||
| 				case 0: | ||||
| 					dma_request_ = false; | ||||
| 				break; | ||||
| 				case SCSI::Line::Request: | ||||
| 					dma_request_ = true; | ||||
| 				break; | ||||
| 				case SCSI::Line::Request | SCSI::Line::Acknowledge: | ||||
| 					dma_request_ = false; | ||||
| 				break; | ||||
| 				case SCSI::Line::Acknowledge: | ||||
| 					dma_acknowledge_ = false; | ||||
| 					dma_request_ = false; | ||||
| 					update_control_output(); | ||||
| 					bus_.set_device_output(device_id_, bus_output_); | ||||
| 				break; | ||||
| 			} | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void NCR5380::set_execution_state(ExecutionState state) { | ||||
| 	state_ = state; | ||||
| 	if(state != ExecutionState::PerformingDMA) dma_operation_ = DMAOperation::Ready; | ||||
| } | ||||
							
								
								
									
										75
									
								
								Components/5380/ncr5380.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								Components/5380/ncr5380.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| // | ||||
| //  ncr5380.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/08/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef ncr5380_hpp | ||||
| #define ncr5380_hpp | ||||
|  | ||||
| #include <cstdint> | ||||
|  | ||||
| #include "../../Storage/MassStorage/SCSI/SCSI.hpp" | ||||
|  | ||||
|  | ||||
| namespace NCR { | ||||
| namespace NCR5380 { | ||||
|  | ||||
| /*! | ||||
| 	Models the NCR 5380, a SCSI interface chip. | ||||
| */ | ||||
| class NCR5380 final: public SCSI::Bus::Observer { | ||||
| 	public: | ||||
| 		NCR5380(SCSI::Bus &bus, int clock_rate); | ||||
|  | ||||
| 		/*! Writes @c value to @c address.  */ | ||||
| 		void write(int address, uint8_t value, bool dma_acknowledge = false); | ||||
|  | ||||
| 		/*! Reads from @c address. */ | ||||
| 		uint8_t read(int address, bool dma_acknowledge = false); | ||||
|  | ||||
| 	private: | ||||
| 		SCSI::Bus &bus_; | ||||
|  | ||||
| 		const int clock_rate_; | ||||
| 		size_t device_id_; | ||||
|  | ||||
| 		SCSI::BusState bus_output_ = SCSI::DefaultBusState; | ||||
| 		SCSI::BusState expected_phase_ = SCSI::DefaultBusState; | ||||
| 		uint8_t mode_ = 0xff; | ||||
| 		uint8_t initiator_command_ = 0xff; | ||||
| 		uint8_t data_bus_ = 0xff; | ||||
| 		uint8_t target_command_ = 0xff; | ||||
| 		bool test_mode_ = false; | ||||
| 		bool assert_data_bus_ = false; | ||||
| 		bool dma_request_ = false; | ||||
| 		bool dma_acknowledge_ = false; | ||||
|  | ||||
| 		enum class ExecutionState { | ||||
| 			None, | ||||
| 			WaitingForBusy, | ||||
| 			WatchingBusy, | ||||
| 			PerformingDMA, | ||||
| 		} state_ = ExecutionState::None; | ||||
| 		enum class DMAOperation { | ||||
| 			Ready, | ||||
| 			Send, | ||||
| 			TargetReceive, | ||||
| 			InitiatorReceive | ||||
| 		} dma_operation_ = DMAOperation::Ready; | ||||
| 		bool lost_arbitration_ = false, arbitration_in_progress_ = false; | ||||
|  | ||||
| 		void set_execution_state(ExecutionState state); | ||||
|  | ||||
| 		SCSI::BusState target_output(); | ||||
| 		void update_control_output(); | ||||
|  | ||||
| 		void scsi_bus_did_change(SCSI::Bus *, SCSI::BusState new_state, double time_since_change) final; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* ncr5380_hpp */ | ||||
| @@ -47,6 +47,12 @@ class PortHandler { | ||||
|  | ||||
| 		/// Sets the current logical value of the interrupt line. | ||||
| 		void set_interrupt_status(bool status)									{} | ||||
|  | ||||
| 		/// Provides a measure of time elapsed between other calls. | ||||
| 		void run_for(HalfCycles duration)										{} | ||||
|  | ||||
| 		/// Receives passed-on flush() calls from the 6522. | ||||
| 		void flush()															{} | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| @@ -71,8 +77,31 @@ class IRQDelegatePortHandler: public PortHandler { | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| }; | ||||
|  | ||||
| class MOS6522Base: public MOS6522Storage { | ||||
| /*! | ||||
| 	Implements a template for emulation of the MOS 6522 Versatile Interface Adaptor ('VIA'). | ||||
|  | ||||
| 	The VIA provides: | ||||
| 		* two timers, each of which may trigger interrupts and one of which may repeat; | ||||
| 		* two digial input/output ports; and | ||||
| 		* a serial-to-parallel shifter. | ||||
|  | ||||
| 	Consumers should derive their own curiously-recurring-template-pattern subclass, | ||||
| 	implementing bus communications as required. | ||||
| */ | ||||
| template <class T> class MOS6522: public MOS6522Storage { | ||||
| 	public: | ||||
| 		MOS6522(T &bus_handler) noexcept : bus_handler_(bus_handler) {} | ||||
| 		MOS6522(const MOS6522 &) = delete; | ||||
|  | ||||
| 		/*! Sets a register value. */ | ||||
| 		void write(int address, uint8_t value); | ||||
|  | ||||
| 		/*! Gets a register value. */ | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		/*! @returns the bus handler. */ | ||||
| 		T &bus_handler(); | ||||
|  | ||||
| 		/// Sets the input value of line @c line on port @c port. | ||||
| 		void set_control_line_input(Port port, Line line, bool value); | ||||
|  | ||||
| @@ -85,47 +114,32 @@ class MOS6522Base: public MOS6522Storage { | ||||
| 		/// @returns @c true if the IRQ line is currently active; @c false otherwise. | ||||
| 		bool get_interrupt_line(); | ||||
|  | ||||
| 	private: | ||||
| 		inline void do_phase1(); | ||||
| 		inline void do_phase2(); | ||||
| 		virtual void reevaluate_interrupts() = 0; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Implements a template for emulation of the MOS 6522 Versatile Interface Adaptor ('VIA'). | ||||
|  | ||||
| 	The VIA provides: | ||||
| 		* two timers, each of which may trigger interrupts and one of which may repeat; | ||||
| 		* two digial input/output ports; and | ||||
| 		* a serial-to-parallel shifter. | ||||
|  | ||||
| 	Consumers should derive their own curiously-recurring-template-pattern subclass, | ||||
| 	implementing bus communications as required. | ||||
| */ | ||||
| template <class T> class MOS6522: public MOS6522Base { | ||||
| 	public: | ||||
| 		MOS6522(T &bus_handler) noexcept : bus_handler_(bus_handler) {} | ||||
| 		MOS6522(const MOS6522 &) = delete; | ||||
|  | ||||
| 		/*! Sets a register value. */ | ||||
| 		void set_register(int address, uint8_t value); | ||||
|  | ||||
| 		/*! Gets a register value. */ | ||||
| 		uint8_t get_register(int address); | ||||
|  | ||||
| 		/*! @returns the bus handler. */ | ||||
| 		T &bus_handler(); | ||||
| 		/// Updates the port handler to the current time and then requests that it flush. | ||||
| 		void flush(); | ||||
|  | ||||
| 	private: | ||||
| 		void do_phase1(); | ||||
| 		void do_phase2(); | ||||
| 		void shift_in(); | ||||
| 		void shift_out(); | ||||
|  | ||||
| 		T &bus_handler_; | ||||
| 		HalfCycles time_since_bus_handler_call_; | ||||
|  | ||||
| 		void access(int address); | ||||
|  | ||||
| 		uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output); | ||||
| 		inline void reevaluate_interrupts(); | ||||
|  | ||||
| 		/// Sets the current intended output value for the port and line; | ||||
| 		/// if this affects the visible output, it will be passed to the handler. | ||||
| 		void set_control_line_output(Port port, Line line, LineState value); | ||||
| 		void evaluate_cb2_output(); | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #include "Implementation/6522Implementation.hpp" | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* _522_hpp */ | ||||
|   | ||||
| @@ -1,116 +0,0 @@ | ||||
| // | ||||
| //  6522Base.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 04/09/2017. | ||||
| //  Copyright 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "../6522.hpp" | ||||
|  | ||||
| using namespace MOS::MOS6522; | ||||
|  | ||||
| void MOS6522Base::set_control_line_input(Port port, Line line, bool value) { | ||||
| 	switch(line) { | ||||
| 		case Line::One: | ||||
| 			if(	value != control_inputs_[port].line_one && | ||||
| 				value == !!(registers_.peripheral_control & (port ? 0x10 : 0x01)) | ||||
| 			) { | ||||
| 				registers_.interrupt_flags |= port ? InterruptFlag::CB1ActiveEdge : InterruptFlag::CA1ActiveEdge; | ||||
| 				reevaluate_interrupts(); | ||||
| 			} | ||||
| 			control_inputs_[port].line_one = value; | ||||
| 		break; | ||||
|  | ||||
| 		case Line::Two: | ||||
| 			// TODO: output modes, but probably elsewhere? | ||||
| 			if(	value != control_inputs_[port].line_two &&							// i.e. value has changed ... | ||||
| 				!(registers_.peripheral_control & (port ? 0x80 : 0x08)) &&			// ... and line is input ... | ||||
| 				value == !!(registers_.peripheral_control & (port ? 0x40 : 0x04))	// ... and it's either high or low, as required | ||||
| 			) { | ||||
| 				registers_.interrupt_flags |= port ? InterruptFlag::CB2ActiveEdge : InterruptFlag::CA2ActiveEdge; | ||||
| 				reevaluate_interrupts(); | ||||
| 			} | ||||
| 			control_inputs_[port].line_two = value; | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MOS6522Base::do_phase2() { | ||||
| 	registers_.last_timer[0] = registers_.timer[0]; | ||||
| 	registers_.last_timer[1] = registers_.timer[1]; | ||||
|  | ||||
| 	if(registers_.timer_needs_reload) { | ||||
| 		registers_.timer_needs_reload = false; | ||||
| 		registers_.timer[0] = registers_.timer_latch[0]; | ||||
| 	} else { | ||||
| 		registers_.timer[0] --; | ||||
| 	} | ||||
|  | ||||
| 	registers_.timer[1] --; | ||||
| 	if(registers_.next_timer[0] >= 0) { | ||||
| 		registers_.timer[0] = static_cast<uint16_t>(registers_.next_timer[0]); | ||||
| 		registers_.next_timer[0] = -1; | ||||
| 	} | ||||
| 	if(registers_.next_timer[1] >= 0) { | ||||
| 		registers_.timer[1] = static_cast<uint16_t>(registers_.next_timer[1]); | ||||
| 		registers_.next_timer[1] = -1; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MOS6522Base::do_phase1() { | ||||
| 	// IRQ is raised on the half cycle after overflow | ||||
| 	if((registers_.timer[1] == 0xffff) && !registers_.last_timer[1] && timer_is_running_[1]) { | ||||
| 		timer_is_running_[1] = false; | ||||
| 		registers_.interrupt_flags |= InterruptFlag::Timer2; | ||||
| 		reevaluate_interrupts(); | ||||
| 	} | ||||
|  | ||||
| 	if((registers_.timer[0] == 0xffff) && !registers_.last_timer[0] && timer_is_running_[0]) { | ||||
| 		registers_.interrupt_flags |= InterruptFlag::Timer1; | ||||
| 		reevaluate_interrupts(); | ||||
|  | ||||
| 		if(registers_.auxiliary_control&0x40) | ||||
| 			registers_.timer_needs_reload = true; | ||||
| 		else | ||||
| 			timer_is_running_[0] = false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /*! Runs for a specified number of half cycles. */ | ||||
| void MOS6522Base::run_for(const HalfCycles half_cycles) { | ||||
| 	int number_of_half_cycles = half_cycles.as_int(); | ||||
|  | ||||
| 	if(is_phase2_) { | ||||
| 		do_phase2(); | ||||
| 		number_of_half_cycles--; | ||||
| 	} | ||||
|  | ||||
| 	while(number_of_half_cycles >= 2) { | ||||
| 		do_phase1(); | ||||
| 		do_phase2(); | ||||
| 		number_of_half_cycles -= 2; | ||||
| 	} | ||||
|  | ||||
| 	if(number_of_half_cycles) { | ||||
| 		do_phase1(); | ||||
| 		is_phase2_ = true; | ||||
| 	} else { | ||||
| 		is_phase2_ = false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /*! Runs for a specified number of cycles. */ | ||||
| void MOS6522Base::run_for(const Cycles cycles) { | ||||
| 	int number_of_cycles = cycles.as_int(); | ||||
| 	while(number_of_cycles--) { | ||||
| 		do_phase1(); | ||||
| 		do_phase2(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /*! @returns @c true if the IRQ line is currently active; @c false otherwise. */ | ||||
| bool MOS6522Base::get_interrupt_line() { | ||||
| 	uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f; | ||||
| 	return !!interrupt_status; | ||||
| } | ||||
| @@ -6,29 +6,63 @@ | ||||
| //  Copyright 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) { | ||||
| 	address &= 0xf; | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| namespace MOS { | ||||
| namespace MOS6522 { | ||||
|  | ||||
| template <typename T> void MOS6522<T>::access(int address) { | ||||
| 	switch(address) { | ||||
| 		case 0x0: | ||||
| 			// In both handshake and pulse modes, CB2 goes low on any read or write of Port B. | ||||
| 			if(handshake_modes_[1] != HandshakeMode::None) { | ||||
| 				set_control_line_output(Port::B, Line::Two, LineState::Off); | ||||
| 			} | ||||
| 		break; | ||||
|  | ||||
| 		case 0xf: | ||||
| 		case 0x1: | ||||
| 			// In both handshake and pulse modes, CA2 goes low on any read or write of Port A. | ||||
| 			if(handshake_modes_[0] != HandshakeMode::None) { | ||||
| 				set_control_line_output(Port::A, Line::Two, LineState::Off); | ||||
| 			} | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::write(int address, uint8_t value) { | ||||
| 	address &= 0xf; | ||||
| 	access(address); | ||||
| 	switch(address) { | ||||
| 		case 0x0:	// Write Port B. | ||||
| 			// Store locally and communicate outwards. | ||||
| 			registers_.output[1] = value; | ||||
| 			bus_handler_.set_port_output(Port::B, value, registers_.data_direction[1]);	// TODO: handshake | ||||
|  | ||||
| 			bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 			bus_handler_.set_port_output(Port::B, value, registers_.data_direction[1]); | ||||
|  | ||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | ((registers_.peripheral_control&0x20) ? 0 : InterruptFlag::CB2ActiveEdge)); | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
| 		case 0xf: | ||||
| 		case 0x1: | ||||
| 		case 0x1:	// Write Port A. | ||||
| 			registers_.output[0] = value; | ||||
| 			bus_handler_.set_port_output(Port::A, value, registers_.data_direction[0]);	// TODO: handshake | ||||
|  | ||||
| 			bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 			bus_handler_.set_port_output(Port::A, value, registers_.data_direction[0]); | ||||
|  | ||||
| 			if(handshake_modes_[1] != HandshakeMode::None) { | ||||
| 				set_control_line_output(Port::A, Line::Two, LineState::Off); | ||||
| 			} | ||||
|  | ||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | ((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge)); | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
|  | ||||
| 		case 0x2: | ||||
| 		case 0x2:	// Port B direction. | ||||
| 			registers_.data_direction[1] = value; | ||||
| 		break; | ||||
| 		case 0x3: | ||||
| 		case 0x3:	// Port A direction. | ||||
| 			registers_.data_direction[0] = value; | ||||
| 		break; | ||||
|  | ||||
| @@ -54,33 +88,58 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) | ||||
| 		break; | ||||
|  | ||||
| 		// Shift | ||||
| 		case 0xa:	registers_.shift = value;				break; | ||||
| 		case 0xa: | ||||
| 			registers_.shift = value; | ||||
| 			shift_bits_remaining_ = 8; | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister; | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
|  | ||||
| 		// Control | ||||
| 		case 0xb: | ||||
| 			registers_.auxiliary_control = value; | ||||
| 			evaluate_cb2_output(); | ||||
| 		break; | ||||
| 		case 0xc: | ||||
| //			printf("Peripheral control %02x\n", value); | ||||
| 		case 0xc: { | ||||
| //			const auto old_peripheral_control = registers_.peripheral_control; | ||||
| 			registers_.peripheral_control = value; | ||||
|  | ||||
| 			// TODO: simplify below; trying to avoid improper logging of unimplemented warnings in input mode | ||||
| 			if(value & 0x08) { | ||||
| 				switch(value & 0x0e) { | ||||
| 					default: printf("Unimplemented control line mode %d\n", (value >> 1)&7);		break; | ||||
| 					case 0x0c:	bus_handler_.set_control_line_output(Port::A, Line::Two, false);	break; | ||||
| 					case 0x0e:	bus_handler_.set_control_line_output(Port::A, Line::Two, true);		break; | ||||
| 				} | ||||
| 			} | ||||
| 			if(value & 0x80) { | ||||
| 				switch(value & 0xe0) { | ||||
| 					default: printf("Unimplemented control line mode %d\n", (value >> 5)&7);		break; | ||||
| 					case 0xc0:	bus_handler_.set_control_line_output(Port::B, Line::Two, false);	break; | ||||
| 					case 0xe0:	bus_handler_.set_control_line_output(Port::B, Line::Two, true);		break; | ||||
| 				} | ||||
| 			} | ||||
| 			int shift = 0; | ||||
| 			for(int port = 0; port < 2; ++port) { | ||||
| 				handshake_modes_[port] = HandshakeMode::None; | ||||
| 				switch((value >> shift) & 0x0e) { | ||||
| 					default: break; | ||||
|  | ||||
| 					case 0x00:	// Negative interrupt input; set Cx2 interrupt on negative Cx2 transition, clear on access to Port x register. | ||||
| 					case 0x02:	// Independent negative interrupt input; set Cx2 interrupt on negative transition, don't clear automatically. | ||||
| 					case 0x04:	// Positive interrupt input; set Cx2 interrupt on positive Cx2 transition, clear on access to Port x register. | ||||
| 					case 0x06:	// Independent positive interrupt input; set Cx2 interrupt on positive transition, don't clear automatically. | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::Input); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x08:	// Handshake: set Cx2 to low on any read or write of Port x; set to high on an active transition of Cx1. | ||||
| 						handshake_modes_[port] = HandshakeMode::Handshake; | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::Off);	// At a guess. | ||||
| 					break; | ||||
|  | ||||
| 					case 0x0a:	// Pulse output: Cx2 is low for one cycle following a read or write of Port x. | ||||
| 						handshake_modes_[port] = HandshakeMode::Pulse; | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::On); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x0c:	// Manual output: Cx2 low. | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::Off); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x0e:	// Manual output: Cx2 high. | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::On); | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				shift += 4; | ||||
| 			} | ||||
| 		} break; | ||||
|  | ||||
| 		// Interrupt control | ||||
| 		case 0xd: | ||||
| 			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; | ||||
| 	access(address); | ||||
| 	switch(address) { | ||||
| 		case 0x0: | ||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge); | ||||
| 			reevaluate_interrupts(); | ||||
| 		return get_port_input(Port::B, registers_.data_direction[1], registers_.output[1]); | ||||
| 		case 0xf:	// TODO: handshake, latching | ||||
| 		case 0xf: | ||||
| 		case 0x1: | ||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge); | ||||
| 			reevaluate_interrupts(); | ||||
| @@ -128,7 +188,11 @@ template <typename T> uint8_t MOS6522<T>::get_register(int address) { | ||||
| 		return registers_.timer[1] & 0x00ff; | ||||
| 		case 0x9:	return registers_.timer[1] >> 8; | ||||
|  | ||||
| 		case 0xa:	return registers_.shift; | ||||
| 		case 0xa: | ||||
| 			shift_bits_remaining_ = 8; | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister; | ||||
| 			reevaluate_interrupts(); | ||||
| 		return registers_.shift; | ||||
|  | ||||
| 		case 0xb:	return registers_.auxiliary_control; | ||||
| 		case 0xc:	return registers_.peripheral_control; | ||||
| @@ -141,7 +205,8 @@ template <typename T> uint8_t MOS6522<T>::get_register(int address) { | ||||
| } | ||||
|  | ||||
| template <typename T> uint8_t MOS6522<T>::get_port_input(Port port, uint8_t output_mask, uint8_t output) { | ||||
| 	uint8_t input = bus_handler_.get_port_input(port); | ||||
| 	bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 	const uint8_t input = bus_handler_.get_port_input(port); | ||||
| 	return (input & ~output_mask) | (output & output_mask); | ||||
| } | ||||
|  | ||||
| @@ -154,6 +219,238 @@ template <typename T> void MOS6522<T>::reevaluate_interrupts() { | ||||
| 	bool new_interrupt_status = get_interrupt_line(); | ||||
| 	if(new_interrupt_status != last_posted_interrupt_status_) { | ||||
| 		last_posted_interrupt_status_ = new_interrupt_status; | ||||
|  | ||||
| 		bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 		bus_handler_.set_interrupt_status(new_interrupt_status); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::set_control_line_input(Port port, Line line, bool value) { | ||||
| 	switch(line) { | ||||
| 		case Line::One: | ||||
| 			if(value != control_inputs_[port].lines[line]) { | ||||
| 				// In handshake mode, any transition on C[A/B]1 sets output high on C[A/B]2. | ||||
| 				if(handshake_modes_[port] == HandshakeMode::Handshake) { | ||||
| 					set_control_line_output(port, Line::Two, LineState::On); | ||||
| 				} | ||||
|  | ||||
| 				// Set the proper transition interrupt bit if enabled. | ||||
| 				if(value == !!(registers_.peripheral_control & (port ? 0x10 : 0x01))) { | ||||
| 					registers_.interrupt_flags |= port ? InterruptFlag::CB1ActiveEdge : InterruptFlag::CA1ActiveEdge; | ||||
| 					reevaluate_interrupts(); | ||||
| 				} | ||||
|  | ||||
| 				// If this is a transition on CB1, consider updating the shift register. | ||||
| 				// TODO: and at least one full clock since the shift register was written? | ||||
| 				if(port == Port::B) { | ||||
| 					switch(shift_mode()) { | ||||
| 						default: 													break; | ||||
| 						case ShiftMode::InUnderCB1:		if(value)	shift_in();		break;	// Shifts in are captured on a low-to-high transition. | ||||
| 						case ShiftMode::OutUnderCB1:	if(!value)	shift_out();	break;	// Shifts out are updated on a high-to-low transition. | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			control_inputs_[port].lines[line] = value; | ||||
| 		break; | ||||
|  | ||||
| 		case Line::Two: | ||||
| 			if(	value != control_inputs_[port].lines[line] &&						// i.e. value has changed ... | ||||
| 				!(registers_.peripheral_control & (port ? 0x80 : 0x08)) &&			// ... and line is input ... | ||||
| 				value == !!(registers_.peripheral_control & (port ? 0x40 : 0x04))	// ... and it's either high or low, as required | ||||
| 			) { | ||||
| 				registers_.interrupt_flags |= port ? InterruptFlag::CB2ActiveEdge : InterruptFlag::CA2ActiveEdge; | ||||
| 				reevaluate_interrupts(); | ||||
| 			} | ||||
| 			control_inputs_[port].lines[line] = value; | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::do_phase2() { | ||||
| 	++ time_since_bus_handler_call_; | ||||
|  | ||||
| 	registers_.last_timer[0] = registers_.timer[0]; | ||||
| 	registers_.last_timer[1] = registers_.timer[1]; | ||||
|  | ||||
| 	if(registers_.timer_needs_reload) { | ||||
| 		registers_.timer_needs_reload = false; | ||||
| 		registers_.timer[0] = registers_.timer_latch[0]; | ||||
| 	} else { | ||||
| 		registers_.timer[0] --; | ||||
| 	} | ||||
|  | ||||
| 	registers_.timer[1] --; | ||||
| 	if(registers_.next_timer[0] >= 0) { | ||||
| 		registers_.timer[0] = static_cast<uint16_t>(registers_.next_timer[0]); | ||||
| 		registers_.next_timer[0] = -1; | ||||
| 	} | ||||
| 	if(registers_.next_timer[1] >= 0) { | ||||
| 		registers_.timer[1] = static_cast<uint16_t>(registers_.next_timer[1]); | ||||
| 		registers_.next_timer[1] = -1; | ||||
| 	} | ||||
|  | ||||
| 	// In pulse modes, CA2 and CB2 go high again on the next clock edge. | ||||
| 	if(handshake_modes_[1] == HandshakeMode::Pulse) { | ||||
| 		set_control_line_output(Port::B, Line::Two, LineState::On); | ||||
| 	} | ||||
| 	if(handshake_modes_[0] == HandshakeMode::Pulse) { | ||||
| 		set_control_line_output(Port::A, Line::Two, LineState::On); | ||||
| 	} | ||||
|  | ||||
| 	// If the shift register is shifting according to the input clock, do a shift. | ||||
| 	switch(shift_mode()) { | ||||
| 		default: 											break; | ||||
| 		case ShiftMode::InUnderPhase2:		shift_in();		break; | ||||
| 		case ShiftMode::OutUnderPhase2:		shift_out();	break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::do_phase1() { | ||||
| 	++ time_since_bus_handler_call_; | ||||
|  | ||||
| 	// IRQ is raised on the half cycle after overflow | ||||
| 	if((registers_.timer[1] == 0xffff) && !registers_.last_timer[1] && timer_is_running_[1]) { | ||||
| 		timer_is_running_[1] = false; | ||||
|  | ||||
| 		// If the shift register is shifting according to this timer, do a shift. | ||||
| 		// TODO: "shift register is driven by only the low order 8 bits of timer 2"? | ||||
| 		switch(shift_mode()) { | ||||
| 			default: 												break; | ||||
| 			case ShiftMode::InUnderT2:				shift_in();		break; | ||||
| 			case ShiftMode::OutUnderT2FreeRunning: 	shift_out();	break; | ||||
| 			case ShiftMode::OutUnderT2:				shift_out();	break;	// TODO: present a clock on CB1. | ||||
| 		} | ||||
|  | ||||
| 		registers_.interrupt_flags |= InterruptFlag::Timer2; | ||||
| 		reevaluate_interrupts(); | ||||
| 	} | ||||
|  | ||||
| 	if((registers_.timer[0] == 0xffff) && !registers_.last_timer[0] && timer_is_running_[0]) { | ||||
| 		registers_.interrupt_flags |= InterruptFlag::Timer1; | ||||
| 		reevaluate_interrupts(); | ||||
|  | ||||
| 		// Determine whether to reload. | ||||
| 		if(registers_.auxiliary_control&0x40) | ||||
| 			registers_.timer_needs_reload = true; | ||||
| 		else | ||||
| 			timer_is_running_[0] = false; | ||||
|  | ||||
| 		// Determine whether to toggle PB7. | ||||
| 		if(registers_.auxiliary_control&0x80) { | ||||
| 			registers_.output[1] ^= 0x80; | ||||
| 			bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 			bus_handler_.set_port_output(Port::B, registers_.output[1], registers_.data_direction[1]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /*! Runs for a specified number of half cycles. */ | ||||
| template <typename T> void MOS6522<T>::run_for(const HalfCycles half_cycles) { | ||||
| 	auto number_of_half_cycles = half_cycles.as_integral(); | ||||
| 	if(!number_of_half_cycles) return; | ||||
|  | ||||
| 	if(is_phase2_) { | ||||
| 		do_phase2(); | ||||
| 		number_of_half_cycles--; | ||||
| 	} | ||||
|  | ||||
| 	while(number_of_half_cycles >= 2) { | ||||
| 		do_phase1(); | ||||
| 		do_phase2(); | ||||
| 		number_of_half_cycles -= 2; | ||||
| 	} | ||||
|  | ||||
| 	if(number_of_half_cycles) { | ||||
| 		do_phase1(); | ||||
| 		is_phase2_ = true; | ||||
| 	} else { | ||||
| 		is_phase2_ = false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::flush() { | ||||
| 	bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 	bus_handler_.flush(); | ||||
| } | ||||
|  | ||||
| /*! Runs for a specified number of cycles. */ | ||||
| template <typename T> void MOS6522<T>::run_for(const Cycles cycles) { | ||||
| 	auto number_of_cycles = cycles.as_integral(); | ||||
| 	while(number_of_cycles--) { | ||||
| 		do_phase1(); | ||||
| 		do_phase2(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /*! @returns @c true if the IRQ line is currently active; @c false otherwise. */ | ||||
| template <typename T> bool MOS6522<T>::get_interrupt_line() { | ||||
| 	uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f; | ||||
| 	return !!interrupt_status; | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::evaluate_cb2_output() { | ||||
| 	// CB2 is a special case, being both the line the shift register can output to, | ||||
| 	// and one that can be used as an input or handshaking output according to the | ||||
| 	// peripheral control register. | ||||
|  | ||||
| 	// My guess: other CB2 functions work only if the shift register is disabled (?). | ||||
| 	if(shift_mode() != ShiftMode::Disabled) { | ||||
| 		// Shift register is enabled, one way or the other; but announce only output. | ||||
| 		if(is_shifting_out()) { | ||||
| 			// Output mode; set the level according to the current top of the shift register. | ||||
| 			bus_handler_.set_control_line_output(Port::B, Line::Two, !!(registers_.shift & 0x80)); | ||||
| 		} else { | ||||
| 			// Input mode. | ||||
| 			bus_handler_.set_control_line_output(Port::B, Line::Two, true); | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Shift register is disabled. | ||||
| 		bus_handler_.set_control_line_output(Port::B, Line::Two, control_outputs_[1].lines[1] != LineState::Off); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::set_control_line_output(Port port, Line line, LineState value) { | ||||
| 	if(port == Port::B && line == Line::Two) { | ||||
| 		control_outputs_[port].lines[line] = value; | ||||
| 		evaluate_cb2_output(); | ||||
| 	} else { | ||||
| 		// Do nothing if unchanged. | ||||
| 		if(value == control_outputs_[port].lines[line]) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		control_outputs_[port].lines[line] = value; | ||||
|  | ||||
| 		if(value != LineState::Input) { | ||||
| 			bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 			bus_handler_.set_control_line_output(port, line, value != LineState::Off); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::shift_in() { | ||||
| 	registers_.shift = uint8_t((registers_.shift << 1) | (control_inputs_[1].lines[1] ? 1 : 0)); | ||||
| 	--shift_bits_remaining_; | ||||
| 	if(!shift_bits_remaining_) { | ||||
| 		registers_.interrupt_flags |= InterruptFlag::ShiftRegister; | ||||
| 		reevaluate_interrupts(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::shift_out() { | ||||
| 	// When shifting out, the shift register rotates rather than strictly shifts. | ||||
| 	// TODO: is that true for all modes? | ||||
| 	if(shift_mode() == ShiftMode::OutUnderT2FreeRunning || shift_bits_remaining_) { | ||||
| 		registers_.shift = uint8_t((registers_.shift << 1) | (registers_.shift >> 7)); | ||||
| 		evaluate_cb2_output(); | ||||
|  | ||||
| 		--shift_bits_remaining_; | ||||
| 		if(!shift_bits_remaining_) { | ||||
| 			registers_.interrupt_flags |= InterruptFlag::ShiftRegister; | ||||
| 			reevaluate_interrupts(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -37,14 +37,27 @@ class MOS6522Storage { | ||||
| 			bool timer_needs_reload = false; | ||||
| 		} registers_; | ||||
|  | ||||
| 		// control state | ||||
| 		// Control state. | ||||
| 		struct { | ||||
| 			bool line_one = false; | ||||
| 			bool line_two = false; | ||||
| 			bool lines[2] = {false, false}; | ||||
| 		} control_inputs_[2]; | ||||
|  | ||||
| 		enum class LineState { | ||||
| 			On, Off, Input | ||||
| 		}; | ||||
| 		struct { | ||||
| 			LineState lines[2] = {LineState::Input, LineState::Input}; | ||||
| 		} control_outputs_[2]; | ||||
|  | ||||
| 		enum class HandshakeMode { | ||||
| 			None, | ||||
| 			Handshake, | ||||
| 			Pulse | ||||
| 		} handshake_modes_[2] = { HandshakeMode::None, HandshakeMode::None }; | ||||
|  | ||||
| 		bool timer_is_running_[2] = {false, false}; | ||||
| 		bool last_posted_interrupt_status_ = false; | ||||
| 		int shift_bits_remaining_ = 8; | ||||
|  | ||||
| 		enum InterruptFlag: uint8_t { | ||||
| 			CA2ActiveEdge	= 1 << 0, | ||||
| @@ -55,6 +68,23 @@ class MOS6522Storage { | ||||
| 			Timer2			= 1 << 5, | ||||
| 			Timer1			= 1 << 6, | ||||
| 		}; | ||||
|  | ||||
| 		enum class ShiftMode { | ||||
| 			Disabled = 0, | ||||
| 			InUnderT2 = 1, | ||||
| 			InUnderPhase2 = 2, | ||||
| 			InUnderCB1 = 3, | ||||
| 			OutUnderT2FreeRunning = 4, | ||||
| 			OutUnderT2 = 5, | ||||
| 			OutUnderPhase2 = 6, | ||||
| 			OutUnderCB1 = 7 | ||||
| 		}; | ||||
| 		ShiftMode shift_mode() const { | ||||
| 			return ShiftMode((registers_.auxiliary_control >> 2) & 7); | ||||
| 		} | ||||
| 		bool is_shifting_out() const { | ||||
| 			return registers_.auxiliary_control & 0x10; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -32,7 +32,7 @@ template <class T> class MOS6532 { | ||||
| 		inline void set_ram(uint16_t address, uint8_t value)	{	ram_[address&0x7f] = value;		} | ||||
| 		inline uint8_t get_ram(uint16_t address)				{	return ram_[address & 0x7f];	} | ||||
|  | ||||
| 		inline void set_register(int address, uint8_t value) { | ||||
| 		inline void write(int address, uint8_t value) { | ||||
| 			const uint8_t decodedAddress = address & 0x07; | ||||
| 			switch(decodedAddress) { | ||||
| 				// Port output | ||||
| @@ -63,7 +63,7 @@ template <class T> class MOS6532 { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		inline uint8_t get_register(int address) { | ||||
| 		inline uint8_t read(int address) { | ||||
| 			const uint8_t decodedAddress = address & 0x7; | ||||
| 			switch(decodedAddress) { | ||||
| 				// Port input | ||||
| @@ -107,7 +107,7 @@ template <class T> class MOS6532 { | ||||
| 		} | ||||
|  | ||||
| 		inline void run_for(const Cycles cycles) { | ||||
| 			unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_int()); | ||||
| 			unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_integral()); | ||||
|  | ||||
| 			// permit counting _to_ zero; counting _through_ zero initiates the other behaviour | ||||
| 			if(timer_.value >= number_of_cycles) { | ||||
|   | ||||
| @@ -58,29 +58,18 @@ enum class OutputMode { | ||||
| 	To run the VIC for a cycle, the caller should call @c get_address, make the requested bus access | ||||
| 	and call @c set_graphics_value with the result. | ||||
|  | ||||
| 	@c set_register and @c get_register provide register access. | ||||
| 	@c write and @c read provide register access. | ||||
| */ | ||||
| template <class BusHandler> class MOS6560 { | ||||
| 	public: | ||||
| 		MOS6560(BusHandler &bus_handler) : | ||||
| 				bus_handler_(bus_handler), | ||||
| 				crt_(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::DisplayType::NTSC60, 2)), | ||||
| 				crt_(65*4, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8), | ||||
| 				audio_generator_(audio_queue_), | ||||
| 				speaker_(audio_generator_) | ||||
| 		{ | ||||
| 			crt_->set_svideo_sampling_function( | ||||
| 				"vec2 svideo_sample(usampler2D texID, vec2 coordinate, float phase, float amplitude)" | ||||
| 				"{" | ||||
| 					"vec2 yc = texture(texID, coordinate).rg / vec2(255.0);" | ||||
|  | ||||
| 					"float phaseOffset = 6.283185308 * 2.0 * yc.y;" | ||||
| 					"float chroma = step(yc.y, 0.75) * cos(phase + phaseOffset);" | ||||
|  | ||||
| 					"return vec2(yc.x, chroma);" | ||||
| 				"}"); | ||||
|  | ||||
| 			// default to s-video output | ||||
| 			crt_->set_video_signal(Outputs::CRT::VideoSignal::SVideo); | ||||
| 			crt_.set_display_type(Outputs::Display::DisplayType::SVideo); | ||||
|  | ||||
| 			// default to NTSC | ||||
| 			set_output_mode(OutputMode::NTSC); | ||||
| @@ -94,7 +83,8 @@ template <class BusHandler> class MOS6560 { | ||||
| 			speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0)); | ||||
| 		} | ||||
|  | ||||
| 		Outputs::CRT::CRT *get_crt() { return crt_.get(); } | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target)		{ crt_.set_scan_target(scan_target); 	} | ||||
| 		void set_display_type(Outputs::Display::DisplayType display_type)	{ crt_.set_display_type(display_type); 	} | ||||
| 		Outputs::Speaker::Speaker *get_speaker() { return &speaker_; } | ||||
|  | ||||
| 		void set_high_frequency_cutoff(float cutoff) { | ||||
| @@ -117,12 +107,12 @@ template <class BusHandler> class MOS6560 { | ||||
|  | ||||
| 			// Chrominances are encoded such that 0-128 is a complete revolution of phase; | ||||
| 			// anything above 191 disables the colour subcarrier. Phase is relative to the | ||||
| 			// colour burst, so 0 is green. | ||||
| 			// colour burst, so 0 is green (NTSC) or blue/violet (PAL). | ||||
| 			const uint8_t pal_chrominances[16] = { | ||||
| 				255,	255,	37,		101, | ||||
| 				19,		86,		123,	59, | ||||
| 				46,		53,		37,		101, | ||||
| 				19,		86,		123,	59, | ||||
| 				255,	255,	90,		20, | ||||
| 				96,		42,		8,		72, | ||||
| 				84,		90,		90,		20, | ||||
| 				96,		42,		8,		72, | ||||
| 			}; | ||||
| 			const uint8_t ntsc_chrominances[16] = { | ||||
| 				255,	255,	121,	57, | ||||
| @@ -131,12 +121,12 @@ template <class BusHandler> class MOS6560 { | ||||
| 				103,	42,		80,		16, | ||||
| 			}; | ||||
| 			const uint8_t *chrominances; | ||||
| 			Outputs::CRT::DisplayType display_type; | ||||
| 			Outputs::Display::Type display_type; | ||||
|  | ||||
| 			switch(output_mode) { | ||||
| 				default: | ||||
| 					chrominances = pal_chrominances; | ||||
| 					display_type = Outputs::CRT::DisplayType::PAL50; | ||||
| 					display_type = Outputs::Display::Type::PAL50; | ||||
| 					timing_.cycles_per_line = 71; | ||||
| 					timing_.line_counter_increment_offset = 4; | ||||
| 					timing_.final_line_increment_position = timing_.cycles_per_line - timing_.line_counter_increment_offset; | ||||
| @@ -146,7 +136,7 @@ template <class BusHandler> class MOS6560 { | ||||
|  | ||||
| 				case OutputMode::NTSC: | ||||
| 					chrominances = ntsc_chrominances; | ||||
| 					display_type = Outputs::CRT::DisplayType::NTSC60; | ||||
| 					display_type = Outputs::Display::Type::NTSC60; | ||||
| 					timing_.cycles_per_line = 65; | ||||
| 					timing_.line_counter_increment_offset = 40; | ||||
| 					timing_.final_line_increment_position = 58; | ||||
| @@ -155,14 +145,14 @@ template <class BusHandler> class MOS6560 { | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			crt_->set_new_display_type(static_cast<unsigned int>(timing_.cycles_per_line*4), display_type); | ||||
| 			crt_.set_new_display_type(timing_.cycles_per_line*4, display_type); | ||||
|  | ||||
| 			switch(output_mode) { | ||||
| 				case OutputMode::PAL: | ||||
| 					crt_->set_visible_area(Outputs::CRT::Rect(0.1f, 0.07f, 0.9f, 0.9f)); | ||||
| 					crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.07f, 0.9f, 0.9f)); | ||||
| 				break; | ||||
| 				case OutputMode::NTSC: | ||||
| 					crt_->set_visible_area(Outputs::CRT::Rect(0.05f, 0.05f, 0.9f, 0.9f)); | ||||
| 					crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f)); | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| @@ -180,7 +170,7 @@ template <class BusHandler> class MOS6560 { | ||||
| 			// keep track of the amount of time since the speaker was updated; lazy updates are applied | ||||
| 			cycles_since_speaker_update_ += cycles; | ||||
|  | ||||
| 			int number_of_cycles = cycles.as_int(); | ||||
| 			auto number_of_cycles = cycles.as_integral(); | ||||
| 			while(number_of_cycles--) { | ||||
| 				// keep an old copy of the vertical count because that test is a cycle later than the actual changes | ||||
| 				int previous_vertical_counter = vertical_counter_; | ||||
| @@ -284,17 +274,17 @@ template <class BusHandler> class MOS6560 { | ||||
| 				// update the CRT | ||||
| 				if(this_state_ != output_state_) { | ||||
| 					switch(output_state_) { | ||||
| 						case State::Sync:			crt_->output_sync(cycles_in_state_ * 4);														break; | ||||
| 						case State::ColourBurst:	crt_->output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0);		break; | ||||
| 						case State::Sync:			crt_.output_sync(cycles_in_state_ * 4);														break; | ||||
| 						case State::ColourBurst:	crt_.output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0);	break; | ||||
| 						case State::Border:			output_border(cycles_in_state_ * 4);														break; | ||||
| 						case State::Pixels:			crt_->output_data(cycles_in_state_ * 4);														break; | ||||
| 						case State::Pixels:			crt_.output_data(cycles_in_state_ * 4);														break; | ||||
| 					} | ||||
| 					output_state_ = this_state_; | ||||
| 					cycles_in_state_ = 0; | ||||
|  | ||||
| 					pixel_pointer = nullptr; | ||||
| 					if(output_state_ == State::Pixels) { | ||||
| 						pixel_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(260)); | ||||
| 						pixel_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(260)); | ||||
| 					} | ||||
| 				} | ||||
| 				cycles_in_state_++; | ||||
| @@ -363,7 +353,7 @@ template <class BusHandler> class MOS6560 { | ||||
| 		/*! | ||||
| 			Writes to a 6560 register. | ||||
| 		*/ | ||||
| 		void set_register(int address, uint8_t value) { | ||||
| 		void write(int address, uint8_t value) { | ||||
| 			address &= 0xf; | ||||
| 			registers_.direct_values[address] = value; | ||||
| 			switch(address) { | ||||
| @@ -427,7 +417,7 @@ template <class BusHandler> class MOS6560 { | ||||
| 		/* | ||||
| 			Reads from a 6560 register. | ||||
| 		*/ | ||||
| 		uint8_t get_register(int address) { | ||||
| 		uint8_t read(int address) { | ||||
| 			address &= 0xf; | ||||
| 			switch(address) { | ||||
| 				default: return registers_.direct_values[address]; | ||||
| @@ -438,7 +428,7 @@ template <class BusHandler> class MOS6560 { | ||||
|  | ||||
| 	private: | ||||
| 		BusHandler &bus_handler_; | ||||
| 		std::unique_ptr<Outputs::CRT::CRT> crt_; | ||||
| 		Outputs::CRT::CRT crt_; | ||||
|  | ||||
| 		Concurrency::DeferringAsyncTaskQueue audio_queue_; | ||||
| 		AudioGenerator audio_generator_; | ||||
| @@ -465,7 +455,7 @@ template <class BusHandler> class MOS6560 { | ||||
| 		enum State { | ||||
| 			Sync, ColourBurst, Border, Pixels | ||||
| 		} this_state_, output_state_; | ||||
| 		unsigned int cycles_in_state_; | ||||
| 		int cycles_in_state_; | ||||
|  | ||||
| 		// counters that cover an entire field | ||||
| 		int horizontal_counter_ = 0, vertical_counter_ = 0; | ||||
| @@ -511,10 +501,10 @@ template <class BusHandler> class MOS6560 { | ||||
| 		uint16_t colours_[16]; | ||||
|  | ||||
| 		uint16_t *pixel_pointer; | ||||
| 		void output_border(unsigned int number_of_cycles) { | ||||
| 			uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(1)); | ||||
| 		void output_border(int number_of_cycles) { | ||||
| 			uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1)); | ||||
| 			if(colour_pointer) *colour_pointer = registers_.borderColour; | ||||
| 			crt_->output_level(number_of_cycles); | ||||
| 			crt_.output_level(number_of_cycles); | ||||
| 		} | ||||
|  | ||||
| 		struct { | ||||
|   | ||||
| @@ -111,7 +111,7 @@ template <class T> class CRTC6845 { | ||||
| 		} | ||||
|  | ||||
| 		void run_for(Cycles cycles) { | ||||
| 			int cyles_remaining = cycles.as_int(); | ||||
| 			auto cyles_remaining = cycles.as_integral(); | ||||
| 			while(cyles_remaining--) { | ||||
| 				// check for end of visible characters | ||||
| 				if(character_counter_ == registers_[1]) { | ||||
|   | ||||
							
								
								
									
										228
									
								
								Components/6850/6850.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								Components/6850/6850.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,228 @@ | ||||
| // | ||||
| //  6850.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/10/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "6850.hpp" | ||||
|  | ||||
| #include <cassert> | ||||
|  | ||||
| using namespace Motorola::ACIA; | ||||
|  | ||||
| const HalfCycles ACIA::SameAsTransmit; | ||||
|  | ||||
| ACIA::ACIA(HalfCycles transmit_clock_rate, HalfCycles receive_clock_rate) : | ||||
| 	transmit_clock_rate_(transmit_clock_rate), | ||||
| 	receive_clock_rate_((receive_clock_rate != SameAsTransmit) ? receive_clock_rate : transmit_clock_rate) { | ||||
| 	transmit.set_writer_clock_rate(transmit_clock_rate); | ||||
| 	request_to_send.set_writer_clock_rate(transmit_clock_rate); | ||||
| } | ||||
|  | ||||
| uint8_t ACIA::read(int address) { | ||||
| 	if(address&1) { | ||||
| 		overran_ = false; | ||||
| 		received_data_ |= NoValueMask; | ||||
| 		update_interrupt_line(); | ||||
| 		return uint8_t(received_data_); | ||||
| 	} else { | ||||
| 		return get_status(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void ACIA::reset() { | ||||
| 	transmit.reset_writing(); | ||||
| 	transmit.write(true); | ||||
| 	request_to_send.reset_writing(); | ||||
|  | ||||
| 	bits_received_ = bits_incoming_ = 0; | ||||
| 	receive_interrupt_enabled_ = transmit_interrupt_enabled_ = false; | ||||
| 	overran_ = false; | ||||
| 	next_transmission_ = received_data_ = NoValueMask; | ||||
|  | ||||
| 	update_interrupt_line(); | ||||
| 	assert(!interrupt_line_); | ||||
| } | ||||
|  | ||||
| void ACIA::write(int address, uint8_t value) { | ||||
| 	if(address&1) { | ||||
| 		next_transmission_ = value; | ||||
| 		consider_transmission(); | ||||
| 		update_interrupt_line(); | ||||
| 	} else { | ||||
| 		if((value&3) == 3) { | ||||
| 			reset(); | ||||
| 		} else { | ||||
| 			switch(value & 3) { | ||||
| 				default: | ||||
| 				case 0: divider_ = 1;	break; | ||||
| 				case 1: divider_ = 16;	break; | ||||
| 				case 2: divider_ = 64;	break; | ||||
| 			} | ||||
| 			switch((value >> 2) & 7) { | ||||
| 				default: | ||||
| 				case 0:	data_bits_ = 7; stop_bits_ = 2; parity_ = Parity::Even;	break; | ||||
| 				case 1:	data_bits_ = 7; stop_bits_ = 2; parity_ = Parity::Odd;	break; | ||||
| 				case 2:	data_bits_ = 7; stop_bits_ = 1; parity_ = Parity::Even;	break; | ||||
| 				case 3:	data_bits_ = 7; stop_bits_ = 1; parity_ = Parity::Odd;	break; | ||||
| 				case 4:	data_bits_ = 8; stop_bits_ = 2; parity_ = Parity::None;	break; | ||||
| 				case 5:	data_bits_ = 8; stop_bits_ = 1; parity_ = Parity::None;	break; | ||||
| 				case 6:	data_bits_ = 8; stop_bits_ = 1; parity_ = Parity::Even;	break; | ||||
| 				case 7:	data_bits_ = 8; stop_bits_ = 1; parity_ = Parity::Odd;	break; | ||||
| 			} | ||||
| 			switch((value >> 5) & 3) { | ||||
| 				case 0:	request_to_send.write(false); transmit_interrupt_enabled_ = false;	break; | ||||
| 				case 1:	request_to_send.write(false); transmit_interrupt_enabled_ = true;	break; | ||||
| 				case 2:	request_to_send.write(true); transmit_interrupt_enabled_ = false;	break; | ||||
| 				case 3: | ||||
| 					request_to_send.write(false); | ||||
| 					transmit_interrupt_enabled_ = false; | ||||
| 					transmit.reset_writing(); | ||||
| 					transmit.write(false); | ||||
| 				break; | ||||
| 			} | ||||
| 			receive.set_read_delegate(this, Storage::Time(divider_ * 2, int(receive_clock_rate_.as_integral()))); | ||||
| 			receive_interrupt_enabled_ = value & 0x80; | ||||
|  | ||||
| 			update_interrupt_line(); | ||||
| 		} | ||||
| 	} | ||||
| 	update_clocking_observer(); | ||||
| } | ||||
|  | ||||
| void ACIA::consider_transmission() { | ||||
| 	if(next_transmission_ != NoValueMask && !transmit.write_data_time_remaining()) { | ||||
| 		// Establish start bit and [7 or 8] data bits. | ||||
| 		if(data_bits_ == 7) next_transmission_ &= 0x7f; | ||||
| 		int transmission = next_transmission_ << 1; | ||||
|  | ||||
| 		// Add a parity bit, if any. | ||||
| 		int mask = 0x2 << data_bits_; | ||||
| 		if(parity_ != Parity::None) { | ||||
| 			transmission |= parity(uint8_t(next_transmission_)) ? mask : 0; | ||||
| 			mask <<= 1; | ||||
| 		} | ||||
|  | ||||
| 		// Add stop bits. | ||||
| 		for(int c = 0; c < stop_bits_; ++c) { | ||||
| 			transmission |= mask; | ||||
| 			mask <<= 1; | ||||
| 		} | ||||
|  | ||||
| 		// Output all that. | ||||
| 		const int total_bits = expected_bits(); | ||||
| 		transmit.write(divider_ * 2, total_bits, transmission); | ||||
|  | ||||
| 		// Mark the transmit register as empty again. | ||||
| 		next_transmission_ = NoValueMask; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| ClockingHint::Preference ACIA::preferred_clocking() { | ||||
| 	// Real-time clocking is required if a transmission is ongoing; this is a courtesy for whomever | ||||
| 	// is on the receiving end. | ||||
| 	if(transmit.transmission_data_time_remaining() > 0) return ClockingHint::Preference::RealTime; | ||||
|  | ||||
| 	// If a bit reception is ongoing that might lead to an interrupt, ask for real-time clocking | ||||
| 	// because it's unclear when the interrupt might come. | ||||
| 	if(bits_incoming_ && receive_interrupt_enabled_) return ClockingHint::Preference::RealTime; | ||||
|  | ||||
| 	// No clocking required then. | ||||
| 	return ClockingHint::Preference::None; | ||||
| } | ||||
|  | ||||
| bool ACIA::get_interrupt_line() const { | ||||
| 	return interrupt_line_; | ||||
| } | ||||
|  | ||||
| int ACIA::expected_bits() { | ||||
| 	return 1 + data_bits_ + stop_bits_ + (parity_ != Parity::None); | ||||
| } | ||||
|  | ||||
| uint8_t ACIA::parity(uint8_t value) { | ||||
| 	value ^= value >> 4; | ||||
| 	value ^= value >> 2; | ||||
| 	value ^= value >> 1; | ||||
| 	return value ^ (parity_ == Parity::Even); | ||||
| } | ||||
|  | ||||
| bool ACIA::serial_line_did_produce_bit(Serial::Line *line, int bit) { | ||||
| 	// Shift this bit into the 11-bit input register; this is big enough to hold | ||||
| 	// the largest transmission symbol. | ||||
| 	++bits_received_; | ||||
| 	bits_incoming_ = (bits_incoming_ >> 1) | (bit << 10); | ||||
|  | ||||
| 	// If that's the now-expected number of bits, update. | ||||
| 	const int bit_target = expected_bits(); | ||||
| 	if(bits_received_ >= bit_target) { | ||||
| 		bits_received_ = 0; | ||||
| 		overran_ |= get_status() & 1; | ||||
| 		received_data_ = uint8_t(bits_incoming_ >> (12 - bit_target)); | ||||
| 		update_interrupt_line(); | ||||
| 		update_clocking_observer(); | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	// TODO: overrun, and parity. | ||||
|  | ||||
| 	// Keep receiving, and consider a potential clocking change. | ||||
| 	if(bits_received_ == 1) update_clocking_observer(); | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| void ACIA::set_interrupt_delegate(InterruptDelegate *delegate) { | ||||
| 	interrupt_delegate_ = delegate; | ||||
| } | ||||
|  | ||||
| void ACIA::update_interrupt_line() { | ||||
| 	const bool old_line = interrupt_line_; | ||||
|  | ||||
| 	/* | ||||
| 		"Bit 7 of the control register is the rie bit. When the rie bit is high, the rdrf, ndcd, | ||||
| 		and ovr bits will assert the nirq output. When the rie bit is low, nirq generation is disabled." | ||||
|  | ||||
| 		rie = read interrupt enable | ||||
| 		rdrf = receive data register full (status word bit 0) | ||||
| 		ndcd = data carrier detect (status word bit 2) | ||||
| 		over = receiver overrun (status word bit 5) | ||||
|  | ||||
| 		"Bit 1 of the status register is the tdre bit. When high, the tdre bit indicates that data has been | ||||
| 		transferred from the transmitter data register to the output shift register. At this point, the a6850 | ||||
| 		is ready to accept a new transmit data byte. However, if the ncts signal is high, the tdre bit remains | ||||
| 		low regardless of the status of the transmitter data register. Also, if transmit interrupt is enabled, | ||||
| 		the nirq output is asserted." | ||||
|  | ||||
| 		tdre = transmitter data register empty | ||||
| 		ncts = clear to send | ||||
| 	*/ | ||||
| 	const auto status = get_status(); | ||||
| 	interrupt_line_ = | ||||
| 		(receive_interrupt_enabled_ && (status & 0x25)) || | ||||
| 		(transmit_interrupt_enabled_ && (status & 0x02)); | ||||
|  | ||||
| 	if(interrupt_delegate_ && old_line != interrupt_line_) { | ||||
| 		interrupt_delegate_->acia6850_did_change_interrupt_status(this); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t ACIA::get_status() { | ||||
| 	return | ||||
| 		((received_data_ & NoValueMask) ? 0x00 : 0x01) | | ||||
| 		((next_transmission_ == NoValueMask) ? 0x02 : 0x00) | | ||||
| //		(data_carrier_detect.read() ? 0x04 : 0x00) | | ||||
| //		(clear_to_send.read() ? 0x08 : 0x00) | | ||||
| 		(overran_ ? 0x20 : 0x00) | | ||||
| 		(interrupt_line_ ? 0x80 : 0x00) | ||||
| 	; | ||||
|  | ||||
| 	// b0: receive data full. | ||||
| 	// b1: transmit data empty. | ||||
| 	// b2: DCD. | ||||
| 	// b3: CTS. | ||||
| 	// b4: framing error (i.e. no first stop bit where expected). | ||||
| 	// b5: receiver overran. | ||||
| 	// b6: parity error. | ||||
| 	// b7: IRQ state. | ||||
| } | ||||
							
								
								
									
										132
									
								
								Components/6850/6850.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								Components/6850/6850.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| // | ||||
| //  6850.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/10/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Motorola_ACIA_6850_hpp | ||||
| #define Motorola_ACIA_6850_hpp | ||||
|  | ||||
| #include <cstdint> | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../ClockReceiver/ClockingHintSource.hpp" | ||||
| #include "../Serial/Line.hpp" | ||||
|  | ||||
| namespace Motorola { | ||||
| namespace ACIA { | ||||
|  | ||||
| class ACIA: public ClockingHint::Source, private Serial::Line::ReadDelegate { | ||||
| 	public: | ||||
| 		static constexpr const HalfCycles SameAsTransmit = HalfCycles(0); | ||||
|  | ||||
| 		/*! | ||||
| 			Constructs a new instance of ACIA which will receive a transmission clock at a rate of | ||||
| 			@c transmit_clock_rate, and a receive clock at a rate of @c receive_clock_rate. | ||||
| 		*/ | ||||
| 		ACIA(HalfCycles transmit_clock_rate, HalfCycles receive_clock_rate = SameAsTransmit); | ||||
|  | ||||
| 		/*! | ||||
| 			Reads from the ACIA. | ||||
|  | ||||
| 			Bit 0 of the address is used as the ACIA's register select line — | ||||
| 			so even addresses select control/status registers, odd addresses | ||||
| 			select transmit/receive data registers. | ||||
| 		*/ | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		/*! | ||||
| 			Writes to the ACIA. | ||||
|  | ||||
| 			Bit 0 of the address is used as the ACIA's register select line — | ||||
| 			so even addresses select control/status registers, odd addresses | ||||
| 			select transmit/receive data registers. | ||||
| 		*/ | ||||
| 		void write(int address, uint8_t value); | ||||
|  | ||||
| 		/*! | ||||
| 			Advances @c transmission_cycles in time, which should be | ||||
| 			counted relative to the @c transmit_clock_rate. | ||||
| 		*/ | ||||
| 		forceinline void run_for(HalfCycles transmission_cycles) { | ||||
| 			if(transmit.transmission_data_time_remaining() > HalfCycles(0)) { | ||||
| 				const auto write_data_time_remaining = transmit.write_data_time_remaining(); | ||||
|  | ||||
| 				// There's at most one further byte available to enqueue, so a single 'if' | ||||
| 				// rather than a 'while' is correct here. It's the responsibilit of the caller | ||||
| 				// to ensure run_for lengths are appropriate for longer sequences. | ||||
| 				if(transmission_cycles >= write_data_time_remaining) { | ||||
| 					if(next_transmission_ != NoValueMask) { | ||||
| 						transmit.advance_writer(write_data_time_remaining); | ||||
| 						consider_transmission(); | ||||
| 						transmit.advance_writer(transmission_cycles - write_data_time_remaining); | ||||
| 					} else { | ||||
| 						transmit.advance_writer(transmission_cycles); | ||||
| 						update_clocking_observer(); | ||||
| 						update_interrupt_line(); | ||||
| 					} | ||||
| 				} else { | ||||
| 					transmit.advance_writer(transmission_cycles); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		bool get_interrupt_line() const; | ||||
| 		void reset(); | ||||
|  | ||||
| 		// Input lines. | ||||
| 		Serial::Line receive; | ||||
| 		Serial::Line clear_to_send; | ||||
| 		Serial::Line data_carrier_detect; | ||||
|  | ||||
| 		// Output lines. | ||||
| 		Serial::Line transmit; | ||||
| 		Serial::Line request_to_send; | ||||
|  | ||||
| 		// ClockingHint::Source. | ||||
| 		ClockingHint::Preference preferred_clocking() final; | ||||
|  | ||||
| 		struct InterruptDelegate { | ||||
| 			virtual void acia6850_did_change_interrupt_status(ACIA *acia) = 0; | ||||
| 		}; | ||||
| 		void set_interrupt_delegate(InterruptDelegate *delegate); | ||||
|  | ||||
| 	private: | ||||
| 		int divider_ = 1; | ||||
| 		enum class Parity { | ||||
| 			Even, Odd, None | ||||
| 		} parity_ = Parity::None; | ||||
| 		int data_bits_ = 7, stop_bits_ = 2; | ||||
|  | ||||
| 		static constexpr int NoValueMask = 0x100; | ||||
| 		int next_transmission_ = NoValueMask; | ||||
| 		int received_data_ = NoValueMask; | ||||
|  | ||||
| 		int bits_received_ = 0; | ||||
| 		int bits_incoming_ = 0; | ||||
| 		bool overran_ = false; | ||||
|  | ||||
| 		void consider_transmission(); | ||||
| 		int expected_bits(); | ||||
| 		uint8_t parity(uint8_t value); | ||||
|  | ||||
| 		bool receive_interrupt_enabled_ = false; | ||||
| 		bool transmit_interrupt_enabled_ = false; | ||||
|  | ||||
| 		HalfCycles transmit_clock_rate_; | ||||
| 		HalfCycles receive_clock_rate_; | ||||
|  | ||||
| 		bool serial_line_did_produce_bit(Serial::Line *line, int bit) final; | ||||
|  | ||||
| 		bool interrupt_line_ = false; | ||||
| 		void update_interrupt_line(); | ||||
| 		InterruptDelegate *interrupt_delegate_ = nullptr; | ||||
| 		uint8_t get_status(); | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Motorola_ACIA_6850_hpp */ | ||||
							
								
								
									
										374
									
								
								Components/68901/MFP68901.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										374
									
								
								Components/68901/MFP68901.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,374 @@ | ||||
| // | ||||
| //  MFP68901.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 06/10/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MFP68901.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cstring> | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| #define NDEBUG | ||||
| #endif | ||||
|  | ||||
| #define LOG_PREFIX "[MFP] " | ||||
| #include "../../Outputs/Log.hpp" | ||||
|  | ||||
| using namespace Motorola::MFP68901; | ||||
|  | ||||
| ClockingHint::Preference MFP68901::preferred_clocking() { | ||||
| 	// Rule applied: if any timer is actively running and permitted to produce an | ||||
| 	// interrupt, request real-time running. | ||||
| 	return | ||||
| 		(timers_[0].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerA) || | ||||
| 		(timers_[1].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerB) || | ||||
| 		(timers_[2].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerC) || | ||||
| 		(timers_[3].mode >= TimerMode::Delay && interrupt_enable_&Interrupt::TimerD) | ||||
| 			? ClockingHint::Preference::RealTime : ClockingHint::Preference::JustInTime; | ||||
| } | ||||
|  | ||||
| uint8_t MFP68901::read(int address) { | ||||
| 	address &= 0x1f; | ||||
|  | ||||
| 	// Interrupt block: various bits of state can be read, all passively. | ||||
| 	if(address >= 0x03 && address <= 0x0b) { | ||||
| 		const int shift = (address&1) << 3; | ||||
| 		switch(address) { | ||||
| 			case 0x03:	case 0x04:	return uint8_t(interrupt_enable_ >> shift); | ||||
| 			case 0x05:	case 0x06:	return uint8_t(interrupt_pending_ >> shift); | ||||
| 			case 0x07:	case 0x08:	return uint8_t(interrupt_in_service_ >> shift); | ||||
| 			case 0x09:	case 0x0a:	return uint8_t(interrupt_mask_ >> shift); | ||||
| 			case 0x0b:	return interrupt_vector_; | ||||
|  | ||||
| 			default: break; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	switch(address) { | ||||
| 		// GPIP block: input, and configured active edge and direction values. | ||||
| 		case 0x00:	return (gpip_input_ & ~gpip_direction_) | (gpip_output_ & gpip_direction_); | ||||
| 		case 0x01:	return gpip_active_edge_; | ||||
| 		case 0x02:	return gpip_direction_; | ||||
|  | ||||
| 		/* Interrupt block dealt with above. */ | ||||
| 		default: break; | ||||
|  | ||||
| 		// Timer block: read back A, B and C/D control, and read current timer values. | ||||
| 		case 0x0c:	case 0x0d:	return timer_ab_control_[address - 0xc]; | ||||
| 		case 0x0e:				return timer_cd_control_; | ||||
| 		case 0x0f:	case 0x10: | ||||
| 		case 0x11:	case 0x12:	return get_timer_data(address - 0xf); | ||||
|  | ||||
| 		// USART block: TODO. | ||||
| 		case 0x13:		LOG("Read: sync character generator");	break; | ||||
| 		case 0x14:		LOG("Read: USART control");				break; | ||||
| 		case 0x15:		LOG("Read: receiver status");			break; | ||||
| 		case 0x16:		LOG("Read: transmitter status");		break; | ||||
| 		case 0x17:		LOG("Read: USART data");				break; | ||||
| 	} | ||||
| 	return 0x00; | ||||
| } | ||||
|  | ||||
| void MFP68901::write(int address, uint8_t value) { | ||||
| 	address &= 0x1f; | ||||
|  | ||||
| 	// Interrupt block: enabled and masked interrupts can be set; pending and in-service interrupts can be masked. | ||||
| 	if(address >= 0x03 && address <= 0x0b) { | ||||
| 		const int shift = (address&1) << 3; | ||||
| 		const int preserve = 0xff00 >> shift; | ||||
| 		const int word_value = value << shift; | ||||
|  | ||||
| 		switch(address) { | ||||
| 			default: break; | ||||
| 			case 0x03: case 0x04:	// Adjust enabled interrupts; disabled ones also cease to be pending. | ||||
| 				interrupt_enable_ = (interrupt_enable_ & preserve) | word_value; | ||||
| 				interrupt_pending_ &= interrupt_enable_; | ||||
| 			break; | ||||
| 			case 0x05: case 0x06:	// Resolve pending interrupts. | ||||
| 				interrupt_pending_ &= (preserve | word_value); | ||||
| 			break; | ||||
| 			case 0x07: case 0x08:	// Resolve in-service interrupts. | ||||
| 				interrupt_in_service_ &= (preserve | word_value); | ||||
| 			break; | ||||
| 			case 0x09: case 0x0a:	// Adjust interrupt mask. | ||||
| 				interrupt_mask_ = (interrupt_mask_ & preserve) | word_value; | ||||
| 			break; | ||||
| 			case 0x0b:				// Set the interrupt vector, possibly changing end-of-interrupt mode. | ||||
| 				interrupt_vector_ = value; | ||||
|  | ||||
| 				// If automatic end-of-interrupt mode has now been enabled, clear | ||||
| 				// the in-process mask and re-evaluate. | ||||
| 				if(interrupt_vector_ & 0x08) return; | ||||
| 				interrupt_in_service_ = 0; | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		// Whatever just happened may have affected the state of the interrupt line. | ||||
| 		update_interrupts(); | ||||
| 		update_clocking_observer(); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	constexpr int timer_prescales[] = { | ||||
| 		1, 4, 10, 16, 50, 64, 100, 200 | ||||
| 	}; | ||||
|  | ||||
| 	switch(address) { | ||||
| 		// GPIP block: output and configuration of active edge and direction values. | ||||
| 		case 0x00: | ||||
| 			gpip_output_ = value; | ||||
| 		break; | ||||
| 		case 0x01: | ||||
| 			gpip_active_edge_ = value; | ||||
| 			reevaluate_gpip_interrupts(); | ||||
| 		break; | ||||
| 		case 0x02: | ||||
| 			gpip_direction_ = value; | ||||
| 			reevaluate_gpip_interrupts(); | ||||
| 		break; | ||||
|  | ||||
| 		/* Interrupt block dealt with above. */ | ||||
| 		default: break; | ||||
|  | ||||
| 		// Timer block. | ||||
| 		case 0x0c: | ||||
| 		case 0x0d: { | ||||
| 			const auto timer = address - 0xc; | ||||
| 			const bool reset = value & 0x10; | ||||
| 			timer_ab_control_[timer] = value; | ||||
| 			switch(value & 0xf) { | ||||
| 				case 0x0:	set_timer_mode(timer, TimerMode::Stopped, 1, reset);		break; | ||||
| 				case 0x1:	set_timer_mode(timer, TimerMode::Delay, 4, reset);			break; | ||||
| 				case 0x2:	set_timer_mode(timer, TimerMode::Delay, 10, reset);			break; | ||||
| 				case 0x3:	set_timer_mode(timer, TimerMode::Delay, 16, reset);			break; | ||||
| 				case 0x4:	set_timer_mode(timer, TimerMode::Delay, 50, reset);			break; | ||||
| 				case 0x5:	set_timer_mode(timer, TimerMode::Delay, 64, reset);			break; | ||||
| 				case 0x6:	set_timer_mode(timer, TimerMode::Delay, 100, reset);		break; | ||||
| 				case 0x7:	set_timer_mode(timer, TimerMode::Delay, 200, reset);		break; | ||||
| 				case 0x8:	set_timer_mode(timer, TimerMode::EventCount, 1, reset);		break; | ||||
| 				case 0x9:	set_timer_mode(timer, TimerMode::PulseWidth, 4, reset);		break; | ||||
| 				case 0xa:	set_timer_mode(timer, TimerMode::PulseWidth, 10, reset);	break; | ||||
| 				case 0xb:	set_timer_mode(timer, TimerMode::PulseWidth, 16, reset);	break; | ||||
| 				case 0xc:	set_timer_mode(timer, TimerMode::PulseWidth, 50, reset);	break; | ||||
| 				case 0xd:	set_timer_mode(timer, TimerMode::PulseWidth, 64, reset);	break; | ||||
| 				case 0xe:	set_timer_mode(timer, TimerMode::PulseWidth, 100, reset);	break; | ||||
| 				case 0xf:	set_timer_mode(timer, TimerMode::PulseWidth, 200, reset);	break; | ||||
| 			} | ||||
| 		} break; | ||||
| 		case 0x0e: | ||||
| 			timer_cd_control_ = value; | ||||
| 			set_timer_mode(3, (value & 7) ? TimerMode::Delay : TimerMode::Stopped, timer_prescales[value & 7], false); | ||||
| 			set_timer_mode(2, ((value >> 4) & 7) ? TimerMode::Delay : TimerMode::Stopped, timer_prescales[(value >> 4) & 7], false); | ||||
| 		break; | ||||
| 		case 0x0f:	case 0x10:	case 0x11:	case 0x12: | ||||
| 			set_timer_data(address - 0xf, value); | ||||
| 		break; | ||||
|  | ||||
| 		// USART block: TODO. | ||||
| 		case 0x13:		LOG("Write: sync character generator");	break; | ||||
| 		case 0x14:		LOG("Write: USART control");			break; | ||||
| 		case 0x15:		LOG("Write: receiver status");			break; | ||||
| 		case 0x16:		LOG("Write: transmitter status");		break; | ||||
| 		case 0x17:		LOG("Write: USART data");				break; | ||||
| 	} | ||||
|  | ||||
| 	update_clocking_observer(); | ||||
| } | ||||
|  | ||||
| void MFP68901::run_for(HalfCycles time) { | ||||
| 	cycles_left_ += time; | ||||
|  | ||||
| 	const int cycles = int(cycles_left_.flush<Cycles>().as_integral()); | ||||
| 	if(!cycles) return; | ||||
|  | ||||
| 	for(int c = 0; c < 4; ++c) { | ||||
| 		if(timers_[c].mode >= TimerMode::Delay) { | ||||
| 			// This code applies the timer prescaling only. prescale_count is used to count | ||||
| 			// upwards rather than downwards for simplicity, but on the real hardware it's | ||||
| 			// pretty safe to assume it actually counted downwards. So the clamp to 0 is | ||||
| 			// because gymnastics may need to occur when the prescale value is altered, e.g. | ||||
| 			// if a prescale of 256 is set and the prescale_count is currently 2 then the | ||||
| 			// counter should roll over in 254 cycles. If the user at that point changes the | ||||
| 			// prescale_count to 1 then the counter will need to be altered to -253 and | ||||
| 			// allowed to keep counting up until it crosses both 0 and 1. | ||||
| 			const int dividend = timers_[c].prescale_count + cycles; | ||||
| 			const int decrements = std::max(dividend / timers_[c].prescale, 0); | ||||
| 			if(decrements) { | ||||
| 				decrement_timer(c, decrements); | ||||
| 				timers_[c].prescale_count = dividend % timers_[c].prescale; | ||||
| 			} else { | ||||
| 				timers_[c].prescale_count += cycles; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| HalfCycles MFP68901::get_next_sequence_point() { | ||||
| 	return HalfCycles(-1); | ||||
| } | ||||
|  | ||||
| // MARK: - Timers | ||||
|  | ||||
| void MFP68901::set_timer_mode(int timer, TimerMode mode, int prescale, bool reset_timer) { | ||||
| 	LOG("Timer " << timer << " mode set: " << int(mode) << "; prescale: " << prescale); | ||||
| 	timers_[timer].mode = mode; | ||||
| 	if(reset_timer) { | ||||
| 		timers_[timer].prescale_count = 0; | ||||
| 		timers_[timer].value = timers_[timer].reload_value; | ||||
| 	} else { | ||||
| 		// This hoop is because the prescale_count here goes upward but I'm assuming it goes downward in | ||||
| 		// real hardware. Therefore this deals with the "switched to a lower prescaling" case whereby the | ||||
| 		// old cycle should be allowed naturally to expire. | ||||
| 		timers_[timer].prescale_count = prescale - (timers_[timer].prescale - timers_[timer].prescale_count); | ||||
| 	} | ||||
|  | ||||
| 	timers_[timer].prescale = prescale; | ||||
| } | ||||
|  | ||||
| void MFP68901::set_timer_data(int timer, uint8_t value) { | ||||
| 	if(timers_[timer].mode == TimerMode::Stopped) { | ||||
| 		timers_[timer].value = value; | ||||
| 	} | ||||
| 	timers_[timer].reload_value = value; | ||||
| } | ||||
|  | ||||
| uint8_t MFP68901::get_timer_data(int timer) { | ||||
| 	return timers_[timer].value; | ||||
| } | ||||
|  | ||||
| void MFP68901::set_timer_event_input(int channel, bool value) { | ||||
| 	if(timers_[channel].event_input == value) return; | ||||
|  | ||||
| 	timers_[channel].event_input = value; | ||||
| 	if(timers_[channel].mode == TimerMode::EventCount && (value == !!(gpip_active_edge_ & (0x10 >> channel)))) { | ||||
| 		// "The active state of the signal on TAI or TBI is dependent upon the associated | ||||
| 		// Interrupt Channel’s edge bit (GPIP 4 for TAI and GPIP 3 for TBI [...] ). | ||||
| 		// If the edge bit associated with the TAI or TBI input is a one, it will be active high. | ||||
| 		decrement_timer(channel, 1); | ||||
| 	} | ||||
|  | ||||
| 	// TODO: | ||||
| 	// | ||||
| 	// Altering the edge bit while the timer is in the event count mode can produce a count pulse. | ||||
| 	// The interrupt channel associated with the input (I3 for I4 for TAI) is allowed to function normally. | ||||
| 	// To count transitions reliably, the input must remain in each state (1/O) for a length of time equal | ||||
| 	// to four periods of the timer clock. | ||||
| 	// | ||||
| 	// (the final bit probably explains 13 cycles of the DE to interrupt latency; not sure about the other ~15) | ||||
| } | ||||
|  | ||||
| void MFP68901::decrement_timer(int timer, int amount) { | ||||
| 	while(amount--) { | ||||
| 		--timers_[timer].value; | ||||
| 		if(timers_[timer].value < 1) { | ||||
| 			switch(timer) { | ||||
| 				case 0: begin_interrupts(Interrupt::TimerA);	break; | ||||
| 				case 1: begin_interrupts(Interrupt::TimerB);	break; | ||||
| 				case 2: begin_interrupts(Interrupt::TimerC);	break; | ||||
| 				case 3: begin_interrupts(Interrupt::TimerD);	break; | ||||
| 			} | ||||
|  | ||||
| 			// Re: reloading when in event counting mode; I found the data sheet thoroughly unclear on | ||||
| 			// this, but it appears empirically to be correct. See e.g. Pompey Pirates menu 27. | ||||
| 			if(timers_[timer].mode == TimerMode::Delay || timers_[timer].mode == TimerMode::EventCount) { | ||||
| 				timers_[timer].value += timers_[timer].reload_value;	// TODO: properly. | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - GPIP | ||||
| void MFP68901::set_port_input(uint8_t input) { | ||||
| 	gpip_input_ = input; | ||||
| 	reevaluate_gpip_interrupts(); | ||||
| } | ||||
|  | ||||
| uint8_t MFP68901::get_port_output() { | ||||
| 	return 0xff;	// TODO. | ||||
| } | ||||
|  | ||||
| void MFP68901::reevaluate_gpip_interrupts() { | ||||
| 	const uint8_t gpip_state = (gpip_input_ & ~gpip_direction_) ^ gpip_active_edge_; | ||||
|  | ||||
| 	// An interrupt is detected on any falling edge. | ||||
| 	const uint8_t new_interrupt_mask = (gpip_state ^ gpip_interrupt_state_) & gpip_interrupt_state_; | ||||
| 	if(new_interrupt_mask) { | ||||
| 		begin_interrupts( | ||||
| 			(new_interrupt_mask & 0x0f) | | ||||
| 			((new_interrupt_mask & 0x30) << 2) | | ||||
| 			((new_interrupt_mask & 0xc0) << 8) | ||||
| 		); | ||||
| 	} | ||||
| 	gpip_interrupt_state_ = gpip_state; | ||||
| } | ||||
|  | ||||
| // MARK: - Interrupts | ||||
|  | ||||
| void MFP68901::begin_interrupts(int interrupt) { | ||||
| 	interrupt_pending_ |= interrupt & interrupt_enable_; | ||||
| 	update_interrupts(); | ||||
| } | ||||
|  | ||||
| void MFP68901::end_interrupts(int interrupt) { | ||||
| 	interrupt_pending_ &= ~interrupt; | ||||
| 	update_interrupts(); | ||||
| } | ||||
|  | ||||
| void MFP68901::update_interrupts() { | ||||
| 	const auto old_interrupt_line = interrupt_line_; | ||||
| 	const auto firing_interrupts = interrupt_pending_ & interrupt_mask_; | ||||
|  | ||||
| 	if(!firing_interrupts) { | ||||
| 		interrupt_line_ = false; | ||||
| 	} else { | ||||
| 		if(interrupt_vector_ & 0x8) { | ||||
| 			// Software interrupt mode: permit only if neither this interrupt | ||||
| 			// nor a higher interrupt is currently in service. | ||||
| 			const int highest_bit = msb16(firing_interrupts); | ||||
| 			interrupt_line_ = !(interrupt_in_service_ & ~(highest_bit + highest_bit - 1)); | ||||
| 		} else { | ||||
| 			// Auto-interrupt mode; just signal. | ||||
| 			interrupt_line_ = true; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Update the delegate if necessary. | ||||
| 	if(interrupt_delegate_ && interrupt_line_ != old_interrupt_line) { | ||||
| 		interrupt_delegate_->mfp68901_did_change_interrupt_status(this); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool MFP68901::get_interrupt_line() { | ||||
| 	return interrupt_line_; | ||||
| } | ||||
|  | ||||
| int MFP68901::acknowledge_interrupt() { | ||||
| 	if(!(interrupt_pending_ & interrupt_mask_)) { | ||||
| 		return NoAcknowledgement; | ||||
| 	} | ||||
|  | ||||
| 	const int mask = msb16(interrupt_pending_ & interrupt_mask_); | ||||
|  | ||||
| 	// Clear the pending bit regardless. | ||||
| 	interrupt_pending_ &= ~mask; | ||||
|  | ||||
| 	// If this is software interrupt mode, set the in-service bit. | ||||
| 	if(interrupt_vector_ & 0x8) { | ||||
| 		interrupt_in_service_ |= mask; | ||||
| 	} | ||||
|  | ||||
| 	update_interrupts(); | ||||
|  | ||||
| 	int selected = 0; | ||||
| 	while((1 << selected) != mask) ++selected; | ||||
| //	LOG("Interrupt acknowledged: " << selected); | ||||
| 	return (interrupt_vector_ & 0xf0) | uint8_t(selected); | ||||
| } | ||||
|  | ||||
| void MFP68901::set_interrupt_delegate(InterruptDelegate *delegate) { | ||||
| 	interrupt_delegate_ = delegate; | ||||
| } | ||||
							
								
								
									
										187
									
								
								Components/68901/MFP68901.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								Components/68901/MFP68901.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | ||||
| // | ||||
| //  MFP68901.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 06/10/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MFP68901_hpp | ||||
| #define MFP68901_hpp | ||||
|  | ||||
| #include <cstdint> | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../ClockReceiver/ClockingHintSource.hpp" | ||||
|  | ||||
| namespace Motorola { | ||||
| namespace MFP68901 { | ||||
|  | ||||
| class PortHandler { | ||||
| 	public: | ||||
| 		// TODO: announce changes in output. | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Models the Motorola 68901 Multi-Function Peripheral ('MFP'). | ||||
| */ | ||||
| class MFP68901: public ClockingHint::Source { | ||||
| 	public: | ||||
| 		/// @returns the result of a read from @c address. | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		/// Performs a write of @c value to @c address. | ||||
| 		void write(int address, uint8_t value); | ||||
|  | ||||
| 		/// Advances the MFP by the supplied number of HalfCycles. | ||||
| 		void run_for(HalfCycles); | ||||
|  | ||||
| 		/// @returns the number of cycles until the next possible sequence point — the next time | ||||
| 		/// at which the interrupt line _might_ change. This object conforms to ClockingHint::Source | ||||
| 		/// so that mechanism can also be used to reduce the quantity of calls into this class. | ||||
| 		/// | ||||
| 		/// @discussion TODO, alas. | ||||
| 		HalfCycles get_next_sequence_point(); | ||||
|  | ||||
| 		/// Sets the current level of either of the timer event inputs — TAI and TBI in datasheet terms. | ||||
| 		void set_timer_event_input(int channel, bool value); | ||||
|  | ||||
| 		/// Sets a port handler, a receiver that will be notified upon any change in GPIP output. | ||||
| 		/// | ||||
| 		/// @discussion TODO. | ||||
| 		void set_port_handler(PortHandler *); | ||||
|  | ||||
| 		/// Sets the current input GPIP values. | ||||
| 		void set_port_input(uint8_t); | ||||
|  | ||||
| 		/// @returns the current GPIP output values. | ||||
| 		/// | ||||
| 		/// @discussion TODO. | ||||
| 		uint8_t get_port_output(); | ||||
|  | ||||
| 		/// @returns @c true if the interrupt output is currently active; @c false otherwise.s | ||||
| 		bool get_interrupt_line(); | ||||
|  | ||||
| 		static constexpr int NoAcknowledgement = 0x100; | ||||
|  | ||||
| 		/// Communicates an interrupt acknowledge cycle. | ||||
| 		/// | ||||
| 		/// @returns the vector placed on the bus if any; @c NoAcknowledgement if nothing is loaded. | ||||
| 		int acknowledge_interrupt(); | ||||
|  | ||||
| 		struct InterruptDelegate { | ||||
| 			/// Informs the delegate of a change in the interrupt line of the nominated MFP. | ||||
| 			virtual void mfp68901_did_change_interrupt_status(MFP68901 *) = 0; | ||||
| 		}; | ||||
| 		/// Sets a delegate that will be notified upon any change in the interrupt line. | ||||
| 		void set_interrupt_delegate(InterruptDelegate *delegate); | ||||
|  | ||||
| 		// ClockingHint::Source. | ||||
| 		ClockingHint::Preference preferred_clocking() final; | ||||
|  | ||||
| 	private: | ||||
| 		// MARK: - Timers | ||||
| 		enum class TimerMode { | ||||
| 			Stopped, EventCount, Delay, PulseWidth | ||||
| 		}; | ||||
| 		void set_timer_mode(int timer, TimerMode, int prescale, bool reset_timer); | ||||
| 		void set_timer_data(int timer, uint8_t); | ||||
| 		uint8_t get_timer_data(int timer); | ||||
| 		void decrement_timer(int timer, int amount); | ||||
|  | ||||
| 		struct Timer { | ||||
| 			TimerMode mode = TimerMode::Stopped; | ||||
| 			uint8_t value = 0; | ||||
| 			uint8_t reload_value = 0; | ||||
| 			int prescale = 1; | ||||
| 			int prescale_count = 1; | ||||
| 			bool event_input = false; | ||||
| 		} timers_[4]; | ||||
| 		uint8_t timer_ab_control_[2] = { 0, 0 }; | ||||
| 		uint8_t timer_cd_control_ = 0; | ||||
|  | ||||
| 		HalfCycles cycles_left_; | ||||
|  | ||||
| 		// MARK: - GPIP | ||||
| 		uint8_t gpip_input_ = 0; | ||||
| 		uint8_t gpip_output_ = 0; | ||||
| 		uint8_t gpip_active_edge_ = 0; | ||||
| 		uint8_t gpip_direction_ = 0; | ||||
| 		uint8_t gpip_interrupt_state_ = 0; | ||||
|  | ||||
| 		void reevaluate_gpip_interrupts(); | ||||
|  | ||||
| 		// MARK: - Interrupts | ||||
|  | ||||
| 		InterruptDelegate *interrupt_delegate_ = nullptr; | ||||
|  | ||||
| 		// Ad hoc documentation: | ||||
| 		// | ||||
| 		// An interrupt becomes pending if it is enabled at the time it occurs. | ||||
| 		// | ||||
| 		// If a pending interrupt is enabled in the interrupt mask, a processor | ||||
| 		// interrupt is generated. Otherwise no processor interrupt is generated. | ||||
| 		// | ||||
| 		// (Disabling a bit in the enabled mask also instantaneously clears anything | ||||
| 		// in the pending mask.) | ||||
| 		// | ||||
| 		// The user can write to the pending interrupt register; a write | ||||
| 		// masks whatever is there — so you can disable bits but you cannot set them. | ||||
| 		// | ||||
| 		// If the vector register's 'S' bit is set then software end-of-interrupt mode applies: | ||||
| 		// Acknowledgement of an interrupt clears that interrupt's pending bit, but also sets | ||||
| 		// its in-service bit. That bit will remain set until the user writes a zero to its position. | ||||
| 		// If any bits are set in the in-service register, then they will prevent lower-priority | ||||
| 		// interrupts from being signalled to the CPU. Further interrupts of the same or a higher | ||||
| 		// priority may occur. | ||||
| 		// | ||||
| 		// If the vector register's 'S' bit is clear then automatic end-of-interrupt mode applies: | ||||
| 		// Acknowledgement of an interrupt will automatically clear the corresponding | ||||
| 		// pending bit. | ||||
| 		// | ||||
| 		int interrupt_enable_ = 0; | ||||
| 		int interrupt_pending_ = 0; | ||||
| 		int interrupt_mask_ = 0; | ||||
| 		int interrupt_in_service_ = 0; | ||||
| 		bool interrupt_line_ = false; | ||||
| 		uint8_t interrupt_vector_ = 0; | ||||
|  | ||||
| 		enum Interrupt { | ||||
| 			GPIP0				= (1 << 0), | ||||
| 			GPIP1				= (1 << 1), | ||||
| 			GPIP2				= (1 << 2), | ||||
| 			GPIP3				= (1 << 3), | ||||
| 			TimerD				= (1 << 4), | ||||
| 			TimerC				= (1 << 5), | ||||
| 			GPIP4				= (1 << 6), | ||||
| 			GPIP5				= (1 << 7), | ||||
|  | ||||
| 			TimerB				= (1 << 8), | ||||
| 			TransmitError		= (1 << 9), | ||||
| 			TransmitBufferEmpty	= (1 << 10), | ||||
| 			ReceiveError		= (1 << 11), | ||||
| 			ReceiveBufferFull	= (1 << 12), | ||||
| 			TimerA				= (1 << 13), | ||||
| 			GPIP6				= (1 << 14), | ||||
| 			GPIP7				= (1 << 15), | ||||
| 		}; | ||||
| 		void begin_interrupts(int interrupt); | ||||
| 		void end_interrupts(int interrupt); | ||||
| 		void update_interrupts(); | ||||
|  | ||||
| 		/// @returns the most significant bit set in v, assuming it is one of the least significant 16. | ||||
| 		inline static int msb16(int v) { | ||||
| 			// Saturate all bits below the MSB. | ||||
| 			v |= v >> 1; | ||||
| 			v |= v >> 2; | ||||
| 			v |= v >> 4; | ||||
| 			v |= v >> 8; | ||||
|  | ||||
| 			// Throw away lesser bits. | ||||
| 			return (v+1) >> 1; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* MFP68901_hpp */ | ||||
| @@ -27,7 +27,7 @@ template <class T> class i8255 { | ||||
| 			Stores the value @c value to the register at @c address. If this causes a change in 8255 output | ||||
| 			then the PortHandler will be informed. | ||||
| 		*/ | ||||
| 		void set_register(int address, uint8_t value) { | ||||
| 		void write(int address, uint8_t value) { | ||||
| 			switch(address & 3) { | ||||
| 				case 0: | ||||
| 					if(!(control_ & 0x10)) { | ||||
| @@ -60,7 +60,7 @@ template <class T> class i8255 { | ||||
| 			Obtains the current value for the register at @c address. If this provides a reading | ||||
| 			of input then the PortHandler will be queried. | ||||
| 		*/ | ||||
| 		uint8_t get_register(int address) { | ||||
| 		uint8_t read(int address) { | ||||
| 			switch(address & 3) { | ||||
| 				case 0:	return (control_ & 0x10) ? port_handler_.get_value(0) : outputs_[0]; | ||||
| 				case 1:	return (control_ & 0x02) ? port_handler_.get_value(1) : outputs_[1]; | ||||
|   | ||||
| @@ -95,11 +95,11 @@ void i8272::run_for(Cycles cycles) { | ||||
|  | ||||
| 	// check for an expired timer | ||||
| 	if(delay_time_ > 0) { | ||||
| 		if(cycles.as_int() >= delay_time_) { | ||||
| 		if(cycles.as_integral() >= delay_time_) { | ||||
| 			delay_time_ = 0; | ||||
| 			posit_event(static_cast<int>(Event8272::Timer)); | ||||
| 		} else { | ||||
| 			delay_time_ -= cycles.as_int(); | ||||
| 			delay_time_ -= cycles.as_integral(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -108,8 +108,8 @@ void i8272::run_for(Cycles cycles) { | ||||
| 		int drives_left = drives_seeking_; | ||||
| 		for(int c = 0; c < 4; c++) { | ||||
| 			if(drives_[c].phase == Drive::Seeking) { | ||||
| 				drives_[c].step_rate_counter += cycles.as_int(); | ||||
| 				int steps = drives_[c].step_rate_counter / (8000 * step_rate_time_); | ||||
| 				drives_[c].step_rate_counter += cycles.as_integral(); | ||||
| 				auto steps = drives_[c].step_rate_counter / (8000 * step_rate_time_); | ||||
| 				drives_[c].step_rate_counter %= (8000 * step_rate_time_); | ||||
| 				while(steps--) { | ||||
| 					// Perform a step. | ||||
| @@ -141,12 +141,12 @@ void i8272::run_for(Cycles cycles) { | ||||
| 			int head = c&1; | ||||
|  | ||||
| 			if(drives_[drive].head_unload_delay[head] > 0) { | ||||
| 				if(cycles.as_int() >= drives_[drive].head_unload_delay[head]) { | ||||
| 				if(cycles.as_integral() >= drives_[drive].head_unload_delay[head]) { | ||||
| 					drives_[drive].head_unload_delay[head] = 0; | ||||
| 					drives_[drive].head_is_loaded[head] = false; | ||||
| 					head_timers_running_--; | ||||
| 				} else { | ||||
| 					drives_[drive].head_unload_delay[head] -= cycles.as_int(); | ||||
| 					drives_[drive].head_unload_delay[head] -= cycles.as_integral(); | ||||
| 				} | ||||
| 				timers_left--; | ||||
| 				if(!timers_left) break; | ||||
| @@ -163,7 +163,7 @@ void i8272::run_for(Cycles cycles) { | ||||
| 	if(is_sleeping_) update_clocking_observer(); | ||||
| } | ||||
|  | ||||
| void i8272::set_register(int address, uint8_t value) { | ||||
| void i8272::write(int address, uint8_t value) { | ||||
| 	// don't consider attempted sets to the status register | ||||
| 	if(!address) return; | ||||
|  | ||||
| @@ -181,7 +181,7 @@ void i8272::set_register(int address, uint8_t value) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t i8272::get_register(int address) { | ||||
| uint8_t i8272::read(int address) { | ||||
| 	if(address) { | ||||
| 		if(result_stack_.empty()) return 0xff; | ||||
| 		uint8_t result = result_stack_.back(); | ||||
| @@ -292,7 +292,7 @@ void i8272::posit_event(int event_type) { | ||||
| 			WAIT_FOR_EVENT(Event8272::CommandByte) | ||||
| 			SetBusy(); | ||||
|  | ||||
| 			static const std::size_t required_lengths[32] = { | ||||
| 			static constexpr std::size_t required_lengths[32] = { | ||||
| 				0,	0,	9,	3,	2,	9,	9,	2, | ||||
| 				1,	9,	2,	0,	9,	6,	0,	3, | ||||
| 				0,	9,	0,	0,	0,	0,	0,	0, | ||||
| @@ -865,7 +865,7 @@ void i8272::posit_event(int event_type) { | ||||
| 			SetDataRequest(); | ||||
| 			SetDataDirectionToProcessor(); | ||||
|  | ||||
| 			// The actual stuff of unwinding result_stack_ is handled by ::get_register; wait | ||||
| 			// The actual stuff of unwinding result_stack_ is handled by ::read; wait | ||||
| 			// until the processor has read all result bytes. | ||||
| 			WAIT_FOR_EVENT(Event8272::ResultEmpty); | ||||
|  | ||||
|   | ||||
| @@ -24,7 +24,7 @@ class BusHandler { | ||||
| 		virtual void set_interrupt(bool irq) {} | ||||
| }; | ||||
|  | ||||
| class i8272: public Storage::Disk::MFMController { | ||||
| class i8272 : public Storage::Disk::MFMController { | ||||
| 	public: | ||||
| 		i8272(BusHandler &bus_handler, Cycles clock_rate); | ||||
|  | ||||
| @@ -33,13 +33,13 @@ class i8272: public Storage::Disk::MFMController { | ||||
| 		void set_data_input(uint8_t value); | ||||
| 		uint8_t get_data_output(); | ||||
|  | ||||
| 		void set_register(int address, uint8_t value); | ||||
| 		uint8_t get_register(int address); | ||||
| 		void write(int address, uint8_t value); | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		void set_dma_acknowledge(bool dack); | ||||
| 		void set_terminal_count(bool tc); | ||||
|  | ||||
| 		ClockingHint::Preference preferred_clocking() override; | ||||
| 		ClockingHint::Preference preferred_clocking() final; | ||||
|  | ||||
| 	protected: | ||||
| 		virtual void select_drive(int number) = 0; | ||||
| @@ -73,7 +73,7 @@ class i8272: public Storage::Disk::MFMController { | ||||
| 		bool is_access_command_ = false; | ||||
|  | ||||
| 		// The counter used for ::Timer events. | ||||
| 		int delay_time_ = 0; | ||||
| 		Cycles::IntType delay_time_ = 0; | ||||
|  | ||||
| 		// The connected drives. | ||||
| 		struct Drive { | ||||
| @@ -89,12 +89,12 @@ class i8272: public Storage::Disk::MFMController { | ||||
| 			bool seek_failed = false; | ||||
|  | ||||
| 			// Seeking: transient state. | ||||
| 			int step_rate_counter = 0; | ||||
| 			Cycles::IntType step_rate_counter = 0; | ||||
| 			int steps_taken = 0; | ||||
| 			int target_head_position = 0;	// either an actual number, or -1 to indicate to step until track zero | ||||
|  | ||||
| 			// Head state. | ||||
| 			int head_unload_delay[2] = {0, 0}; | ||||
| 			Cycles::IntType head_unload_delay[2] = {0, 0}; | ||||
| 			bool head_is_loaded[2] = {false, false}; | ||||
|  | ||||
| 		} drives_[4]; | ||||
|   | ||||
							
								
								
									
										268
									
								
								Components/8530/z8530.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								Components/8530/z8530.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | ||||
| // | ||||
| //  8530.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 07/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "z8530.hpp" | ||||
|  | ||||
| #include "../../Outputs/Log.hpp" | ||||
|  | ||||
| using namespace Zilog::SCC; | ||||
|  | ||||
| void z8530::reset() { | ||||
| 	// TODO. | ||||
| } | ||||
|  | ||||
| bool z8530::get_interrupt_line() { | ||||
| 	return | ||||
| 		(master_interrupt_control_ & 0x8) && | ||||
| 		( | ||||
| 			channels_[0].get_interrupt_line() || | ||||
| 			channels_[1].get_interrupt_line() | ||||
| 		); | ||||
| } | ||||
|  | ||||
| std::uint8_t z8530::read(int address) { | ||||
| 	if(address & 2) { | ||||
| 		// Read data register for channel | ||||
| 		return 0x00; | ||||
| 	} else { | ||||
| 		// Read control register for channel. | ||||
| 		uint8_t result = 0; | ||||
|  | ||||
| 		switch(pointer_) { | ||||
| 			default: | ||||
| 				result = channels_[address & 1].read(address & 2, pointer_); | ||||
| 			break; | ||||
|  | ||||
| 			case 2:		// Handled non-symmetrically between channels. | ||||
| 				if(address & 1) { | ||||
| 					LOG("[SCC] Unimplemented: register 2 status bits"); | ||||
| 				} else { | ||||
| 					result = interrupt_vector_; | ||||
|  | ||||
| 					// Modify the vector if permitted. | ||||
| //					if(master_interrupt_control_ & 1) { | ||||
| 						for(int port = 0; port < 2; ++port) { | ||||
| 							// TODO: the logic below assumes that DCD is the only implemented interrupt. Fix. | ||||
| 							if(channels_[port].get_interrupt_line()) { | ||||
| 								const uint8_t shift = 1 + 3*((master_interrupt_control_ & 0x10) >> 4); | ||||
| 								const uint8_t mask = uint8_t(~(7 << shift)); | ||||
| 								result = uint8_t( | ||||
| 									(result & mask) | | ||||
| 									((1 | ((port == 1) ? 4 : 0)) << shift) | ||||
| 								); | ||||
| 								break; | ||||
| 							} | ||||
| 						} | ||||
| //					} | ||||
| 				} | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		pointer_ = 0; | ||||
| 		update_delegate(); | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	return 0x00; | ||||
| } | ||||
|  | ||||
| void z8530::write(int address, std::uint8_t value) { | ||||
| 	if(address & 2) { | ||||
| 		// Write data register for channel. | ||||
| 	} else { | ||||
| 		// Write control register for channel. | ||||
|  | ||||
| 		// Most registers are per channel, but a couple are shared; sever | ||||
| 		// them here. | ||||
| 		switch(pointer_) { | ||||
| 			default: | ||||
| 				channels_[address & 1].write(address & 2, pointer_, value); | ||||
| 			break; | ||||
|  | ||||
| 			case 2:	// Interrupt vector register; shared between both channels. | ||||
| 				interrupt_vector_ = value; | ||||
| 				LOG("[SCC] Interrupt vector set to " << PADHEX(2) << int(value)); | ||||
| 			break; | ||||
|  | ||||
| 			case 9:	// Master interrupt and reset register; also shared between both channels. | ||||
| 				LOG("[SCC] Master interrupt and reset register: " << PADHEX(2) << int(value)); | ||||
| 				master_interrupt_control_ = value; | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		// The pointer number resets to 0 after every access, but if it is zero | ||||
| 		// then crib at least the next set of pointer bits (which, similarly, are shared | ||||
| 		// between the two channels). | ||||
| 		if(pointer_) { | ||||
| 			pointer_ = 0; | ||||
| 		} else { | ||||
| 			// The lowest three bits are the lowest three bits of the pointer. | ||||
| 			pointer_ = value & 7; | ||||
|  | ||||
| 			// If the command part of the byte is a 'point high', also set the | ||||
| 			// top bit of the pointer. | ||||
| 			if(((value >> 3)&7) == 1) { | ||||
| 				pointer_ |= 8; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	update_delegate(); | ||||
| } | ||||
|  | ||||
| void z8530::set_dcd(int port, bool level) { | ||||
| 	channels_[port].set_dcd(level); | ||||
| 	update_delegate(); | ||||
| } | ||||
|  | ||||
| // MARK: - Channel implementations | ||||
|  | ||||
| uint8_t z8530::Channel::read(bool data, uint8_t pointer) { | ||||
| 	// If this is a data read, just return it. | ||||
| 	if(data) { | ||||
| 		return data_; | ||||
| 	} else { | ||||
| 		// Otherwise, this is a control read... | ||||
| 		switch(pointer) { | ||||
| 			default: | ||||
| 				LOG("[SCC] Unrecognised control read from register " << int(pointer)); | ||||
| 			return 0x00; | ||||
|  | ||||
| 			case 0: | ||||
| 			return dcd_ ? 0x8 : 0x0; | ||||
|  | ||||
| 			case 0xf: | ||||
| 			return external_interrupt_status_; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return 0x00; | ||||
| } | ||||
|  | ||||
| void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) { | ||||
| 	if(data) { | ||||
| 		data_ = value; | ||||
| 		return; | ||||
| 	} else { | ||||
| 		switch(pointer) { | ||||
| 			default: | ||||
| 				LOG("[SCC] Unrecognised control write: " << PADHEX(2) << int(value) << " to register " << int(pointer)); | ||||
| 			break; | ||||
|  | ||||
| 			case 0x0:	// Write register 0 — CRC reset and other functions. | ||||
| 				// Decode CRC reset instructions. | ||||
| 				switch(value >> 6) { | ||||
| 					default:	/* Do nothing. */		break; | ||||
| 					case 1: | ||||
| 						LOG("[SCC] TODO: reset Rx CRC checker."); | ||||
| 					break; | ||||
| 					case 2: | ||||
| 						LOG("[SCC] TODO: reset Tx CRC checker."); | ||||
| 					break; | ||||
| 					case 3: | ||||
| 						LOG("[SCC] TODO: reset Tx underrun/EOM latch."); | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				// Decode command code. | ||||
| 				switch((value >> 3)&7) { | ||||
| 					default:	/* Do nothing. */		break; | ||||
| 					case 2: | ||||
| //						LOG("[SCC] reset ext/status interrupts."); | ||||
| 						external_status_interrupt_ = false; | ||||
| 						external_interrupt_status_ = 0; | ||||
| 					break; | ||||
| 					case 3: | ||||
| 						LOG("[SCC] TODO: send abort (SDLC)."); | ||||
| 					break; | ||||
| 					case 4: | ||||
| 						LOG("[SCC] TODO: enable interrupt on next Rx character."); | ||||
| 					break; | ||||
| 					case 5: | ||||
| 						LOG("[SCC] TODO: reset Tx interrupt pending."); | ||||
| 					break; | ||||
| 					case 6: | ||||
| 						LOG("[SCC] TODO: reset error."); | ||||
| 					break; | ||||
| 					case 7: | ||||
| 						LOG("[SCC] TODO: reset highest IUS."); | ||||
| 					break; | ||||
| 				} | ||||
| 			break; | ||||
|  | ||||
| 			case 0x1:	// Write register 1 — Transmit/Receive Interrupt and Data Transfer Mode Definition. | ||||
| 				interrupt_mask_ = value; | ||||
| 			break; | ||||
|  | ||||
| 			case 0x4:	// Write register 4 — Transmit/Receive Miscellaneous Parameters and Modes. | ||||
| 				// Bits 0 and 1 select parity mode. | ||||
| 				if(!(value&1)) { | ||||
| 					parity_ = Parity::Off; | ||||
| 				} else { | ||||
| 					parity_ = (value&2) ? Parity::Even : Parity::Odd; | ||||
| 				} | ||||
|  | ||||
| 				// Bits 2 and 3 select stop bits. | ||||
| 				switch((value >> 2)&3) { | ||||
| 					default:	stop_bits_ = StopBits::Synchronous;			break; | ||||
| 					case 1:		stop_bits_ = StopBits::OneBit;				break; | ||||
| 					case 2:		stop_bits_ = StopBits::OneAndAHalfBits;		break; | ||||
| 					case 3:		stop_bits_ = StopBits::TwoBits;				break; | ||||
| 				} | ||||
|  | ||||
| 				// Bits 4 and 5 pick a sync mode. | ||||
| 				switch((value >> 4)&3) { | ||||
| 					default:	sync_mode_ = Sync::Monosync;	break; | ||||
| 					case 1:		sync_mode_ = Sync::Bisync;		break; | ||||
| 					case 2:		sync_mode_ = Sync::SDLC;		break; | ||||
| 					case 3:		sync_mode_ = Sync::External;	break; | ||||
| 				} | ||||
|  | ||||
| 				// Bits 6 and 7 select a clock rate multiplier, unless synchronous | ||||
| 				// mode is enabled (and this is ignored if sync mode is external). | ||||
| 				if(stop_bits_ == StopBits::Synchronous) { | ||||
| 					clock_rate_multiplier_ = 1; | ||||
| 				} else { | ||||
| 					switch((value >> 6)&3) { | ||||
| 						default:	clock_rate_multiplier_ = 1;		break; | ||||
| 						case 1:		clock_rate_multiplier_ = 16;	break; | ||||
| 						case 2:		clock_rate_multiplier_ = 32;	break; | ||||
| 						case 3:		clock_rate_multiplier_ = 64;	break; | ||||
| 					} | ||||
| 				} | ||||
| 			break; | ||||
|  | ||||
| 			case 0xf:	// Write register 15 — External/Status Interrupt Control. | ||||
| 				external_interrupt_mask_ = value; | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void z8530::Channel::set_dcd(bool level) { | ||||
| 	if(dcd_ == level) return; | ||||
| 	dcd_ = level; | ||||
|  | ||||
| 	if(external_interrupt_mask_ & 0x8) { | ||||
| 		external_status_interrupt_ = true; | ||||
| 		external_interrupt_status_ |= 0x8; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool z8530::Channel::get_interrupt_line() { | ||||
| 	return | ||||
| 		(interrupt_mask_ & 1) && external_status_interrupt_; | ||||
| 	// TODO: other potential causes of an interrupt. | ||||
| } | ||||
|  | ||||
| void z8530::update_delegate() { | ||||
| 	const bool interrupt_line = get_interrupt_line(); | ||||
| 	if(interrupt_line != previous_interrupt_line_) { | ||||
| 		previous_interrupt_line_ = interrupt_line; | ||||
| 		if(delegate_) delegate_->did_change_interrupt_status(this, interrupt_line); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										99
									
								
								Components/8530/z8530.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								Components/8530/z8530.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| // | ||||
| //  z8530.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 07/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef z8530_hpp | ||||
| #define z8530_hpp | ||||
|  | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace Zilog { | ||||
| namespace SCC { | ||||
|  | ||||
| /*! | ||||
| 	Models the Zilog 8530 SCC, a serial adaptor. | ||||
| */ | ||||
| class z8530 { | ||||
| 	public: | ||||
| 		/* | ||||
| 			**Interface for emulated machine.** | ||||
|  | ||||
| 			Notes on addressing below: | ||||
|  | ||||
| 			There's no inherent ordering of the two 'address' lines, | ||||
| 			A/B and C/D, but the methods below assume: | ||||
|  | ||||
| 				A/B = A0 | ||||
| 				C/D = A1 | ||||
| 		*/ | ||||
| 		std::uint8_t read(int address); | ||||
| 		void write(int address, std::uint8_t value); | ||||
| 		void reset(); | ||||
| 		bool get_interrupt_line(); | ||||
|  | ||||
| 		struct Delegate { | ||||
| 			virtual void did_change_interrupt_status(z8530 *, bool new_status) = 0; | ||||
| 		}; | ||||
| 		void set_delegate(Delegate *delegate) { | ||||
| 			delegate_ = delegate; | ||||
| 		} | ||||
|  | ||||
| 		/* | ||||
| 			**Interface for serial port input.** | ||||
| 		*/ | ||||
| 		void set_dcd(int port, bool level); | ||||
|  | ||||
| 	private: | ||||
| 		class Channel { | ||||
| 			public: | ||||
| 				uint8_t read(bool data, uint8_t pointer); | ||||
| 				void write(bool data, uint8_t pointer, uint8_t value); | ||||
| 				void set_dcd(bool level); | ||||
| 				bool get_interrupt_line(); | ||||
|  | ||||
| 			private: | ||||
| 				uint8_t data_ = 0xff; | ||||
|  | ||||
| 				enum class Parity { | ||||
| 					Even, Odd, Off | ||||
| 				} parity_ = Parity::Off; | ||||
|  | ||||
| 				enum class StopBits { | ||||
| 					Synchronous, OneBit, OneAndAHalfBits, TwoBits | ||||
| 				} stop_bits_ = StopBits::Synchronous; | ||||
|  | ||||
| 				enum class Sync { | ||||
| 					Monosync, Bisync, SDLC, External | ||||
| 				} sync_mode_ = Sync::Monosync; | ||||
|  | ||||
| 				int clock_rate_multiplier_ = 1; | ||||
|  | ||||
| 				uint8_t interrupt_mask_ = 0;			// i.e. Write Register 0x1. | ||||
|  | ||||
| 				uint8_t external_interrupt_mask_ = 0;	// i.e. Write Register 0xf. | ||||
| 				bool external_status_interrupt_ = false; | ||||
| 				uint8_t external_interrupt_status_ = 0; | ||||
|  | ||||
| 				bool dcd_ = false; | ||||
| 		} channels_[2]; | ||||
|  | ||||
| 		uint8_t pointer_ = 0; | ||||
|  | ||||
| 		uint8_t interrupt_vector_ = 0; | ||||
|  | ||||
| 		uint8_t master_interrupt_control_ = 0; | ||||
|  | ||||
| 		bool previous_interrupt_line_ = false; | ||||
| 		void update_delegate(); | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| #endif /* z8530_hpp */ | ||||
| @@ -11,21 +11,22 @@ | ||||
| #include <cassert> | ||||
| #include <cstring> | ||||
| #include <cstdlib> | ||||
| #include "../../Outputs/Log.hpp" | ||||
|  | ||||
| using namespace TI::TMS; | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| const uint8_t StatusInterrupt = 0x80; | ||||
| const uint8_t StatusSpriteOverflow = 0x40; | ||||
| constexpr uint8_t StatusInterrupt = 0x80; | ||||
| constexpr uint8_t StatusSpriteOverflow = 0x40; | ||||
|  | ||||
| const int StatusSpriteCollisionShift = 5; | ||||
| const uint8_t StatusSpriteCollision = 0x20; | ||||
| constexpr int StatusSpriteCollisionShift = 5; | ||||
| constexpr uint8_t StatusSpriteCollision = 0x20; | ||||
|  | ||||
| // 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole | ||||
| // line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. | ||||
| const unsigned int CRTCyclesPerLine = 1365; | ||||
| const unsigned int CRTCyclesDivider = 4; | ||||
| constexpr unsigned int CRTCyclesPerLine = 1365; | ||||
| constexpr unsigned int CRTCyclesDivider = 4; | ||||
|  | ||||
| struct ReverseTable { | ||||
| 	std::uint8_t map[256]; | ||||
| @@ -50,7 +51,9 @@ struct ReverseTable { | ||||
|  | ||||
| Base::Base(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) { | ||||
| 		case TI::TMS::TMS9918A: | ||||
| @@ -84,21 +87,14 @@ Base::Base(Personality p) : | ||||
|  | ||||
| TMS9918::TMS9918(Personality p): | ||||
| 	Base(p) { | ||||
| 	// Unimaginatively, this class just passes RGB through to the shader. Investigation is needed | ||||
| 	// into whether there's a more natural form. | ||||
| 	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)); | ||||
| 	crt_.set_display_type(Outputs::Display::DisplayType::RGB); | ||||
| 	crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f)); | ||||
|  | ||||
| 	// 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 | ||||
| 	// 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. | ||||
| 	crt_->set_immediate_default_phase(0.85f); | ||||
| 	crt_.set_immediate_default_phase(0.85f); | ||||
| } | ||||
|  | ||||
| void TMS9918::set_tv_standard(TVStandard standard) { | ||||
| @@ -107,18 +103,22 @@ void TMS9918::set_tv_standard(TVStandard standard) { | ||||
| 		case TVStandard::PAL: | ||||
| 			mode_timing_.total_lines = 313; | ||||
| 			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; | ||||
| 		default: | ||||
| 			mode_timing_.total_lines = 262; | ||||
| 			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; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Outputs::CRT::CRT *TMS9918::get_crt() { | ||||
| 	return crt_.get(); | ||||
| void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||
| 	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() { | ||||
| @@ -166,7 +166,7 @@ void TMS9918::run_for(const HalfCycles cycles) { | ||||
| 	// Convert 456 clocked half cycles per line to 342 internal cycles per line; | ||||
| 	// the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised | ||||
| 	// for this part. So multiply by three quarters. | ||||
| 	int int_cycles = (cycles.as_int() * 3) + cycles_error_; | ||||
| 	int int_cycles = int(cycles.as_integral() * 3) + cycles_error_; | ||||
| 	cycles_error_ = int_cycles & 3; | ||||
| 	int_cycles >>= 2; | ||||
| 	if(!int_cycles) return; | ||||
| @@ -352,8 +352,7 @@ void TMS9918::run_for(const HalfCycles cycles) { | ||||
| 				// Output video stream. | ||||
| 				// -------------------- | ||||
|  | ||||
| #define intersect(left, right, code)	\ | ||||
| 	{	\ | ||||
| #define intersect(left, right, code)	{	\ | ||||
| 		const int start = std::max(read_pointer_.column, left);	\ | ||||
| 		const int end = std::min(end_column, right);	\ | ||||
| 		if(end > start) {\ | ||||
| @@ -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) { | ||||
| 						// Vertical sync. | ||||
| 						if(end_column == 342) { | ||||
| 							crt_->output_sync(342 * 4); | ||||
| 							crt_.output_sync(342 * 4); | ||||
| 						} | ||||
| 					} else { | ||||
| 						// Right border. | ||||
| @@ -377,11 +376,11 @@ void TMS9918::run_for(const HalfCycles cycles) { | ||||
| 						// and 58+15 = 73. So output the lot when the | ||||
| 						// cursor passes 73. | ||||
| 						if(read_pointer_.column < 73 && end_column >= 73) { | ||||
| 							crt_->output_blank(8*4); | ||||
| 							crt_->output_sync(26*4); | ||||
| 							crt_->output_blank(2*4); | ||||
| 							crt_->output_default_colour_burst(14*4); | ||||
| 							crt_->output_blank(8*4); | ||||
| 							crt_.output_blank(8*4); | ||||
| 							crt_.output_sync(26*4); | ||||
| 							crt_.output_blank(2*4); | ||||
| 							crt_.output_default_colour_burst(14*4); | ||||
| 							crt_.output_blank(8*4); | ||||
| 						} | ||||
|  | ||||
| 						// Border colour for the rest of the line. | ||||
| @@ -393,11 +392,11 @@ void TMS9918::run_for(const HalfCycles cycles) { | ||||
|  | ||||
| 					// Blanking region. | ||||
| 					if(read_pointer_.column < 73 && end_column >= 73) { | ||||
| 						crt_->output_blank(8*4); | ||||
| 						crt_->output_sync(26*4); | ||||
| 						crt_->output_blank(2*4); | ||||
| 						crt_->output_default_colour_burst(14*4); | ||||
| 						crt_->output_blank(8*4); | ||||
| 						crt_.output_blank(8*4); | ||||
| 						crt_.output_sync(26*4); | ||||
| 						crt_.output_blank(2*4); | ||||
| 						crt_.output_default_colour_burst(14*4); | ||||
| 						crt_.output_blank(8*4); | ||||
| 					} | ||||
|  | ||||
| 					// Left border. | ||||
| @@ -410,7 +409,7 @@ void TMS9918::run_for(const HalfCycles cycles) { | ||||
| 						if(!asked_for_write_area_) { | ||||
| 							asked_for_write_area_ = true; | ||||
| 							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) { | ||||
| 							const unsigned int length = static_cast<unsigned int>(line_buffer.next_border_column - line_buffer.first_pixel_output_column); | ||||
| 							crt_->output_data(length * 4, length); | ||||
| 							const int length = line_buffer.next_border_column - line_buffer.first_pixel_output_column; | ||||
| 							crt_.output_data(length * 4, size_t(length)); | ||||
| 							pixel_origin_ = pixel_target_ = nullptr; | ||||
| 							asked_for_write_area_ = false; | ||||
| 						} | ||||
| @@ -470,20 +469,30 @@ void Base::output_border(int cycles, uint32_t cram_dot) { | ||||
| 			palette[background_colour_]; | ||||
|  | ||||
| 	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; | ||||
| 		crt_->output_level(4); | ||||
| 		} | ||||
| 		crt_.output_level(4); | ||||
| 		cycles -= 4; | ||||
| 	} | ||||
|  | ||||
| 	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; | ||||
| 		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 | ||||
| 	// the value and return. | ||||
| 	if(!(address & 1)) { | ||||
| @@ -582,7 +591,6 @@ void TMS9918::set_register(int address, uint8_t value) { | ||||
| 			case 8: | ||||
| 				if(is_sega_vdp(personality_)) { | ||||
| 					master_system_.horizontal_scroll = low_write_; | ||||
| //					printf("Set to %d at %d, %d\n", low_write_, row_, column_); | ||||
| 				} | ||||
| 			break; | ||||
|  | ||||
| @@ -599,7 +607,7 @@ void TMS9918::set_register(int address, uint8_t value) { | ||||
| 			break; | ||||
|  | ||||
| 			default: | ||||
| //				printf("Unknown TMS write: %d to %d\n", low_write_, value); | ||||
| 				LOG("Unknown TMS write: " << int(low_write_) << " to " << int(value)); | ||||
| 			break; | ||||
| 		} | ||||
| 	} else { | ||||
| @@ -616,7 +624,7 @@ void TMS9918::set_register(int address, uint8_t value) { | ||||
|  | ||||
| uint8_t TMS9918::get_current_line() { | ||||
| 	// Determine the row to return. | ||||
| 	static const int row_change_position = 63;	// This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. | ||||
| 	constexpr int row_change_position = 63;	// This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. | ||||
| 	int source_row = | ||||
| 		(write_pointer_.column < row_change_position) | ||||
| 			? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines | ||||
| @@ -662,7 +670,7 @@ void TMS9918::latch_horizontal_counter() { | ||||
| 	latched_column_ = write_pointer_.column; | ||||
| } | ||||
|  | ||||
| uint8_t TMS9918::get_register(int address) { | ||||
| uint8_t TMS9918::read(int address) { | ||||
| 	write_phase_ = false; | ||||
|  | ||||
| 	// Reads from address 0 read video RAM, via the read-ahead buffer. | ||||
| @@ -821,8 +829,8 @@ void Base::draw_tms_character(int start, int end) { | ||||
| 		int sprite_collision = 0; | ||||
| 		memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); | ||||
|  | ||||
| 		static const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; | ||||
| 		static const int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; | ||||
| 		constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; | ||||
| 		constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; | ||||
|  | ||||
| 		// Draw all sprites into the sprite buffer. | ||||
| 		const int shifter_target = sprites_16x16_ ? 32 : 16; | ||||
|   | ||||
| @@ -41,8 +41,11 @@ class TMS9918: public Base { | ||||
| 		/*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */ | ||||
| 		void set_tv_standard(TVStandard standard); | ||||
|  | ||||
| 		/*! Provides the CRT this TMS is connected to. */ | ||||
| 		Outputs::CRT::CRT *get_crt(); | ||||
| 		/*! Sets the scan target this TMS will post content to. */ | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *); | ||||
|  | ||||
| 		/*! Sets the type of display the CRT will request. */ | ||||
| 		void set_display_type(Outputs::Display::DisplayType); | ||||
|  | ||||
| 		/*! | ||||
| 			Runs the VCP for the number of cycles indicate; it is an implicit assumption of the code | ||||
| @@ -51,10 +54,10 @@ class TMS9918: public Base { | ||||
| 		void run_for(const HalfCycles cycles); | ||||
|  | ||||
| 		/*! Sets a register value. */ | ||||
| 		void set_register(int address, uint8_t value); | ||||
| 		void write(int address, uint8_t value); | ||||
|  | ||||
| 		/*! Gets a register value. */ | ||||
| 		uint8_t get_register(int address); | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		/*! Gets the current scan line; provided by the Master System only. */ | ||||
| 		uint8_t get_current_line(); | ||||
| @@ -66,8 +69,8 @@ class TMS9918: public Base { | ||||
| 		void latch_horizontal_counter(); | ||||
|  | ||||
| 		/*! | ||||
| 			Returns the amount of time until get_interrupt_line would next return true if | ||||
| 			there are no interceding calls to set_register or get_register. | ||||
| 			Returns the amount of time until @c get_interrupt_line would next return true if | ||||
| 			there are no interceding calls to @c write or to @c read. | ||||
|  | ||||
| 			If get_interrupt_line is true now, returns zero. If get_interrupt_line would | ||||
| 			never return true, returns -1. | ||||
|   | ||||
| @@ -78,8 +78,8 @@ class Base { | ||||
|  | ||||
| 		Base(Personality p); | ||||
|  | ||||
| 		Personality personality_; | ||||
| 		std::unique_ptr<Outputs::CRT::CRT> crt_; | ||||
| 		const Personality personality_; | ||||
| 		Outputs::CRT::CRT crt_; | ||||
| 		TVStandard tv_standard_ = TVStandard::NTSC; | ||||
|  | ||||
| 		// 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 | ||||
| 			// external slot after this number of cycles after the | ||||
| 			// device has requested the read or write). | ||||
| 			return 7; | ||||
| 			return 6; | ||||
| 		} | ||||
|  | ||||
| 		// Holds the main status register. | ||||
|   | ||||
| @@ -12,45 +12,56 @@ | ||||
|  | ||||
| using namespace GI::AY38910; | ||||
|  | ||||
| AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) { | ||||
| 	// set up envelope lookup tables | ||||
| AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) { | ||||
| 	// Don't use the low bit of the envelope position if this is an AY. | ||||
| 	envelope_position_mask_ |= personality == Personality::AY38910; | ||||
|  | ||||
| 	// Set up envelope lookup tables. | ||||
| 	for(int c = 0; c < 16; c++) { | ||||
| 		for(int p = 0; p < 32; p++) { | ||||
| 		for(int p = 0; p < 64; p++) { | ||||
| 			switch(c) { | ||||
| 				case 0: case 1: case 2: case 3: case 9: | ||||
| 					envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0; | ||||
| 					envelope_overflow_masks_[c] = 0x1f; | ||||
| 					/* Envelope: \____ */ | ||||
| 					envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0; | ||||
| 					envelope_overflow_masks_[c] = 0x3f; | ||||
| 				break; | ||||
| 				case 4: case 5: case 6: case 7: case 15: | ||||
| 					envelope_shapes_[c][p] = (p < 16) ? p : 0; | ||||
| 					envelope_overflow_masks_[c] = 0x1f; | ||||
| 					/* Envelope: /____ */ | ||||
| 					envelope_shapes_[c][p] = (p < 32) ? p : 0; | ||||
| 					envelope_overflow_masks_[c] = 0x3f; | ||||
| 				break; | ||||
|  | ||||
| 				case 8: | ||||
| 					envelope_shapes_[c][p] = (p & 0xf) ^ 0xf; | ||||
| 					/* Envelope: \\\\\\\\ */ | ||||
| 					envelope_shapes_[c][p] = (p & 0x1f) ^ 0x1f; | ||||
| 					envelope_overflow_masks_[c] = 0x00; | ||||
| 				break; | ||||
| 				case 12: | ||||
| 					envelope_shapes_[c][p] = (p & 0xf); | ||||
| 					/* Envelope: //////// */ | ||||
| 					envelope_shapes_[c][p] = (p & 0x1f); | ||||
| 					envelope_overflow_masks_[c] = 0x00; | ||||
| 				break; | ||||
|  | ||||
| 				case 10: | ||||
| 					envelope_shapes_[c][p] = (p & 0xf) ^ ((p < 16) ? 0xf : 0x0); | ||||
| 					/* Envelope: \/\/\/\/ */ | ||||
| 					envelope_shapes_[c][p] = (p & 0x1f) ^ ((p < 32) ? 0x1f : 0x0); | ||||
| 					envelope_overflow_masks_[c] = 0x00; | ||||
| 				break; | ||||
| 				case 14: | ||||
| 					envelope_shapes_[c][p] = (p & 0xf) ^ ((p < 16) ? 0x0 : 0xf); | ||||
| 					/* Envelope: /\/\/\/\ */ | ||||
| 					envelope_shapes_[c][p] = (p & 0x1f) ^ ((p < 32) ? 0x0 : 0x1f); | ||||
| 					envelope_overflow_masks_[c] = 0x00; | ||||
| 				break; | ||||
|  | ||||
| 				case 11: | ||||
| 					envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0xf; | ||||
| 					envelope_overflow_masks_[c] = 0x1f; | ||||
| 					/* Envelope: \------	(if - is high) */ | ||||
| 					envelope_shapes_[c][p] = (p < 32) ? (p^0x1f) : 0x1f; | ||||
| 					envelope_overflow_masks_[c] = 0x3f; | ||||
| 				break; | ||||
| 				case 13: | ||||
| 					envelope_shapes_[c][p] = (p < 16) ? p : 0xf; | ||||
| 					envelope_overflow_masks_[c] = 0x1f; | ||||
| 					/* Envelope: /------- */ | ||||
| 					envelope_shapes_[c][p] = (p < 32) ? p : 0x1f; | ||||
| 					envelope_overflow_masks_[c] = 0x3f; | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
| @@ -61,18 +72,27 @@ AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_ | ||||
|  | ||||
| void AY38910::set_sample_volume_range(std::int16_t range) { | ||||
| 	// set up volume lookup table | ||||
| 	const float max_volume = static_cast<float>(range) / 3.0f;	// As there are three channels. | ||||
| 	const float root_two = sqrtf(2.0f); | ||||
| 	for(int v = 0; v < 16; v++) { | ||||
| 		volumes_[v] = static_cast<int>(max_volume / powf(root_two, static_cast<float>(v ^ 0xf))); | ||||
| 	const float max_volume = float(range) / 3.0f;	// As there are three channels. | ||||
| 	constexpr float root_two = 1.414213562373095f; | ||||
| 	for(int v = 0; v < 32; v++) { | ||||
| 		volumes_[v] = int(max_volume / powf(root_two, float(v ^ 0x1f) / 2.0f)); | ||||
| 	} | ||||
| 	volumes_[0] = 0; | ||||
| 	volumes_[0] = 0;	// Tie level 0 to silence. | ||||
| 	evaluate_output_volume(); | ||||
| } | ||||
|  | ||||
| void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| 	// Note on structure below: the real AY has a built-in divider of 8 | ||||
| 	// prior to applying its tone and noise dividers. But the YM fills the | ||||
| 	// same total periods for noise and tone with double-precision envelopes. | ||||
| 	// Therefore this class implements a divider of 4 and doubles the tone | ||||
| 	// and noise periods. The envelope ticks along at the divide-by-four rate, | ||||
| 	// but if this is an AY rather than a YM then its lowest bit is forced to 1, | ||||
| 	// matching the YM datasheet's depiction of envelope level 31 as equal to | ||||
| 	// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc. | ||||
|  | ||||
| 	std::size_t c = 0; | ||||
| 	while((master_divider_&7) && c < number_of_samples) { | ||||
| 	while((master_divider_&3) && c < number_of_samples) { | ||||
| 		target[c] = output_volume_; | ||||
| 		master_divider_++; | ||||
| 		c++; | ||||
| @@ -83,49 +103,49 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| 	if(tone_counters_[c]) tone_counters_[c]--;\ | ||||
| 	else {\ | ||||
| 		tone_outputs_[c] ^= 1;\ | ||||
| 		tone_counters_[c] = tone_periods_[c];\ | ||||
| 		tone_counters_[c] = tone_periods_[c] << 1;\ | ||||
| 	} | ||||
|  | ||||
| 		// update the tone channels | ||||
| 		// Update the tone channels. | ||||
| 		step_channel(0); | ||||
| 		step_channel(1); | ||||
| 		step_channel(2); | ||||
|  | ||||
| #undef step_channel | ||||
|  | ||||
| 		// ... the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting | ||||
| 		// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting | ||||
| 		// it into the official 17 upon divider underflow. | ||||
| 		if(noise_counter_) noise_counter_--; | ||||
| 		else { | ||||
| 			noise_counter_ = noise_period_; | ||||
| 			noise_counter_ = noise_period_ << 1;	// To cover the double resolution of envelopes. | ||||
| 			noise_output_ ^= noise_shift_register_&1; | ||||
| 			noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17; | ||||
| 			noise_shift_register_ >>= 1; | ||||
| 		} | ||||
|  | ||||
| 		// ... and the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of | ||||
| 		// implementing non-repeating patterns by locking them to table position 0x1f. | ||||
| 		// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of | ||||
| 		// implementing non-repeating patterns by locking them to the final table position. | ||||
| 		if(envelope_divider_) envelope_divider_--; | ||||
| 		else { | ||||
| 			envelope_divider_ = envelope_period_; | ||||
| 			envelope_position_ ++; | ||||
| 			if(envelope_position_ == 32) envelope_position_ = envelope_overflow_masks_[output_registers_[13]]; | ||||
| 			if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]]; | ||||
| 		} | ||||
|  | ||||
| 		evaluate_output_volume(); | ||||
|  | ||||
| 		for(int ic = 0; ic < 8 && c < number_of_samples; ic++) { | ||||
| 		for(int ic = 0; ic < 4 && c < number_of_samples; ic++) { | ||||
| 			target[c] = output_volume_; | ||||
| 			c++; | ||||
| 			master_divider_++; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	master_divider_ &= 7; | ||||
| 	master_divider_ &= 3; | ||||
| } | ||||
|  | ||||
| void AY38910::evaluate_output_volume() { | ||||
| 	int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_]; | ||||
| 	int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_]; | ||||
|  | ||||
| 	// The output level for a channel is: | ||||
| 	//	1 if neither tone nor noise is enabled; | ||||
| @@ -142,9 +162,20 @@ void AY38910::evaluate_output_volume() { | ||||
| 	}; | ||||
| #undef level | ||||
|  | ||||
| 		// Channel volume is a simple selection: if the bit at 0x10 is set, use the envelope volume; otherwise use the lower four bits | ||||
| 	// This remapping table seeks to map 'channel volumes', i.e. the levels produced from the | ||||
| 	// 16-step progammatic volumes set per channel to 'envelope volumes', i.e. the 32-step | ||||
| 	// volumes that are produced by the envelope generators (on a YM at least). My reading of | ||||
| 	// the data sheet is that '0' is still off, but 15 should be as loud as peak envelope. So | ||||
| 	// I've thrown in the discontinuity at the low end, where it'll be very quiet. | ||||
| 	const int channel_volumes[] = { | ||||
| 		0, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31 | ||||
| 	}; | ||||
| 	static_assert(sizeof(channel_volumes) == 16*sizeof(int)); | ||||
|  | ||||
| 		// Channel volume is a simple selection: if the bit at 0x10 is set, use the envelope volume; otherwise use the lower four bits, | ||||
| 		// mapped to the range 1–31 in case this is a YM. | ||||
| #define channel_volume(c)	\ | ||||
| 	((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * (output_registers_[c]&0xf) | ||||
| 	((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * channel_volumes[output_registers_[c]&0xf] | ||||
|  | ||||
| 	const int volumes[3] = { | ||||
| 		channel_volume(8), | ||||
| @@ -173,11 +204,15 @@ void AY38910::select_register(uint8_t r) { | ||||
| } | ||||
|  | ||||
| void AY38910::set_register_value(uint8_t value) { | ||||
| 	// There are only 16 registers. | ||||
| 	if(selected_register_ > 15) return; | ||||
| 	registers_[selected_register_] = value; | ||||
|  | ||||
| 	// If this is a register that affects audio output, enqueue a mutation onto the | ||||
| 	// audio generation thread. | ||||
| 	if(selected_register_ < 14) { | ||||
| 		int selected_register = selected_register_; | ||||
| 		const int selected_register = selected_register_; | ||||
| 		task_queue_.defer([=] () { | ||||
| 			// Perform any register-specific mutation to output generation. | ||||
| 			uint8_t masked_value = value; | ||||
| 			switch(selected_register) { | ||||
| 				case 0: case 2: case 4: | ||||
| @@ -208,12 +243,34 @@ void AY38910::set_register_value(uint8_t value) { | ||||
| 					envelope_position_ = 0; | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			// Store a copy of the current register within the storage used by the audio generation | ||||
| 			// thread, and apply any changes to output volume. | ||||
| 			output_registers_[selected_register] = masked_value; | ||||
| 			evaluate_output_volume(); | ||||
| 		}); | ||||
| 	} else { | ||||
| 		if(port_handler_) port_handler_->set_port_output(selected_register_ == 15, value); | ||||
| 	} | ||||
|  | ||||
| 	// Decide which outputs are going to need updating (if any). | ||||
| 	bool update_port_a = false; | ||||
| 	bool update_port_b = true; | ||||
| 	if(port_handler_) { | ||||
| 		if(selected_register_ == 7) { | ||||
| 			const uint8_t io_change = registers_[7] ^ value; | ||||
| 			update_port_b = !!(io_change&0x80); | ||||
| 			update_port_a = !!(io_change&0x40); | ||||
| 		} else { | ||||
| 			update_port_b = selected_register_ == 15; | ||||
| 			update_port_a = selected_register_ != 15; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Keep a copy of the new value that is usable from the emulation thread. | ||||
| 	registers_[selected_register_] = value; | ||||
|  | ||||
| 	// Update ports as required. | ||||
| 	if(update_port_b) set_port_output(true); | ||||
| 	if(update_port_a) set_port_output(false); | ||||
| } | ||||
|  | ||||
| uint8_t AY38910::get_register_value() { | ||||
| @@ -238,6 +295,8 @@ uint8_t AY38910::get_port_output(bool port_b) { | ||||
|  | ||||
| void AY38910::set_port_handler(PortHandler *handler) { | ||||
| 	port_handler_ = handler; | ||||
| 	set_port_output(true); | ||||
| 	set_port_output(false); | ||||
| } | ||||
|  | ||||
| void AY38910::set_data_input(uint8_t r) { | ||||
| @@ -245,6 +304,16 @@ void AY38910::set_data_input(uint8_t r) { | ||||
| 	update_bus(); | ||||
| } | ||||
|  | ||||
| void AY38910::set_port_output(bool port_b) { | ||||
| 	// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor, | ||||
| 	// so that when in the "input" mode, all pins will read normally high". Therefore, | ||||
| 	// report programmer selection of input mode as creating an output of 0xff. | ||||
| 	if(port_handler_) { | ||||
| 		const bool is_output = !!(registers_[7] & (port_b ? 0x80 : 0x40)); | ||||
| 		port_handler_->set_port_output(port_b, is_output ? registers_[port_b ? 15 : 14] : 0xff); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t AY38910::get_data_output() { | ||||
| 	if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) { | ||||
| 		// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the | ||||
|   | ||||
| @@ -35,9 +35,10 @@ class PortHandler { | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Requests the current input on an AY port. | ||||
| 			Sets the current output on an AY port. | ||||
|  | ||||
| 			@param port_b @c true if the input being queried is Port B. @c false if it is Port A. | ||||
| 			@param port_b @c true if the output being posted is Port B. @c false if it is Port A. | ||||
| 			@param value the value now being output. | ||||
| 		*/ | ||||
| 		virtual void set_port_output(bool port_b, uint8_t value) {} | ||||
| }; | ||||
| @@ -51,6 +52,13 @@ enum ControlLines { | ||||
| 	BDIR	= (1 << 2) | ||||
| }; | ||||
|  | ||||
| enum class Personality { | ||||
| 	/// Provides 16 volume levels to envelopes. | ||||
| 	AY38910, | ||||
| 	/// Provides 32 volume levels to envelopes. | ||||
| 	YM2149F | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Provides emulation of an AY-3-8910 / YM2149, which is a three-channel sound chip with a | ||||
| 	noise generator and a volume envelope generator, which also provides two bidirectional | ||||
| @@ -59,7 +67,7 @@ enum ControlLines { | ||||
| class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
| 	public: | ||||
| 		/// Creates a new AY38910. | ||||
| 		AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue); | ||||
| 		AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &); | ||||
|  | ||||
| 		/// Sets the value the AY would read from its data lines if it were not outputting. | ||||
| 		void set_data_input(uint8_t r); | ||||
| @@ -83,7 +91,7 @@ class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
| 		*/ | ||||
| 		void set_port_handler(PortHandler *); | ||||
|  | ||||
| 		// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter; not for public consumption | ||||
| 		// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter. | ||||
| 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||
| 		bool is_zero_level(); | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| @@ -108,11 +116,11 @@ class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
|  | ||||
| 		int envelope_period_ = 0; | ||||
| 		int envelope_divider_ = 0; | ||||
| 		int envelope_position_ = 0; | ||||
| 		int envelope_shapes_[16][32]; | ||||
| 		int envelope_position_ = 0, envelope_position_mask_ = 0; | ||||
| 		int envelope_shapes_[16][64]; | ||||
| 		int envelope_overflow_masks_[16]; | ||||
|  | ||||
| 		int volumes_[16]; | ||||
| 		int volumes_[32]; | ||||
|  | ||||
| 		enum ControlState { | ||||
| 			Inactive, | ||||
| @@ -128,10 +136,11 @@ class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
| 		uint8_t data_input_, data_output_; | ||||
|  | ||||
| 		int16_t output_volume_; | ||||
| 		inline void evaluate_output_volume(); | ||||
| 		void evaluate_output_volume(); | ||||
|  | ||||
| 		inline void update_bus(); | ||||
| 		void update_bus(); | ||||
| 		PortHandler *port_handler_ = nullptr; | ||||
| 		void set_port_output(bool port_b); | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -22,7 +22,7 @@ namespace  { | ||||
| DiskII::DiskII(int clock_rate) : | ||||
| 	clock_rate_(clock_rate), | ||||
| 	inputs_(input_command), | ||||
| 	drives_{{static_cast<unsigned int>(clock_rate), 300, 1}, {static_cast<unsigned int>(clock_rate), 300, 1}} | ||||
| 	drives_{{clock_rate, 300, 1}, {clock_rate, 300, 1}} | ||||
| { | ||||
| 	drives_[0].set_clocking_hint_observer(this); | ||||
| 	drives_[1].set_clocking_hint_observer(this); | ||||
| @@ -78,7 +78,7 @@ void DiskII::select_drive(int drive) { | ||||
| void DiskII::run_for(const Cycles cycles) { | ||||
| 	if(preferred_clocking() == ClockingHint::Preference::None) return; | ||||
|  | ||||
| 	int integer_cycles = cycles.as_int(); | ||||
| 	auto integer_cycles = cycles.as_integral(); | ||||
| 	while(integer_cycles--) { | ||||
| 		const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6); | ||||
| 		if(flux_duration_) { | ||||
| @@ -124,7 +124,7 @@ void DiskII::run_for(const Cycles cycles) { | ||||
| 	// motor switch being flipped and the drive motor actually switching off. | ||||
| 	// This models that, accepting overrun as a risk. | ||||
| 	if(motor_off_time_ >= 0) { | ||||
| 		motor_off_time_ -= cycles.as_int(); | ||||
| 		motor_off_time_ -= cycles.as_integral(); | ||||
| 		if(motor_off_time_ < 0) { | ||||
| 			set_control(Control::Motor, false); | ||||
| 		} | ||||
| @@ -211,7 +211,7 @@ void DiskII::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int driv | ||||
| 	drives_[drive].set_disk(disk); | ||||
| } | ||||
|  | ||||
| void DiskII::process_event(const Storage::Disk::Track::Event &event) { | ||||
| void DiskII::process_event(const Storage::Disk::Drive::Event &event) { | ||||
| 	if(event.type == Storage::Disk::Track::Event::FluxTransition) { | ||||
| 		inputs_ &= ~input_flux; | ||||
| 		flux_duration_ = 2;	// Upon detection of a flux transition, the flux flag should stay set for 1us. Emulate that as two cycles. | ||||
| @@ -266,7 +266,7 @@ int DiskII::read_address(int address) { | ||||
| 		break; | ||||
| 		case 0xf: | ||||
| 			if(!(inputs_ & input_mode)) | ||||
| 				drives_[active_drive_].begin_writing(Storage::Time(1, clock_rate_), false); | ||||
| 				drives_[active_drive_].begin_writing(Storage::Time(1, int(clock_rate_)), false); | ||||
| 			inputs_ |= input_mode; | ||||
| 		break; | ||||
| 	} | ||||
|   | ||||
| @@ -26,7 +26,7 @@ namespace Apple { | ||||
| /*! | ||||
| 	Provides an emulation of the Apple Disk II. | ||||
| */ | ||||
| class DiskII: | ||||
| class DiskII final: | ||||
| 	public Storage::Disk::Drive::EventDelegate, | ||||
| 	public ClockingHint::Source, | ||||
| 	public ClockingHint::Observer { | ||||
| @@ -48,7 +48,7 @@ class DiskII: | ||||
| 			The value returned by @c read_address if accessing that address | ||||
| 			didn't cause the disk II to place anything onto the bus. | ||||
| 		*/ | ||||
| 		const int DidNotLoad = -1; | ||||
| 		static constexpr int DidNotLoad = -1; | ||||
|  | ||||
| 		/// Advances the controller by @c cycles. | ||||
| 		void run_for(const Cycles cycles); | ||||
| @@ -76,7 +76,7 @@ class DiskII: | ||||
| 		void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive); | ||||
|  | ||||
| 		// As per Sleeper. | ||||
| 		ClockingHint::Preference preferred_clocking() override; | ||||
| 		ClockingHint::Preference preferred_clocking() final; | ||||
|  | ||||
| 		// The Disk II functions as a potential target for @c Activity::Sources. | ||||
| 		void set_activity_observer(Activity::Observer *observer); | ||||
| @@ -98,10 +98,10 @@ class DiskII: | ||||
| 		void select_drive(int drive); | ||||
|  | ||||
| 		uint8_t trigger_address(int address, uint8_t value); | ||||
| 		void process_event(const Storage::Disk::Track::Event &event) override; | ||||
| 		void process_event(const Storage::Disk::Drive::Event &event) override; | ||||
| 		void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) override; | ||||
|  | ||||
| 		const int clock_rate_ = 0; | ||||
| 		const Cycles::IntType clock_rate_ = 0; | ||||
|  | ||||
| 		uint8_t state_ = 0; | ||||
| 		uint8_t inputs_ = 0; | ||||
| @@ -109,7 +109,7 @@ class DiskII: | ||||
|  | ||||
| 		int stepper_mask_ = 0; | ||||
| 		int stepper_position_ = 0; | ||||
| 		int motor_off_time_ = -1; | ||||
| 		Cycles::IntType motor_off_time_ = -1; | ||||
|  | ||||
| 		bool is_write_protected(); | ||||
| 		std::array<uint8_t, 256> state_machine_; | ||||
|   | ||||
							
								
								
									
										406
									
								
								Components/DiskII/IWM.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										406
									
								
								Components/DiskII/IWM.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,406 @@ | ||||
| // | ||||
| //  IWM.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 05/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "IWM.hpp" | ||||
|  | ||||
| #include "../../Outputs/Log.hpp" | ||||
|  | ||||
| using namespace Apple; | ||||
|  | ||||
| namespace  { | ||||
| 	constexpr int CA0		= 1 << 0; | ||||
| 	constexpr int CA1		= 1 << 1; | ||||
| 	constexpr int CA2		= 1 << 2; | ||||
| 	constexpr int LSTRB		= 1 << 3; | ||||
| 	constexpr int ENABLE	= 1 << 4; | ||||
| 	constexpr int DRIVESEL	= 1 << 5;	/* This means drive select, like on the original Disk II. */ | ||||
| 	constexpr int Q6		= 1 << 6; | ||||
| 	constexpr int Q7		= 1 << 7; | ||||
| 	constexpr int SEL		= 1 << 8;	/* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */ | ||||
| } | ||||
|  | ||||
| IWM::IWM(int clock_rate) : | ||||
| 	clock_rate_(clock_rate) {} | ||||
|  | ||||
| // MARK: - Bus accessors | ||||
|  | ||||
| uint8_t IWM::read(int address) { | ||||
| 	access(address); | ||||
|  | ||||
| 	// Per Inside Macintosh: | ||||
| 	// | ||||
| 	// "Before you can read from any of the disk registers you must set up the state of the IWM so that it | ||||
| 	// can pass the data through to the MC68000's address space where you'll be able to read it. To do that, | ||||
| 	// you must first turn off Q7 by reading or writing dBase+q7L. Then turn on Q6 by accessing dBase+q6H. | ||||
| 	// After that, the IWM will be able to pass data from the disk's RD/SENSE line through to you." | ||||
| 	// | ||||
| 	// My understanding: | ||||
| 	// | ||||
| 	//	Q6 = 1, Q7 = 0 reads the status register. The meaning of the top 'SENSE' bit is then determined by | ||||
| 	//	the CA0,1,2 and SEL switches as described in Inside Macintosh, summarised above as RD/SENSE. | ||||
|  | ||||
| 	if(address&1) { | ||||
| 		return 0xff; | ||||
| 	} | ||||
|  | ||||
| 	switch(state_ & (Q6 | Q7 | ENABLE)) { | ||||
| 		default: | ||||
| 			LOG("[IWM] Invalid read\n"); | ||||
| 		return 0xff; | ||||
|  | ||||
| 		// "Read all 1s". | ||||
| //			printf("Reading all 1s\n"); | ||||
| //		return 0xff; | ||||
|  | ||||
| 		case 0: | ||||
| 		case ENABLE: {				/* Read data register. Zeroing afterwards is a guess. */ | ||||
| 			const auto result = data_register_; | ||||
|  | ||||
| 			if(data_register_ & 0x80) { | ||||
| //				printf("\n\nIWM:%02x\n\n", data_register_); | ||||
| //				printf("."); | ||||
| 				data_register_ = 0; | ||||
| 			} | ||||
| //			LOG("Reading data register: " << PADHEX(2) << int(result)); | ||||
|  | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		case Q6: case Q6|ENABLE: { | ||||
| 			/* | ||||
| 				[If A = 0], Read status register: | ||||
|  | ||||
| 				bits 0-4: same as mode register. | ||||
| 				bit 5: 1 = either /ENBL1 or /ENBL2 is currently low. | ||||
| 				bit 6: 1 = MZ (reserved for future compatibility; should always be read as 0). | ||||
| 				bit 7: 1 = SENSE input high; 0 = SENSE input low. | ||||
|  | ||||
| 				(/ENBL1 is low when the first drive's motor is on; /ENBL2 is low when the second drive's motor is on. | ||||
| 				If the 1-second timer is enabled, motors remain on for one second after being programmatically disabled.) | ||||
| 			*/ | ||||
|  | ||||
| 			return uint8_t( | ||||
| 				(mode_&0x1f) | | ||||
| 				((state_ & ENABLE) ? 0x20 : 0x00) | | ||||
| 				(sense() & 0x80) | ||||
| 			); | ||||
| 		} break; | ||||
|  | ||||
| 		case Q7: case Q7|ENABLE: | ||||
| 			/* | ||||
| 				Read write-handshake register: | ||||
|  | ||||
| 				bits 0-5: reserved for future use (currently read as 1). | ||||
| 				bit 6: 1 = write state (0 = underrun has occurred; 1 = no underrun so far). | ||||
| 				bit 7: 1 = write data buffer ready for data (1 = ready; 0 = busy). | ||||
| 			*/ | ||||
| //			LOG("Reading write handshake: " << PADHEX(2) << (0x3f | write_handshake_)); | ||||
| 		return 0x3f | write_handshake_; | ||||
| 	} | ||||
|  | ||||
| 	return 0xff; | ||||
| } | ||||
|  | ||||
| void IWM::write(int address, uint8_t input) { | ||||
| 	access(address); | ||||
|  | ||||
| 	switch(state_ & (Q6 | Q7 | ENABLE)) { | ||||
| 		default: break; | ||||
|  | ||||
| 		case Q7|Q6: | ||||
| 			/* | ||||
| 				Write mode register: | ||||
|  | ||||
| 				bit 0: 1 = latch mode (should be set in asynchronous mode). | ||||
| 				bit 1: 0 = synchronous handshake protocol; 1 = asynchronous. | ||||
| 				bit 2: 0 = 1-second on-board timer enable; 1 = timer disable. | ||||
| 				bit 3: 0 = slow mode; 1 = fast mode. | ||||
| 				bit 4: 0 = 7Mhz; 1 = 8Mhz (7 or 8 mHz clock descriptor). | ||||
| 				bit 5: 1 = test mode; 0 = normal operation. | ||||
| 				bit 6: 1 = MZ-reset. | ||||
| 				bit 7: reserved for future expansion. | ||||
| 			*/ | ||||
|  | ||||
| 			mode_ = input; | ||||
|  | ||||
| 			switch(mode_ & 0x18) { | ||||
| 				case 0x00:		bit_length_ = Cycles(24);		break;	// slow mode, 7Mhz | ||||
| 				case 0x08:		bit_length_ = Cycles(12);		break;	// fast mode, 7Mhz | ||||
| 				case 0x10:		bit_length_ = Cycles(32);		break;	// slow mode, 8Mhz | ||||
| 				case 0x18:		bit_length_ = Cycles(16);		break;	// fast mode, 8Mhz | ||||
| 			} | ||||
| 			LOG("IWM mode is now " << PADHEX(2) << int(mode_)); | ||||
| 		break; | ||||
|  | ||||
| 		case Q7|Q6|ENABLE:	// Write data register. | ||||
| 			next_output_ = input; | ||||
| 			write_handshake_ &= ~0x80; | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - Switch access | ||||
|  | ||||
| void IWM::access(int address) { | ||||
| 	// Keep a record of switch state; bits in state_ | ||||
| 	// should correlate with the anonymous namespace constants | ||||
| 	// defined at the top of this file — CA0, CA1, etc. | ||||
| 	address &= 0xf; | ||||
| 	const auto mask = 1 << (address >> 1); | ||||
| 	const auto old_state = state_; | ||||
|  | ||||
| 	if(address & 1) { | ||||
| 		state_ |= mask; | ||||
| 	} else { | ||||
| 		state_ &= ~mask; | ||||
| 	} | ||||
|  | ||||
| 	// React appropriately to ENABLE and DRIVESEL changes, and changes into/out of write mode. | ||||
| 	if(old_state != state_) { | ||||
| 		push_drive_state(); | ||||
|  | ||||
| 		switch(mask) { | ||||
| 			default: break; | ||||
|  | ||||
| 			case ENABLE: | ||||
| 				if(address & 1) { | ||||
| 					if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(true); | ||||
| 				} else { | ||||
| 					// If the 1-second delay is enabled, set up a timer for that. | ||||
| 					if(!(mode_ & 4)) { | ||||
| 						cycles_until_disable_ = Cycles(clock_rate_); | ||||
| 					} else { | ||||
| 						if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(false); | ||||
| 					} | ||||
| 				} | ||||
| 			break; | ||||
|  | ||||
| 			case DRIVESEL: { | ||||
| 				const int new_drive = address & 1; | ||||
| 				if(new_drive != active_drive_) { | ||||
| 					if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(false); | ||||
| 					active_drive_ = new_drive; | ||||
| 					if(drives_[active_drive_]) { | ||||
| 						drives_[active_drive_]->set_enabled(state_ & ENABLE || (cycles_until_disable_ > Cycles(0))); | ||||
| 						push_drive_state(); | ||||
| 					} | ||||
| 				} | ||||
| 			} break; | ||||
|  | ||||
| 			case Q6: | ||||
| 			case Q7: | ||||
| 				select_shift_mode(); | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void IWM::set_select(bool enabled) { | ||||
| 	// Store SEL as an extra state bit. | ||||
| 	if(enabled) state_ |= SEL; | ||||
| 	else state_ &= ~SEL; | ||||
| 	push_drive_state(); | ||||
| } | ||||
|  | ||||
| void IWM::push_drive_state() { | ||||
| 	if(drives_[active_drive_])  { | ||||
| 		const uint8_t drive_control_lines = | ||||
| 			((state_ & CA0) ? IWMDrive::CA0 : 0) | | ||||
| 			((state_ & CA1) ? IWMDrive::CA1 : 0) | | ||||
| 			((state_ & CA2) ? IWMDrive::CA2 : 0) | | ||||
| 			((state_ & SEL) ? IWMDrive::SEL : 0) | | ||||
| 			((state_ & LSTRB) ? IWMDrive::LSTRB : 0); | ||||
| 		drives_[active_drive_]->set_control_lines(drive_control_lines); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - Active logic | ||||
|  | ||||
| void IWM::run_for(const Cycles cycles) { | ||||
| 	// Check for a timeout of the motor-off timer. | ||||
| 	if(cycles_until_disable_ > Cycles(0)) { | ||||
| 		cycles_until_disable_ -= cycles; | ||||
| 		if(cycles_until_disable_ <= Cycles(0)) { | ||||
| 			cycles_until_disable_ = Cycles(0); | ||||
| 			if(drives_[active_drive_]) | ||||
| 				drives_[active_drive_]->set_enabled(false); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Activity otherwise depends on mode and motor state. | ||||
| 	auto integer_cycles = cycles.as_integral(); | ||||
| 	switch(shift_mode_) { | ||||
| 		case ShiftMode::Reading: { | ||||
| 			// Per the IWM patent, column 7, around line 35 onwards: "The expected time | ||||
| 			// is widened by approximately one-half an interval before and after the | ||||
| 			// expected time since the data is not precisely spaced when read due to | ||||
| 			// variations in drive speed and other external factors". The error_margin | ||||
| 			// here implements the 'after' part of that contract. | ||||
| 			const auto error_margin = Cycles(bit_length_.as_integral() >> 1); | ||||
|  | ||||
| 			if(drive_is_rotating_[active_drive_]) { | ||||
| 				while(integer_cycles--) { | ||||
| 					drives_[active_drive_]->run_for(Cycles(1)); | ||||
| 					++cycles_since_shift_; | ||||
| 					if(cycles_since_shift_ == bit_length_ + error_margin) { | ||||
| 						propose_shift(0); | ||||
| 					} | ||||
| 				} | ||||
| 			} else { | ||||
| 				while(cycles_since_shift_ + integer_cycles >= bit_length_ + error_margin) { | ||||
| 					const auto run_length = bit_length_ + error_margin - cycles_since_shift_; | ||||
| 					integer_cycles -= run_length.as_integral(); | ||||
| 					cycles_since_shift_ += run_length; | ||||
| 					propose_shift(0); | ||||
| 				} | ||||
| 				cycles_since_shift_ += Cycles(integer_cycles); | ||||
| 			} | ||||
| 		} break; | ||||
|  | ||||
| 		case ShiftMode::Writing: | ||||
| 			if(drives_[active_drive_]->is_writing()) { | ||||
| 				while(cycles_since_shift_ + integer_cycles >= bit_length_) { | ||||
| 					const auto cycles_until_write = bit_length_ - cycles_since_shift_; | ||||
| 					drives_[active_drive_]->run_for(cycles_until_write); | ||||
|  | ||||
| 					// Output a flux transition if the top bit is set. | ||||
| 					drives_[active_drive_]->write_bit(shift_register_ & 0x80); | ||||
| 					shift_register_ <<= 1; | ||||
|  | ||||
| 					integer_cycles -= cycles_until_write.as_integral(); | ||||
| 					cycles_since_shift_ = Cycles(0); | ||||
|  | ||||
| 					--output_bits_remaining_; | ||||
| 					if(!output_bits_remaining_) { | ||||
| 						if(!(write_handshake_ & 0x80)) { | ||||
| 							write_handshake_ |= 0x80; | ||||
| 							shift_register_ = next_output_; | ||||
| 							output_bits_remaining_ = 8; | ||||
| //							LOG("Next byte: " << PADHEX(2) << int(shift_register_)); | ||||
| 						} else { | ||||
| 							write_handshake_ &= ~0x40; | ||||
| 							drives_[active_drive_]->end_writing(); | ||||
| //							printf("\n"); | ||||
| 							LOG("Overrun; done."); | ||||
| 							select_shift_mode(); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				cycles_since_shift_ = integer_cycles; | ||||
| 				if(integer_cycles) { | ||||
| 					drives_[active_drive_]->run_for(cycles_since_shift_); | ||||
| 				} | ||||
| 			} else { | ||||
| 				drives_[active_drive_]->run_for(cycles); | ||||
| 			} | ||||
| 		break; | ||||
|  | ||||
| 		case ShiftMode::CheckingWriteProtect: | ||||
| 			if(integer_cycles < 8) { | ||||
| 				shift_register_ = (shift_register_ >> integer_cycles) | (sense() & (0xff << (8 - integer_cycles))); | ||||
| 			} else { | ||||
| 				shift_register_ = sense(); | ||||
| 			} | ||||
|  | ||||
| 		/* Deliberate fallthrough. */ | ||||
| 		default: | ||||
| 			if(drive_is_rotating_[active_drive_]) drives_[active_drive_]->run_for(cycles); | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void IWM::select_shift_mode() { | ||||
| 	// Don't allow an ongoing write to be interrupted. | ||||
| 	if(shift_mode_ == ShiftMode::Writing && drives_[active_drive_] && drives_[active_drive_]->is_writing()) return; | ||||
|  | ||||
| 	const auto old_shift_mode = shift_mode_; | ||||
|  | ||||
| 	switch(state_ & (Q6|Q7)) { | ||||
| 		default:	shift_mode_ = ShiftMode::CheckingWriteProtect;		break; | ||||
| 		case 0:		shift_mode_ = ShiftMode::Reading;					break; | ||||
| 		case Q7: | ||||
| 			// "The IWM is put into the write state by a transition from the write protect sense state to the | ||||
| 			// write load state". | ||||
| 			if(shift_mode_ == ShiftMode::CheckingWriteProtect) shift_mode_ = ShiftMode::Writing; | ||||
| 		break; | ||||
| 	} | ||||
|  | ||||
| 	// If writing mode just began, set the drive into write mode and cue up the first output byte. | ||||
| 	if(drives_[active_drive_] && old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) { | ||||
| 		drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_integral()), false); | ||||
| 		shift_register_ = next_output_; | ||||
| 		write_handshake_ |= 0x80 | 0x40; | ||||
| 		output_bits_remaining_ = 8; | ||||
| 		LOG("Seeding output with " << PADHEX(2) << shift_register_); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t IWM::sense() { | ||||
| 	return drives_[active_drive_] ? (drives_[active_drive_]->read() ? 0xff : 0x00) : 0xff; | ||||
| } | ||||
|  | ||||
| void IWM::process_event(const Storage::Disk::Drive::Event &event) { | ||||
| 	if(shift_mode_ != ShiftMode::Reading) return; | ||||
|  | ||||
| 	switch(event.type) { | ||||
| 		case Storage::Disk::Track::Event::IndexHole: return; | ||||
| 		case Storage::Disk::Track::Event::FluxTransition: | ||||
| 			propose_shift(1); | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void IWM::propose_shift(uint8_t bit) { | ||||
| 	// TODO: synchronous mode. | ||||
|  | ||||
| //	LOG("Shifting input"); | ||||
|  | ||||
| 	// See above for text from the IWM patent, column 7, around line 35 onwards. | ||||
| 	// The error_margin here implements the 'before' part of that contract. | ||||
| 	// | ||||
| 	// Basic effective logic: if at least 1 is fozund in the bit_length_ cycles centred | ||||
| 	// on the current expected bit delivery time as implied by cycles_since_shift_, | ||||
| 	// shift in a 1 and start a new window wherever the first found 1 was. | ||||
| 	// | ||||
| 	// If no 1s are found, shift in a 0 and don't alter expectations as to window placement. | ||||
| 	const auto error_margin = Cycles(bit_length_.as_integral() >> 1); | ||||
| 	if(bit && cycles_since_shift_ < error_margin) return; | ||||
|  | ||||
| 	shift_register_ = uint8_t((shift_register_ << 1) | bit); | ||||
| 	if(shift_register_ & 0x80) { | ||||
| 		data_register_ = shift_register_; | ||||
| 		shift_register_ = 0; | ||||
| 	} | ||||
|  | ||||
| 	if(bit) | ||||
| 		cycles_since_shift_ = Cycles(0); | ||||
| 	else | ||||
| 		cycles_since_shift_ -= bit_length_; | ||||
| } | ||||
|  | ||||
| void IWM::set_drive(int slot, IWMDrive *drive) { | ||||
| 	drives_[slot] = drive; | ||||
| 	drive->set_event_delegate(this); | ||||
| 	drive->set_clocking_hint_observer(this); | ||||
| } | ||||
|  | ||||
| void IWM::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) { | ||||
| 	const bool is_rotating = clocking != ClockingHint::Preference::None; | ||||
|  | ||||
| 	if(component == static_cast<ClockingHint::Source *>(drives_[0])) { | ||||
| 		drive_is_rotating_[0] = is_rotating; | ||||
| 	} else { | ||||
| 		drive_is_rotating_[1] = is_rotating; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void IWM::set_activity_observer(Activity::Observer *observer) { | ||||
| 	if(drives_[0]) drives_[0]->set_activity_observer(observer, "Internal Floppy", true); | ||||
| 	if(drives_[1]) drives_[1]->set_activity_observer(observer, "External Floppy", true); | ||||
| } | ||||
							
								
								
									
										124
									
								
								Components/DiskII/IWM.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								Components/DiskII/IWM.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| // | ||||
| //  IWM.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 05/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef IWM_hpp | ||||
| #define IWM_hpp | ||||
|  | ||||
| #include "../../Activity/Observer.hpp" | ||||
|  | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../ClockReceiver/ClockingHintSource.hpp" | ||||
|  | ||||
| #include "../../Storage/Disk/Drive.hpp" | ||||
|  | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace Apple { | ||||
|  | ||||
| /*! | ||||
| 	Defines the drive interface used by the IWM, derived from the external pinout as | ||||
| 	per e.g. https://old.pinouts.ru/HD/MacExtDrive_pinout.shtml | ||||
|  | ||||
| 	These are subclassed of Storage::Disk::Drive, so accept any disk the emulator supports, | ||||
| 	and provide the usual read/write interface for on-disk data. | ||||
| */ | ||||
| struct IWMDrive: public Storage::Disk::Drive { | ||||
| 	IWMDrive(int input_clock_rate, int number_of_heads) : Storage::Disk::Drive(input_clock_rate, number_of_heads) {} | ||||
|  | ||||
| 	enum Line: int { | ||||
| 		CA0		= 1 << 0, | ||||
| 		CA1		= 1 << 1, | ||||
| 		CA2		= 1 << 2, | ||||
| 		LSTRB	= 1 << 3, | ||||
| 		SEL		= 1 << 4, | ||||
| 	}; | ||||
|  | ||||
| 	virtual void set_enabled(bool) = 0; | ||||
| 	virtual void set_control_lines(int) = 0; | ||||
| 	virtual bool read() = 0; | ||||
| }; | ||||
|  | ||||
| class IWM: | ||||
| 	public Storage::Disk::Drive::EventDelegate, | ||||
| 	public ClockingHint::Observer { | ||||
| 	public: | ||||
| 		IWM(int clock_rate); | ||||
|  | ||||
| 		/// Sets the current external value of the data bus. | ||||
| 		void write(int address, uint8_t value); | ||||
|  | ||||
| 		/*! | ||||
| 			Submits an access to address @c address. | ||||
|  | ||||
| 			@returns The 8-bit value loaded to the data bus by the IWM. | ||||
| 		*/ | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		/*! | ||||
| 			Sets the current input of the IWM's SEL line. | ||||
| 		*/ | ||||
| 		void set_select(bool enabled); | ||||
|  | ||||
| 		/// Advances the controller by @c cycles. | ||||
| 		void run_for(const Cycles cycles); | ||||
|  | ||||
| 		/// Connects a drive to the IWM. | ||||
| 		void set_drive(int slot, IWMDrive *drive); | ||||
|  | ||||
| 		/// Registers the currently-connected drives as @c Activity::Sources ; | ||||
| 		/// the first will be declared 'Internal', the second 'External'. | ||||
| 		void set_activity_observer(Activity::Observer *observer); | ||||
|  | ||||
| 	private: | ||||
| 		// Storage::Disk::Drive::EventDelegate. | ||||
| 		void process_event(const Storage::Disk::Drive::Event &event) override; | ||||
|  | ||||
| 		const int clock_rate_; | ||||
|  | ||||
| 		uint8_t data_register_ = 0; | ||||
| 		uint8_t mode_ = 0; | ||||
| 		bool read_write_ready_ = true; | ||||
| 		bool write_overran_ = false; | ||||
|  | ||||
| 		int state_ = 0; | ||||
|  | ||||
| 		int active_drive_ = 0; | ||||
| 		IWMDrive *drives_[2] = {nullptr, nullptr}; | ||||
| 		bool drive_is_rotating_[2] = {false, false}; | ||||
|  | ||||
| 		void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override; | ||||
|  | ||||
| 		Cycles cycles_until_disable_; | ||||
| 		uint8_t write_handshake_ = 0x80; | ||||
|  | ||||
| 		void access(int address); | ||||
|  | ||||
| 		uint8_t shift_register_ = 0; | ||||
| 		uint8_t next_output_ = 0; | ||||
| 		int output_bits_remaining_ = 0; | ||||
|  | ||||
| 		void propose_shift(uint8_t bit); | ||||
| 		Cycles cycles_since_shift_; | ||||
| 		Cycles bit_length_ = Cycles(16); | ||||
|  | ||||
| 		void push_drive_state(); | ||||
|  | ||||
| 		enum class ShiftMode { | ||||
| 			Reading, | ||||
| 			Writing, | ||||
| 			CheckingWriteProtect | ||||
| 		} shift_mode_; | ||||
|  | ||||
| 		uint8_t sense(); | ||||
| 		void select_shift_mode(); | ||||
| }; | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* IWM_hpp */ | ||||
							
								
								
									
										181
									
								
								Components/DiskII/MacintoshDoubleDensityDrive.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								Components/DiskII/MacintoshDoubleDensityDrive.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| // | ||||
| //  MacintoshDoubleDensityDrive.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/07/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MacintoshDoubleDensityDrive.hpp" | ||||
|  | ||||
| /* | ||||
| 	Sources used pervasively: | ||||
|  | ||||
| 	http://members.iinet.net.au/~kalandi/apple/AUG/1991/11%20NOV.DEC/DISK.STUFF.html | ||||
| 	Apple Guide to the Macintosh Family Hardware | ||||
| 	Inside Macintosh III | ||||
| */ | ||||
|  | ||||
| using namespace Apple::Macintosh; | ||||
|  | ||||
| DoubleDensityDrive::DoubleDensityDrive(int input_clock_rate, bool is_800k) : | ||||
| 	IWMDrive(input_clock_rate, is_800k ? 2 : 1),	// Only 800kb drives are double sided. | ||||
| 	is_800k_(is_800k) { | ||||
| 	// Start with a valid rotation speed. | ||||
| 	if(is_800k) { | ||||
| 		Drive::set_rotation_speed(393.3807f); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - Speed Selection | ||||
|  | ||||
| void DoubleDensityDrive::did_step(Storage::Disk::HeadPosition to_position) { | ||||
| //	printf("At track %d\n", to_position.as_int()); | ||||
| 	// The 800kb drive automatically selects rotation speed as a function of | ||||
| 	// head position; the 400kb drive doesn't do so. | ||||
| 	if(is_800k_) { | ||||
| 		/* | ||||
| 			Numbers below cribbed from the Kryoflux forums; specifically: | ||||
| 			https://forum.kryoflux.com/viewtopic.php?t=1090 | ||||
|  | ||||
| 			They can almost be worked out algorithmically, since the point is to | ||||
| 			produce an almost-constant value for speed*(number of sectors), and: | ||||
|  | ||||
| 			393.3807 * 12 = 4720.5684 | ||||
| 			429.1723 * 11 = 4720.895421 | ||||
| 			472.1435 * 10 = 4721.435 | ||||
| 			524.5672 * 9 = 4721.1048 | ||||
| 			590.1098 * 8 = 4720.8784 | ||||
|  | ||||
| 			So 4721 / (number of sectors per track in zone) would give essentially | ||||
| 			the same results. | ||||
| 		*/ | ||||
| 		const int zone = to_position.as_int() >> 4; | ||||
| 		switch(zone) { | ||||
| 			case 0:		Drive::set_rotation_speed(393.3807f);	break; | ||||
| 			case 1:		Drive::set_rotation_speed(429.1723f);	break; | ||||
| 			case 2:		Drive::set_rotation_speed(472.1435f);	break; | ||||
| 			case 3:		Drive::set_rotation_speed(524.5672f);	break; | ||||
| 			default:	Drive::set_rotation_speed(590.1098f);	break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void DoubleDensityDrive::set_rotation_speed(float revolutions_per_minute) { | ||||
| 	if(!is_800k_) { | ||||
| 		// Don't allow drive speeds to drop below 10 RPM, as a temporary sop | ||||
| 		// to sanity. | ||||
| 		Drive::set_rotation_speed(std::max(10.0f, revolutions_per_minute)); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - Control input/output. | ||||
|  | ||||
| void DoubleDensityDrive::set_enabled(bool enabled) { | ||||
| 	// Disabling a drive also stops its motor. | ||||
| 	if(!enabled) set_motor_on(false); | ||||
| } | ||||
|  | ||||
| void DoubleDensityDrive::set_control_lines(int lines) { | ||||
| 	const auto old_state = control_state_; | ||||
| 	control_state_ = lines; | ||||
|  | ||||
| 	// Catch low-to-high LSTRB transitions. | ||||
| 	if((old_state ^ control_state_) & control_state_ & Line::LSTRB) { | ||||
| 		switch(control_state_ & (Line::CA2 | Line::CA1 | Line::CA0 | Line::SEL)) { | ||||
| 			default: | ||||
| 			break; | ||||
|  | ||||
| 			case 0:						// Set step direction — CA2 set => step outward. | ||||
| 			case Line::CA2: | ||||
| 				step_direction_ = (control_state_ & Line::CA2) ? -1 : 1; | ||||
| 			break; | ||||
|  | ||||
| 			case Line::CA1:				// Set drive motor — CA2 set => motor off. | ||||
| 			case Line::CA1|Line::CA2: | ||||
| 				set_motor_on(!(control_state_ & Line::CA2)); | ||||
| 			break; | ||||
|  | ||||
| 			case Line::CA0:				// Initiate a step. | ||||
| 				step(Storage::Disk::HeadPosition(step_direction_)); | ||||
| 			break; | ||||
|  | ||||
| 			case Line::SEL|Line::CA2:	// Reset new disk flag. | ||||
| 				has_new_disk_ = false; | ||||
| 			break; | ||||
|  | ||||
| 			case Line::CA2 | Line::CA1 | Line::CA0:	// Eject the disk. | ||||
| 				set_disk(nullptr); | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool DoubleDensityDrive::read() { | ||||
| 	switch(control_state_ & (CA2 | CA1 | CA0 | SEL)) { | ||||
| 		default: | ||||
| 		return false; | ||||
|  | ||||
| 		case 0:					// Head step direction. | ||||
| 								// (0 = inward) | ||||
| 		return step_direction_ <= 0; | ||||
|  | ||||
| 		case SEL:				// Disk in place. | ||||
| 								// (0 = disk present) | ||||
| 		return !has_disk(); | ||||
|  | ||||
| 		case CA0:				// Disk head step completed. | ||||
| 								// (0 = still stepping) | ||||
| 		return true;	// TODO: stepping delay. But at the main Drive level. | ||||
|  | ||||
| 		case CA0|SEL:			// Disk locked. | ||||
| 								// (0 = write protected) | ||||
| 		return !get_is_read_only(); | ||||
|  | ||||
| 		case CA1:				// Disk motor running. | ||||
| 								// (0 = motor on) | ||||
| 		return !get_motor_on(); | ||||
|  | ||||
| 		case CA1|SEL:			// Head at track 0. | ||||
| 								// (0 = at track 0) | ||||
| 								// "This bit becomes valid beginning 12 msec after the step that places the head at track 0." | ||||
| 		return !get_is_track_zero(); | ||||
|  | ||||
| 		case CA1|CA0:			// Disk has been ejected. | ||||
| 								// (0 = user has ejected disk) | ||||
| 		return !has_new_disk_; | ||||
|  | ||||
| 		case CA1|CA0|SEL:		// Tachometer. | ||||
| 								// (arbitrary) | ||||
| 		return get_tachometer(); | ||||
|  | ||||
| 		case CA2:				// Read data, lower head. | ||||
| 			set_head(0); | ||||
| 		return false; | ||||
|  | ||||
| 		case CA2|SEL:			// Read data, upper head. | ||||
| 			set_head(1); | ||||
| 		return false; | ||||
|  | ||||
| 		case CA2|CA1:			// Single- or double-sided drive. | ||||
| 								// (0 = single sided) | ||||
| 		return get_head_count() != 1; | ||||
|  | ||||
| 		case CA2|CA1|CA0:		// "Present/HD" (per the Mac Plus ROM) | ||||
| 								// (0 = ??HD??) | ||||
| 								// | ||||
| 								// Alternative explanation: "Disk ready for reading?" | ||||
| 								// (0 = ready) | ||||
| 		return false; | ||||
|  | ||||
| 		case CA2|CA1|CA0|SEL:	// Drive installed. | ||||
| 								// (0 = present, 1 = missing) | ||||
| 								// | ||||
| 								// TODO: why do I need to return this the wrong way around for the Mac Plus? | ||||
| 		return true; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void DoubleDensityDrive::did_set_disk() { | ||||
| 	has_new_disk_ = true; | ||||
| } | ||||
							
								
								
									
										53
									
								
								Components/DiskII/MacintoshDoubleDensityDrive.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								Components/DiskII/MacintoshDoubleDensityDrive.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| // | ||||
| //  MacintoshDoubleDensityDrive.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/07/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MacintoshDoubleDensityDrive_hpp | ||||
| #define MacintoshDoubleDensityDrive_hpp | ||||
|  | ||||
| #include "IWM.hpp" | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| class DoubleDensityDrive: public IWMDrive { | ||||
| 	public: | ||||
| 		DoubleDensityDrive(int input_clock_rate, bool is_800k); | ||||
|  | ||||
| 		/*! | ||||
| 			@returns @c true if this is an 800kb drive; @c false otherwise. | ||||
| 		*/ | ||||
| 		bool is_800k() { | ||||
| 			return is_800k_; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Sets the current rotation speed of this drive only if it is a 400kb drive. | ||||
| 			800kb drives select their own rotation speed based on head position, | ||||
| 			and ignore this input. | ||||
| 		*/ | ||||
| 		void set_rotation_speed(float revolutions_per_minute); | ||||
|  | ||||
| 		void set_enabled(bool) override; | ||||
| 		void set_control_lines(int) override; | ||||
| 		bool read() override; | ||||
|  | ||||
| 	private: | ||||
| 		// To receive the proper notifications from Storage::Disk::Drive. | ||||
| 		void did_step(Storage::Disk::HeadPosition to_position) override; | ||||
| 		void did_set_disk() override; | ||||
|  | ||||
| 		const bool is_800k_; | ||||
| 		bool has_new_disk_ = false; | ||||
| 		int control_state_ = 0; | ||||
| 		int step_direction_ = 1; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* MacintoshDoubleDensityDrive_hpp */ | ||||
| @@ -48,7 +48,7 @@ void SN76489::set_sample_volume_range(std::int16_t range) { | ||||
| 	evaluate_output_volume(); | ||||
| } | ||||
|  | ||||
| void SN76489::set_register(uint8_t value) { | ||||
| void SN76489::write(uint8_t value) { | ||||
| 	task_queue_.defer([value, this] () { | ||||
| 		if(value & 0x80) { | ||||
| 			active_register_ = value; | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class SN76489: public Outputs::Speaker::SampleSource { | ||||
| 		SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1); | ||||
|  | ||||
| 		/// Writes a new value to the SN76489. | ||||
| 		void set_register(uint8_t value); | ||||
| 		void write(uint8_t value); | ||||
|  | ||||
| 		// As per SampleSource. | ||||
| 		void get_samples(std::size_t number_of_samples, std::int16_t *target); | ||||
|   | ||||
							
								
								
									
										140
									
								
								Components/Serial/Line.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								Components/Serial/Line.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| // | ||||
| //  SerialPort.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 12/10/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Line.hpp" | ||||
|  | ||||
| using namespace Serial; | ||||
|  | ||||
| void Line::set_writer_clock_rate(HalfCycles clock_rate) { | ||||
| 	clock_rate_ = clock_rate; | ||||
| } | ||||
|  | ||||
| void Line::advance_writer(HalfCycles cycles) { | ||||
| 	if(cycles == HalfCycles(0)) return; | ||||
|  | ||||
| 	const auto integral_cycles = cycles.as_integral(); | ||||
| 	remaining_delays_ = std::max(remaining_delays_ - integral_cycles, Cycles::IntType(0)); | ||||
| 	if(events_.empty()) { | ||||
| 		write_cycles_since_delegate_call_ += integral_cycles; | ||||
| 		if(transmission_extra_) { | ||||
| 			transmission_extra_ -= integral_cycles; | ||||
| 			if(transmission_extra_ <= 0) { | ||||
| 				transmission_extra_ = 0; | ||||
| 				update_delegate(level_); | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		while(!events_.empty()) { | ||||
| 			if(events_.front().delay <= integral_cycles) { | ||||
| 				cycles -= events_.front().delay; | ||||
| 				write_cycles_since_delegate_call_ += events_.front().delay; | ||||
| 				const auto old_level = level_; | ||||
|  | ||||
| 				auto iterator = events_.begin() + 1; | ||||
| 				while(iterator != events_.end() && iterator->type != Event::Delay) { | ||||
| 					level_ = iterator->type == Event::SetHigh; | ||||
| 					++iterator; | ||||
| 				} | ||||
| 				events_.erase(events_.begin(), iterator); | ||||
|  | ||||
| 				if(old_level != level_) { | ||||
| 					update_delegate(old_level); | ||||
| 				} | ||||
|  | ||||
| 				// Book enough extra time for the read delegate to be posted | ||||
| 				// the final bit if one is attached. | ||||
| 				if(events_.empty()) { | ||||
| 					transmission_extra_ = minimum_write_cycles_for_read_delegate_bit(); | ||||
| 				} | ||||
| 			} else { | ||||
| 				events_.front().delay -= integral_cycles; | ||||
| 				write_cycles_since_delegate_call_ += integral_cycles; | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void Line::write(bool level) { | ||||
| 	if(!events_.empty()) { | ||||
| 		events_.emplace_back(); | ||||
| 		events_.back().type = level ? Event::SetHigh : Event::SetLow; | ||||
| 	} else { | ||||
| 		level_ = level; | ||||
| 		transmission_extra_ = minimum_write_cycles_for_read_delegate_bit(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void Line::write(HalfCycles cycles, int count, int levels) { | ||||
| 	remaining_delays_ += count * cycles.as_integral(); | ||||
|  | ||||
| 	auto event = events_.size(); | ||||
| 	events_.resize(events_.size() + size_t(count)*2); | ||||
| 	while(count--) { | ||||
| 		events_[event].type = Event::Delay; | ||||
| 		events_[event].delay = int(cycles.as_integral()); | ||||
| 		events_[event+1].type = (levels&1) ? Event::SetHigh : Event::SetLow; | ||||
| 		levels >>= 1; | ||||
| 		event += 2; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void Line::reset_writing() { | ||||
| 	remaining_delays_ = 0; | ||||
| 	events_.clear(); | ||||
| } | ||||
|  | ||||
| bool Line::read() { | ||||
| 	return level_; | ||||
| } | ||||
|  | ||||
| void Line::set_read_delegate(ReadDelegate *delegate, Storage::Time bit_length) { | ||||
| 	read_delegate_ = delegate; | ||||
| 	read_delegate_bit_length_ = bit_length; | ||||
| 	read_delegate_bit_length_.simplify(); | ||||
| 	write_cycles_since_delegate_call_ = 0; | ||||
| } | ||||
|  | ||||
| void Line::update_delegate(bool level) { | ||||
| 	// Exit early if there's no delegate, or if the delegate is waiting for | ||||
| 	// zero and this isn't zero. | ||||
| 	if(!read_delegate_) return; | ||||
|  | ||||
| 	const int cycles_to_forward = write_cycles_since_delegate_call_; | ||||
| 	write_cycles_since_delegate_call_ = 0; | ||||
| 	if(level && read_delegate_phase_ == ReadDelegatePhase::WaitingForZero) return; | ||||
|  | ||||
| 	// Deal with a transition out of waiting-for-zero mode by seeding time left | ||||
| 	// in bit at half a bit. | ||||
| 	if(read_delegate_phase_ == ReadDelegatePhase::WaitingForZero) { | ||||
| 		time_left_in_bit_ = read_delegate_bit_length_; | ||||
| 		time_left_in_bit_.clock_rate <<= 1; | ||||
| 		read_delegate_phase_ = ReadDelegatePhase::Serialising; | ||||
| 	} | ||||
|  | ||||
| 	// Forward as many bits as occur. | ||||
| 	Storage::Time time_left(cycles_to_forward, int(clock_rate_.as_integral())); | ||||
| 	const int bit = level ? 1 : 0; | ||||
| 	int bits = 0; | ||||
| 	while(time_left >= time_left_in_bit_) { | ||||
| 		++bits; | ||||
| 		if(!read_delegate_->serial_line_did_produce_bit(this, bit)) { | ||||
| 			read_delegate_phase_ = ReadDelegatePhase::WaitingForZero; | ||||
| 			if(bit) return; | ||||
| 		} | ||||
|  | ||||
| 		time_left -= time_left_in_bit_; | ||||
| 		time_left_in_bit_ = read_delegate_bit_length_; | ||||
| 	} | ||||
| 	time_left_in_bit_ -= time_left; | ||||
| } | ||||
|  | ||||
| Cycles::IntType Line::minimum_write_cycles_for_read_delegate_bit() { | ||||
| 	if(!read_delegate_) return 0; | ||||
| 	return 1 + (read_delegate_bit_length_ * static_cast<unsigned int>(clock_rate_.as_integral())).get<int>(); | ||||
| } | ||||
							
								
								
									
										112
									
								
								Components/Serial/Line.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Components/Serial/Line.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| // | ||||
| //  SerialPort.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 12/10/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef SerialPort_hpp | ||||
| #define SerialPort_hpp | ||||
|  | ||||
| #include <vector> | ||||
| #include "../../Storage/Storage.hpp" | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../ClockReceiver/ForceInline.hpp" | ||||
|  | ||||
| namespace Serial { | ||||
|  | ||||
| /*! | ||||
| 	@c Line connects a single reader and a single writer, allowing timestamped events to be | ||||
| 	published and consumed, potentially with a clock conversion in between. It allows line | ||||
| 	levels to be written and read in larger collections. | ||||
|  | ||||
| 	It is assumed that the owner of the reader and writer will ensure that the reader will never | ||||
| 	get ahead of the writer. If the writer posts events behind the reader they will simply be | ||||
| 	given instanteous effect. | ||||
| */ | ||||
| class Line { | ||||
| 	public: | ||||
| 		void set_writer_clock_rate(HalfCycles clock_rate); | ||||
|  | ||||
| 		/// Advances the read position by @c cycles relative to the writer's | ||||
| 		/// clock rate. | ||||
| 		void advance_writer(HalfCycles cycles); | ||||
|  | ||||
| 		/// Sets the line to @c level. | ||||
| 		void write(bool level); | ||||
|  | ||||
| 		/// Enqueues @c count level changes, the first occurring immediately | ||||
| 		/// after the final event currently posted and each subsequent event | ||||
| 		/// occurring @c cycles after the previous. An additional gap of @c cycles | ||||
| 		/// is scheduled after the final output. The levels to output are | ||||
| 		/// taken from @c levels which is read from lsb to msb. @c cycles is | ||||
| 		/// relative to the writer's clock rate. | ||||
| 		void write(HalfCycles cycles, int count, int levels); | ||||
|  | ||||
| 		/// @returns the number of cycles until currently enqueued write data is exhausted. | ||||
| 		forceinline HalfCycles write_data_time_remaining() const { | ||||
| 			return HalfCycles(remaining_delays_); | ||||
| 		} | ||||
|  | ||||
| 		/// @returns the number of cycles left until it is guaranteed that a passive reader | ||||
| 		/// has received all currently-enqueued bits. | ||||
| 		forceinline HalfCycles transmission_data_time_remaining() const { | ||||
| 			return HalfCycles(remaining_delays_ + transmission_extra_); | ||||
| 		} | ||||
|  | ||||
| 		/// Eliminates all future write states, leaving the output at whatever it is now. | ||||
| 		void reset_writing(); | ||||
|  | ||||
| 		/// @returns The instantaneous level of this line. | ||||
| 		bool read(); | ||||
|  | ||||
| 		struct ReadDelegate { | ||||
| 			virtual bool serial_line_did_produce_bit(Line *line, int bit) = 0; | ||||
| 		}; | ||||
| 		/*! | ||||
| 			Sets a read delegate, which will receive samples of the output level every | ||||
| 			@c bit_lengths of a second apart subject to a state machine: | ||||
|  | ||||
| 				* initially no bits will be delivered; | ||||
| 				* when a zero level is first detected, the line will wait half a bit's length, then start | ||||
| 				sampling at single-bit intervals, passing each bit to the delegate while it returns @c true; | ||||
| 				* as soon as the delegate returns @c false, the line will return to the initial state. | ||||
| 		*/ | ||||
| 		void set_read_delegate(ReadDelegate *delegate, Storage::Time bit_length); | ||||
|  | ||||
| 	private: | ||||
| 		struct Event { | ||||
| 			enum Type { | ||||
| 				Delay, SetHigh, SetLow | ||||
| 			} type; | ||||
| 			int delay; | ||||
| 		}; | ||||
| 		std::vector<Event> events_; | ||||
| 		HalfCycles::IntType remaining_delays_ = 0; | ||||
| 		HalfCycles::IntType transmission_extra_ = 0; | ||||
| 		bool level_ = true; | ||||
| 		HalfCycles clock_rate_ = 0; | ||||
|  | ||||
| 		ReadDelegate *read_delegate_ = nullptr; | ||||
| 		Storage::Time read_delegate_bit_length_, time_left_in_bit_; | ||||
| 		int write_cycles_since_delegate_call_ = 0; | ||||
| 		enum class ReadDelegatePhase { | ||||
| 			WaitingForZero, | ||||
| 			Serialising | ||||
| 		} read_delegate_phase_ = ReadDelegatePhase::WaitingForZero; | ||||
|  | ||||
| 		void update_delegate(bool level); | ||||
| 		HalfCycles::IntType minimum_write_cycles_for_read_delegate_bit(); | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Defines an RS-232-esque srial port. | ||||
| */ | ||||
| class Port { | ||||
| 	public: | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* SerialPort_hpp */ | ||||
| @@ -18,7 +18,7 @@ AsyncTaskQueue::AsyncTaskQueue() | ||||
| #ifdef __APPLE__ | ||||
| 	serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL); | ||||
| #else | ||||
| 	thread_.reset(new std::thread([this]() { | ||||
| 	thread_ = std::make_unique<std::thread>([this]() { | ||||
| 		while(!should_destruct_) { | ||||
| 			std::function<void(void)> next_function; | ||||
|  | ||||
| @@ -39,7 +39,7 @@ AsyncTaskQueue::AsyncTaskQueue() | ||||
| 				processing_condition_.wait(lock); | ||||
| 			} | ||||
| 		} | ||||
| 	})); | ||||
| 	}); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| @@ -70,8 +70,8 @@ void AsyncTaskQueue::flush() { | ||||
| #ifdef __APPLE__ | ||||
| 	dispatch_sync(serial_dispatch_queue_, ^{}); | ||||
| #else | ||||
| 	std::shared_ptr<std::mutex> flush_mutex(new std::mutex); | ||||
| 	std::shared_ptr<std::condition_variable> flush_condition(new std::condition_variable); | ||||
| 	auto flush_mutex = std::make_shared<std::mutex>(); | ||||
| 	auto flush_condition = std::make_shared<std::condition_variable>(); | ||||
| 	std::unique_lock<std::mutex> lock(*flush_mutex); | ||||
| 	enqueue([=] () { | ||||
| 		std::unique_lock<std::mutex> inner_lock(*flush_mutex); | ||||
| @@ -88,7 +88,7 @@ DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() { | ||||
|  | ||||
| void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) { | ||||
| 	if(!deferred_tasks_) { | ||||
| 		deferred_tasks_.reset(new std::list<std::function<void(void)>>); | ||||
| 		deferred_tasks_ = std::make_shared<std::list<std::function<void(void)>>>(); | ||||
| 	} | ||||
| 	deferred_tasks_->push_back(function); | ||||
| } | ||||
|   | ||||
| @@ -14,16 +14,16 @@ namespace { | ||||
| 	Appends a Boolean selection of @c selection for option @c name to @c selection_set. | ||||
| */ | ||||
| void append_bool(Configurable::SelectionSet &selection_set, const std::string &name, bool selection) { | ||||
| 	selection_set[name] = std::unique_ptr<Configurable::Selection>(new Configurable::BooleanSelection(selection)); | ||||
| 	selection_set[name] = std::make_unique<Configurable::BooleanSelection>(selection); | ||||
| } | ||||
|  | ||||
| /*! | ||||
| 	Enquires for a Boolean selection for option @c name from @c selections_by_option, storing it to @c result if found. | ||||
| */ | ||||
| bool get_bool(const Configurable::SelectionSet &selections_by_option, const std::string &name, bool &result) { | ||||
| 	auto quickload = Configurable::selection<Configurable::BooleanSelection>(selections_by_option, "quickload"); | ||||
| 	if(!quickload) return false; | ||||
| 	result = quickload->value; | ||||
| 	auto selection = Configurable::selection<Configurable::BooleanSelection>(selections_by_option, name); | ||||
| 	if(!selection) return false; | ||||
| 	result = selection->value; | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| @@ -33,14 +33,16 @@ bool get_bool(const Configurable::SelectionSet &selections_by_option, const std: | ||||
| std::vector<std::unique_ptr<Configurable::Option>> Configurable::standard_options(Configurable::StandardOptions mask) { | ||||
| 	std::vector<std::unique_ptr<Configurable::Option>> options; | ||||
| 	if(mask & QuickLoadTape)				options.emplace_back(new Configurable::BooleanOption("Load Tapes Quickly", "quickload")); | ||||
| 	if(mask & (DisplayRGB | DisplayComposite | DisplaySVideo)) { | ||||
| 	if(mask & (DisplayRGB | DisplayCompositeColour | DisplayCompositeMonochrome | DisplaySVideo)) { | ||||
| 		std::vector<std::string> display_options; | ||||
| 		if(mask & DisplayComposite)	display_options.emplace_back("composite"); | ||||
| 		if(mask & DisplayCompositeColour)		display_options.emplace_back("composite"); | ||||
| 		if(mask & DisplayCompositeMonochrome)	display_options.emplace_back("composite-mono"); | ||||
| 		if(mask & DisplaySVideo)				display_options.emplace_back("svideo"); | ||||
| 		if(mask & DisplayRGB)					display_options.emplace_back("rgb"); | ||||
| 		options.emplace_back(new Configurable::ListOption("Display", "display", display_options)); | ||||
| 	} | ||||
| 	if(mask & AutomaticTapeMotorControl)	options.emplace_back(new Configurable::BooleanOption("Automatic Tape Motor Control", "autotapemotor")); | ||||
| 	if(mask & QuickBoot)					options.emplace_back(new Configurable::BooleanOption("Boot Quickly", "quickboot")); | ||||
| 	return options; | ||||
| } | ||||
|  | ||||
| @@ -59,9 +61,14 @@ void Configurable::append_display_selection(Configurable::SelectionSet &selectio | ||||
| 		default: | ||||
| 		case Display::RGB:					string_selection = "rgb";				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 | ||||
| @@ -85,9 +92,17 @@ bool Configurable::get_display(const Configurable::SelectionSet &selections_by_o | ||||
| 			return true; | ||||
| 		} | ||||
| 		if(display->value == "composite") { | ||||
| 			result = Configurable::Display::Composite; | ||||
| 			result = Configurable::Display::CompositeColour; | ||||
| 			return true; | ||||
| 		} | ||||
| 		if(display->value == "composite-mono") { | ||||
| 			result = Configurable::Display::CompositeMonochrome; | ||||
| 			return true; | ||||
| 		} | ||||
| 	} | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| bool Configurable::get_quick_boot(const Configurable::SelectionSet &selections_by_option, bool &result) { | ||||
| 	return get_bool(selections_by_option, "quickboot", result); | ||||
| } | ||||
|   | ||||
| @@ -16,15 +16,18 @@ namespace Configurable { | ||||
| enum StandardOptions { | ||||
| 	DisplayRGB					= (1 << 0), | ||||
| 	DisplaySVideo				= (1 << 1), | ||||
| 	DisplayComposite			= (1 << 2), | ||||
| 	QuickLoadTape				= (1 << 3), | ||||
| 	AutomaticTapeMotorControl	= (1 << 4) | ||||
| 	DisplayCompositeColour		= (1 << 2), | ||||
| 	DisplayCompositeMonochrome	= (1 << 3), | ||||
| 	QuickLoadTape				= (1 << 4), | ||||
| 	AutomaticTapeMotorControl	= (1 << 5), | ||||
| 	QuickBoot					= (1 << 6), | ||||
| }; | ||||
|  | ||||
| enum class Display { | ||||
| 	RGB, | ||||
| 	SVideo, | ||||
| 	Composite | ||||
| 	CompositeColour, | ||||
| 	CompositeMonochrome | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| @@ -47,6 +50,11 @@ void append_automatic_tape_motor_control_selection(SelectionSet &selection_set, | ||||
| */ | ||||
| void append_display_selection(SelectionSet &selection_set, Display selection); | ||||
|  | ||||
| /*! | ||||
| 	Appends to @c selection_set a selection of @c selection for QuickBoot. | ||||
| */ | ||||
| void append_quick_boot_selection(SelectionSet &selection_set, bool selection); | ||||
|  | ||||
| /*! | ||||
| 	Attempts to discern a QuickLoadTape selection from @c selections_by_option. | ||||
|   | ||||
| @@ -74,6 +82,15 @@ bool get_automatic_tape_motor_control_selection(const SelectionSet &selections_b | ||||
| */ | ||||
| bool get_display(const SelectionSet &selections_by_option, Display &result); | ||||
|  | ||||
| /*! | ||||
| 	Attempts to QuickBoot a QuickLoadTape selection from @c selections_by_option. | ||||
|  | ||||
| 	@param selections_by_option The user selections. | ||||
| 	@param result The location to which the selection will be stored if found. | ||||
| 	@returns @c true if a selection is found; @c false otherwise. | ||||
| */ | ||||
| bool get_quick_boot(const SelectionSet &selections_by_option, bool &result); | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* StandardOptions_hpp */ | ||||
|   | ||||
| @@ -10,13 +10,14 @@ | ||||
|  | ||||
| using namespace Inputs; | ||||
|  | ||||
| Keyboard::Keyboard() { | ||||
| Keyboard::Keyboard(const std::set<Key> &essential_modifiers) : essential_modifiers_(essential_modifiers) { | ||||
| 	for(int k = 0; k < int(Key::Help); ++k) { | ||||
| 		observed_keys_.insert(Key(k)); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Keyboard::Keyboard(const std::set<Key> &observed_keys) : observed_keys_(observed_keys), is_exclusive_(false) {} | ||||
| Keyboard::Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers) : | ||||
| 	observed_keys_(observed_keys), essential_modifiers_(essential_modifiers), is_exclusive_(false) {} | ||||
|  | ||||
| void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) { | ||||
| 	std::size_t key_offset = static_cast<std::size_t>(key); | ||||
| @@ -28,6 +29,10 @@ void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) { | ||||
| 	if(delegate_) delegate_->keyboard_did_change_key(this, key, is_pressed); | ||||
| } | ||||
|  | ||||
| const std::set<Inputs::Keyboard::Key> &Keyboard::get_essential_modifiers() { | ||||
| 	return essential_modifiers_; | ||||
| } | ||||
|  | ||||
| void Keyboard::reset_all_keys() { | ||||
| 	std::fill(key_states_.begin(), key_states_.end(), false); | ||||
| 	if(delegate_) delegate_->reset_all_keys(this); | ||||
|   | ||||
| @@ -6,8 +6,8 @@ | ||||
| //  Copyright 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Keyboard_hpp | ||||
| #define Keyboard_hpp | ||||
| #ifndef Inputs_Keyboard_hpp | ||||
| #define Inputs_Keyboard_hpp | ||||
|  | ||||
| #include <vector> | ||||
| #include <set> | ||||
| @@ -23,26 +23,26 @@ class Keyboard { | ||||
| 	public: | ||||
| 		enum class Key { | ||||
| 			Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, PrintScreen, ScrollLock, Pause, | ||||
| 			BackTick, k1, k2, k3, k4, k5, k6, k7, k8, k9, k0, Hyphen, Equals, BackSpace, | ||||
| 			Tab, Q, W, E, R, T, Y, U, I, O, P, OpenSquareBracket, CloseSquareBracket, BackSlash, | ||||
| 			BackTick, k1, k2, k3, k4, k5, k6, k7, k8, k9, k0, Hyphen, Equals, Backspace, | ||||
| 			Tab, Q, W, E, R, T, Y, U, I, O, P, OpenSquareBracket, CloseSquareBracket, Backslash, | ||||
| 			CapsLock, A, S, D, F, G, H, J, K, L, Semicolon, Quote, Hash, Enter, | ||||
| 			LeftShift, Z, X, C, V, B, N, M, Comma, FullStop, ForwardSlash, RightShift, | ||||
| 			LeftControl, LeftOption, LeftMeta, Space, RightMeta, RightOption, RightControl, | ||||
| 			Left, Right, Up, Down, | ||||
| 			Insert, Home, PageUp, Delete, End, PageDown, | ||||
| 			NumLock, KeyPadSlash, KeyPadAsterisk, KeyPadDelete, | ||||
| 			KeyPad7, KeyPad8, KeyPad9, KeyPadPlus, | ||||
| 			KeyPad4, KeyPad5, KeyPad6, KeyPadMinus, | ||||
| 			KeyPad1, KeyPad2, KeyPad3, KeyPadEnter, | ||||
| 			KeyPad0, KeyPadDecimalPoint, KeyPadEquals, | ||||
| 			NumLock, KeypadSlash, KeypadAsterisk, KeypadDelete, | ||||
| 			Keypad7, Keypad8, Keypad9, KeypadPlus, | ||||
| 			Keypad4, Keypad5, Keypad6, KeypadMinus, | ||||
| 			Keypad1, Keypad2, Keypad3, KeypadEnter, | ||||
| 			Keypad0, KeypadDecimalPoint, KeypadEquals, | ||||
| 			Help | ||||
| 		}; | ||||
|  | ||||
| 		/// Constructs a Keyboard that declares itself to observe all keys. | ||||
| 		Keyboard(); | ||||
| 		Keyboard(const std::set<Key> &essential_modifiers = {}); | ||||
|  | ||||
| 		/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys. | ||||
| 		Keyboard(const std::set<Key> &observed_keys); | ||||
| 		Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers); | ||||
|  | ||||
| 		// Host interface. | ||||
| 		virtual void set_key_pressed(Key key, char value, bool is_pressed); | ||||
| @@ -51,10 +51,18 @@ class Keyboard { | ||||
| 		/// @returns a set of all Keys that this keyboard responds to. | ||||
| 		virtual const std::set<Key> &observed_keys(); | ||||
|  | ||||
| 		/* | ||||
| 		/// @returns the list of modifiers that this keyboard considers 'essential' (i.e. both mapped and highly used). | ||||
| 		virtual const std::set<Inputs::Keyboard::Key> &get_essential_modifiers(); | ||||
|  | ||||
| 		/*! | ||||
| 			@returns @c true if this keyboard, on its original machine, looked | ||||
| 			like a complete keyboard — i.e. if a user would expect this keyboard | ||||
| 			to be the only thing a real keyboard maps to. | ||||
|  | ||||
| 			So this would be true of something like the Amstrad CPC, which has a full | ||||
| 			keyboard, but it would be false of something like the Sega Master System | ||||
| 			which has some buttons that you'd expect an emulator to map to its host | ||||
| 			keyboard but which does not offer a full keyboard. | ||||
| 		*/ | ||||
| 		virtual bool is_exclusive(); | ||||
|  | ||||
| @@ -68,6 +76,7 @@ class Keyboard { | ||||
|  | ||||
| 	private: | ||||
| 		std::set<Key> observed_keys_; | ||||
| 		std::set<Key> essential_modifiers_; | ||||
| 		std::vector<bool> key_states_; | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| 		bool is_exclusive_ = true; | ||||
| @@ -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 "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../Outputs/CRT/CRT.hpp" | ||||
|  | ||||
| #include "../../Analyser/Static/AmstradCPC/Target.hpp" | ||||
|  | ||||
| @@ -40,7 +41,7 @@ namespace AmstradCPC { | ||||
|  | ||||
| std::vector<std::unique_ptr<Configurable::Option>> get_options() { | ||||
| 	return Configurable::standard_options( | ||||
| 		static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayComposite) | ||||
| 		Configurable::StandardOptions(Configurable::DisplayRGB | Configurable::DisplayCompositeColour) | ||||
| 	); | ||||
| } | ||||
|  | ||||
| @@ -123,7 +124,7 @@ class InterruptTimer { | ||||
| class AYDeferrer { | ||||
| 	public: | ||||
| 		/// Constructs a new AY instance and sets its clock rate. | ||||
| 		AYDeferrer() : ay_(audio_queue_), speaker_(ay_) { | ||||
| 		AYDeferrer() : ay_(GI::AY38910::Personality::AY38910, audio_queue_), speaker_(ay_) { | ||||
| 			speaker_.set_input_rate(1000000); | ||||
| 		} | ||||
|  | ||||
| @@ -170,11 +171,15 @@ class AYDeferrer { | ||||
| */ | ||||
| class CRTCBusHandler { | ||||
| 	public: | ||||
| 		CRTCBusHandler(uint8_t *ram, InterruptTimer &interrupt_timer) : | ||||
| 		CRTCBusHandler(const uint8_t *ram, InterruptTimer &interrupt_timer) : | ||||
| 			crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2), | ||||
| 			ram_(ram), | ||||
| 			interrupt_timer_(interrupt_timer) { | ||||
| 				establish_palette_hits(); | ||||
| 				build_mode_table(); | ||||
| 				crt_.set_visible_area(Outputs::Display::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f)); | ||||
| 				crt_.set_brightness(3.0f / 2.0f);	// As only the values 0, 1 and 2 will be used in each channel, | ||||
| 													// whereas Red2Green2Blue2 defines a range of 0-3. | ||||
| 			} | ||||
|  | ||||
| 		/*! | ||||
| @@ -217,12 +222,12 @@ class CRTCBusHandler { | ||||
| 				if(cycles_) { | ||||
| 					switch(previous_output_mode_) { | ||||
| 						default: | ||||
| 						case OutputMode::Blank:			crt_->output_blank(cycles_ * 16);					break; | ||||
| 						case OutputMode::Sync:			crt_->output_sync(cycles_ * 16);					break; | ||||
| 						case OutputMode::Blank:			crt_.output_blank(cycles_ * 16);				break; | ||||
| 						case OutputMode::Sync:			crt_.output_sync(cycles_ * 16);					break; | ||||
| 						case OutputMode::Border:		output_border(cycles_);							break; | ||||
| 						case OutputMode::ColourBurst:	crt_->output_default_colour_burst(cycles_ * 16);	break; | ||||
| 						case OutputMode::ColourBurst:	crt_.output_default_colour_burst(cycles_ * 16);	break; | ||||
| 						case OutputMode::Pixels: | ||||
| 							crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_); | ||||
| 							crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_)); | ||||
| 							pixel_pointer_ = pixel_data_ = nullptr; | ||||
| 						break; | ||||
| 					} | ||||
| @@ -238,52 +243,54 @@ class CRTCBusHandler { | ||||
| 			// collect some more pixels if output is ongoing | ||||
| 			if(previous_output_mode_ == OutputMode::Pixels) { | ||||
| 				if(!pixel_data_) { | ||||
| 					pixel_pointer_ = pixel_data_ = crt_->allocate_write_area(320, 8); | ||||
| 					pixel_pointer_ = pixel_data_ = crt_.begin_data(320, 8); | ||||
| 				} | ||||
| 				if(pixel_pointer_) { | ||||
| 					// the CPC shuffles output lines as: | ||||
| 					//	MA13 MA12	RA2 RA1 RA0		MA9 MA8 MA7 MA6 MA5 MA4 MA3 MA2 MA1 MA0		CCLK | ||||
| 					// ... so form the real access address. | ||||
| 					uint16_t address = | ||||
| 						static_cast<uint16_t>( | ||||
| 					const uint16_t address = | ||||
| 						uint16_t( | ||||
| 							((state.refresh_address & 0x3ff) << 1) | | ||||
| 							((state.row_address & 0x7) << 11) | | ||||
| 							((state.refresh_address & 0x3000) << 2) | ||||
| 						); | ||||
|  | ||||
| 					// fetch two bytes and translate into pixels | ||||
| 					// Fetch two bytes and translate into pixels. Guaranteed: the mode can change only at | ||||
| 					// hsync, so there's no risk of pixel_pointer_ overrunning 320 output pixels without | ||||
| 					// exactly reaching 320 output pixels. | ||||
| 					switch(mode_) { | ||||
| 						case 0: | ||||
| 							reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode0_output_[ram_[address]]; | ||||
| 							reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode0_output_[ram_[address+1]]; | ||||
| 							pixel_pointer_ += 4; | ||||
| 							pixel_pointer_ += 2 * sizeof(uint16_t); | ||||
| 						break; | ||||
|  | ||||
| 						case 1: | ||||
| 							reinterpret_cast<uint32_t *>(pixel_pointer_)[0] = mode1_output_[ram_[address]]; | ||||
| 							reinterpret_cast<uint32_t *>(pixel_pointer_)[1] = mode1_output_[ram_[address+1]]; | ||||
| 							pixel_pointer_ += 8; | ||||
| 							pixel_pointer_ += 2 * sizeof(uint32_t); | ||||
| 						break; | ||||
|  | ||||
| 						case 2: | ||||
| 							reinterpret_cast<uint64_t *>(pixel_pointer_)[0] = mode2_output_[ram_[address]]; | ||||
| 							reinterpret_cast<uint64_t *>(pixel_pointer_)[1] = mode2_output_[ram_[address+1]]; | ||||
| 							pixel_pointer_ += 16; | ||||
| 							pixel_pointer_ += 2 * sizeof(uint64_t); | ||||
| 						break; | ||||
|  | ||||
| 						case 3: | ||||
| 							reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode3_output_[ram_[address]]; | ||||
| 							reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode3_output_[ram_[address+1]]; | ||||
| 							pixel_pointer_ += 4; | ||||
| 							pixel_pointer_ += 2 * sizeof(uint16_t); | ||||
| 						break; | ||||
|  | ||||
| 					} | ||||
|  | ||||
| 					// flush the current buffer pixel if full; the CRTC allows many different display | ||||
| 					// Flush the current buffer pixel if full; the CRTC allows many different display | ||||
| 					// widths so it's not necessarily possible to predict the correct number in advance | ||||
| 					// and using the upper bound could lead to inefficient behaviour | ||||
| 					// and using the upper bound could lead to inefficient behaviour. | ||||
| 					if(pixel_pointer_ == pixel_data_ + 320) { | ||||
| 						crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_); | ||||
| 						crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_)); | ||||
| 						pixel_pointer_ = pixel_data_ = nullptr; | ||||
| 						cycles_ = 0; | ||||
| 					} | ||||
| @@ -323,27 +330,14 @@ class CRTCBusHandler { | ||||
| 			was_hsync_ = state.hsync; | ||||
| 		} | ||||
|  | ||||
| 		/// Constructs an appropriate CRT for video output. | ||||
| 		void setup_output(float aspect_ratio) { | ||||
| 			crt_.reset(new Outputs::CRT::CRT(1024, 16, Outputs::CRT::DisplayType::PAL50, 1)); | ||||
| 			crt_->set_rgb_sampling_function( | ||||
| 				"vec3 rgb_sample(usampler2D sampler, vec2 coordinate)" | ||||
| 				"{" | ||||
| 					"uint sample = texture(texID, coordinate).r;" | ||||
| 					"return vec3(float((sample >> 4) & 3u), float((sample >> 2) & 3u), float(sample & 3u)) / 2.0;" | ||||
| 				"}"); | ||||
| 			crt_->set_visible_area(Outputs::CRT::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f)); | ||||
| 			crt_->set_video_signal(Outputs::CRT::VideoSignal::RGB); | ||||
| 		/// Sets the destination for output. | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||
| 			crt_.set_scan_target(scan_target); | ||||
| 		} | ||||
|  | ||||
| 		/// Destructs the CRT. | ||||
| 		void close_output() { | ||||
| 			crt_.reset(); | ||||
| 		} | ||||
|  | ||||
| 		/// @returns the CRT. | ||||
| 		Outputs::CRT::CRT *get_crt() { | ||||
| 			return crt_.get(); | ||||
| 		/// Sets the type of display. | ||||
| 		void set_display_type(Outputs::Display::DisplayType display_type) { | ||||
| 			crt_.set_display_type(display_type); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| @@ -376,10 +370,18 @@ class CRTCBusHandler { | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		void output_border(unsigned int length) { | ||||
| 			uint8_t *colour_pointer = static_cast<uint8_t *>(crt_->allocate_write_area(1)); | ||||
| 		void output_border(int length) { | ||||
| 			assert(length >= 0); | ||||
|  | ||||
| 			// A black border can be output via crt_.output_blank for a minor performance | ||||
| 			// win; otherwise paint whatever the border colour really is. | ||||
| 			if(border_) { | ||||
| 				uint8_t *const colour_pointer = static_cast<uint8_t *>(crt_.begin_data(1)); | ||||
| 				if(colour_pointer) *colour_pointer = border_; | ||||
| 			crt_->output_level(length * 16); | ||||
| 				crt_.output_level(length * 16); | ||||
| 			} else { | ||||
| 				crt_.output_blank(length * 16); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| #define Mode0Colour0(c) ((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2) | ||||
| @@ -395,16 +397,16 @@ class CRTCBusHandler { | ||||
|  | ||||
| 		void establish_palette_hits() { | ||||
| 			for(int c = 0; c < 256; c++) { | ||||
| 				mode0_palette_hits_[Mode0Colour0(c)].push_back(static_cast<uint8_t>(c)); | ||||
| 				mode0_palette_hits_[Mode0Colour1(c)].push_back(static_cast<uint8_t>(c)); | ||||
| 				mode0_palette_hits_[Mode0Colour0(c)].push_back(uint8_t(c)); | ||||
| 				mode0_palette_hits_[Mode0Colour1(c)].push_back(uint8_t(c)); | ||||
|  | ||||
| 				mode1_palette_hits_[Mode1Colour0(c)].push_back(static_cast<uint8_t>(c)); | ||||
| 				mode1_palette_hits_[Mode1Colour1(c)].push_back(static_cast<uint8_t>(c)); | ||||
| 				mode1_palette_hits_[Mode1Colour2(c)].push_back(static_cast<uint8_t>(c)); | ||||
| 				mode1_palette_hits_[Mode1Colour3(c)].push_back(static_cast<uint8_t>(c)); | ||||
| 				mode1_palette_hits_[Mode1Colour0(c)].push_back(uint8_t(c)); | ||||
| 				mode1_palette_hits_[Mode1Colour1(c)].push_back(uint8_t(c)); | ||||
| 				mode1_palette_hits_[Mode1Colour2(c)].push_back(uint8_t(c)); | ||||
| 				mode1_palette_hits_[Mode1Colour3(c)].push_back(uint8_t(c)); | ||||
|  | ||||
| 				mode3_palette_hits_[Mode3Colour0(c)].push_back(static_cast<uint8_t>(c)); | ||||
| 				mode3_palette_hits_[Mode3Colour1(c)].push_back(static_cast<uint8_t>(c)); | ||||
| 				mode3_palette_hits_[Mode3Colour0(c)].push_back(uint8_t(c)); | ||||
| 				mode3_palette_hits_[Mode3Colour1(c)].push_back(uint8_t(c)); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| @@ -414,7 +416,7 @@ class CRTCBusHandler { | ||||
| 					// Mode 0: abcdefgh -> [gcea] [hdfb] | ||||
| 					for(int c = 0; c < 256; c++) { | ||||
| 						// prepare mode 0 | ||||
| 						uint8_t *mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]); | ||||
| 						uint8_t *const mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]); | ||||
| 						mode0_pixels[0] = palette_[Mode0Colour0(c)]; | ||||
| 						mode0_pixels[1] = palette_[Mode0Colour1(c)]; | ||||
| 					} | ||||
| @@ -423,7 +425,7 @@ class CRTCBusHandler { | ||||
| 				case 1: | ||||
| 					for(int c = 0; c < 256; c++) { | ||||
| 						// prepare mode 1 | ||||
| 						uint8_t *mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]); | ||||
| 						uint8_t *const mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]); | ||||
| 						mode1_pixels[0] = palette_[Mode1Colour0(c)]; | ||||
| 						mode1_pixels[1] = palette_[Mode1Colour1(c)]; | ||||
| 						mode1_pixels[2] = palette_[Mode1Colour2(c)]; | ||||
| @@ -434,7 +436,7 @@ class CRTCBusHandler { | ||||
| 				case 2: | ||||
| 					for(int c = 0; c < 256; c++) { | ||||
| 						// prepare mode 2 | ||||
| 						uint8_t *mode2_pixels = reinterpret_cast<uint8_t *>(&mode2_output_[c]); | ||||
| 						uint8_t *const mode2_pixels = reinterpret_cast<uint8_t *>(&mode2_output_[c]); | ||||
| 						mode2_pixels[0] = palette_[((c & 0x80) >> 7)]; | ||||
| 						mode2_pixels[1] = palette_[((c & 0x40) >> 6)]; | ||||
| 						mode2_pixels[2] = palette_[((c & 0x20) >> 5)]; | ||||
| @@ -449,7 +451,7 @@ class CRTCBusHandler { | ||||
| 				case 3: | ||||
| 					for(int c = 0; c < 256; c++) { | ||||
| 						// prepare mode 3 | ||||
| 						uint8_t *mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]); | ||||
| 						uint8_t *const mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]); | ||||
| 						mode3_pixels[0] = palette_[Mode3Colour0(c)]; | ||||
| 						mode3_pixels[1] = palette_[Mode3Colour1(c)]; | ||||
| 					} | ||||
| @@ -461,7 +463,7 @@ class CRTCBusHandler { | ||||
| 			switch(mode_) { | ||||
| 				case 0: { | ||||
| 					for(uint8_t c : mode0_palette_hits_[pen]) { | ||||
| 						uint8_t *mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]); | ||||
| 						uint8_t *const mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]); | ||||
| 						mode0_pixels[0] = palette_[Mode0Colour0(c)]; | ||||
| 						mode0_pixels[1] = palette_[Mode0Colour1(c)]; | ||||
| 					} | ||||
| @@ -469,7 +471,7 @@ class CRTCBusHandler { | ||||
| 				case 1: | ||||
| 					if(pen > 3) return; | ||||
| 					for(uint8_t c : mode1_palette_hits_[pen]) { | ||||
| 						uint8_t *mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]); | ||||
| 						uint8_t *const mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]); | ||||
| 						mode1_pixels[0] = palette_[Mode1Colour0(c)]; | ||||
| 						mode1_pixels[1] = palette_[Mode1Colour1(c)]; | ||||
| 						mode1_pixels[2] = palette_[Mode1Colour2(c)]; | ||||
| @@ -486,7 +488,7 @@ class CRTCBusHandler { | ||||
| 					if(pen > 3) return; | ||||
| 					// Same argument applies here as to case 1, as the unused bits aren't masked out. | ||||
| 					for(uint8_t c : mode3_palette_hits_[pen]) { | ||||
| 						uint8_t *mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]); | ||||
| 						uint8_t *const mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]); | ||||
| 						mode3_pixels[0] = palette_[Mode3Colour0(c)]; | ||||
| 						mode3_pixels[1] = palette_[Mode3Colour1(c)]; | ||||
| 					} | ||||
| @@ -507,7 +509,7 @@ class CRTCBusHandler { | ||||
|  | ||||
| 		uint8_t mapped_palette_value(uint8_t colour) { | ||||
| #define COL(r, g, b) (r << 4) | (g << 2) | b | ||||
| 			static const uint8_t mapping[32] = { | ||||
| 			constexpr uint8_t mapping[32] = { | ||||
| 				COL(1, 1, 1),	COL(1, 1, 1),	COL(0, 2, 1),	COL(2, 2, 1), | ||||
| 				COL(0, 0, 1),	COL(2, 0, 1),	COL(0, 1, 1),	COL(2, 1, 1), | ||||
| 				COL(2, 0, 1),	COL(2, 2, 1),	COL(2, 2, 0),	COL(2, 2, 2), | ||||
| @@ -528,19 +530,19 @@ class CRTCBusHandler { | ||||
| 			Border, | ||||
| 			Pixels | ||||
| 		} previous_output_mode_ = OutputMode::Sync; | ||||
| 		unsigned int cycles_ = 0; | ||||
| 		int cycles_ = 0; | ||||
|  | ||||
| 		bool was_hsync_ = false, was_vsync_ = false; | ||||
| 		int cycles_into_hsync_ = 0; | ||||
|  | ||||
| 		std::unique_ptr<Outputs::CRT::CRT> crt_; | ||||
| 		Outputs::CRT::CRT crt_; | ||||
| 		uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr; | ||||
|  | ||||
| 		uint8_t *ram_ = nullptr; | ||||
| 		const uint8_t *const ram_ = nullptr; | ||||
|  | ||||
| 		int next_mode_ = 2, mode_ = 2; | ||||
|  | ||||
| 		unsigned int pixel_divider_ = 1; | ||||
| 		int pixel_divider_ = 1; | ||||
| 		uint16_t mode0_output_[256]; | ||||
| 		uint32_t mode1_output_[256]; | ||||
| 		uint64_t mode2_output_[256]; | ||||
| @@ -572,7 +574,7 @@ class KeyboardState: public GI::AY38910::PortHandler { | ||||
| 			Sets the row currently being reported to the AY. | ||||
| 		*/ | ||||
| 		void set_row(int row) { | ||||
| 			row_ = static_cast<size_t>(row); | ||||
| 			row_ = size_t(row); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| @@ -591,7 +593,7 @@ class KeyboardState: public GI::AY38910::PortHandler { | ||||
| 		*/ | ||||
| 		void set_is_pressed(bool is_pressed, int line, int key) { | ||||
| 			int mask = 1 << key; | ||||
| 			assert(static_cast<size_t>(line) < sizeof(rows_)); | ||||
| 			assert(size_t(line) < sizeof(rows_)); | ||||
| 			if(is_pressed) rows_[line] &= ~mask; else rows_[line] |= mask; | ||||
| 		} | ||||
|  | ||||
| @@ -602,7 +604,7 @@ class KeyboardState: public GI::AY38910::PortHandler { | ||||
| 			memset(rows_, 0xff, sizeof(rows_)); | ||||
| 		} | ||||
|  | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() { | ||||
| 		const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() { | ||||
| 			return joysticks_; | ||||
| 		} | ||||
|  | ||||
| @@ -789,33 +791,43 @@ template <bool has_fdc> class ConcreteMachine: | ||||
| 			ay_.ay().set_port_handler(&key_state_); | ||||
|  | ||||
| 			// construct the list of necessary ROMs | ||||
| 			std::vector<std::string> required_roms = {"amsdos.rom"}; | ||||
| 			const std::string machine_name = "AmstradCPC"; | ||||
| 			std::vector<ROMMachine::ROM> required_roms = { | ||||
| 				ROMMachine::ROM(machine_name, "the Amstrad Disk Operating System", "amsdos.rom", 16*1024, 0x1fe22ecd) | ||||
| 			}; | ||||
| 			std::string model_number; | ||||
| 			uint32_t crcs[2]; | ||||
| 			switch(target.model) { | ||||
| 				default: | ||||
| 					model_number = "6128"; | ||||
| 					has_128k_ = true; | ||||
| 					crcs[0] = 0x0219bb74; | ||||
| 					crcs[1] = 0xca6af63d; | ||||
| 				break; | ||||
| 				case Analyser::Static::AmstradCPC::Target::Model::CPC464: | ||||
| 					model_number = "464"; | ||||
| 					has_128k_ = false; | ||||
| 					crcs[0] = 0x815752df; | ||||
| 					crcs[1] = 0x7d9a3bac; | ||||
| 				break; | ||||
| 				case Analyser::Static::AmstradCPC::Target::Model::CPC664: | ||||
| 					model_number = "664"; | ||||
| 					has_128k_ = false; | ||||
| 					crcs[0] = 0x3f5a6dc4; | ||||
| 					crcs[1] = 0x32fee492; | ||||
| 				break; | ||||
| 			} | ||||
| 			required_roms.push_back("os" + model_number + ".rom"); | ||||
| 			required_roms.push_back("basic" + model_number + ".rom"); | ||||
| 			required_roms.emplace_back(machine_name, "the CPC " + model_number + " firmware", "os" + model_number + ".rom", 16*1024, crcs[0]); | ||||
| 			required_roms.emplace_back(machine_name, "the CPC " + model_number + " BASIC ROM", "basic" + model_number + ".rom", 16*1024, crcs[1]); | ||||
|  | ||||
| 			// fetch and verify the ROMs | ||||
| 			const auto roms = rom_fetcher("AmstradCPC", required_roms); | ||||
| 			const auto roms = rom_fetcher(required_roms); | ||||
|  | ||||
| 			for(std::size_t index = 0; index < roms.size(); ++index) { | ||||
| 				auto &data = roms[index]; | ||||
| 				if(!data) throw ROMMachine::Error::MissingROMs; | ||||
| 				roms_[static_cast<int>(index)] = std::move(*data); | ||||
| 				roms_[static_cast<int>(index)].resize(16384); | ||||
| 				roms_[int(index)] = std::move(*data); | ||||
| 				roms_[int(index)].resize(16384); | ||||
| 			} | ||||
|  | ||||
| 			// Establish default memory map | ||||
| @@ -860,13 +872,15 @@ template <bool has_fdc> class ConcreteMachine: | ||||
|  | ||||
| 			// TODO (in the player, not here): adapt it to accept an input clock rate and | ||||
| 			// run_for as HalfCycles | ||||
| 			if(!tape_player_is_sleeping_) tape_player_.run_for(cycle.length.as_int()); | ||||
| 			if(!tape_player_is_sleeping_) tape_player_.run_for(cycle.length.as_integral()); | ||||
|  | ||||
| 			// Pump the AY | ||||
| 			ay_.run_for(cycle.length); | ||||
|  | ||||
| 			if constexpr (has_fdc) { | ||||
| 				// Clock the FDC, if connected, using a lazy scale by two | ||||
| 				time_since_fdc_update_ += cycle.length; | ||||
| 			} | ||||
|  | ||||
| 			// Update typing activity | ||||
| 			if(typer_) typer_->run_for(cycle.length); | ||||
| @@ -892,10 +906,12 @@ template <bool has_fdc> class ConcreteMachine: | ||||
| 					} | ||||
|  | ||||
| 					// 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; | ||||
| 							if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data(); | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					// Check for a CRTC access | ||||
| 					if(!(address & 0x4000)) { | ||||
| @@ -908,20 +924,22 @@ template <bool has_fdc> class ConcreteMachine: | ||||
|  | ||||
| 					// Check for an 8255 PIO access | ||||
| 					if(!(address & 0x800)) { | ||||
| 						i8255_.set_register((address >> 8) & 3, *cycle.value); | ||||
| 						i8255_.write((address >> 8) & 3, *cycle.value); | ||||
| 					} | ||||
|  | ||||
| 					if constexpr (has_fdc) { | ||||
| 						// Check for an FDC access | ||||
| 					if(has_fdc && (address & 0x580) == 0x100) { | ||||
| 						if((address & 0x580) == 0x100) { | ||||
| 							flush_fdc(); | ||||
| 						fdc_.set_register(address & 1, *cycle.value); | ||||
| 							fdc_.write(address & 1, *cycle.value); | ||||
| 						} | ||||
|  | ||||
| 						// Check for a disk motor access | ||||
| 					if(has_fdc && !(address & 0x580)) { | ||||
| 						if(!(address & 0x580)) { | ||||
| 							flush_fdc(); | ||||
| 							fdc_.set_motor_on(!!(*cycle.value)); | ||||
| 						} | ||||
| 					} | ||||
| 				break; | ||||
| 				case CPU::Z80::PartialMachineCycle::Input: | ||||
| 					// Default to nothing answering | ||||
| @@ -929,13 +947,15 @@ template <bool has_fdc> class ConcreteMachine: | ||||
|  | ||||
| 					// Check for a PIO access | ||||
| 					if(!(address & 0x800)) { | ||||
| 						*cycle.value &= i8255_.get_register((address >> 8) & 3); | ||||
| 						*cycle.value &= i8255_.read((address >> 8) & 3); | ||||
| 					} | ||||
|  | ||||
| 					// Check for an FDC access | ||||
| 					if(has_fdc && (address & 0x580) == 0x100) { | ||||
| 					if constexpr (has_fdc) { | ||||
| 						if((address & 0x580) == 0x100) { | ||||
| 							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 | ||||
| @@ -981,19 +1001,14 @@ template <bool has_fdc> class ConcreteMachine: | ||||
| 			flush_fdc(); | ||||
| 		} | ||||
|  | ||||
| 		/// A CRTMachine function; indicates that outputs should be created now. | ||||
| 		void setup_output(float aspect_ratio) override final { | ||||
| 			crtc_bus_handler_.setup_output(aspect_ratio); | ||||
| 		/// A CRTMachine function; sets the destination for video. | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final { | ||||
| 			crtc_bus_handler_.set_scan_target(scan_target); | ||||
| 		} | ||||
|  | ||||
| 		/// A CRTMachine function; indicates that outputs should be destroyed now. | ||||
| 		void close_output() override final { | ||||
| 			crtc_bus_handler_.close_output(); | ||||
| 		} | ||||
|  | ||||
| 		/// @returns the CRT in use. | ||||
| 		Outputs::CRT::CRT *get_crt() override final { | ||||
| 			return crtc_bus_handler_.get_crt(); | ||||
| 		/// A CRTMachine function; sets the output display type. | ||||
| 		void set_display_type(Outputs::Display::DisplayType display_type) override final { | ||||
| 			crtc_bus_handler_.set_display_type(display_type); | ||||
| 		} | ||||
|  | ||||
| 		/// @returns the speaker in use. | ||||
| @@ -1058,7 +1073,7 @@ template <bool has_fdc> class ConcreteMachine: | ||||
|  | ||||
| 		// MARK: - Activity Source | ||||
| 		void set_activity_observer(Activity::Observer *observer) override { | ||||
| 			if(has_fdc) fdc_.set_activity_observer(observer); | ||||
| 			if constexpr (has_fdc) fdc_.set_activity_observer(observer); | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - Configuration options. | ||||
| @@ -1086,7 +1101,7 @@ template <bool has_fdc> class ConcreteMachine: | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - Joysticks | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override { | ||||
| 		const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override { | ||||
| 			return key_state_.get_joysticks(); | ||||
| 		} | ||||
|  | ||||
| @@ -1148,12 +1163,14 @@ template <bool has_fdc> class ConcreteMachine: | ||||
| 		FDC fdc_; | ||||
| 		HalfCycles time_since_fdc_update_; | ||||
| 		void flush_fdc() { | ||||
| 			if constexpr (has_fdc) { | ||||
| 				// Clock the FDC, if connected, using a lazy scale by two | ||||
| 			if(has_fdc && !fdc_is_sleeping_) { | ||||
| 				fdc_.run_for(Cycles(time_since_fdc_update_.as_int())); | ||||
| 				if(!fdc_is_sleeping_) { | ||||
| 					fdc_.run_for(Cycles(time_since_fdc_update_.as_integral())); | ||||
| 				} | ||||
| 				time_since_fdc_update_ = HalfCycles(0); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		InterruptTimer interrupt_timer_; | ||||
| 		Storage::Tape::BinaryTapePlayer tape_player_; | ||||
|   | ||||
| @@ -31,12 +31,12 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) { | ||||
| 		BIND(F11, KeyRightSquareBracket); | ||||
| 		BIND(F12, KeyClear); | ||||
|  | ||||
| 		BIND(Hyphen, KeyMinus);		BIND(Equals, KeyCaret);		BIND(BackSpace, KeyDelete); | ||||
| 		BIND(Hyphen, KeyMinus);		BIND(Equals, KeyCaret);		BIND(Backspace, KeyDelete); | ||||
| 		BIND(Tab, KeyTab); | ||||
|  | ||||
| 		BIND(OpenSquareBracket, KeyAt); | ||||
| 		BIND(CloseSquareBracket, KeyLeftSquareBracket); | ||||
| 		BIND(BackSlash, KeyBackSlash); | ||||
| 		BIND(Backslash, KeyBackSlash); | ||||
|  | ||||
| 		BIND(CapsLock, KeyCapsLock); | ||||
| 		BIND(Semicolon, KeyColon); | ||||
| @@ -57,19 +57,19 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) { | ||||
| 		BIND(Left, KeyLeft);	BIND(Right, KeyRight); | ||||
| 		BIND(Up, KeyUp);		BIND(Down, KeyDown); | ||||
|  | ||||
| 		BIND(KeyPad0, KeyF0); | ||||
| 		BIND(KeyPad1, KeyF1);		BIND(KeyPad2, KeyF2);		BIND(KeyPad3, KeyF3); | ||||
| 		BIND(KeyPad4, KeyF4);		BIND(KeyPad5, KeyF5);		BIND(KeyPad6, KeyF6); | ||||
| 		BIND(KeyPad7, KeyF7);		BIND(KeyPad8, KeyF8);		BIND(KeyPad9, KeyF9); | ||||
| 		BIND(KeyPadPlus, KeySemicolon); | ||||
| 		BIND(KeyPadMinus, KeyMinus); | ||||
| 		BIND(Keypad0, KeyF0); | ||||
| 		BIND(Keypad1, KeyF1);		BIND(Keypad2, KeyF2);		BIND(Keypad3, KeyF3); | ||||
| 		BIND(Keypad4, KeyF4);		BIND(Keypad5, KeyF5);		BIND(Keypad6, KeyF6); | ||||
| 		BIND(Keypad7, KeyF7);		BIND(Keypad8, KeyF8);		BIND(Keypad9, KeyF9); | ||||
| 		BIND(KeypadPlus, KeySemicolon); | ||||
| 		BIND(KeypadMinus, KeyMinus); | ||||
|  | ||||
| 		BIND(KeyPadEnter, KeyEnter); | ||||
| 		BIND(KeyPadDecimalPoint, KeyFullStop); | ||||
| 		BIND(KeyPadEquals, KeyMinus); | ||||
| 		BIND(KeyPadSlash, KeyForwardSlash); | ||||
| 		BIND(KeyPadAsterisk, KeyColon); | ||||
| 		BIND(KeyPadDelete, KeyDelete); | ||||
| 		BIND(KeypadEnter, KeyEnter); | ||||
| 		BIND(KeypadDecimalPoint, KeyFullStop); | ||||
| 		BIND(KeypadEquals, KeyMinus); | ||||
| 		BIND(KeypadSlash, KeyForwardSlash); | ||||
| 		BIND(KeypadAsterisk, KeyColon); | ||||
| 		BIND(KeypadDelete, KeyDelete); | ||||
| 	} | ||||
| #undef BIND | ||||
| } | ||||
|   | ||||
| @@ -8,32 +8,40 @@ | ||||
| 
 | ||||
| #include "AppleII.hpp" | ||||
| 
 | ||||
| #include "../../Activity/Source.hpp" | ||||
| #include "../MediaTarget.hpp" | ||||
| #include "../CRTMachine.hpp" | ||||
| #include "../JoystickMachine.hpp" | ||||
| #include "../KeyboardMachine.hpp" | ||||
| #include "../Utility/MemoryFuzzer.hpp" | ||||
| #include "../Utility/StringSerialiser.hpp" | ||||
| #include "../../../Activity/Source.hpp" | ||||
| #include "../../MediaTarget.hpp" | ||||
| #include "../../CRTMachine.hpp" | ||||
| #include "../../JoystickMachine.hpp" | ||||
| #include "../../KeyboardMachine.hpp" | ||||
| #include "../../Utility/MemoryFuzzer.hpp" | ||||
| #include "../../Utility/StringSerialiser.hpp" | ||||
| 
 | ||||
| #include "../../Processors/6502/6502.hpp" | ||||
| #include "../../Components/AudioToggle/AudioToggle.hpp" | ||||
| #include "../../../Processors/6502/6502.hpp" | ||||
| #include "../../../Components/AudioToggle/AudioToggle.hpp" | ||||
| 
 | ||||
| #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../Outputs/Log.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
| 
 | ||||
| #include "Card.hpp" | ||||
| #include "DiskIICard.hpp" | ||||
| #include "Video.hpp" | ||||
| 
 | ||||
| #include "../../Analyser/Static/AppleII/Target.hpp" | ||||
| #include "../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../../Analyser/Static/AppleII/Target.hpp" | ||||
| #include "../../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../../Configurable/StandardOptions.hpp" | ||||
| 
 | ||||
| #include <algorithm> | ||||
| #include <array> | ||||
| #include <memory> | ||||
| 
 | ||||
| namespace { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| 
 | ||||
| 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)) | ||||
| 
 | ||||
| @@ -43,12 +51,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 	public KeyboardMachine::MappedMachine, | ||||
| 	public CPU::MOS6502::BusHandler, | ||||
| 	public Inputs::Keyboard, | ||||
| 	public AppleII::Machine, | ||||
| 	public Configurable::Device, | ||||
| 	public Apple::II::Machine, | ||||
| 	public Activity::Source, | ||||
| 	public JoystickMachine::Machine, | ||||
| 	public AppleII::Card::Delegate { | ||||
| 	public Apple::II::Card::Delegate { | ||||
| 	private: | ||||
| 		struct VideoBusHandler : public AppleII::Video::BusHandler { | ||||
| 		struct VideoBusHandler : public Apple::II::Video::BusHandler { | ||||
| 			public: | ||||
| 				VideoBusHandler(uint8_t *ram, uint8_t *aux_ram) : ram_(ram), aux_ram_(aux_ram) {} | ||||
| 
 | ||||
| @@ -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_; | ||||
| 		VideoBusHandler video_bus_handler_; | ||||
| 		std::unique_ptr<AppleII::Video::Video<VideoBusHandler, is_iie()>> video_; | ||||
| 		Apple::II::Video::Video<VideoBusHandler, is_iie()> video_; | ||||
| 		int cycles_into_current_line_ = 0; | ||||
| 		Cycles cycles_since_video_update_; | ||||
| 
 | ||||
| 		void update_video() { | ||||
| 			video_->run_for(cycles_since_video_update_.flush()); | ||||
| 			video_.run_for(cycles_since_video_update_.flush<Cycles>()); | ||||
| 		} | ||||
| 		static const int audio_divider = 8; | ||||
| 		static constexpr int audio_divider = 8; | ||||
| 		void update_audio() { | ||||
| 			speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider))); | ||||
| 		} | ||||
| 		void update_just_in_time_cards() { | ||||
| 			if(cycles_since_card_update_ > Cycles(0)) { | ||||
| 				for(const auto &card : just_in_time_cards_) { | ||||
| 					card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_); | ||||
| 				} | ||||
| 			} | ||||
| 			cycles_since_card_update_ = 0; | ||||
| 			stretched_cycles_since_card_update_ = 0; | ||||
| 		} | ||||
| 
 | ||||
| 		uint8_t ram_[65536], aux_ram_[65536]; | ||||
| 		std::vector<uint8_t> rom_; | ||||
| 		std::vector<uint8_t> character_rom_; | ||||
| 		uint8_t keyboard_input_ = 0x00; | ||||
| 		bool key_is_down_ = false; | ||||
| 
 | ||||
| @@ -102,41 +112,47 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 		Cycles cycles_since_audio_update_; | ||||
| 
 | ||||
| 		// MARK: - Cards
 | ||||
| 		std::array<std::unique_ptr<AppleII::Card>, 7> cards_; | ||||
| 		std::array<std::unique_ptr<Apple::II::Card>, 7> cards_; | ||||
| 		Cycles cycles_since_card_update_; | ||||
| 		std::vector<AppleII::Card *> every_cycle_cards_; | ||||
| 		std::vector<AppleII::Card *> just_in_time_cards_; | ||||
| 		std::vector<Apple::II::Card *> every_cycle_cards_; | ||||
| 		std::vector<Apple::II::Card *> just_in_time_cards_; | ||||
| 
 | ||||
| 		int stretched_cycles_since_card_update_ = 0; | ||||
| 
 | ||||
| 		void install_card(std::size_t slot, AppleII::Card *card) { | ||||
| 		void install_card(std::size_t slot, Apple::II::Card *card) { | ||||
| 			assert(slot >= 1 && slot < 8); | ||||
| 			cards_[slot - 1].reset(card); | ||||
| 			card->set_delegate(this); | ||||
| 			pick_card_messaging_group(card); | ||||
| 		} | ||||
| 
 | ||||
| 		bool is_every_cycle_card(AppleII::Card *card) { | ||||
| 		bool is_every_cycle_card(const Apple::II::Card *card) { | ||||
| 			return !card->get_select_constraints(); | ||||
| 		} | ||||
| 
 | ||||
| 		void pick_card_messaging_group(AppleII::Card *card) { | ||||
| 		bool card_lists_are_dirty_ = true; | ||||
| 		bool card_became_just_in_time_ = false; | ||||
| 		void pick_card_messaging_group(Apple::II::Card *card) { | ||||
| 			// Simplify to a card being either just-in-time or realtime.
 | ||||
| 			// Don't worry about exactly what it's watching,
 | ||||
| 			const bool is_every_cycle = is_every_cycle_card(card); | ||||
| 			std::vector<AppleII::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_; | ||||
| 		 	std::vector<AppleII::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_; | ||||
| 			std::vector<Apple::II::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_; | ||||
| 
 | ||||
| 			// If the card is already in the proper group, stop.
 | ||||
| 			if(std::find(intended.begin(), intended.end(), card) != intended.end()) return; | ||||
| 			auto old_membership = std::find(undesired.begin(), undesired.end(), card); | ||||
| 			if(old_membership != undesired.end()) undesired.erase(old_membership); | ||||
| 			intended.push_back(card); | ||||
| 
 | ||||
| 			// Otherwise, mark the sets as dirty. It isn't safe to transition the card here,
 | ||||
| 			// as the main loop may be part way through iterating the two lists.
 | ||||
| 			card_lists_are_dirty_ = true; | ||||
| 			card_became_just_in_time_ |= !is_every_cycle; | ||||
| 		} | ||||
| 
 | ||||
| 		void card_did_change_select_constraints(AppleII::Card *card) override { | ||||
| 		void card_did_change_select_constraints(Apple::II::Card *card) override { | ||||
| 			pick_card_messaging_group(card); | ||||
| 		} | ||||
| 
 | ||||
| 		AppleII::DiskIICard *diskii_card() { | ||||
| 			return dynamic_cast<AppleII::DiskIICard *>(cards_[5].get()); | ||||
| 		Apple::II::DiskIICard *diskii_card() { | ||||
| 			return dynamic_cast<Apple::II::DiskIICard *>(cards_[5].get()); | ||||
| 		} | ||||
| 
 | ||||
| 		// MARK: - Memory Map.
 | ||||
| @@ -234,13 +250,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 				read_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200], | ||||
| 				write_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200]); | ||||
| 
 | ||||
| 			if(video_ && video_->get_80_store()) { | ||||
| 				bool use_aux_ram = video_->get_page2(); | ||||
| 			if(video_.get_80_store()) { | ||||
| 				bool use_aux_ram = video_.get_page2(); | ||||
| 				page(0x04, 0x08, | ||||
| 					use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400], | ||||
| 					use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400]); | ||||
| 
 | ||||
| 				if(video_->get_high_resolution()) { | ||||
| 				if(video_.get_high_resolution()) { | ||||
| 					page(0x20, 0x40, | ||||
| 						use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000], | ||||
| 						use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000]); | ||||
| @@ -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): | ||||
| 			m6502_(*this), | ||||
| 			video_bus_handler_(ram_, aux_ram_), | ||||
| 			video_(video_bus_handler_), | ||||
| 			audio_toggle_(audio_queue_), | ||||
| 			speaker_(audio_toggle_) { | ||||
| 			// The system's master clock rate.
 | ||||
| 		 	const float master_clock = 14318180.0; | ||||
| 			constexpr float master_clock = 14318180.0; | ||||
| 
 | ||||
| 			// This is where things get slightly convoluted: establish the machine as having a clock rate
 | ||||
| 			// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
 | ||||
| @@ -338,30 +355,39 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 
 | ||||
| 			// Pick the required ROMs.
 | ||||
| 			using Target = Analyser::Static::AppleII::Target; | ||||
| 			std::vector<std::string> rom_names; | ||||
| 			const std::string machine_name = "AppleII"; | ||||
| 			std::vector<ROMMachine::ROM> rom_descriptions; | ||||
| 			size_t rom_size = 12*1024; | ||||
| 			switch(target.model) { | ||||
| 				default: | ||||
| 					rom_names.push_back("apple2-character.rom"); | ||||
| 					rom_names.push_back("apple2o.rom"); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588); | ||||
| 				break; | ||||
| 				case Target::Model::IIplus: | ||||
| 					rom_names.push_back("apple2-character.rom"); | ||||
| 					rom_names.push_back("apple2.rom"); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26); | ||||
| 				break; | ||||
| 				case Target::Model::IIe: | ||||
| 					rom_size += 3840; | ||||
| 					rom_names.push_back("apple2eu-character.rom"); | ||||
| 					rom_names.push_back("apple2eu.rom"); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d); | ||||
| 				break; | ||||
| 				case Target::Model::EnhancedIIe: | ||||
| 					rom_size += 3840; | ||||
| 					rom_names.push_back("apple2e-character.rom"); | ||||
| 					rom_names.push_back("apple2e.rom"); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942); | ||||
| 				break; | ||||
| 			} | ||||
| 			const auto roms = rom_fetcher("AppleII", rom_names); | ||||
| 			const auto roms = rom_fetcher(rom_descriptions); | ||||
| 
 | ||||
| 			// Try to install a Disk II card now, before checking the ROM list,
 | ||||
| 			// to make sure that Disk II dependencies have been communicated.
 | ||||
| 			if(target.disk_controller != Target::DiskController::None) { | ||||
| 				// Apple recommended slot 6 for the (first) Disk II.
 | ||||
| 				install_card(6, new Apple::II::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector)); | ||||
| 			} | ||||
| 
 | ||||
| 			// Now, check and move the ROMs.
 | ||||
| 			if(!roms[0] || !roms[1]) { | ||||
| 				throw ROMMachine::Error::MissingROMs; | ||||
| 			} | ||||
| @@ -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)); | ||||
| 			} | ||||
| 
 | ||||
| 			character_rom_ = std::move(*roms[0]); | ||||
| 
 | ||||
| 			if(target.disk_controller != Target::DiskController::None) { | ||||
| 				// Apple recommended slot 6 for the (first) Disk II.
 | ||||
| 				install_card(6, new AppleII::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector)); | ||||
| 			} | ||||
| 			video_.set_character_rom(*roms[0]); | ||||
| 
 | ||||
| 			// Set up the default memory blocks. On a II or II+ these values will never change.
 | ||||
| 			// On a IIe they'll be affected by selection of auxiliary RAM.
 | ||||
| @@ -396,17 +417,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 			audio_queue_.flush(); | ||||
| 		} | ||||
| 
 | ||||
| 		void setup_output(float aspect_ratio) override { | ||||
| 			video_.reset(new AppleII::Video::Video<VideoBusHandler, is_iie()>(video_bus_handler_)); | ||||
| 			video_->set_character_rom(character_rom_); | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { | ||||
| 			video_.set_scan_target(scan_target); | ||||
| 		} | ||||
| 
 | ||||
| 		void close_output() override { | ||||
| 			video_.reset(); | ||||
| 		} | ||||
| 
 | ||||
| 		Outputs::CRT::CRT *get_crt() override { | ||||
| 			return video_->get_crt(); | ||||
| 		/// Sets the type of display.
 | ||||
| 		void set_display_type(Outputs::Display::DisplayType display_type) override { | ||||
| 			video_.set_display_type(display_type); | ||||
| 		} | ||||
| 
 | ||||
| 		Outputs::Speaker::Speaker *get_speaker() override { | ||||
| @@ -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
 | ||||
| 				// too terrible.
 | ||||
| 				if(isReadOperation(operation) && address != 0xc000) { | ||||
| 					*value = video_->get_last_read_value(cycles_since_video_update_); | ||||
| 					*value = video_.get_last_read_value(cycles_since_video_update_); | ||||
| 				} | ||||
| 
 | ||||
| 				switch(address) { | ||||
| @@ -523,18 +540,18 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 								case 0xc015:	IIeSwitchRead(internal_CX_rom_);											break; | ||||
| 								case 0xc016:	IIeSwitchRead(alternative_zero_page_);										break; | ||||
| 								case 0xc017:	IIeSwitchRead(slot_C3_rom_);												break; | ||||
| 								case 0xc018:	IIeSwitchRead(video_->get_80_store());										break; | ||||
| 								case 0xc019:	IIeSwitchRead(video_->get_is_vertical_blank(cycles_since_video_update_));	break; | ||||
| 								case 0xc01a:	IIeSwitchRead(video_->get_text());											break; | ||||
| 								case 0xc01b:	IIeSwitchRead(video_->get_mixed());											break; | ||||
| 								case 0xc01c:	IIeSwitchRead(video_->get_page2());											break; | ||||
| 								case 0xc01d:	IIeSwitchRead(video_->get_high_resolution());								break; | ||||
| 								case 0xc01e:	IIeSwitchRead(video_->get_alternative_character_set());						break; | ||||
| 								case 0xc01f:	IIeSwitchRead(video_->get_80_columns());									break; | ||||
| 								case 0xc018:	IIeSwitchRead(video_.get_80_store());										break; | ||||
| 								case 0xc019:	IIeSwitchRead(video_.get_is_vertical_blank(cycles_since_video_update_));	break; | ||||
| 								case 0xc01a:	IIeSwitchRead(video_.get_text());											break; | ||||
| 								case 0xc01b:	IIeSwitchRead(video_.get_mixed());											break; | ||||
| 								case 0xc01c:	IIeSwitchRead(video_.get_page2());											break; | ||||
| 								case 0xc01d:	IIeSwitchRead(video_.get_high_resolution());								break; | ||||
| 								case 0xc01e:	IIeSwitchRead(video_.get_alternative_character_set());						break; | ||||
| 								case 0xc01f:	IIeSwitchRead(video_.get_80_columns());										break; | ||||
| #undef IIeSwitchRead | ||||
| 
 | ||||
| 								case 0xc07f: | ||||
| 									if(is_iie()) *value = (*value & 0x7f) | (video_->get_annunciator_3() ? 0x80 : 0x00); | ||||
| 									if(is_iie()) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00); | ||||
| 								break; | ||||
| 							} | ||||
| 						} else { | ||||
| @@ -546,7 +563,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 									case 0xc000: | ||||
| 									case 0xc001: | ||||
| 										update_video(); | ||||
| 										video_->set_80_store(!!(address&1)); | ||||
| 										video_.set_80_store(!!(address&1)); | ||||
| 										set_main_paging(); | ||||
| 									break; | ||||
| 
 | ||||
| @@ -586,13 +603,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 									case 0xc00c: | ||||
| 									case 0xc00d: | ||||
| 										update_video(); | ||||
| 										video_->set_80_columns(!!(address&1)); | ||||
| 										video_.set_80_columns(!!(address&1)); | ||||
| 									break; | ||||
| 
 | ||||
| 									case 0xc00e: | ||||
| 									case 0xc00f: | ||||
| 										update_video(); | ||||
| 										video_->set_alternative_character_set(!!(address&1)); | ||||
| 										video_.set_alternative_character_set(!!(address&1)); | ||||
| 									break; | ||||
| 								} | ||||
| 							} | ||||
| @@ -615,20 +632,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					case 0xc050: | ||||
| 					case 0xc051: | ||||
| 						update_video(); | ||||
| 						video_->set_text(!!(address&1)); | ||||
| 						video_.set_text(!!(address&1)); | ||||
| 					break; | ||||
| 					case 0xc052:	update_video();		video_->set_mixed(false);			break; | ||||
| 					case 0xc053:	update_video();		video_->set_mixed(true);			break; | ||||
| 					case 0xc052:	update_video();		video_.set_mixed(false);		break; | ||||
| 					case 0xc053:	update_video();		video_.set_mixed(true);			break; | ||||
| 					case 0xc054: | ||||
| 					case 0xc055: | ||||
| 						update_video(); | ||||
| 						video_->set_page2(!!(address&1)); | ||||
| 						video_.set_page2(!!(address&1)); | ||||
| 						set_main_paging(); | ||||
| 					break; | ||||
| 					case 0xc056: | ||||
| 					case 0xc057: | ||||
| 						update_video(); | ||||
| 						video_->set_high_resolution(!!(address&1)); | ||||
| 						video_.set_high_resolution(!!(address&1)); | ||||
| 						set_main_paging(); | ||||
| 					break; | ||||
| 
 | ||||
| @@ -636,7 +653,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					case 0xc05f: | ||||
| 						if(is_iie()) { | ||||
| 							update_video(); | ||||
| 							video_->set_annunciator_3(!(address&1)); | ||||
| 							video_.set_annunciator_3(!(address&1)); | ||||
| 						} | ||||
| 					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
 | ||||
| 					// the totality of who needs messaging.
 | ||||
| 					size_t card_number = 0; | ||||
| 					AppleII::Card::Select select = AppleII::Card::None; | ||||
| 					Apple::II::Card::Select select = Apple::II::Card::None; | ||||
| 
 | ||||
| 					if(address >= 0xc100) { | ||||
| 						/*
 | ||||
| @@ -704,20 +721,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 								0xCn00 to 0xCnff: card n. | ||||
| 						*/ | ||||
| 						card_number = (address - 0xc100) >> 8; | ||||
| 						select = AppleII::Card::Device; | ||||
| 						select = Apple::II::Card::Device; | ||||
| 					} else { | ||||
| 						/*
 | ||||
| 							Decode the area conventionally used by cards for registers: | ||||
| 								C0n0 to C0nF: card n - 8. | ||||
| 						*/ | ||||
| 						card_number = (address - 0xc090) >> 4; | ||||
| 						select = AppleII::Card::IO; | ||||
| 						select = Apple::II::Card::IO; | ||||
| 					} | ||||
| 
 | ||||
| 					// If the selected card is a just-in-time card, update the just-in-time cards,
 | ||||
| 					// and then message it specifically.
 | ||||
| 					const bool is_read = isReadOperation(operation); | ||||
| 					AppleII::Card *const target = cards_[static_cast<size_t>(card_number)].get(); | ||||
| 					Apple::II::Card *const target = cards_[static_cast<size_t>(card_number)].get(); | ||||
| 					if(target && !is_every_cycle_card(target)) { | ||||
| 						update_just_in_time_cards(); | ||||
| 						target->perform_bus_operation(select, is_read, address, value); | ||||
| @@ -728,7 +745,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					for(const auto &card: every_cycle_cards_) { | ||||
| 						card->run_for(Cycles(1), is_stretched_cycle); | ||||
| 						card->perform_bus_operation( | ||||
| 							(card == target) ? select : AppleII::Card::None, | ||||
| 							(card == target) ? select : Apple::II::Card::None, | ||||
| 							is_read, address, value); | ||||
| 					} | ||||
| 					has_updated_cards = true; | ||||
| @@ -740,7 +757,32 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 				const bool is_read = isReadOperation(operation); | ||||
| 				for(const auto &card: every_cycle_cards_) { | ||||
| 					card->run_for(Cycles(1), is_stretched_cycle); | ||||
| 					card->perform_bus_operation(AppleII::Card::None, is_read, address, value); | ||||
| 					card->perform_bus_operation(Apple::II::Card::None, is_read, address, value); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			// Update the card lists if any mutations are due.
 | ||||
| 			if(card_lists_are_dirty_) { | ||||
| 				card_lists_are_dirty_ = false; | ||||
| 
 | ||||
| 				// There's only one counter of time since update
 | ||||
| 				// for just-in-time cards. If something new is
 | ||||
| 				// transitioning, that needs to be zeroed.
 | ||||
| 				if(card_became_just_in_time_) { | ||||
| 					card_became_just_in_time_ = false; | ||||
| 					update_just_in_time_cards(); | ||||
| 				} | ||||
| 
 | ||||
| 				// Clear the two lists and repopulate.
 | ||||
| 				every_cycle_cards_.clear(); | ||||
| 				just_in_time_cards_.clear(); | ||||
| 				for(const auto &card: cards_) { | ||||
| 					if(!card) continue; | ||||
| 					if(is_every_cycle_card(card.get())) { | ||||
| 						every_cycle_cards_.push_back(card.get()); | ||||
| 					} else { | ||||
| 						just_in_time_cards_.push_back(card.get()); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| @@ -786,7 +828,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					case Key::Right:		value = 0x15;	break; | ||||
| 					case Key::Down:			value = 0x0a;	break; | ||||
| 					case Key::Up:			value = 0x0b;	break; | ||||
| 					case Key::BackSpace:	value = 0x7f;	break; | ||||
| 					case Key::Backspace:	value = 0x7f;	break; | ||||
| 					default: return; | ||||
| 				} | ||||
| 			} | ||||
| @@ -809,7 +851,29 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 		} | ||||
| 
 | ||||
| 		void type_string(const std::string &string) override { | ||||
| 			string_serialiser_.reset(new Utility::StringSerialiser(string, true)); | ||||
| 			string_serialiser_ = std::make_unique<Utility::StringSerialiser>(string, true); | ||||
| 		} | ||||
| 
 | ||||
| 		// MARK:: Configuration options.
 | ||||
| 		std::vector<std::unique_ptr<Configurable::Option>> get_options() override { | ||||
| 			return Apple::II::get_options(); | ||||
| 		} | ||||
| 
 | ||||
| 		void set_selections(const Configurable::SelectionSet &selections_by_option) override { | ||||
| 			Configurable::Display display; | ||||
| 			if(Configurable::get_display(selections_by_option, display)) { | ||||
| 				set_video_signal_configurable(display); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		Configurable::SelectionSet get_accurate_selections() override { | ||||
| 			Configurable::SelectionSet selection_set; | ||||
| 			Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour); | ||||
| 			return selection_set; | ||||
| 		} | ||||
| 
 | ||||
| 		Configurable::SelectionSet get_user_friendly_selections() override { | ||||
| 			return get_accurate_selections(); | ||||
| 		} | ||||
| 
 | ||||
| 		// MARK: MediaTarget
 | ||||
| @@ -829,14 +893,15 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 		} | ||||
| 
 | ||||
| 		// MARK: JoystickMachine
 | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override { | ||||
| 		const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override { | ||||
| 			return joysticks_; | ||||
| 		} | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| using namespace AppleII; | ||||
| using namespace Apple::II; | ||||
| 
 | ||||
| Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { | ||||
| 	using Target = Analyser::Static::AppleII::Target; | ||||
| @@ -9,14 +9,18 @@ | ||||
| #ifndef AppleII_hpp | ||||
| #define AppleII_hpp | ||||
| 
 | ||||
| #include "../../Configurable/Configurable.hpp" | ||||
| #include "../../Analyser/Static/StaticAnalyser.hpp" | ||||
| #include "../ROMMachine.hpp" | ||||
| #include "../../../Configurable/Configurable.hpp" | ||||
| #include "../../../Analyser/Static/StaticAnalyser.hpp" | ||||
| #include "../../ROMMachine.hpp" | ||||
| 
 | ||||
| #include <memory> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace AppleII { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| 
 | ||||
| /// @returns The options available for an Apple II.
 | ||||
| std::vector<std::unique_ptr<Configurable::Option>> get_options(); | ||||
| 
 | ||||
| class Machine { | ||||
| 	public: | ||||
| @@ -26,6 +30,7 @@ class Machine { | ||||
| 		static Machine *AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||
| }; | ||||
| 
 | ||||
| }; | ||||
| } | ||||
| } | ||||
| 
 | ||||
| #endif /* AppleII_hpp */ | ||||
| @@ -9,11 +9,12 @@ | ||||
| #ifndef Card_h | ||||
| #define Card_h | ||||
| 
 | ||||
| #include "../../Processors/6502/6502.hpp" | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../Activity/Observer.hpp" | ||||
| #include "../../../Processors/6502/6502.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../Activity/Observer.hpp" | ||||
| 
 | ||||
| namespace AppleII { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| 
 | ||||
| /*!
 | ||||
| 	This provides a small subset of the interface offered to cards installed in | ||||
| @@ -39,6 +40,7 @@ namespace AppleII { | ||||
| */ | ||||
| class Card { | ||||
| 	public: | ||||
| 		virtual ~Card() {} | ||||
| 		enum Select: int { | ||||
| 			None	= 0,		// No select line is active
 | ||||
| 			IO		= 1 << 0,	// IO select is active
 | ||||
| @@ -81,10 +83,8 @@ class Card { | ||||
| 			will receive a perform_bus_operation every cycle. To reduce the number of | ||||
| 			virtual method calls, they **will not** receive run_for. run_for will propagate | ||||
| 			only to cards that register for IO and/or Device accesses only. | ||||
| 
 | ||||
| 
 | ||||
| 		*/ | ||||
| 		int get_select_constraints() { | ||||
| 		int get_select_constraints() const { | ||||
| 			return select_constraints_; | ||||
| 		} | ||||
| 
 | ||||
| @@ -108,6 +108,7 @@ class Card { | ||||
| 		} | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| #endif /* Card_h */ | ||||
| @@ -8,15 +8,25 @@ | ||||
| 
 | ||||
| #include "DiskIICard.hpp" | ||||
| 
 | ||||
| using namespace AppleII; | ||||
| using namespace Apple::II; | ||||
| 
 | ||||
| DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector) : diskii_(2045454) { | ||||
| 	const auto roms = rom_fetcher( | ||||
| 		"DiskII", | ||||
| 		{ | ||||
| 			is_16_sector ? "boot-16.rom" : "boot-13.rom", | ||||
| 			is_16_sector ? "state-machine-16.rom" : "state-machine-13.rom" | ||||
| 	std::vector<std::unique_ptr<std::vector<uint8_t>>> roms; | ||||
| 	if(is_16_sector) { | ||||
| 		roms = rom_fetcher({ | ||||
| 			{"DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6}, | ||||
| 			{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } } | ||||
| 		}); | ||||
| 	} else { | ||||
| 		roms = rom_fetcher({ | ||||
| 			{"DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ff}, | ||||
| 			{"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 } | ||||
| 		}); | ||||
| 	} | ||||
| 	if(!roms[0] || !roms[1]) { | ||||
| 		throw ROMMachine::Error::MissingROMs; | ||||
| 	} | ||||
| 
 | ||||
| 	boot_ = std::move(*roms[0]); | ||||
| 	diskii_.set_state_machine(*roms[1]); | ||||
| 	set_select_constraints(None); | ||||
| @@ -42,7 +52,7 @@ void DiskIICard::perform_bus_operation(Select select, bool is_read, uint16_t add | ||||
| 
 | ||||
| void DiskIICard::run_for(Cycles cycles, int stretches) { | ||||
| 	if(diskii_clocking_preference_ == ClockingHint::Preference::None) return; | ||||
| 	diskii_.run_for(Cycles(cycles.as_int() * 2)); | ||||
| 	diskii_.run_for(Cycles(cycles.as_integral() * 2)); | ||||
| } | ||||
| 
 | ||||
| void DiskIICard::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive) { | ||||
| @@ -55,7 +65,7 @@ void DiskIICard::set_activity_observer(Activity::Observer *observer) { | ||||
| 
 | ||||
| void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) { | ||||
| 	diskii_clocking_preference_ = preference; | ||||
| 	set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : 0); | ||||
| 	set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : None); | ||||
| } | ||||
| 
 | ||||
| Storage::Disk::Drive &DiskIICard::get_drive(int drive) { | ||||
| @@ -10,17 +10,18 @@ | ||||
| #define DiskIICard_hpp | ||||
| 
 | ||||
| #include "Card.hpp" | ||||
| #include "../ROMMachine.hpp" | ||||
| #include "../../ROMMachine.hpp" | ||||
| 
 | ||||
| #include "../../Components/DiskII/DiskII.hpp" | ||||
| #include "../../Storage/Disk/Disk.hpp" | ||||
| #include "../../ClockReceiver/ClockingHintSource.hpp" | ||||
| #include "../../../Components/DiskII/DiskII.hpp" | ||||
| #include "../../../Storage/Disk/Disk.hpp" | ||||
| #include "../../../ClockReceiver/ClockingHintSource.hpp" | ||||
| 
 | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace AppleII { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| 
 | ||||
| class DiskIICard: public Card, public ClockingHint::Observer { | ||||
| 	public: | ||||
| @@ -41,6 +42,7 @@ class DiskIICard: public Card, public ClockingHint::Observer { | ||||
| 		ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| #endif /* DiskIICard_hpp */ | ||||
| @@ -8,25 +8,22 @@ | ||||
| 
 | ||||
| #include "Video.hpp" | ||||
| 
 | ||||
| using namespace AppleII::Video; | ||||
| using namespace Apple::II::Video; | ||||
| 
 | ||||
| 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), | ||||
| 	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.
 | ||||
| 	crt_->set_video_signal(Outputs::CRT::VideoSignal::Composite); | ||||
| 	crt_->set_visible_area(Outputs::CRT::Rect(0.118f, 0.122f, 0.77f, 0.77f)); | ||||
| 	crt_->set_immediate_default_phase(0.0f); | ||||
| 	crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour); | ||||
| 	crt_.set_visible_area(Outputs::Display::Rect(0.118f, 0.122f, 0.77f, 0.77f)); | ||||
| 
 | ||||
| 	// TODO: there seems to be some sort of bug whereby switching modes can cause
 | ||||
| 	// a signal discontinuity that knocks phase out of whack. So it isn't safe to
 | ||||
| 	// use default_colour_bursts elsewhere, though it otherwise should be. If/when
 | ||||
| 	// it is, start doing so and return to setting the immediate phase up here.
 | ||||
| //	crt_.set_immediate_default_phase(0.5f);
 | ||||
| 
 | ||||
| 	character_zones[0].xor_mask = 0; | ||||
| 	character_zones[0].address_mask = 0x3f; | ||||
| @@ -46,8 +43,12 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) : | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| Outputs::CRT::CRT *VideoBase::get_crt() { | ||||
| 	return crt_.get(); | ||||
| void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||
| 	crt_.set_scan_target(scan_target); | ||||
| } | ||||
| 
 | ||||
| void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) { | ||||
| 	crt_.set_display_type(display_type); | ||||
| } | ||||
| 
 | ||||
| /*
 | ||||
| @@ -9,14 +9,15 @@ | ||||
| #ifndef Video_hpp | ||||
| #define Video_hpp | ||||
| 
 | ||||
| #include "../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../ClockReceiver/ClockDeferrer.hpp" | ||||
| #include "../../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../ClockReceiver/DeferredQueue.hpp" | ||||
| 
 | ||||
| #include <array> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace AppleII { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| namespace Video { | ||||
| 
 | ||||
| class BusHandler { | ||||
| @@ -36,8 +37,11 @@ class VideoBase { | ||||
| 	public: | ||||
| 		VideoBase(bool is_iie, std::function<void(Cycles)> &&target); | ||||
| 
 | ||||
| 		/// @returns The CRT this video feed is feeding.
 | ||||
| 		Outputs::CRT::CRT *get_crt(); | ||||
| 		/// Sets the scan target.
 | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target); | ||||
| 
 | ||||
| 		/// Sets the type of output.
 | ||||
| 		void set_display_type(Outputs::Display::DisplayType); | ||||
| 
 | ||||
| 		/*
 | ||||
| 			Descriptions for the setters below are taken verbatim from | ||||
| @@ -146,7 +150,7 @@ class VideoBase { | ||||
| 		void set_character_rom(const std::vector<uint8_t> &); | ||||
| 
 | ||||
| 	protected: | ||||
| 		std::unique_ptr<Outputs::CRT::CRT> crt_; | ||||
| 		Outputs::CRT::CRT crt_; | ||||
| 
 | ||||
| 		// State affecting output video stream generation.
 | ||||
| 		uint8_t *pixel_pointer_ = nullptr; | ||||
| @@ -199,7 +203,7 @@ class VideoBase { | ||||
| 		std::array<uint8_t, 40> auxiliary_stream_; | ||||
| 
 | ||||
| 		bool is_iie_ = false; | ||||
| 		static const int flash_length = 8406; | ||||
| 		static constexpr int flash_length = 8406; | ||||
| 
 | ||||
| 		// Describes the current text mode mapping from in-memory character index
 | ||||
| 		// to output character.
 | ||||
| @@ -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; | ||||
| 
 | ||||
| 		// Maintain a ClockDeferrer for delayed mode switches.
 | ||||
| 		ClockDeferrer<Cycles> deferrer_; | ||||
| 		// Maintain a DeferredQueue for delayed mode switches.
 | ||||
| 		DeferredQueue<Cycles> deferrer_; | ||||
| }; | ||||
| 
 | ||||
| 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
 | ||||
| 
 | ||||
| 			// Determine column at offset.
 | ||||
| 			int mapped_column = column_ + offset.as_int(); | ||||
| 			int mapped_column = column_ + int(offset.as_integral()); | ||||
| 
 | ||||
| 			// Map that backwards from the internal pixels-at-start generation to pixels-at-end
 | ||||
| 			// (so what was column 0 is now column 25).
 | ||||
| @@ -311,7 +315,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 		bool get_is_vertical_blank(Cycles offset) { | ||||
| 			// Map that backwards from the internal pixels-at-start generation to pixels-at-end
 | ||||
| 			// (so what was column 0 is now column 25).
 | ||||
| 			int mapped_column = column_ + offset.as_int(); | ||||
| 			int mapped_column = column_ + int(offset.as_integral()); | ||||
| 
 | ||||
| 			// Map that backwards from the internal pixels-at-start generation to pixels-at-end
 | ||||
| 			// (so what was column 0 is now column 25).
 | ||||
| @@ -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. | ||||
| 			*/ | ||||
| 			static const int first_sync_line = 220;		// A complete guess. Information needed.
 | ||||
| 			static const int first_sync_column = 49;	// Also a guess.
 | ||||
| 			static const int sync_length = 4;			// One of the two likely candidates.
 | ||||
| 			constexpr int first_sync_line = 220;		// A complete guess. Information needed.
 | ||||
| 			constexpr int first_sync_column = 49;	// Also a guess.
 | ||||
| 			constexpr int sync_length = 4;			// One of the two likely candidates.
 | ||||
| 
 | ||||
| 			int int_cycles = cycles.as_int(); | ||||
| 			int int_cycles = int(cycles.as_integral()); | ||||
| 			while(int_cycles) { | ||||
| 				const int cycles_this_line = std::min(65 - column_, int_cycles); | ||||
| 				const int ending_column = column_ + cycles_this_line; | ||||
| 				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
 | ||||
| 					// pulses (and hencce keep hsync approximately where it should be during vsync).
 | ||||
| 					const int blank_start = std::max(first_sync_column - sync_length, column_); | ||||
| 					const int blank_end = std::min(first_sync_column, ending_column); | ||||
| 					if(blank_end > blank_start) { | ||||
| 						if(blank_start > column_) { | ||||
| 							crt_->output_sync(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) { | ||||
| 							crt_->output_sync(static_cast<unsigned int>(ending_column - blank_end) * 14); | ||||
| 							crt_.output_sync((ending_column - blank_end) * 14); | ||||
| 						} | ||||
| 					} else { | ||||
| 						crt_->output_sync(static_cast<unsigned int>(cycles_this_line) * 14); | ||||
| 						crt_.output_sync(cycles_this_line * 14); | ||||
| 					} | ||||
| 				} else { | ||||
| 					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_), | ||||
| 							&base_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) { | ||||
| @@ -402,7 +406,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 						// remain where they would naturally be but auxiliary
 | ||||
| 						// graphics appear to the left of that.
 | ||||
| 						if(!column_) { | ||||
| 							pixel_pointer_ = crt_->allocate_write_area(568); | ||||
| 							pixel_pointer_ = crt_.begin_data(568); | ||||
| 							graphics_carry_ = 0; | ||||
| 							was_double_ = true; | ||||
| 						} | ||||
| @@ -413,7 +417,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 							const int pixel_row = row_ & 7; | ||||
| 
 | ||||
| 							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 + 1] = | ||||
| 								pixel_pointer_[pixel_start*14 + 2] = | ||||
| @@ -424,6 +428,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 							} | ||||
| 							was_double_ = is_double; | ||||
| 
 | ||||
| 							if(pixel_pointer_) { | ||||
| 								switch(line_mode) { | ||||
| 									case GraphicsMode::Text: | ||||
| 										output_text( | ||||
| @@ -487,8 +492,10 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 
 | ||||
| 									default: break; | ||||
| 								} | ||||
| 							} | ||||
| 
 | ||||
| 							if(pixel_end == 40) { | ||||
| 								if(pixel_pointer_) { | ||||
| 									if(was_double_) { | ||||
| 										pixel_pointer_[560] = pixel_pointer_[561] = pixel_pointer_[562] = pixel_pointer_[563] = | ||||
| 										pixel_pointer_[564] = pixel_pointer_[565] = pixel_pointer_[566] = pixel_pointer_[567] = 0; | ||||
| @@ -498,14 +505,15 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 										else | ||||
| 											pixel_pointer_[567] = 0; | ||||
| 									} | ||||
| 								} | ||||
| 
 | ||||
| 								crt_->output_data(568, 568); | ||||
| 								crt_.output_data(568, 568); | ||||
| 								pixel_pointer_ = nullptr; | ||||
| 							} | ||||
| 						} | ||||
| 					} else { | ||||
| 						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) { | ||||
| 						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)) { | ||||
| 						crt_->output_sync(sync_length*14); | ||||
| 						crt_.output_sync(sync_length*14); | ||||
| 					} | ||||
| 
 | ||||
| 					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_end = std::min(first_sync_column + sync_length + 4, ending_column); | ||||
| 						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_); | ||||
| @@ -536,7 +544,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 					} | ||||
| 
 | ||||
| 					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
 | ||||
| 					// count explicitly but is promised.
 | ||||
| 					crt_->output_blank(2); | ||||
| 					// count explicitly but is promised. If this is a vertical sync line, output sync
 | ||||
| 					// instead of blank, taking that to be the default level.
 | ||||
| 					if(is_vertical_sync_line) { | ||||
| 						crt_.output_sync(2); | ||||
| 					} else { | ||||
| 						crt_.output_blank(2); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| @@ -588,6 +601,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 		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