mirror of
				https://github.com/TomHarte/CLK.git
				synced 2025-10-31 20:16:07 +00:00 
			
		
		
		
	Compare commits
	
		
			1758 Commits
		
	
	
		
			2019-03-10
			...
			2020-05-10
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d964ebd4c1 | ||
|  | 9458963311 | ||
|  | 44690b1066 | ||
|  | c41028cdc7 | ||
|  | 64c62c16fb | ||
|  | afef4f05fe | ||
|  | fc0f290c85 | ||
|  | 81d70ee325 | ||
|  | 6dc7a4471d | ||
|  | fcb8bd00b6 | ||
|  | 05c3f2a30d | ||
|  | 25996ce180 | ||
|  | 3729bddb2a | ||
|  | 4136428db3 | ||
|  | 31c6faf3c8 | ||
|  | 5c1ae40a9c | ||
|  | 4c6d0f7fa0 | ||
|  | 40b60fe5d4 | ||
|  | eed357abb4 | ||
|  | 8f541602c1 | ||
|  | 668f4b77f3 | ||
|  | 303965fbb8 | ||
|  | 792aed242d | ||
|  | dc5654b941 | ||
|  | e51e2425cc | ||
|  | 95c6b9b55d | ||
|  | ea25ead19d | ||
|  | 24100ec3b0 | ||
|  | 32437fbf8b | ||
|  | 5219a86a41 | ||
|  | e12dc5d894 | ||
|  | 75315406bb | ||
|  | ea42fe638a | ||
|  | 744211cec0 | ||
|  | 1a4321d7d0 | ||
|  | b943441901 | ||
|  | 0505b82384 | ||
|  | c9fb5721cd | ||
|  | 386a7ca442 | ||
|  | e929d5d819 | ||
|  | 94614ae4c3 | ||
|  | 1223c99e0f | ||
|  | 1ff5ea0a6e | ||
|  | 9d2691d1d2 | ||
|  | e4ef2c68bb | ||
|  | 7fffafdfd4 | ||
|  | 5896288edd | ||
|  | c4135fad2b | ||
|  | 1f34214fb3 | ||
|  | f899af0eef | ||
|  | 9f0c8bcae7 | ||
|  | 2bc36a6cde | ||
|  | ee10fe3d2c | ||
|  | a424e867f9 | ||
|  | f52b40396a | ||
|  | cd2ab70a58 | ||
|  | a5d1941d28 | ||
|  | 65a3783dd2 | ||
|  | b9b5c2a3bc | ||
|  | 12c618642e | ||
|  | 6ebc93c995 | ||
|  | 6d4e29c851 | ||
|  | b3979e2fda | ||
|  | 983c32bf75 | ||
|  | 9e3614066a | ||
|  | c7ad6b1b50 | ||
|  | 676dcf7fbb | ||
|  | 50d725330c | ||
|  | 2886dd1dae | ||
|  | 40424ac38b | ||
|  | a4d3865394 | ||
|  | 0ac99e8d42 | ||
|  | bdce1c464a | ||
|  | 475d75c16a | ||
|  | 32fd1897d0 | ||
|  | 39e6a28730 | ||
|  | 3852e119aa | ||
|  | f19fd7c166 | ||
|  | 100fddcee1 | ||
|  | 99fa86a67e | ||
|  | 6568c29c54 | ||
|  | c54bbc5a04 | ||
|  | 92d0c466c2 | ||
|  | 020c760976 | ||
|  | cdfd7de221 | ||
|  | 3da2e91acf | ||
|  | 3948304172 | ||
|  | 4a295cd95e | ||
|  | 6f7c8b35c5 | ||
|  | e58ba27c00 | ||
|  | 0aceddd088 | ||
|  | 30ff399218 | ||
|  | a7e63b61eb | ||
|  | b13b0d9311 | ||
|  | d8380dc3e2 | ||
|  | d805e9a8f0 | ||
|  | aa45142728 | ||
|  | 09d1aed3a5 | ||
|  | a1f80b5142 | ||
|  | cb1970ebab | ||
|  | d3fbdba77c | ||
|  | 632d797c9d | ||
|  | 559a2d81c1 | ||
|  | 7a5f23c0a5 | ||
|  | 84b115f15f | ||
|  | a0d14f4030 | ||
|  | dd6769bfbc | ||
|  | 027af5acca | ||
|  | db4b71fc9a | ||
|  | d9e41d42b5 | ||
|  | 0ed7d257e1 | ||
|  | 335a68396f | ||
|  | 84cdf6130f | ||
|  | b0abc4f7bb | ||
|  | ab81d1093d | ||
|  | e4d4e4e002 | ||
|  | cc357a6afa | ||
|  | dfc1c7d358 | ||
|  | 7ed8e33622 | ||
|  | 474822e83d | ||
|  | fe3942c5b3 | ||
|  | f417fa82a4 | ||
|  | c4b114133a | ||
|  | 2f4b0c2b9a | ||
|  | a491650c8b | ||
|  | 6805acd74f | ||
|  | 95c68c76e1 | ||
|  | 60aa383c95 | ||
|  | edc553fa1d | ||
|  | 4f2ebad8e0 | ||
|  | 1810ef60be | ||
|  | f720a6201b | ||
|  | cfb75b58ca | ||
|  | 4fbe983527 | ||
|  | 272383cac7 | ||
|  | 39380c63cb | ||
|  | ea26f4f7bf | ||
|  | 5fd2be3c8e | ||
|  | 2320b5c1fe | ||
|  | e5cbdfc67c | ||
|  | 894d196b64 | ||
|  | af037649c3 | ||
|  | cfca3e2507 | ||
|  | 7a12a0149a | ||
|  | fcdc1bfbd0 | ||
|  | d1d14ba9a0 | ||
|  | 0e502f6d5c | ||
|  | d3bac57d6a | ||
|  | bd1b4b8a9f | ||
|  | 38d81c394f | ||
|  | 72103a4adb | ||
|  | e6bae261c4 | ||
|  | 5edb0c0ee7 | ||
|  | 442ce403f9 | ||
|  | 7398cb44e2 | ||
|  | 15d54dfb4c | ||
|  | 9087bb9b08 | ||
|  | 0c689e85a5 | ||
|  | 75f2b0487e | ||
|  | 5a1bae8a9c | ||
|  | 129bc485bf | ||
|  | 69277bbb27 | ||
|  | b8b335f67d | ||
|  | eef7868199 | ||
|  | 23aa7ea85f | ||
|  | c1b69fd091 | ||
|  | 7ab7efdbc1 | ||
|  | b8ebdc012f | ||
|  | 9995d776de | ||
|  | c6f35c9aac | ||
|  | 615ea2f573 | ||
|  | 311458f41f | ||
|  | b2a381d401 | ||
|  | ffc1b0ff29 | ||
|  | ead2823322 | ||
|  | a7e1920597 | ||
|  | ec6664f590 | ||
|  | 8c6ca89da2 | ||
|  | b6e81242e7 | ||
|  | f9ca443667 | ||
|  | 394ee61c78 | ||
|  | 1d40aa687e | ||
|  | 8e3bf0dbca | ||
|  | 2031a33edf | ||
|  | fc3d3c76f8 | ||
|  | 880bed04f5 | ||
|  | f9c8470b20 | ||
|  | 36acc2dddd | ||
|  | a59963b6a0 | ||
|  | cab4bead72 | ||
|  | 1a2872c815 | ||
|  | f27e0a141d | ||
|  | 52f644c4f1 | ||
|  | 06c08a0574 | ||
|  | 724e2e6d27 | ||
|  | fd052189ca | ||
|  | 044a2b67e1 | ||
|  | 7e8b86e9bb | ||
|  | ce80825abb | ||
|  | a99bb3ba6d | ||
|  | 3428e9887d | ||
|  | 5a8fcac4dc | ||
|  | 6a9b14f7d1 | ||
|  | a74d8bd6e8 | ||
|  | 3c70f056ed | ||
|  | a546880a65 | ||
|  | 238145f27f | ||
|  | 0502e6be67 | ||
|  | 6a8c6f5a06 | ||
|  | 5248475e73 | ||
|  | d6c6b9bdb8 | ||
|  | 7bf04d5338 | ||
|  | 9668ec789a | ||
|  | ead32fb6b2 | ||
|  | 2ee24d29e5 | ||
|  | a560601338 | ||
|  | a51fe70498 | ||
|  | e47aa7653b | ||
|  | 58b8dfb929 | ||
|  | 462a76dd96 | ||
|  | 3758ec79ac | ||
|  | a0311858f9 | ||
|  | f08d500fd6 | ||
|  | df76d57c47 | ||
|  | 0ef953a1ea | ||
|  | 05cbed6b6c | ||
|  | 9225c4ef70 | ||
|  | 32136b75cd | ||
|  | 1f41d9c5f5 | ||
|  | dc47a2b7d7 | ||
|  | 1a539521f2 | ||
|  | 2db30a91c6 | ||
|  | b2c07b3110 | ||
|  | 90e6bef6d7 | ||
|  | 535634daca | ||
|  | 575d0da4d1 | ||
|  | ed18092088 | ||
|  | 611182910a | ||
|  | 9273e9b6ed | ||
|  | 77c0cc8b5f | ||
|  | 0705a99ea0 | ||
|  | 560394fead | ||
|  | 86a09b5e7d | ||
|  | 32b2026734 | ||
|  | b33f568fdd | ||
|  | 6e4bd4f505 | ||
|  | b971e2a42c | ||
|  | 3c103506c9 | ||
|  | 41d2062342 | ||
|  | 672c59f970 | ||
|  | 99229df017 | ||
|  | 346d80e30b | ||
|  | 54b3e511e9 | ||
|  | f25683ebec | ||
|  | d5e781e8e1 | ||
|  | 4572c86f0f | ||
|  | 8a5c4e384a | ||
|  | 4594a3c02b | ||
|  | bd45c1c963 | ||
|  | 5f8bb92f36 | ||
|  | 3f64cdaff8 | ||
|  | 7ac0ea8529 | ||
|  | a3569d7201 | ||
|  | 01faffd5bf | ||
|  | 26de5be07c | ||
|  | 87474d5916 | ||
|  | a366077509 | ||
|  | 06163165d9 | ||
|  | ec82c075be | ||
|  | 3b0df172a7 | ||
|  | 7058dbc3cc | ||
|  | b64de89d2d | ||
|  | 8878396339 | ||
|  | da6d5e2e24 | ||
|  | 18bb90329a | ||
|  | 604bb50adf | ||
|  | e4887c0c56 | ||
|  | 3097c4ccae | ||
|  | 7959d243f6 | ||
|  | 79dd402bc8 | ||
|  | 3f3229851b | ||
|  | 989628a024 | ||
|  | e0475343f5 | ||
|  | da0a9113d4 | ||
|  | cf7ab97451 | ||
|  | 2370575eb5 | ||
|  | 825b68e5c4 | ||
|  | 851cba0b25 | ||
|  | f0ec168ac7 | ||
|  | fa933952f7 | ||
|  | ba6e23784c | ||
|  | 614032198e | ||
|  | 3715e6b48a | ||
|  | 91e7400bbb | ||
|  | a8d082c7d2 | ||
|  | 95756f9716 | ||
|  | a5e1765ce4 | ||
|  | f43c31da1f | ||
|  | 95d0adf10e | ||
|  | 2e1b245cd8 | ||
|  | 5400c47f07 | ||
|  | 4153442703 | ||
|  | 5e4b721e97 | ||
|  | aca41ac089 | ||
|  | 01a883e669 | ||
|  | 1e4356f83a | ||
|  | 545a6177bb | ||
|  | 50d356be2f | ||
|  | 9835e800ec | ||
|  | 5242362f31 | ||
|  | 808e4e8537 | ||
|  | 43740a4b2f | ||
|  | f99d672237 | ||
|  | 337cb4fb86 | ||
|  | 90856a0e7a | ||
|  | ea1c8a3b81 | ||
|  | d55d077a95 | ||
|  | f760a68173 | ||
|  | e66a3523b6 | ||
|  | 89d6b85b83 | ||
|  | e02d109864 | ||
|  | 743981e9ad | ||
|  | 49b8e771b5 | ||
|  | dde672701f | ||
|  | 9ca2d8f9f2 | ||
|  | fd786412aa | ||
|  | eb88c7cfba | ||
|  | e1892ff370 | ||
|  | 763159a6f6 | ||
|  | 6810a6ee58 | ||
|  | 65e6c3a9fe | ||
|  | dcbbf988c1 | ||
|  | 199cafebcf | ||
|  | 555d807d76 | ||
|  | 003c6ad11b | ||
|  | dc77d87427 | ||
|  | cfc44cf778 | ||
|  | 3df99788ff | ||
|  | 3600d2d193 | ||
|  | 5f661adb7f | ||
|  | 109d072cb6 | ||
|  | 0c1c5a0ab8 | ||
|  | e01c66fd65 | ||
|  | 9f32fa7f5b | ||
|  | 91a3d42919 | ||
|  | 3cb6bbf771 | ||
|  | 452e281009 | ||
|  | 3da948db52 | ||
|  | 0c2f77305f | ||
|  | 05bcd73f82 | ||
|  | 654f5b0478 | ||
|  | 886d923e30 | ||
|  | 6624cb7a78 | ||
|  | 6147134423 | ||
|  | bf6bc7c684 | ||
|  | 0b0a7e241b | ||
|  | 705d14259c | ||
|  | f1cd35fa16 | ||
|  | 6bda4034c6 | ||
|  | b04daca98e | ||
|  | 85dcdbfe9e | ||
|  | 24340d1d4f | ||
|  | 6ae42d07a7 | ||
|  | 2ea1e059a8 | ||
|  | b5d6126a2d | ||
|  | dac217c98c | ||
|  | c26c8992ae | ||
|  | b76a5870b3 | ||
|  | 7c0f3bb237 | ||
|  | f615d096ca | ||
|  | 09132306e4 | ||
|  | f95b07efea | ||
|  | 14d976eecb | ||
|  | e1cbad0b6d | ||
|  | e7410b8ed8 | ||
|  | 5caf74b930 | ||
|  | b41920990f | ||
|  | 709c229cd7 | ||
|  | 01fd1b1a2e | ||
|  | 96769c52f6 | ||
|  | cf9729c74f | ||
|  | 0f2783075f | ||
|  | 256f4a6679 | ||
|  | 0310f94f0c | ||
|  | 085529ed72 | ||
|  | 8aabf1b374 | ||
|  | ff39f71ca0 | ||
|  | 019474300d | ||
|  | af976b8b3d | ||
|  | f3db1a0c60 | ||
|  | ce28213a5e | ||
|  | f9ce50d2bb | ||
|  | ee16095863 | ||
|  | f0a6e0f3d5 | ||
|  | 8c4fb0f688 | ||
|  | baa51853c4 | ||
|  | 0e29c6b0ab | ||
|  | 1b27eedf6b | ||
|  | 8b1f183198 | ||
|  | 4766ec55fe | ||
|  | c5edc879b6 | ||
|  | 65309e60c4 | ||
|  | 5c4623e9f7 | ||
|  | 2c0cab9e4d | ||
|  | d0117556d1 | ||
|  | b1ff031b54 | ||
|  | 7e8405e68a | ||
|  | c8fd00217d | ||
|  | 9d340599a6 | ||
|  | 8e094598ca | ||
|  | 189122ab84 | ||
|  | 4b53f6a9f0 | ||
|  | 561e149058 | ||
|  | 5975fc8e63 | ||
|  | 7316a3aa88 | ||
|  | 50be991415 | ||
|  | 52e49439a6 | ||
|  | 6bcdd3177d | ||
|  | 83dbd257e1 | ||
|  | b514756272 | ||
|  | 7e4c13c43e | ||
|  | 79bb0f8222 | ||
|  | 43bf6aca67 | ||
|  | 03d23aad41 | ||
|  | c398aa60c1 | ||
|  | 9666193c67 | ||
|  | 3f57020b00 | ||
|  | 294e09f275 | ||
|  | ba516387ba | ||
|  | 2103e1b470 | ||
|  | 7bac439e95 | ||
|  | 9136917f00 | ||
|  | 6802318784 | ||
|  | 428d141bc9 | ||
|  | a86fb33789 | ||
|  | beefb70f75 | ||
|  | 3c6a00dc3c | ||
|  | 8404409c0d | ||
|  | a5f285b4ce | ||
|  | 9d97a294a7 | ||
|  | 56448373ae | ||
|  | a71c5946f0 | ||
|  | e7fff6e123 | ||
|  | 82e5def7c4 | ||
|  | d97a073d1b | ||
|  | e74f37d6ed | ||
|  | 3aa2c297a2 | ||
|  | 290db67f09 | ||
|  | 4de121142b | ||
|  | 3c760e585a | ||
|  | 8adb2283b5 | ||
|  | cb61e84868 | ||
|  | 8349005c4b | ||
|  | a2847f4f8e | ||
|  | add3ebcb44 | ||
|  | 98daad45c7 | ||
|  | 1b4b6b0aee | ||
|  | 8f94da9daf | ||
|  | 357137918d | ||
|  | b0f7b762af | ||
|  | da3ee381f4 | ||
|  | d27d14d2b0 | ||
|  | b0326530d6 | ||
|  | c2bd5be51a | ||
|  | 84f5feab70 | ||
|  | 4b2c68c3d3 | ||
|  | 5391a699a4 | ||
|  | f3f8345e5e | ||
|  | c755411636 | ||
|  | f02759b76b | ||
|  | f34ddce28f | ||
|  | 50348c9fe7 | ||
|  | 3bfeebf2a1 | ||
|  | dca79ea10e | ||
|  | b7fd4de32f | ||
|  | 78d08278ed | ||
|  | d4be052e76 | ||
|  | d674fd0e67 | ||
|  | 229b7b36ed | ||
|  | 8a8b8db5d1 | ||
|  | d30f83871d | ||
|  | 1422f8a93a | ||
|  | f0da75f8e9 | ||
|  | cb8a7a4137 | ||
|  | efd684dc56 | ||
|  | aeac6b5888 | ||
|  | 9bb294a023 | ||
|  | 1972ca00a4 | ||
|  | 6a185a574a | ||
|  | c606931c93 | ||
|  | 93cecf0882 | ||
|  | aac3d27c10 | ||
|  | 99122efbbc | ||
|  | 30e856b9e4 | ||
|  | 91fae86e73 | ||
|  | f5c194386c | ||
|  | 98f7662185 | ||
|  | 62c3720c97 | ||
|  | 6b08239199 | ||
|  | f258fc2971 | ||
|  | 6b84ae3095 | ||
|  | 5dd8c677f1 | ||
|  | 1cbcd5355f | ||
|  | 9799250f2c | ||
|  | ecb5807ec0 | ||
|  | 942986aadc | ||
|  | 1f539822ee | ||
|  | fab35b360a | ||
|  | 80fcf5b5c0 | ||
|  | b3b2e18c4b | ||
|  | 2d233b6358 | ||
|  | 83ed36eb08 | ||
|  | 89f4032ffc | ||
|  | 8c90ec4636 | ||
|  | 514141f8c5 | ||
|  | 8e3a618619 | ||
|  | 6df6af09de | ||
|  | f42655a0fc | ||
|  | f81a7f0faf | ||
|  | 2b4c924399 | ||
|  | 64517a02b7 | ||
|  | b4befd57a9 | ||
|  | 2c742a051e | ||
|  | 6595f8f527 | ||
|  | 985b36da73 | ||
|  | cdb31b1c2b | ||
|  | 6a44936a7c | ||
|  | 45afb13a54 | ||
|  | 3ced31043a | ||
|  | 7361e7ec34 | ||
|  | 533729638c | ||
|  | 9f30be1c13 | ||
|  | 09289f383d | ||
|  | 20b25ce866 | ||
|  | c1bae49a92 | ||
|  | b3f806201b | ||
|  | 9f2f547932 | ||
|  | f0d5bbecf2 | ||
|  | 3d7ef43293 | ||
|  | 4578b65487 | ||
|  | a28c52c250 | ||
|  | e4349f5e05 | ||
|  | 7b2777ac08 | ||
|  | 0fbcbfc61b | ||
|  | 3ab4fb8c79 | ||
|  | 42a9585321 | ||
|  | 937cba8978 | ||
|  | 627d3c28ea | ||
|  | 19ddfae6d6 | ||
|  | 56ebd08af0 | ||
|  | 7de1181213 | ||
|  | c7a5b054db | ||
|  | ca12ba297b | ||
|  | 7abf527084 | ||
|  | c0b5bfe726 | ||
|  | 414b0cc234 | ||
|  | 134e828336 | ||
|  | 455e831b87 | ||
|  | 617e0bada9 | ||
|  | 7dea99b1cc | ||
|  | 42ccf48966 | ||
|  | 2f8078db22 | ||
|  | ea45ae78d1 | ||
|  | cb7d6c185c | ||
|  | 5be30b1f7b | ||
|  | 0bf1a87f4c | ||
|  | b184426f2b | ||
|  | 2456fb120d | ||
|  | 23ed9ad2de | ||
|  | 017681a97c | ||
|  | 153f60735d | ||
|  | 90b899c00e | ||
|  | 5ce8d7c0e5 | ||
|  | c11fe25537 | ||
|  | c4edd635c5 | ||
|  | 0a12893d63 | ||
|  | 8e777c299f | ||
|  | 09513ec14c | ||
|  | e23d1a2958 | ||
|  | 6449403f6a | ||
|  | c8fe66092b | ||
|  | b33218c61e | ||
|  | 8ce26e7182 | ||
|  | 47068ee081 | ||
|  | 5361ee2526 | ||
|  | 214b6a254a | ||
|  | 93f6964d8a | ||
|  | 13f11e071a | ||
|  | f7825dd2a2 | ||
|  | a9d1f5d925 | ||
|  | 2757e5d600 | ||
|  | 5026de9653 | ||
|  | 5fa8e046d8 | ||
|  | ec9357e080 | ||
|  | f8dd33b645 | ||
|  | de43e86310 | ||
|  | 314973a5ef | ||
|  | d26ce65236 | ||
|  | 1de4f179c0 | ||
|  | 3cb5684d95 | ||
|  | a9a92de954 | ||
|  | daacd6805e | ||
|  | 54fe01b532 | ||
|  | 42dd70dbff | ||
|  | e59de71d79 | ||
|  | a8ba3607b7 | ||
|  | 4205e95883 | ||
|  | f633cf4c3f | ||
|  | dfa6b11737 | ||
|  | 42926e72cc | ||
|  | 80cb06eb33 | ||
|  | 5068328a15 | ||
|  | adc2b77833 | ||
|  | 99415217dc | ||
|  | 48d519d475 | ||
|  | ed831e5912 | ||
|  | 1db7c7989b | ||
|  | b2bed82da6 | ||
|  | afae1443b4 | ||
|  | 0dae608da5 | ||
|  | 8a1fe99fa4 | ||
|  | ac604b30f3 | ||
|  | b035b92f33 | ||
|  | d25b48878c | ||
|  | 34a3790e11 | ||
|  | f3378f3e3e | ||
|  | 78accc1db1 | ||
|  | a756985e18 | ||
|  | 30e0d4aa30 | ||
|  | de72c66c64 | ||
|  | 6edd3c9698 | ||
|  | 5456a4a39d | ||
|  | 66d9b60b98 | ||
|  | 274867579b | ||
|  | a847654ef2 | ||
|  | 05d77d3297 | ||
|  | e9318efeb6 | ||
|  | 25da5ebdae | ||
|  | cf16f41939 | ||
|  | 08f2877382 | ||
|  | 6f4444d834 | ||
|  | 993dfeae1b | ||
|  | b4fd506361 | ||
|  | e5440a4146 | ||
|  | 57ce10418f | ||
|  | 47508d50a7 | ||
|  | 56cc191a8b | ||
|  | 2a1520c04e | ||
|  | 3d83f5ab49 | ||
|  | 0007dc23b3 | ||
|  | 416d68ab3a | ||
|  | ed7f171736 | ||
|  | 0e066f0f70 | ||
|  | 3e6f51f5cf | ||
|  | 797abae4b3 | ||
|  | 4605b1b264 | ||
|  | d802e8aee3 | ||
|  | 206ab380c7 | ||
|  | d85ae21b2f | ||
|  | 470cc572fd | ||
|  | f0d9d8542b | ||
|  | d2390fcb11 | ||
|  | 5ce612cb38 | ||
|  | ec7aa2d355 | ||
|  | 9464658d1e | ||
|  | a3e64cae41 | ||
|  | e969b386f1 | ||
|  | af9c0aca97 | ||
|  | 8a2ac87209 | ||
|  | 096b447b4b | ||
|  | 0d23f141d6 | ||
|  | 84167af54f | ||
|  | 8be26502c4 | ||
|  | ba2436206f | ||
|  | 60a9b260b1 | ||
|  | e603fc6aaa | ||
|  | 81cc278b98 | ||
|  | 4c068e9bb8 | ||
|  | f23c5ada31 | ||
|  | dc1abd874e | ||
|  | 1bf4686c59 | ||
|  | a500fbcd73 | ||
|  | d0ef41f11e | ||
|  | adf6723bf6 | ||
|  | 37e26c0c37 | ||
|  | ac1575be27 | ||
|  | 923287bf01 | ||
|  | 77fe14cdb3 | ||
|  | c00ae7ce6a | ||
|  | d5b2e6514a | ||
|  | fc7f46006e | ||
|  | 41503d7253 | ||
|  | f88c942fef | ||
|  | 4bcf217324 | ||
|  | f6f2b4b90f | ||
|  | 95b5db4d87 | ||
|  | de4403e021 | ||
|  | 0a405d1c06 | ||
|  | 768b3709b8 | ||
|  | 7cc5d0b209 | ||
|  | c2646a415f | ||
|  | e1c7a140d0 | ||
|  | 7cd11ecb7f | ||
|  | 4dd235f677 | ||
|  | a7cfb840ef | ||
|  | acfe2c63b8 | ||
|  | b192381928 | ||
|  | c785797da6 | ||
|  | 0408592ada | ||
|  | 407cc78c78 | ||
|  | 4536c6a224 | ||
|  | 0ed87c61bd | ||
|  | 332f0d6167 | ||
|  | 08a27bdec7 | ||
|  | 288cabbad1 | ||
|  | 7ff57f8cdf | ||
|  | 06edeea866 | ||
|  | 3c77d3bda0 | ||
|  | 72cb3a1cf6 | ||
|  | e0ceab6642 | ||
|  | 894066984c | ||
|  | c91495d068 | ||
|  | e787c03530 | ||
|  | b12136691a | ||
|  | c04d2f6c6e | ||
|  | 6990abc0d3 | ||
|  | 0ce5057fd9 | ||
|  | ade8df7217 | ||
|  | b98703bd5b | ||
|  | 82c984afa4 | ||
|  | 1202b0a65f | ||
|  | facc0a1976 | ||
|  | 25da8b7787 | ||
|  | 253dd84109 | ||
|  | 9d07765823 | ||
|  | 11de0e198f | ||
|  | f16f0897d5 | ||
|  | 3d4d45ef62 | ||
|  | 04c4f5f321 | ||
|  | fa900d22e8 | ||
|  | aee2890b25 | ||
|  | 40e1ec28fb | ||
|  | c6e2b1237c | ||
|  | efdd27a435 | ||
|  | 2c4f372872 | ||
|  | 74be876d72 | ||
|  | e8e166eec5 | ||
|  | 4ec8fa0d20 | ||
|  | b019c6f8dd | ||
|  | 6ec3c47cc0 | ||
|  | ccce127f13 | ||
|  | f4cfca0451 | ||
|  | eb287605f7 | ||
|  | 2026761a07 | ||
|  | 6a82c87320 | ||
|  | f0478225f0 | ||
|  | d6edfa5c6d | ||
|  | e7253a8713 | ||
|  | c6f6bc68e1 | ||
|  | ab34fad8ca | ||
|  | e4c77614c1 | ||
|  | 072b0266af | ||
|  | 7ae0902103 | ||
|  | 8e9428623e | ||
|  | 2c25135d8a | ||
|  | 3741cba88c | ||
|  | 860837d894 | ||
|  | cef07038c1 | ||
|  | 0bf61c9c99 | ||
|  | 837dfd1ab8 | ||
|  | 0204003680 | ||
|  | 5fc4e57db7 | ||
|  | c4fefe1eb3 | ||
|  | 77ef7dc8fc | ||
|  | e3abbc9966 | ||
|  | 70c6010fe0 | ||
|  | 8c736a639a | ||
|  | cc7ff1ec9e | ||
|  | 9d12ca691f | ||
|  | db03b03276 | ||
|  | 45375fb5d0 | ||
|  | d2324e413d | ||
|  | e0c15f43bb | ||
|  | 8b0d550b81 | ||
|  | d1259f829e | ||
|  | 7caef46c05 | ||
|  | 6902251d8b | ||
|  | 0b683b0360 | ||
|  | 5d5dc79f2c | ||
|  | 68f9084d9e | ||
|  | b7c407be10 | ||
|  | f45798faf0 | ||
|  | 7c66d7a13c | ||
|  | f9a35c6636 | ||
|  | 5e1570258d | ||
|  | fc8021c0b0 | ||
|  | c9cd56915e | ||
|  | 1fd19c5786 | ||
|  | ce66b5fd9c | ||
|  | 8aa425c9d8 | ||
|  | ec68bc5047 | ||
|  | 0971bbeebe | ||
|  | 9292f66c2d | ||
|  | f3e2e88986 | ||
|  | 6afefa107e | ||
|  | 0ce807805d | ||
|  | 41f3c29e30 | ||
|  | 6c75c60149 | ||
|  | 015f2101f8 | ||
|  | 35f1a7ab10 | ||
|  | 8df1eea955 | ||
|  | eeafdf2c03 | ||
|  | befe2c2929 | ||
|  | 46ec3510be | ||
|  | e9965c2738 | ||
|  | 48b0d8c329 | ||
|  | 07582cee4a | ||
|  | 4dbd2a805a | ||
|  | 20bf425f98 | ||
|  | 0567410bcf | ||
|  | 6d1e09ba55 | ||
|  | f40dbefa67 | ||
|  | f93cdd21de | ||
|  | e1dc3b1915 | ||
|  | cbf25a16dc | ||
|  | 14e790746b | ||
|  | bf7e9cfd62 | ||
|  | a67e0014a4 | ||
|  | c070f2100c | ||
|  | 75e34b4215 | ||
|  | a5bbf54a27 | ||
|  | 5309ac7c30 | ||
|  | 731dc350b4 | ||
|  | 635e18a50d | ||
|  | 4857ceb3eb | ||
|  | 1c154131f9 | ||
|  | fd02b6fc18 | ||
|  | 553f3b6d8b | ||
|  | 1135576a3a | ||
|  | a5057e6540 | ||
|  | 1d790ec2a9 | ||
|  | 0f2d72c436 | ||
|  | aa52652027 | ||
|  | 4a1fa8fc13 | ||
|  | 95d3b6e79f | ||
|  | 5f6711b72c | ||
|  | d44734d105 | ||
|  | 1aaa6331a0 | ||
|  | de1bfb4e24 | ||
|  | 0cb19421e8 | ||
|  | 92847037b3 | ||
|  | f4556ef6b0 | ||
|  | 4266264449 | ||
|  | 1aba1db62c | ||
|  | 0fc191c87d | ||
|  | dc4a0e4e3b | ||
|  | 3794d94b68 | ||
|  | 0082dc4411 | ||
|  | 22754683f8 | ||
|  | 909685d87d | ||
|  | 55710ea00e | ||
|  | 36a9a5288b | ||
|  | e89be6249d | ||
|  | ac39fd0235 | ||
|  | ecc0cea5a1 | ||
|  | eae11cbf17 | ||
|  | e96386f572 | ||
|  | a8d481a764 | ||
|  | 2207638287 | ||
|  | 872897029e | ||
|  | 51b4b5551d | ||
|  | 7a2de47f58 | ||
|  | f2f98ed60c | ||
|  | 77f14fa638 | ||
|  | f09a240e6c | ||
|  | 092a61f93e | ||
|  | e30ba58e0d | ||
|  | 7cb82fccc0 | ||
|  | ed9a5b0430 | ||
|  | 8f59a73425 | ||
|  | 91223b9ec8 | ||
|  | 83f5f0e2ad | ||
|  | cf37e9f5de | ||
|  | e4f7ead894 | ||
|  | 4134463094 | ||
|  | 83d73fb088 | ||
|  | 75c3e2dacd | ||
|  | cf07982a9b | ||
|  | 313aaa8f95 | ||
|  | 2e86dada1d | ||
|  | 696af5c3a6 | ||
|  | f08b38d0ae | ||
|  | 9a8352282d | ||
|  | 3d03cce6b1 | ||
|  | 34075a7674 | ||
|  | f79c87659f | ||
|  | c10b64e1c0 | ||
|  | 5d5fe52144 | ||
|  | d461331fd2 | ||
|  | ff62eb6dce | ||
|  | 374439693e | ||
|  | c4ef33b23f | ||
|  | a7ed357569 | ||
|  | 4e5b440145 | ||
|  | 2bd7be13b5 | ||
|  | 4b09d7c41d | ||
|  | 97d44129cb | ||
|  | b0f5f7bd37 | ||
|  | d1dd6876b5 | ||
|  | a59ec9e818 | ||
|  | 4ead905c3c | ||
|  | 127bb043e7 | ||
|  | 42ebe06474 | ||
|  | 74fe32da23 | ||
|  | 780916551f | ||
|  | 305b1211ba | ||
|  | 2cf52fb89c | ||
|  | 6e1b606adf | ||
|  | 3bb0bf9e14 | ||
|  | 87a6d22894 | ||
|  | 484a0ceeb8 | ||
|  | da1436abd2 | ||
|  | 125f781ced | ||
|  | c66c484c54 | ||
|  | 345b32d6e3 | ||
|  | 8b397626bf | ||
|  | 0da1881a07 | ||
|  | d4077afd30 | ||
|  | 95c45b5515 | ||
|  | 684644420a | ||
|  | 735586f5f8 | ||
|  | ddae086661 | ||
|  | 9c7aa5f3fc | ||
|  | 418cd07e17 | ||
|  | 2ae5739b8b | ||
|  | e095a622d3 | ||
|  | 9ab49065cd | ||
|  | ab50f17d87 | ||
|  | f5a2e180f9 | ||
|  | f2e1584275 | ||
|  | 0fd8813ddb | ||
|  | b69180ba01 | ||
|  | c352d8ae8c | ||
|  | 530e831064 | ||
|  | 3b165a78f2 | ||
|  | 8d87e9eb1c | ||
|  | f86dc082bb | ||
|  | d7982aa84e | ||
|  | 516d78f5a8 | ||
|  | 8b50a7d6e3 | ||
|  | 4bf81d3b90 | ||
|  | cd75978e4e | ||
|  | fda99d9c5f | ||
|  | c5ebf75351 | ||
|  | 2581b520af | ||
|  | 52e5296544 | ||
|  | d7ce2c26e8 | ||
|  | f88e1b1373 | ||
|  | 021d4dbaf1 | ||
|  | dbde8f2ee7 | ||
|  | 5d06930df4 | ||
|  | 7722596a3b | ||
|  | 1de1818ebb | ||
|  | 885f890df1 | ||
|  | e195497ab7 | ||
|  | fcd2143697 | ||
|  | 3f45cd2380 | ||
|  | a8a34497ff | ||
|  | 953423cc02 | ||
|  | a2ca887b99 | ||
|  | 3c5ae9cf8e | ||
|  | fe621d7e52 | ||
|  | 814bb4ec63 | ||
|  | e8bc254f3f | ||
|  | 3c146a3fb2 | ||
|  | b609ce6fcb | ||
|  | 929475d31e | ||
|  | f14d98452e | ||
|  | 9d17d48bca | ||
|  | 4ac3839185 | ||
|  | c089d1cd09 | ||
|  | cb85ec25cc | ||
|  | fbf95ec2b8 | ||
|  | 6adca98f34 | ||
|  | 48f4d8b875 | ||
|  | 7758f9d0a9 | ||
|  | 7112f0336c | ||
|  | 298694a881 | ||
|  | 7ff4594f09 | ||
|  | e8bd538182 | ||
|  | 8489e8650f | ||
|  | 114f81941e | ||
|  | 077c7d767f | ||
|  | 8f88addf9f | ||
|  | f28c124039 | ||
|  | a416bc0058 | ||
|  | e78b1dcf3c | ||
|  | 8a14f5d814 | ||
|  | e5f983fbac | ||
|  | 3e639e96e7 | ||
|  | 61993f0687 | ||
|  | 5f16fa8c08 | ||
|  | dcea9c9ab2 | ||
|  | e7bf0799b6 | ||
|  | e760421f6f | ||
|  | 8ea4c17315 | ||
|  | 2e24da4614 | ||
|  | e46601872b | ||
|  | 6d0e41b760 | ||
|  | 5a82df837d | ||
|  | 776b819a5a | ||
|  | 1783f6c84b | ||
|  | 2ef2c73efe | ||
|  | 55e003ccc1 | ||
|  | 3d54d55dbb | ||
|  | 72c0a631f7 | ||
|  | 1608a90d5d | ||
|  | 4f8a45a6ce | ||
|  | 4f0f1dcf18 | ||
|  | 839e51d92d | ||
|  | e470cf23d8 | ||
|  | 8d4a96683a | ||
|  | f53411a319 | ||
|  | 128a1da626 | ||
|  | 962275c22a | ||
|  | 3002ac8a4a | ||
|  | ff43674638 | ||
|  | 2f6c366668 | ||
|  | 2ce1f0a3b1 | ||
|  | 210129c3a1 | ||
|  | 934901447a | ||
|  | 960b289e70 | ||
|  | 243e40cd79 | ||
|  | c849188016 | ||
|  | 87e8dade2f | ||
|  | 6fc5b4e825 | ||
|  | 00ce7f8ae0 | ||
|  | 6e0e9afe2f | ||
|  | cb0d994827 | ||
|  | bee782234a | ||
|  | 64dad35026 | ||
|  | cbd1a8cf78 | ||
|  | a4ab0afce3 | ||
|  | 1c7e0f3c9d | ||
|  | 318cdb41ea | ||
|  | 2f8e31bc8b | ||
|  | 310c722cc0 | ||
|  | 25956bd90f | ||
|  | 1a60ced61b | ||
|  | 081316c071 | ||
|  | eafbc12cc1 | ||
|  | ca08716c52 | ||
|  | 30cef1ee22 | ||
|  | 5598802439 | ||
|  | 1c6720b0db | ||
|  | 404b088199 | ||
|  | 7d61df238a | ||
|  | c86db12f1c | ||
|  | ce2e85af8b | ||
|  | 2d82855f26 | ||
|  | faec516a2c | ||
|  | 8e274ec5d0 | ||
|  | bb1a0a0b76 | ||
|  | 252650808d | ||
|  | e3d9254555 | ||
|  | 90cf99b626 | ||
|  | 955e909e61 | ||
|  | 8339e2044c | ||
|  | 0e0c789b02 | ||
|  | 7e001c1d03 | ||
|  | 9047932b81 | ||
|  | f668e4a54c | ||
|  | ce1c96d68c | ||
|  | 0f67e490e8 | ||
|  | 895c315fa5 | ||
|  | a90a74a512 | ||
|  | 3e1286cbef | ||
|  | 949c1e1668 | ||
|  | bbd4e4d3dc | ||
|  | 4c5f596533 | ||
|  | 4859d3781b | ||
|  | bac0461f7f | ||
|  | f26a200d78 | ||
|  | 28ccb7b54e | ||
|  | b6e4c8209b | ||
|  | 16548f0765 | ||
|  | 6a80832140 | ||
|  | c6cf0e914b | ||
|  | 35b1a55c12 | ||
|  | e3794c0c0e | ||
|  | f88dc23c71 | ||
|  | 0e293e4983 | ||
|  | e334abfe20 | ||
|  | fd2fbe0e59 | ||
|  | 330b27d085 | ||
|  | 478f2533b5 | ||
|  | b96972a4b9 | ||
|  | f2b083f4de | ||
|  | 80f6d665d9 | ||
|  | a07488cf1b | ||
|  | d67c5145c0 | ||
|  | 5e76d593af | ||
|  | 83393e8e91 | ||
|  | e08a64d455 | ||
|  | b93f9b3973 | ||
|  | 9c517d07d4 | ||
|  | f45de5b87a | ||
|  | 011d76175c | ||
|  | 96005261c7 | ||
|  | c8177af45a | ||
|  | 97eff5b16d | ||
|  | 917520fb1e | ||
|  | 335dda3d55 | ||
|  | 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 | ||
|  | 98aa597510 | ||
|  | de56d48b2f | ||
|  | 4aeb9a7c56 | ||
|  | b9b52b7c8b | 
							
								
								
									
										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 --fix-missing 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 | ||||
| @@ -11,20 +11,20 @@ | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| float ConfidenceCounter::get_confidence() { | ||||
| 	return static_cast<float>(hits_) / static_cast<float>(hits_ + misses_); | ||||
| 	return float(hits_) / float(hits_ + misses_); | ||||
| } | ||||
|  | ||||
| void ConfidenceCounter::add_hit() { | ||||
| 	hits_++; | ||||
| 	++hits_; | ||||
| } | ||||
|  | ||||
| void ConfidenceCounter::add_miss() { | ||||
| 	misses_++; | ||||
| 	++misses_; | ||||
| } | ||||
|  | ||||
| void ConfidenceCounter::add_equivocal() { | ||||
| 	if(hits_ > misses_) { | ||||
| 		hits_++; | ||||
| 		misses_++; | ||||
| 		++hits_; | ||||
| 		++misses_; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -22,7 +22,7 @@ namespace Dynamic { | ||||
| class ConfidenceCounter: public ConfidenceSource { | ||||
| 	public: | ||||
| 		/*! @returns The computed probability, based on the history of events. */ | ||||
| 		float get_confidence() override; | ||||
| 		float get_confidence() final; | ||||
|  | ||||
| 		/*! Records an event that implies this is the appropriate class: pushes probability up towards 1.0. */ | ||||
| 		void add_hit(); | ||||
|   | ||||
| @@ -32,11 +32,11 @@ class ConfidenceSummary: public ConfidenceSource { | ||||
| 			const std::vector<float> &weights); | ||||
|  | ||||
| 		/*! @returns The weighted sum of all sources. */ | ||||
| 		float get_confidence() override; | ||||
| 		float get_confidence() final; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<ConfidenceSource *> sources_; | ||||
| 		std::vector<float> weights_; | ||||
| 		const std::vector<ConfidenceSource *> sources_; | ||||
| 		const std::vector<float> weights_; | ||||
| 		float weight_sum_; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,87 +0,0 @@ | ||||
| // | ||||
| //  MultiCRTMachine.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 29/01/2018. | ||||
| //  Copyright 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MultiCRTMachine.hpp" | ||||
|  | ||||
| #include <condition_variable> | ||||
| #include <mutex> | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| 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); | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Machine *)> &function) { | ||||
| 	// Apply a blunt force parallelisation of the machines; each run_for is dispatched | ||||
| 	// to a separate queue and this queue will block until all are done. | ||||
| 	volatile std::size_t outstanding_machines; | ||||
| 	std::condition_variable condition; | ||||
| 	std::mutex mutex; | ||||
| 	{ | ||||
| 		std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| 		std::lock_guard<std::mutex> lock(mutex); | ||||
| 		outstanding_machines = machines_.size(); | ||||
|  | ||||
| 		for(std::size_t index = 0; index < machines_.size(); ++index) { | ||||
| 			CRTMachine::Machine *crt_machine = machines_[index]->crt_machine(); | ||||
| 			queues_[index].enqueue([&mutex, &condition, crt_machine, function, &outstanding_machines]() { | ||||
| 				if(crt_machine) function(crt_machine); | ||||
|  | ||||
| 				std::lock_guard<std::mutex> lock(mutex); | ||||
| 				outstanding_machines--; | ||||
| 				condition.notify_all(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	std::unique_lock<std::mutex> lock(mutex); | ||||
| 	condition.wait(lock, [&outstanding_machines] { return !outstanding_machines; }); | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::perform_serial(const std::function<void (::CRTMachine::Machine *)> &function) { | ||||
| 	std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		CRTMachine::Machine *const crt_machine = machine->crt_machine(); | ||||
| 		if(crt_machine) function(crt_machine); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||
| 	scan_target_ = scan_target; | ||||
|  | ||||
| 	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() { | ||||
| 	return speaker_; | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::run_for(Time::Seconds duration) { | ||||
| 	perform_parallel([=](::CRTMachine::Machine *machine) { | ||||
| 		if(machine->get_confidence() >= 0.01f) machine->run_for(duration); | ||||
| 	}); | ||||
|  | ||||
| 	if(delegate_) delegate_->multi_crt_did_run_machines(); | ||||
| } | ||||
|  | ||||
| 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()); | ||||
| 	} | ||||
| } | ||||
| @@ -12,6 +12,81 @@ | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| class MultiStruct: public Reflection::Struct { | ||||
| 	public: | ||||
| 		MultiStruct(const std::vector<Configurable::Device *> &devices) : devices_(devices) { | ||||
| 			for(auto device: devices) { | ||||
| 				options_.emplace_back(device->get_options()); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		void apply() { | ||||
| 			auto options = options_.begin(); | ||||
| 			for(auto device: devices_) { | ||||
| 				device->set_options(*options); | ||||
| 				++options; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		std::vector<std::string> all_keys() const final { | ||||
| 			std::set<std::string> keys; | ||||
| 			for(auto &options: options_) { | ||||
| 				const auto new_keys = options->all_keys(); | ||||
| 				keys.insert(new_keys.begin(), new_keys.end()); | ||||
| 			} | ||||
| 			return std::vector<std::string>(keys.begin(), keys.end()); | ||||
| 		} | ||||
|  | ||||
| 		std::vector<std::string> values_for(const std::string &name) const final { | ||||
| 			std::set<std::string> values; | ||||
| 			for(auto &options: options_) { | ||||
| 				const auto new_values = options->values_for(name); | ||||
| 				values.insert(new_values.begin(), new_values.end()); | ||||
| 			} | ||||
| 			return std::vector<std::string>(values.begin(), values.end()); | ||||
| 		} | ||||
|  | ||||
| 		const std::type_info *type_of(const std::string &name) const final { | ||||
| 			for(auto &options: options_) { | ||||
| 				auto info = options->type_of(name); | ||||
| 				if(info) return info; | ||||
| 			} | ||||
| 			return nullptr; | ||||
| 		} | ||||
|  | ||||
| 		const void *get(const std::string &name) const final { | ||||
| 			for(auto &options: options_) { | ||||
| 				auto value = options->get(name); | ||||
| 				if(value) return value; | ||||
| 			} | ||||
| 			return nullptr; | ||||
| 		} | ||||
|  | ||||
| 		void set(const std::string &name, const void *value) final { | ||||
| 			const auto safe_type = type_of(name); | ||||
| 			if(!safe_type) return; | ||||
|  | ||||
| 			// Set this property only where the child's type is the same as that | ||||
| 			// which was returned from here for type_of. | ||||
| 			for(auto &options: options_) { | ||||
| 				const auto type = options->type_of(name); | ||||
| 				if(!type) continue; | ||||
|  | ||||
| 				if(*type == *safe_type) { | ||||
| 					options->set(name, value); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		const std::vector<Configurable::Device *> &devices_; | ||||
| 		std::vector<std::unique_ptr<Reflection::Struct>> options_; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| MultiConfigurable::MultiConfigurable(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	for(const auto &machine: machines) { | ||||
| 		Configurable::Device *device = machine->configurable_device(); | ||||
| @@ -19,46 +94,11 @@ MultiConfigurable::MultiConfigurable(const std::vector<std::unique_ptr<::Machine | ||||
| 	} | ||||
| } | ||||
|  | ||||
| std::vector<std::unique_ptr<Configurable::Option>> MultiConfigurable::get_options() { | ||||
| 	std::vector<std::unique_ptr<Configurable::Option>> options; | ||||
|  | ||||
| 	// Produce the list of unique options. | ||||
| 	for(const auto &device : devices_) { | ||||
| 		std::vector<std::unique_ptr<Configurable::Option>> device_options = device->get_options(); | ||||
| 		for(auto &option : device_options) { | ||||
| 			if(std::find(options.begin(), options.end(), option) == options.end()) { | ||||
| 				options.push_back(std::move(option)); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return options; | ||||
| void MultiConfigurable::set_options(const std::unique_ptr<Reflection::Struct> &str) { | ||||
| 	const auto options = dynamic_cast<MultiStruct *>(str.get()); | ||||
| 	options->apply(); | ||||
| } | ||||
|  | ||||
| void MultiConfigurable::set_selections(const Configurable::SelectionSet &selection_by_option) { | ||||
| 	for(const auto &device : devices_) { | ||||
| 		device->set_selections(selection_by_option); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Configurable::SelectionSet MultiConfigurable::get_accurate_selections() { | ||||
| 	Configurable::SelectionSet set; | ||||
| 	for(const auto &device : devices_) { | ||||
| 		Configurable::SelectionSet device_set = device->get_accurate_selections(); | ||||
| 		for(auto &selection : device_set) { | ||||
| 			set.insert(std::move(selection)); | ||||
| 		} | ||||
| 	} | ||||
| 	return set; | ||||
| } | ||||
|  | ||||
| Configurable::SelectionSet MultiConfigurable::get_user_friendly_selections() { | ||||
| 	Configurable::SelectionSet set; | ||||
| 	for(const auto &device : devices_) { | ||||
| 		Configurable::SelectionSet device_set = device->get_user_friendly_selections(); | ||||
| 		for(auto &selection : device_set) { | ||||
| 			set.insert(std::move(selection)); | ||||
| 		} | ||||
| 	} | ||||
| 	return set; | ||||
| std::unique_ptr<Reflection::Struct> MultiConfigurable::get_options() { | ||||
| 	return std::make_unique<MultiStruct>(devices_); | ||||
| } | ||||
|   | ||||
| @@ -10,6 +10,7 @@ | ||||
| #define MultiConfigurable_hpp | ||||
|  | ||||
| #include "../../../../Machines/DynamicMachine.hpp" | ||||
| #include "../../../../Configurable/Configurable.hpp" | ||||
|  | ||||
| #include <memory> | ||||
| #include <vector> | ||||
| @@ -28,10 +29,8 @@ class MultiConfigurable: public Configurable::Device { | ||||
| 		MultiConfigurable(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		// Below is the standard Configurable::Device interface; see there for documentation. | ||||
| 		std::vector<std::unique_ptr<Configurable::Option>> get_options() override; | ||||
| 		void set_selections(const Configurable::SelectionSet &selection_by_option) override; | ||||
| 		Configurable::SelectionSet get_accurate_selections() override; | ||||
| 		Configurable::SelectionSet get_user_friendly_selections() override; | ||||
| 		void set_options(const std::unique_ptr<Reflection::Struct> &options) final; | ||||
| 		std::unique_ptr<Reflection::Struct> get_options() final; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<Configurable::Device *> devices_; | ||||
|   | ||||
| @@ -16,7 +16,7 @@ namespace { | ||||
|  | ||||
| class MultiJoystick: public Inputs::Joystick { | ||||
| 	public: | ||||
| 		MultiJoystick(std::vector<JoystickMachine::Machine *> &machines, std::size_t index) { | ||||
| 		MultiJoystick(std::vector<MachineTypes::JoystickMachine *> &machines, std::size_t index) { | ||||
| 			for(const auto &machine: machines) { | ||||
| 				const auto &joysticks = machine->get_joysticks(); | ||||
| 				if(joysticks.size() >= index) { | ||||
| @@ -25,7 +25,7 @@ class MultiJoystick: public Inputs::Joystick { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		std::vector<Input> &get_inputs() override { | ||||
| 		std::vector<Input> &get_inputs() final { | ||||
| 			if(inputs.empty()) { | ||||
| 				for(const auto &joystick: joysticks_) { | ||||
| 					std::vector<Input> joystick_inputs = joystick->get_inputs(); | ||||
| @@ -40,19 +40,19 @@ class MultiJoystick: public Inputs::Joystick { | ||||
| 			return inputs; | ||||
| 		} | ||||
|  | ||||
| 		void set_input(const Input &digital_input, bool is_active) override { | ||||
| 		void set_input(const Input &digital_input, bool is_active) final { | ||||
| 			for(const auto &joystick: joysticks_) { | ||||
| 				joystick->set_input(digital_input, is_active); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		void set_input(const Input &digital_input, float value) override { | ||||
| 		void set_input(const Input &digital_input, float value) final { | ||||
| 			for(const auto &joystick: joysticks_) { | ||||
| 				joystick->set_input(digital_input, value); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		void reset_all_inputs() override { | ||||
| 		void reset_all_inputs() final { | ||||
| 			for(const auto &joystick: joysticks_) { | ||||
| 				joystick->reset_all_inputs(); | ||||
| 			} | ||||
| @@ -67,9 +67,9 @@ class MultiJoystick: public Inputs::Joystick { | ||||
|  | ||||
| MultiJoystickMachine::MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	std::size_t total_joysticks = 0; | ||||
| 	std::vector<JoystickMachine::Machine *> joystick_machines; | ||||
| 	std::vector<MachineTypes::JoystickMachine *> joystick_machines; | ||||
| 	for(const auto &machine: machines) { | ||||
| 		JoystickMachine::Machine *joystick_machine = machine->joystick_machine(); | ||||
| 		auto joystick_machine = machine->joystick_machine(); | ||||
| 		if(joystick_machine) { | ||||
| 			joystick_machines.push_back(joystick_machine); | ||||
| 			total_joysticks = std::max(total_joysticks, joystick_machine->get_joysticks().size()); | ||||
| @@ -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_; | ||||
| } | ||||
|   | ||||
| @@ -23,12 +23,12 @@ namespace Dynamic { | ||||
| 	Makes a static internal copy of the list of machines; makes no guarantees about the | ||||
| 	order of delivered messages. | ||||
| */ | ||||
| class MultiJoystickMachine: public JoystickMachine::Machine { | ||||
| class MultiJoystickMachine: public MachineTypes::JoystickMachine { | ||||
| 	public: | ||||
| 		MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		// Below is the standard JoystickMachine::Machine interface; see there for documentation. | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override; | ||||
| 		const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_; | ||||
|   | ||||
| @@ -13,7 +13,7 @@ using namespace Analyser::Dynamic; | ||||
| MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) : | ||||
| 	keyboard_(machines_) { | ||||
| 	for(const auto &machine: machines) { | ||||
| 		KeyboardMachine::Machine *keyboard_machine = machine->keyboard_machine(); | ||||
| 		auto keyboard_machine = machine->keyboard_machine(); | ||||
| 		if(keyboard_machine) machines_.push_back(keyboard_machine); | ||||
| 	} | ||||
| } | ||||
| @@ -36,11 +36,19 @@ void MultiKeyboardMachine::type_string(const std::string &string) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool MultiKeyboardMachine::can_type(char c) { | ||||
| 	bool can_type = true; | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		can_type &= machine->can_type(c); | ||||
| 	} | ||||
| 	return can_type; | ||||
| } | ||||
|  | ||||
| Inputs::Keyboard &MultiKeyboardMachine::get_keyboard() { | ||||
| 	return keyboard_; | ||||
| } | ||||
|  | ||||
| MultiKeyboardMachine::MultiKeyboard::MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines) | ||||
| MultiKeyboardMachine::MultiKeyboard::MultiKeyboard(const std::vector<::MachineTypes::KeyboardMachine *> &machines) | ||||
| 	: machines_(machines) { | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		observed_keys_.insert(machine->get_keyboard().observed_keys().begin(), machine->get_keyboard().observed_keys().end()); | ||||
| @@ -48,10 +56,12 @@ MultiKeyboardMachine::MultiKeyboard::MultiKeyboard(const std::vector<::KeyboardM | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiKeyboardMachine::MultiKeyboard::set_key_pressed(Key key, char value, bool is_pressed) { | ||||
| bool MultiKeyboardMachine::MultiKeyboard::set_key_pressed(Key key, char value, bool is_pressed) { | ||||
| 	bool was_consumed = false; | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		machine->get_keyboard().set_key_pressed(key, value, is_pressed); | ||||
| 		was_consumed |= machine->get_keyboard().set_key_pressed(key, value, is_pressed); | ||||
| 	} | ||||
| 	return was_consumed; | ||||
| } | ||||
|  | ||||
| void MultiKeyboardMachine::MultiKeyboard::reset_all_keys() { | ||||
|   | ||||
| @@ -24,21 +24,21 @@ namespace Dynamic { | ||||
| 	Makes a static internal copy of the list of machines; makes no guarantees about the | ||||
| 	order of delivered messages. | ||||
| */ | ||||
| class MultiKeyboardMachine: public KeyboardMachine::Machine { | ||||
| class MultiKeyboardMachine: public MachineTypes::KeyboardMachine { | ||||
| 	private: | ||||
| 		std::vector<::KeyboardMachine::Machine *> machines_; | ||||
| 		std::vector<MachineTypes::KeyboardMachine *> machines_; | ||||
|  | ||||
| 		class MultiKeyboard: public Inputs::Keyboard { | ||||
| 			public: | ||||
| 				MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines); | ||||
| 				MultiKeyboard(const std::vector<MachineTypes::KeyboardMachine *> &machines); | ||||
|  | ||||
| 				void set_key_pressed(Key key, char value, bool is_pressed) override; | ||||
| 				void reset_all_keys() override; | ||||
| 				const std::set<Key> &observed_keys() override; | ||||
| 				bool is_exclusive() override; | ||||
| 				bool set_key_pressed(Key key, char value, bool is_pressed) final; | ||||
| 				void reset_all_keys() final; | ||||
| 				const std::set<Key> &observed_keys() final; | ||||
| 				bool is_exclusive() final; | ||||
|  | ||||
| 			private: | ||||
| 				const std::vector<::KeyboardMachine::Machine *> &machines_; | ||||
| 				const std::vector<MachineTypes::KeyboardMachine *> &machines_; | ||||
| 				std::set<Key> observed_keys_; | ||||
| 				bool is_exclusive_ = false; | ||||
| 		}; | ||||
| @@ -48,10 +48,11 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine { | ||||
| 		MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		// Below is the standard KeyboardMachine::Machine interface; see there for documentation. | ||||
| 		void clear_all_keys() override; | ||||
| 		void set_key_state(uint16_t key, bool is_pressed) override; | ||||
| 		void type_string(const std::string &) override; | ||||
| 		Inputs::Keyboard &get_keyboard() override; | ||||
| 		void clear_all_keys() final; | ||||
| 		void set_key_state(uint16_t key, bool is_pressed) final; | ||||
| 		void type_string(const std::string &) final; | ||||
| 		bool can_type(char c) final; | ||||
| 		Inputs::Keyboard &get_keyboard() final; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,7 @@ using namespace Analyser::Dynamic; | ||||
|  | ||||
| MultiMediaTarget::MultiMediaTarget(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	for(const auto &machine: machines) { | ||||
| 		MediaTarget::Machine *media_target = machine->media_target(); | ||||
| 		auto media_target = machine->media_target(); | ||||
| 		if(media_target) targets_.push_back(media_target); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -24,15 +24,15 @@ namespace Dynamic { | ||||
| 	Makes a static internal copy of the list of machines; makes no guarantees about the | ||||
| 	order of delivered messages. | ||||
| */ | ||||
| struct MultiMediaTarget: public MediaTarget::Machine { | ||||
| struct MultiMediaTarget: public MachineTypes::MediaTarget { | ||||
| 	public: | ||||
| 		MultiMediaTarget(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		// Below is the standard MediaTarget::Machine interface; see there for documentation. | ||||
| 		bool insert_media(const Analyser::Static::Media &media) override; | ||||
| 		bool insert_media(const Analyser::Static::Media &media) final; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<MediaTarget::Machine *> targets_; | ||||
| 		std::vector<MachineTypes::MediaTarget *> targets_; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										105
									
								
								Analyser/Dynamic/MultiMachine/Implementation/MultiProducer.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								Analyser/Dynamic/MultiMachine/Implementation/MultiProducer.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| // | ||||
| //  MultiProducer.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 29/01/2018. | ||||
| //  Copyright 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MultiProducer.hpp" | ||||
|  | ||||
| #include <condition_variable> | ||||
| #include <mutex> | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| // MARK: - MultiInterface | ||||
|  | ||||
| template <typename MachineType> | ||||
| void MultiInterface<MachineType>::perform_parallel(const std::function<void(MachineType *)> &function) { | ||||
| 	// Apply a blunt force parallelisation of the machines; each run_for is dispatched | ||||
| 	// to a separate queue and this queue will block until all are done. | ||||
| 	volatile std::size_t outstanding_machines; | ||||
| 	std::condition_variable condition; | ||||
| 	std::mutex mutex; | ||||
| 	{ | ||||
| 		std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| 		std::lock_guard<std::mutex> lock(mutex); | ||||
| 		outstanding_machines = machines_.size(); | ||||
|  | ||||
| 		for(std::size_t index = 0; index < machines_.size(); ++index) { | ||||
| 			const auto machine = ::Machine::get<MachineType>(*machines_[index].get()); | ||||
| 			queues_[index].enqueue([&mutex, &condition, machine, function, &outstanding_machines]() { | ||||
| 				if(machine) function(machine); | ||||
|  | ||||
| 				std::lock_guard<std::mutex> lock(mutex); | ||||
| 				outstanding_machines--; | ||||
| 				condition.notify_all(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	std::unique_lock<std::mutex> lock(mutex); | ||||
| 	condition.wait(lock, [&outstanding_machines] { return !outstanding_machines; }); | ||||
| } | ||||
|  | ||||
| template <typename MachineType> | ||||
| void MultiInterface<MachineType>::perform_serial(const std::function<void(MachineType *)> &function) { | ||||
| 	std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		const auto typed_machine = ::Machine::get<MachineType>(*machine.get()); | ||||
| 		if(typed_machine) function(typed_machine); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - MultiScanProducer | ||||
| void MultiScanProducer::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||
| 	scan_target_ = scan_target; | ||||
|  | ||||
| 	std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| 	const auto machine = machines_.front()->scan_producer(); | ||||
| 	if(machine) machine->set_scan_target(scan_target); | ||||
| } | ||||
|  | ||||
| Outputs::Display::ScanStatus MultiScanProducer::get_scan_status() const { | ||||
| 	std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| 	const auto machine = machines_.front()->scan_producer(); | ||||
| 	if(machine) return machine->get_scan_status(); | ||||
| 	return Outputs::Display::ScanStatus(); | ||||
| } | ||||
|  | ||||
| void MultiScanProducer::did_change_machine_order() { | ||||
| 	if(scan_target_) scan_target_->will_change_owner(); | ||||
|  | ||||
| 	perform_serial([](MachineTypes::ScanProducer *machine) { | ||||
| 		machine->set_scan_target(nullptr); | ||||
| 	}); | ||||
| 	std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| 	const auto machine = machines_.front()->scan_producer(); | ||||
| 	if(machine) machine->set_scan_target(scan_target_); | ||||
| } | ||||
|  | ||||
| // MARK: - MultiAudioProducer | ||||
| MultiAudioProducer::MultiAudioProducer(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::recursive_mutex &machines_mutex) : MultiInterface(machines, machines_mutex) { | ||||
| 	speaker_ = MultiSpeaker::create(machines); | ||||
| } | ||||
|  | ||||
| Outputs::Speaker::Speaker *MultiAudioProducer::get_speaker() { | ||||
| 	return speaker_; | ||||
| } | ||||
|  | ||||
| void MultiAudioProducer::did_change_machine_order() { | ||||
| 	if(speaker_) { | ||||
| 		speaker_->set_new_front_machine(machines_.front().get()); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - MultiTimedMachine | ||||
|  | ||||
| void MultiTimedMachine::run_for(Time::Seconds duration) { | ||||
| 	perform_parallel([duration](::MachineTypes::TimedMachine *machine) { | ||||
| 		if(machine->get_confidence() >= 0.01f) machine->run_for(duration); | ||||
| 	}); | ||||
|  | ||||
| 	if(delegate_) delegate_->did_run_machines(this); | ||||
| } | ||||
| @@ -1,16 +1,16 @@ | ||||
| //
 | ||||
| //  MultiCRTMachine.hpp
 | ||||
| //  MultiProducer.hpp
 | ||||
| //  Clock Signal
 | ||||
| //
 | ||||
| //  Created by Thomas Harte on 29/01/2018.
 | ||||
| //  Copyright 2018 Thomas Harte. All rights reserved.
 | ||||
| //
 | ||||
| 
 | ||||
| #ifndef MultiCRTMachine_hpp | ||||
| #define MultiCRTMachine_hpp | ||||
| #ifndef MultiProducer_hpp | ||||
| #define MultiProducer_hpp | ||||
| 
 | ||||
| #include "../../../../Concurrency/AsyncTaskQueue.hpp" | ||||
| #include "../../../../Machines/CRTMachine.hpp" | ||||
| #include "../../../../Machines/MachineTypes.hpp" | ||||
| #include "../../../../Machines/DynamicMachine.hpp" | ||||
| 
 | ||||
| #include "MultiSpeaker.hpp" | ||||
| @@ -22,6 +22,91 @@ | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
| 
 | ||||
| template <typename MachineType> class MultiInterface { | ||||
| 	public: | ||||
| 		MultiInterface(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::recursive_mutex &machines_mutex) : | ||||
| 			machines_(machines), machines_mutex_(machines_mutex), queues_(machines.size()) {} | ||||
| 
 | ||||
| 	protected: | ||||
| 		/*!
 | ||||
| 			Performs a parallel for operation across all machines, performing the supplied | ||||
| 			function on each and returning only once all applications have completed. | ||||
| 
 | ||||
| 			No guarantees are extended as to which thread operations will occur on. | ||||
| 		*/ | ||||
| 		void perform_parallel(const std::function<void(MachineType *)> &); | ||||
| 
 | ||||
| 		/*!
 | ||||
| 			Performs a serial for operation across all machines, performing the supplied | ||||
| 			function on each on the calling thread. | ||||
| 		*/ | ||||
| 		void perform_serial(const std::function<void(MachineType *)> &); | ||||
| 
 | ||||
| 	protected: | ||||
| 		const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_; | ||||
| 		std::recursive_mutex &machines_mutex_; | ||||
| 
 | ||||
| 	private: | ||||
| 		std::vector<Concurrency::AsyncTaskQueue> queues_; | ||||
| }; | ||||
| 
 | ||||
| class MultiTimedMachine: public MultiInterface<MachineTypes::TimedMachine>, public MachineTypes::TimedMachine { | ||||
| 	public: | ||||
| 		using MultiInterface::MultiInterface; | ||||
| 
 | ||||
| 		/*!
 | ||||
| 			Provides a mechanism by which a delegate can be informed each time a call to run_for has | ||||
| 			been received. | ||||
| 		*/ | ||||
| 		struct Delegate { | ||||
| 			virtual void did_run_machines(MultiTimedMachine *) = 0; | ||||
| 		}; | ||||
| 		/// Sets @c delegate as the receiver of delegate messages.
 | ||||
| 		void set_delegate(Delegate *delegate) { | ||||
| 			delegate_ = delegate; | ||||
| 		} | ||||
| 
 | ||||
| 		void run_for(Time::Seconds duration) final; | ||||
| 
 | ||||
| 	private: | ||||
| 		void run_for(const Cycles cycles) final {} | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| }; | ||||
| 
 | ||||
| class MultiScanProducer: public MultiInterface<MachineTypes::ScanProducer>, public MachineTypes::ScanProducer { | ||||
| 	public: | ||||
| 		using MultiInterface::MultiInterface; | ||||
| 
 | ||||
| 		/*!
 | ||||
| 			Informs the MultiScanProducer that the order of machines has changed; it | ||||
| 			uses this as an opportunity to synthesis any CRTMachine::Machine::Delegate messages that | ||||
| 			are necessary to bridge the gap between one machine and the next. | ||||
| 		*/ | ||||
| 		void did_change_machine_order(); | ||||
| 
 | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) final; | ||||
| 		Outputs::Display::ScanStatus get_scan_status() const final; | ||||
| 
 | ||||
| 	private: | ||||
| 		Outputs::Display::ScanTarget *scan_target_ = nullptr; | ||||
| }; | ||||
| 
 | ||||
| class MultiAudioProducer: public MultiInterface<MachineTypes::AudioProducer>, public MachineTypes::AudioProducer { | ||||
| 	public: | ||||
| 		MultiAudioProducer(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::recursive_mutex &machines_mutex); | ||||
| 
 | ||||
| 		/*!
 | ||||
| 			Informs the MultiAudio that the order of machines has changed; it | ||||
| 			uses this as an opportunity to switch speaker delegates as appropriate. | ||||
| 		*/ | ||||
| 		void did_change_machine_order(); | ||||
| 
 | ||||
| 		Outputs::Speaker::Speaker *get_speaker() final; | ||||
| 
 | ||||
| 	private: | ||||
| 		MultiSpeaker *speaker_ = nullptr; | ||||
| }; | ||||
| 
 | ||||
| /*!
 | ||||
| 	Provides a class that multiplexes the CRT machine interface to multiple machines. | ||||
| 
 | ||||
| @@ -29,60 +114,9 @@ namespace Dynamic { | ||||
| 	acquiring a supplied mutex. The owner should also call did_change_machine_order() | ||||
| 	if the order of machines changes. | ||||
| */ | ||||
| class MultiCRTMachine: public CRTMachine::Machine { | ||||
| 	public: | ||||
| 		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 | ||||
| 			uses this as an opportunity to synthesis any CRTMachine::Machine::Delegate messages that | ||||
| 			are necessary to bridge the gap between one machine and the next. | ||||
| 		*/ | ||||
| 		void did_change_machine_order(); | ||||
| 
 | ||||
| 		/*!
 | ||||
| 			Provides a mechanism by which a delegate can be informed each time a call to run_for has | ||||
| 			been received. | ||||
| 		*/ | ||||
| 		struct Delegate { | ||||
| 			virtual void multi_crt_did_run_machines() = 0; | ||||
| 		}; | ||||
| 		/// Sets @c delegate as the receiver of delegate messages.
 | ||||
| 		void set_delegate(Delegate *delegate) { | ||||
| 			delegate_ = delegate; | ||||
| 		} | ||||
| 
 | ||||
| 		// Below is the standard CRTMachine::Machine interface; see there for documentation.
 | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override; | ||||
| 		Outputs::Speaker::Speaker *get_speaker() override; | ||||
| 		void run_for(Time::Seconds duration) override; | ||||
| 
 | ||||
| 	private: | ||||
| 		void run_for(const Cycles cycles) override {} | ||||
| 		const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_; | ||||
| 		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 | ||||
| 			function on each and returning only once all applications have completed. | ||||
| 
 | ||||
| 			No guarantees are extended as to which thread operations will occur on. | ||||
| 		*/ | ||||
| 		void perform_parallel(const std::function<void(::CRTMachine::Machine *)> &); | ||||
| 
 | ||||
| 		/*!
 | ||||
| 			Performs a serial for operation across all machines, performing the supplied | ||||
| 			function on each on the calling thread. | ||||
| 		*/ | ||||
| 		void perform_serial(const std::function<void(::CRTMachine::Machine *)> &); | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| #endif /* MultiCRTMachine_hpp */ | ||||
| #endif /* MultiProducer_hpp */ | ||||
| @@ -13,7 +13,7 @@ using namespace Analyser::Dynamic; | ||||
| MultiSpeaker *MultiSpeaker::create(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	std::vector<Outputs::Speaker::Speaker *> speakers; | ||||
| 	for(const auto &machine: machines) { | ||||
| 		Outputs::Speaker::Speaker *speaker = machine->crt_machine()->get_speaker(); | ||||
| 		Outputs::Speaker::Speaker *speaker = machine->audio_producer()->get_speaker(); | ||||
| 		if(speaker) speakers.push_back(speaker); | ||||
| 	} | ||||
| 	if(speakers.empty()) return nullptr; | ||||
| @@ -34,12 +34,29 @@ float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum) | ||||
| 		ideal += speaker->get_ideal_clock_rate_in_range(minimum, maximum); | ||||
| 	} | ||||
|  | ||||
| 	return ideal / static_cast<float>(speakers_.size()); | ||||
| 	return ideal / float(speakers_.size()); | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::set_output_rate(float cycles_per_second, int buffer_size) { | ||||
| void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) { | ||||
| 	stereo_output_ = stereo; | ||||
| 	for(const auto &speaker: speakers_) { | ||||
| 		speaker->set_output_rate(cycles_per_second, buffer_size); | ||||
| 		speaker->set_computed_output_rate(cycles_per_second, buffer_size, stereo); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool MultiSpeaker::get_is_stereo() { | ||||
| 	// Return as stereo if any subspeaker is stereo. | ||||
| 	for(const auto &speaker: speakers_) { | ||||
| 		if(speaker->get_is_stereo()) { | ||||
| 			return true; | ||||
| 		} | ||||
| 	} | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::set_output_volume(float volume) { | ||||
| 	for(const auto &speaker: speakers_) { | ||||
| 		speaker->set_output_volume(volume); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -53,7 +70,7 @@ void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vec | ||||
| 		std::lock_guard<std::mutex> lock_guard(front_speaker_mutex_); | ||||
| 		if(speaker != front_speaker_) return; | ||||
| 	} | ||||
| 	delegate_->speaker_did_complete_samples(this, buffer); | ||||
| 	did_complete_samples(this, buffer, stereo_output_); | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::speaker_did_change_input_clock(Speaker *speaker) { | ||||
| @@ -68,7 +85,7 @@ void MultiSpeaker::speaker_did_change_input_clock(Speaker *speaker) { | ||||
| void MultiSpeaker::set_new_front_machine(::Machine::DynamicMachine *machine) { | ||||
| 	{ | ||||
| 		std::lock_guard<std::mutex> lock_guard(front_speaker_mutex_); | ||||
| 		front_speaker_ = machine->crt_machine()->get_speaker(); | ||||
| 		front_speaker_ = machine->audio_producer()->get_speaker(); | ||||
| 	} | ||||
| 	if(delegate_) { | ||||
| 		delegate_->speaker_did_change_input_clock(this); | ||||
|   | ||||
| @@ -39,18 +39,22 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker: | ||||
|  | ||||
| 		// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation. | ||||
| 		float get_ideal_clock_rate_in_range(float minimum, float maximum) override; | ||||
| 		void set_output_rate(float cycles_per_second, int buffer_size) override; | ||||
| 		void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) override; | ||||
| 		void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override; | ||||
| 		bool get_is_stereo() override; | ||||
| 		void set_output_volume(float) override; | ||||
|  | ||||
| 	private: | ||||
| 		void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) override; | ||||
| 		void speaker_did_change_input_clock(Speaker *speaker) override; | ||||
| 		void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final; | ||||
| 		void speaker_did_change_input_clock(Speaker *speaker) final; | ||||
| 		MultiSpeaker(const std::vector<Outputs::Speaker::Speaker *> &speakers); | ||||
|  | ||||
| 		std::vector<Outputs::Speaker::Speaker *> speakers_; | ||||
| 		Outputs::Speaker::Speaker *front_speaker_ = nullptr; | ||||
| 		Outputs::Speaker::Speaker::Delegate *delegate_ = nullptr; | ||||
| 		std::mutex front_speaker_mutex_; | ||||
|  | ||||
| 		bool stereo_output_ = false; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -16,69 +16,55 @@ using namespace Analyser::Dynamic; | ||||
| MultiMachine::MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines) : | ||||
| 	machines_(std::move(machines)), | ||||
| 	configurable_(machines_), | ||||
| 	crt_machine_(machines_, machines_mutex_), | ||||
| 	joystick_machine_(machines), | ||||
| 	timed_machine_(machines_, machines_mutex_), | ||||
| 	scan_producer_(machines_, machines_mutex_), | ||||
| 	audio_producer_(machines_, machines_mutex_), | ||||
| 	joystick_machine_(machines_), | ||||
| 	keyboard_machine_(machines_), | ||||
| 	media_target_(machines_) { | ||||
| 	crt_machine_.set_delegate(this); | ||||
| 	timed_machine_.set_delegate(this); | ||||
| } | ||||
|  | ||||
| Activity::Source *MultiMachine::activity_source() { | ||||
| 	return nullptr; // TODO | ||||
| } | ||||
|  | ||||
| MediaTarget::Machine *MultiMachine::media_target() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->media_target(); | ||||
| 	} else { | ||||
| 		return &media_target_; | ||||
| #define Provider(type, name, member)	\ | ||||
| 	type *MultiMachine::name() {	\ | ||||
| 		if(has_picked_) {	\ | ||||
| 			return machines_.front()->name();	\ | ||||
| 		} else {	\ | ||||
| 			return &member;	\ | ||||
| 		}	\ | ||||
| 	} | ||||
|  | ||||
| Provider(Configurable::Device, configurable_device, configurable_) | ||||
| Provider(MachineTypes::TimedMachine, timed_machine, timed_machine_) | ||||
| Provider(MachineTypes::ScanProducer, scan_producer, scan_producer_) | ||||
| Provider(MachineTypes::AudioProducer, audio_producer, audio_producer_) | ||||
| Provider(MachineTypes::JoystickMachine, joystick_machine, joystick_machine_) | ||||
| Provider(MachineTypes::KeyboardMachine, keyboard_machine, keyboard_machine_) | ||||
| Provider(MachineTypes::MediaTarget, media_target, media_target_) | ||||
|  | ||||
| MachineTypes::MouseMachine *MultiMachine::mouse_machine() { | ||||
| 	// TODO. | ||||
| 	return nullptr; | ||||
| } | ||||
|  | ||||
| CRTMachine::Machine *MultiMachine::crt_machine() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->crt_machine(); | ||||
| 	} else { | ||||
| 		return &crt_machine_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| JoystickMachine::Machine *MultiMachine::joystick_machine() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->joystick_machine(); | ||||
| 	} else { | ||||
| 		return &joystick_machine_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| KeyboardMachine::Machine *MultiMachine::keyboard_machine() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->keyboard_machine(); | ||||
| 	} else { | ||||
| 		return &keyboard_machine_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Configurable::Device *MultiMachine::configurable_device() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->configurable_device(); | ||||
| 	} else { | ||||
| 		return &configurable_; | ||||
| 	} | ||||
| } | ||||
| #undef Provider | ||||
|  | ||||
| bool MultiMachine::would_collapse(const std::vector<std::unique_ptr<DynamicMachine>> &machines) { | ||||
| 	return | ||||
| 		(machines.front()->crt_machine()->get_confidence() > 0.9f) || | ||||
| 		(machines.front()->crt_machine()->get_confidence() >= 2.0f * machines[1]->crt_machine()->get_confidence()); | ||||
| 		(machines.front()->timed_machine()->get_confidence() > 0.9f) || | ||||
| 		(machines.front()->timed_machine()->get_confidence() >= 2.0f * machines[1]->timed_machine()->get_confidence()); | ||||
| } | ||||
|  | ||||
| void MultiMachine::multi_crt_did_run_machines() { | ||||
| void MultiMachine::did_run_machines(MultiTimedMachine *) { | ||||
| 	std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_); | ||||
| #ifndef NDEBUG | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		CRTMachine::Machine *crt = machine->crt_machine(); | ||||
| 		LOGNBR(PADHEX(2) << crt->get_confidence() << " " << crt->debug_type() << "; "); | ||||
| 		auto timed_machine = machine->timed_machine(); | ||||
| 		LOGNBR(PADHEX(2) << timed_machine->get_confidence() << " " << timed_machine->debug_type() << "; "); | ||||
| 	} | ||||
| 	LOGNBR(std::endl); | ||||
| #endif | ||||
| @@ -86,13 +72,14 @@ void MultiMachine::multi_crt_did_run_machines() { | ||||
| 	DynamicMachine *front = machines_.front().get(); | ||||
| 	std::stable_sort(machines_.begin(), machines_.end(), | ||||
| 		[] (const std::unique_ptr<DynamicMachine> &lhs, const std::unique_ptr<DynamicMachine> &rhs){ | ||||
| 			CRTMachine::Machine *lhs_crt = lhs->crt_machine(); | ||||
| 			CRTMachine::Machine *rhs_crt = rhs->crt_machine(); | ||||
| 			return lhs_crt->get_confidence() > rhs_crt->get_confidence(); | ||||
| 			auto lhs_timed = lhs->timed_machine(); | ||||
| 			auto rhs_timed = rhs->timed_machine(); | ||||
| 			return lhs_timed->get_confidence() > rhs_timed->get_confidence(); | ||||
| 		}); | ||||
|  | ||||
| 	if(machines_.front().get() != front) { | ||||
| 		crt_machine_.did_change_machine_order(); | ||||
| 		scan_producer_.did_change_machine_order(); | ||||
| 		audio_producer_.did_change_machine_order(); | ||||
| 	} | ||||
|  | ||||
| 	if(would_collapse(machines_)) { | ||||
|   | ||||
| @@ -11,8 +11,9 @@ | ||||
|  | ||||
| #include "../../../Machines/DynamicMachine.hpp" | ||||
|  | ||||
| #include "Implementation/MultiProducer.hpp" | ||||
| #include "Implementation/MultiConfigurable.hpp" | ||||
| #include "Implementation/MultiCRTMachine.hpp" | ||||
| #include "Implementation/MultiProducer.hpp" | ||||
| #include "Implementation/MultiJoystickMachine.hpp" | ||||
| #include "Implementation/MultiKeyboardMachine.hpp" | ||||
| #include "Implementation/MultiMediaTarget.hpp" | ||||
| @@ -38,7 +39,7 @@ namespace Dynamic { | ||||
| 	If confidence for any machine becomes disproportionately low compared to | ||||
| 	the others in the set, that machine stops running. | ||||
| */ | ||||
| class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::Delegate { | ||||
| class MultiMachine: public ::Machine::DynamicMachine, public MultiTimedMachine::Delegate { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			Allows a potential MultiMachine creator to enquire as to whether there's any benefit in | ||||
| @@ -50,22 +51,27 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De | ||||
| 		static bool would_collapse(const std::vector<std::unique_ptr<DynamicMachine>> &machines); | ||||
| 		MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines); | ||||
|  | ||||
| 		Activity::Source *activity_source() override; | ||||
| 		Configurable::Device *configurable_device() override; | ||||
| 		CRTMachine::Machine *crt_machine() override; | ||||
| 		JoystickMachine::Machine *joystick_machine() override; | ||||
| 		KeyboardMachine::Machine *keyboard_machine() override; | ||||
| 		MediaTarget::Machine *media_target() override; | ||||
| 		void *raw_pointer() override; | ||||
| 		Activity::Source *activity_source() final; | ||||
| 		Configurable::Device *configurable_device() final; | ||||
| 		MachineTypes::TimedMachine *timed_machine() final; | ||||
| 		MachineTypes::ScanProducer *scan_producer() final; | ||||
| 		MachineTypes::AudioProducer *audio_producer() final; | ||||
| 		MachineTypes::JoystickMachine *joystick_machine() final; | ||||
| 		MachineTypes::KeyboardMachine *keyboard_machine() final; | ||||
| 		MachineTypes::MouseMachine *mouse_machine() final; | ||||
| 		MachineTypes::MediaTarget *media_target() final; | ||||
| 		void *raw_pointer() final; | ||||
|  | ||||
| 	private: | ||||
| 		void multi_crt_did_run_machines() override; | ||||
| 		void did_run_machines(MultiTimedMachine *) final; | ||||
|  | ||||
| 		std::vector<std::unique_ptr<DynamicMachine>> machines_; | ||||
| 		std::recursive_mutex machines_mutex_; | ||||
|  | ||||
| 		MultiConfigurable configurable_; | ||||
| 		MultiCRTMachine crt_machine_; | ||||
| 		MultiTimedMachine timed_machine_; | ||||
| 		MultiScanProducer scan_producer_; | ||||
| 		MultiAudioProducer audio_producer_; | ||||
| 		MultiJoystickMachine joystick_machine_; | ||||
| 		MultiKeyboardMachine keyboard_machine_; | ||||
| 		MultiMediaTarget media_target_; | ||||
|   | ||||
| @@ -15,8 +15,10 @@ enum class Machine { | ||||
| 	AmstradCPC, | ||||
| 	AppleII, | ||||
| 	Atari2600, | ||||
| 	AtariST, | ||||
| 	ColecoVision, | ||||
| 	Electron, | ||||
| 	Macintosh, | ||||
| 	MasterSystem, | ||||
| 	MSX, | ||||
| 	Oric, | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
|  | ||||
| #include "../../../Storage/Disk/Controller/DiskController.hpp" | ||||
| #include "../../../Storage/Disk/Encodings/MFM/Parser.hpp" | ||||
| #include "../../../NumberTheory/CRC.hpp" | ||||
| #include "../../../Numeric/CRC.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| @@ -18,11 +18,11 @@ 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); | ||||
| 	Storage::Encodings::MFM::Sector *details = parser.get_sector(0, 0, 1); | ||||
| 	const Storage::Encodings::MFM::Sector *const names = parser.get_sector(0, 0, 0); | ||||
| 	const Storage::Encodings::MFM::Sector *const details = parser.get_sector(0, 0, 1); | ||||
|  | ||||
| 	if(!names || !details) return nullptr; | ||||
| 	if(names->samples.empty() || details->samples.empty()) return nullptr; | ||||
| @@ -48,18 +48,18 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s | ||||
| 		char name[10]; | ||||
| 		snprintf(name, 10, "%c.%.7s", names->samples[0][file_offset + 7] & 0x7f, &names->samples[0][file_offset]); | ||||
| 		new_file.name = name; | ||||
| 		new_file.load_address = (uint32_t)(details->samples[0][file_offset] | (details->samples[0][file_offset+1] << 8) | ((details->samples[0][file_offset+6]&0x0c) << 14)); | ||||
| 		new_file.execution_address = (uint32_t)(details->samples[0][file_offset+2] | (details->samples[0][file_offset+3] << 8) | ((details->samples[0][file_offset+6]&0xc0) << 10)); | ||||
| 		new_file.is_protected = !!(names->samples[0][file_offset + 7] & 0x80); | ||||
| 		new_file.load_address = uint32_t(details->samples[0][file_offset] | (details->samples[0][file_offset+1] << 8) | ((details->samples[0][file_offset+6]&0x0c) << 14)); | ||||
| 		new_file.execution_address = uint32_t(details->samples[0][file_offset+2] | (details->samples[0][file_offset+3] << 8) | ((details->samples[0][file_offset+6]&0xc0) << 10)); | ||||
| 		new_file.is_protected = names->samples[0][file_offset + 7] & 0x80; | ||||
|  | ||||
| 		long data_length = static_cast<long>(details->samples[0][file_offset+4] | (details->samples[0][file_offset+5] << 8) | ((details->samples[0][file_offset+6]&0x30) << 12)); | ||||
| 		long data_length = long(details->samples[0][file_offset+4] | (details->samples[0][file_offset+5] << 8) | ((details->samples[0][file_offset+6]&0x30) << 12)); | ||||
| 		int start_sector = details->samples[0][file_offset+7] | ((details->samples[0][file_offset+6]&0x03) << 8); | ||||
| 		new_file.data.reserve(static_cast<std::size_t>(data_length)); | ||||
| 		new_file.data.reserve(size_t(data_length)); | ||||
|  | ||||
| 		if(start_sector < 2) continue; | ||||
| 		while(data_length > 0) { | ||||
| 			uint8_t sector = static_cast<uint8_t>(start_sector % 10); | ||||
| 			uint8_t track = static_cast<uint8_t>(start_sector / 10); | ||||
| 			uint8_t sector = uint8_t(start_sector % 10); | ||||
| 			uint8_t track = uint8_t(start_sector / 10); | ||||
| 			start_sector++; | ||||
|  | ||||
| 			Storage::Encodings::MFM::Sector *next_sector = parser.get_sector(0, track, sector); | ||||
| @@ -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); | ||||
| @@ -84,7 +84,7 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std:: | ||||
| 	std::vector<uint8_t> root_directory; | ||||
| 	root_directory.reserve(5 * 256); | ||||
| 	for(uint8_t c = 2; c < 7; c++) { | ||||
| 		Storage::Encodings::MFM::Sector *sector = parser.get_sector(0, 0, c); | ||||
| 		const Storage::Encodings::MFM::Sector *const sector = parser.get_sector(0, 0, c); | ||||
| 		if(!sector) return nullptr; | ||||
| 		root_directory.insert(root_directory.end(), sector->samples[0].begin(), sector->samples[0].end()); | ||||
| 	} | ||||
|   | ||||
| @@ -29,7 +29,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| 		if(segment.data.size() != 0x4000 && segment.data.size() != 0x2000) continue; | ||||
|  | ||||
| 		// is a copyright string present? | ||||
| 		uint8_t copyright_offset = segment.data[7]; | ||||
| 		const uint8_t copyright_offset = segment.data[7]; | ||||
| 		if( | ||||
| 			segment.data[copyright_offset] != 0x00 || | ||||
| 			segment.data[copyright_offset+1] != 0x28 || | ||||
| @@ -58,8 +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); | ||||
| 	target->machine = Machine::Electron; | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->confidence = 0.5; // TODO: a proper estimation | ||||
| 	target->has_dfs = false; | ||||
| 	target->has_adfs = false; | ||||
| @@ -84,8 +83,8 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me | ||||
| 			// check also for a continuous threading of BASIC lines; if none then this probably isn't BASIC code, | ||||
| 			// so that's also justification to *RUN | ||||
| 			std::size_t pointer = 0; | ||||
| 			uint8_t *data = &files.front().data[0]; | ||||
| 			std::size_t data_size = files.front().data.size(); | ||||
| 			uint8_t *const data = &files.front().data[0]; | ||||
| 			const std::size_t data_size = files.front().data.size(); | ||||
| 			while(1) { | ||||
| 				if(pointer >= data_size-1 || data[pointer] != 13) { | ||||
| 					is_basic = false; | ||||
|   | ||||
| @@ -10,13 +10,13 @@ | ||||
|  | ||||
| #include <deque> | ||||
|  | ||||
| #include "../../../NumberTheory/CRC.hpp" | ||||
| #include "../../../Numeric/CRC.hpp" | ||||
| #include "../../../Storage/Tape/Parsers/Acorn.hpp" | ||||
|  | ||||
| using namespace Analyser::Static::Acorn; | ||||
|  | ||||
| static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::Tape::Tape> &tape, Storage::Tape::Acorn::Parser &parser) { | ||||
| 	std::unique_ptr<File::Chunk> new_chunk(new File::Chunk); | ||||
| 	auto new_chunk = std::make_unique<File::Chunk>(); | ||||
| 	int shift_register = 0; | ||||
|  | ||||
| // TODO: move this into the parser | ||||
| @@ -41,24 +41,24 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage:: | ||||
| 	char name[11]; | ||||
| 	std::size_t name_ptr = 0; | ||||
| 	while(!tape->is_at_end() && name_ptr < sizeof(name)) { | ||||
| 		name[name_ptr] = (char)parser.get_next_byte(tape); | ||||
| 		name[name_ptr] = char(parser.get_next_byte(tape)); | ||||
| 		if(!name[name_ptr]) break; | ||||
| 		name_ptr++; | ||||
| 		++name_ptr; | ||||
| 	} | ||||
| 	name[sizeof(name)-1] = '\0'; | ||||
| 	new_chunk->name = name; | ||||
|  | ||||
| 	// addresses | ||||
| 	new_chunk->load_address = (uint32_t)parser.get_next_word(tape); | ||||
| 	new_chunk->execution_address = (uint32_t)parser.get_next_word(tape); | ||||
| 	new_chunk->block_number = static_cast<uint16_t>(parser.get_next_short(tape)); | ||||
| 	new_chunk->block_length = static_cast<uint16_t>(parser.get_next_short(tape)); | ||||
| 	new_chunk->block_flag = static_cast<uint8_t>(parser.get_next_byte(tape)); | ||||
| 	new_chunk->next_address = (uint32_t)parser.get_next_word(tape); | ||||
| 	new_chunk->load_address = uint32_t(parser.get_next_word(tape)); | ||||
| 	new_chunk->execution_address = uint32_t(parser.get_next_word(tape)); | ||||
| 	new_chunk->block_number = uint16_t(parser.get_next_short(tape)); | ||||
| 	new_chunk->block_length = uint16_t(parser.get_next_short(tape)); | ||||
| 	new_chunk->block_flag = uint8_t(parser.get_next_byte(tape)); | ||||
| 	new_chunk->next_address = uint32_t(parser.get_next_word(tape)); | ||||
|  | ||||
| 	uint16_t calculated_header_crc = parser.get_crc(); | ||||
| 	uint16_t stored_header_crc = static_cast<uint16_t>(parser.get_next_short(tape)); | ||||
| 	stored_header_crc = static_cast<uint16_t>((stored_header_crc >> 8) | (stored_header_crc << 8)); | ||||
| 	uint16_t stored_header_crc = uint16_t(parser.get_next_short(tape)); | ||||
| 	stored_header_crc = uint16_t((stored_header_crc >> 8) | (stored_header_crc << 8)); | ||||
| 	new_chunk->header_crc_matched = stored_header_crc == calculated_header_crc; | ||||
|  | ||||
| 	if(!new_chunk->header_crc_matched) return nullptr; | ||||
| @@ -66,13 +66,13 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage:: | ||||
| 	parser.reset_crc(); | ||||
| 	new_chunk->data.reserve(new_chunk->block_length); | ||||
| 	for(int c = 0; c < new_chunk->block_length; c++) { | ||||
| 		new_chunk->data.push_back(static_cast<uint8_t>(parser.get_next_byte(tape))); | ||||
| 		new_chunk->data.push_back(uint8_t(parser.get_next_byte(tape))); | ||||
| 	} | ||||
|  | ||||
| 	if(new_chunk->block_length && !(new_chunk->block_flag&0x40)) { | ||||
| 		uint16_t calculated_data_crc = parser.get_crc(); | ||||
| 		uint16_t stored_data_crc = static_cast<uint16_t>(parser.get_next_short(tape)); | ||||
| 		stored_data_crc = static_cast<uint16_t>((stored_data_crc >> 8) | (stored_data_crc << 8)); | ||||
| 		uint16_t stored_data_crc = uint16_t(parser.get_next_short(tape)); | ||||
| 		stored_data_crc = uint16_t((stored_data_crc >> 8) | (stored_data_crc << 8)); | ||||
| 		new_chunk->data_crc_matched = stored_data_crc == calculated_data_crc; | ||||
| 	} else { | ||||
| 		new_chunk->data_crc_matched = true; | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
| #ifndef Analyser_Static_Acorn_Target_h | ||||
| #define Analyser_Static_Acorn_Target_h | ||||
|  | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include <string> | ||||
|  | ||||
| @@ -16,11 +17,18 @@ namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Acorn { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	bool has_adfs = false; | ||||
| 	bool has_dfs = false; | ||||
| 	bool should_shift_restart = false; | ||||
| 	std::string loading_command; | ||||
|  | ||||
| 	Target() : Analyser::Static::Target(Machine::Electron) { | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(has_adfs); | ||||
| 			DeclareField(has_dfs); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -181,8 +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); | ||||
| 	target->machine = Machine::AmstradCPC; | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->confidence = 0.5; | ||||
|  | ||||
| 	target->model = Target::Model::CPC6128; | ||||
|   | ||||
| @@ -9,6 +9,8 @@ | ||||
| #ifndef Analyser_Static_AmstradCPC_Target_h | ||||
| #define Analyser_Static_AmstradCPC_Target_h | ||||
|  | ||||
| #include "../../../Reflection/Enum.hpp" | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include <string> | ||||
|  | ||||
| @@ -16,15 +18,17 @@ namespace Analyser { | ||||
| namespace Static { | ||||
| namespace AmstradCPC { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| 	enum class Model { | ||||
| 		CPC464, | ||||
| 		CPC664, | ||||
| 		CPC6128 | ||||
| 	}; | ||||
|  | ||||
| struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	ReflectableEnum(Model, CPC464, CPC664, CPC6128); | ||||
| 	Model model = Model::CPC464; | ||||
| 	std::string loading_command; | ||||
|  | ||||
| 	Target() : Analyser::Static::Target(Machine::AmstradCPC) { | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(model); | ||||
| 			AnnounceEnum(Model); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -10,8 +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); | ||||
| 	target->machine = Machine::AppleII; | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->media = media; | ||||
|  | ||||
| 	if(!target->media.disks.empty()) | ||||
|   | ||||
| @@ -9,27 +9,38 @@ | ||||
| #ifndef Target_h | ||||
| #define Target_h | ||||
|  | ||||
| #include "../../../Reflection/Enum.hpp" | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace AppleII { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| 	enum class Model { | ||||
| struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	ReflectableEnum(Model, | ||||
| 		II, | ||||
| 		IIplus, | ||||
| 		IIe, | ||||
| 		EnhancedIIe | ||||
| 	}; | ||||
| 	enum class DiskController { | ||||
| 	); | ||||
| 	ReflectableEnum(DiskController, | ||||
| 		None, | ||||
| 		SixteenSector, | ||||
| 		ThirteenSector | ||||
| 	}; | ||||
| 	); | ||||
|  | ||||
| 	Model model = Model::IIe; | ||||
| 	DiskController disk_controller = DiskController::None; | ||||
|  | ||||
| 	Target() : Analyser::Static::Target(Machine::AppleII) { | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(model); | ||||
| 			DeclareField(disk_controller); | ||||
| 			AnnounceEnum(Model); | ||||
| 			AnnounceEnum(DiskController); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -12,27 +12,26 @@ | ||||
| 
 | ||||
| #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) { | ||||
| 	// if this is a 2kb cartridge then it's definitely either unpaged or a CommaVid
 | ||||
| 	uint16_t entry_address, break_address; | ||||
| 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.
 | ||||
| 	const uint16_t entry_address = uint16_t(segment.data[0x7fc] | (segment.data[0x7fd] << 8)) & 0x1fff; | ||||
| 	const uint16_t break_address = uint16_t(segment.data[0x7fe] | (segment.data[0x7ff] << 8)) & 0x1fff; | ||||
| 
 | ||||
| 	entry_address = (static_cast<uint16_t>(segment.data[0x7fc] | (segment.data[0x7fd] << 8))) & 0x1fff; | ||||
| 	break_address = (static_cast<uint16_t>(segment.data[0x7fe] | (segment.data[0x7ff] << 8))) & 0x1fff; | ||||
| 
 | ||||
| 	// a CommaVid start address needs to be outside of its RAM
 | ||||
| 	// A CommaVid start address needs to be outside of its RAM.
 | ||||
| 	if(entry_address < 0x1800 || break_address < 0x1800) return; | ||||
| 
 | ||||
| 	std::function<std::size_t(uint16_t address)> high_location_mapper = [](uint16_t address) { | ||||
| 		address &= 0x1fff; | ||||
| 		return static_cast<std::size_t>(address - 0x1800); | ||||
| 		return size_t(address - 0x1800); | ||||
| 	}; | ||||
| 	Analyser::Static::MOS6502::Disassembly high_location_disassembly = | ||||
| 		Analyser::Static::MOS6502::Disassemble(segment.data, high_location_mapper, {entry_address, break_address}); | ||||
| 
 | ||||
| 	// assume that any kind of store that looks likely to be intended for large amounts of memory implies
 | ||||
| 	// large amounts of memory
 | ||||
| 	// Assume that any kind of store that looks likely to be intended for large amounts of memory implies
 | ||||
| 	// large amounts of memory.
 | ||||
| 	bool has_wide_area_store = false; | ||||
| 	for(std::map<uint16_t, Analyser::Static::MOS6502::Instruction>::value_type &entry : high_location_disassembly.instructions_by_address) { | ||||
| 		if(entry.second.operation == Analyser::Static::MOS6502::Instruction::STA) { | ||||
| @@ -44,28 +43,28 @@ static void DeterminePagingFor2kCartridge(Analyser::Static::Atari::Target &targe | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// conclude that this is a CommaVid if it attempted to write something to the CommaVid RAM locations;
 | ||||
| 	// Conclude that this is a CommaVid if it attempted to write something to the CommaVid RAM locations;
 | ||||
| 	// 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; | ||||
| 	// attempts to modify itself but it probably doesn't.
 | ||||
| 	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?)
 | ||||
| 	// issue an SEI as their first instruction (maybe some sort of relic of the development environment?).
 | ||||
| 	if( | ||||
| 		segment.data[4095] == 0xf0 && segment.data[4093] == 0xf0 && segment.data[4094] == 0x00 && segment.data[4092] == 0x00 && | ||||
| 		(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; | ||||
| 	// Make an assumption that this is the Atari paging model.
 | ||||
| 	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 +84,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) { | ||||
| 	// make an assumption that this is the Atari paging model
 | ||||
| 	target.paging_model = Analyser::Static::Atari::Target::PagingModel::Atari16k; | ||||
| 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 = Target::PagingModel::Atari16k; | ||||
| 
 | ||||
| 	std::set<uint16_t> internal_accesses; | ||||
| 	internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end()); | ||||
| @@ -106,33 +105,31 @@ 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) { | ||||
| 	// make an assumption that this is a Tigervision if there is a write to 3F
 | ||||
| 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; | ||||
| 	} | ||||
| 
 | ||||
| 	uint16_t entry_address, break_address; | ||||
| 
 | ||||
| 	entry_address = static_cast<uint16_t>(segment.data[segment.data.size() - 4] | (segment.data[segment.data.size() - 3] << 8)); | ||||
| 	break_address = static_cast<uint16_t>(segment.data[segment.data.size() - 2] | (segment.data[segment.data.size() - 1] << 8)); | ||||
| 	const uint16_t entry_address = uint16_t(segment.data[segment.data.size() - 4] | (segment.data[segment.data.size() - 3] << 8)); | ||||
| 	const uint16_t break_address = uint16_t(segment.data[segment.data.size() - 2] | (segment.data[segment.data.size() - 1] << 8)); | ||||
| 
 | ||||
| 	std::function<std::size_t(uint16_t address)> address_mapper = [](uint16_t address) { | ||||
| 		if(!(address & 0x1000)) return static_cast<std::size_t>(-1); | ||||
| 		return static_cast<std::size_t>(address & 0xfff); | ||||
| 		if(!(address & 0x1000)) return size_t(-1); | ||||
| 		return size_t(address & 0xfff); | ||||
| 	}; | ||||
| 
 | ||||
| 	std::vector<uint8_t> final_4k(segment.data.end() - 4096, segment.data.end()); | ||||
| 	const std::vector<uint8_t> final_4k(segment.data.end() - 4096, segment.data.end()); | ||||
| 	Analyser::Static::MOS6502::Disassembly disassembly = Analyser::Static::MOS6502::Disassemble(final_4k, address_mapper, {entry_address, break_address}); | ||||
| 
 | ||||
| 	switch(segment.data.size()) { | ||||
| @@ -140,16 +137,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); | ||||
| @@ -158,11 +155,11 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, | ||||
| 		break; | ||||
| 	} | ||||
| 
 | ||||
| 	// check for a Super Chip. Atari ROM images [almost] always have the same value stored over RAM
 | ||||
| 	// Check for a Super Chip. Atari ROM images [almost] always have the same value stored over RAM
 | ||||
| 	// regions; when they don't they at least seem to have the first 128 bytes be the same as the
 | ||||
| 	// 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]) { | ||||
| @@ -173,20 +170,19 @@ static void DeterminePagingForCartridge(Analyser::Static::Atari::Target &target, | ||||
| 		target.uses_superchip = has_superchip; | ||||
| 	} | ||||
| 
 | ||||
| 	// check for a Tigervision or Tigervision-esque scheme
 | ||||
| 	if(target.paging_model == Analyser::Static::Atari::Target::PagingModel::None && segment.data.size() > 4096) { | ||||
| 	// Check for a Tigervision or Tigervision-esque scheme
 | ||||
| 	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); | ||||
| 	target->machine = Machine::Atari2600; | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	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 { | ||||
| @@ -34,6 +34,8 @@ struct Target: public ::Analyser::Static::Target { | ||||
| 	// TODO: shouldn't these be properties of the cartridge?
 | ||||
| 	PagingModel paging_model = PagingModel::None; | ||||
| 	bool uses_superchip = false; | ||||
| 
 | ||||
| 	Target() : Analyser::Static::Target(Machine::Atari2600) {} | ||||
| }; | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										25
									
								
								Analyser/Static/AtariST/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Analyser/Static/AtariST/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // | ||||
| //  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::AtariST::Target; | ||||
| 	auto *const target = new Target(); | ||||
| 	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 */ | ||||
							
								
								
									
										27
									
								
								Analyser/Static/AtariST/Target.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Analyser/Static/AtariST/Target.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  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 | ||||
|  | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace AtariST { | ||||
|  | ||||
| struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	Target() : Analyser::Static::Target(Machine::AtariST) {} | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Analyser_Static_AtariST_Target_h */ | ||||
| @@ -22,7 +22,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
|  | ||||
| 		// the two bytes that will be first must be 0xaa and 0x55, either way around | ||||
| 		auto *start = &segment.data[0]; | ||||
| 		if((data_size & static_cast<std::size_t>(~8191)) > 32768) { | ||||
| 		if((data_size & size_t(~8191)) > 32768) { | ||||
| 			start = &segment.data[segment.data.size() - 16384]; | ||||
| 		} | ||||
| 		if(start[0] != 0xaa && start[0] != 0x55 && start[1] != 0xaa && start[1] != 0x55) continue; | ||||
| @@ -54,8 +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); | ||||
| 	target->machine = Machine::ColecoVision; | ||||
| 	auto target = std::make_unique<Target>(Machine::ColecoVision); | ||||
| 	target->confidence = 1.0f - 1.0f / 32768.0f; | ||||
| 	target->media.cartridges = ColecoCartridgesFrom(media.cartridges); | ||||
| 	if(!target->media.empty()) | ||||
|   | ||||
| @@ -19,12 +19,10 @@ using namespace Analyser::Static::Commodore; | ||||
|  | ||||
| class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 	public: | ||||
| 		std::shared_ptr<Storage::Disk::Drive> drive; | ||||
|  | ||||
| 		CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) { | ||||
| 			drive.reset(new Storage::Disk::Drive(4000000, 300, 2)); | ||||
| 			set_drive(drive); | ||||
| 			drive->set_motor_on(true); | ||||
| 			emplace_drive(4000000, 300, 2); | ||||
| 			set_drive(1); | ||||
| 			get_drive().set_motor_on(true); | ||||
| 		} | ||||
|  | ||||
| 		struct Sector { | ||||
| @@ -40,7 +38,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 			@returns a sector if one was found; @c nullptr otherwise. | ||||
| 		*/ | ||||
| 		std::shared_ptr<Sector> get_sector(uint8_t track, uint8_t sector) { | ||||
| 			int difference = static_cast<int>(track) - static_cast<int>(track_); | ||||
| 			int difference = int(track) - int(track_); | ||||
| 			track_ = track; | ||||
|  | ||||
| 			if(difference) { | ||||
| @@ -61,6 +59,10 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 			return get_sector(sector); | ||||
| 		} | ||||
|  | ||||
| 		void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk) { | ||||
| 			get_drive().set_disk(disk); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		unsigned int shift_register_; | ||||
| 		int index_count_; | ||||
| @@ -69,7 +71,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 		std::shared_ptr<Sector> sector_cache_[65536]; | ||||
|  | ||||
| 		void process_input_bit(int value) { | ||||
| 			shift_register_ = ((shift_register_ << 1) | static_cast<unsigned int>(value)) & 0x3ff; | ||||
| 			shift_register_ = ((shift_register_ << 1) | unsigned(value)) & 0x3ff; | ||||
| 			bit_count_++; | ||||
| 		} | ||||
|  | ||||
| @@ -110,22 +112,22 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 		} | ||||
|  | ||||
| 		std::shared_ptr<Sector> get_sector(uint8_t sector) { | ||||
| 			uint16_t sector_address = static_cast<uint16_t>((track_ << 8) | sector); | ||||
| 			const uint16_t sector_address = uint16_t((track_ << 8) | sector); | ||||
| 			if(sector_cache_[sector_address]) return sector_cache_[sector_address]; | ||||
|  | ||||
| 			std::shared_ptr<Sector> first_sector = get_next_sector(); | ||||
| 			const std::shared_ptr<Sector> first_sector = get_next_sector(); | ||||
| 			if(!first_sector) return first_sector; | ||||
| 			if(first_sector->sector == sector) return first_sector; | ||||
|  | ||||
| 			while(1) { | ||||
| 				std::shared_ptr<Sector> next_sector = get_next_sector(); | ||||
| 				const std::shared_ptr<Sector> next_sector = get_next_sector(); | ||||
| 				if(next_sector->sector == first_sector->sector) return nullptr; | ||||
| 				if(next_sector->sector == sector) return next_sector; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		std::shared_ptr<Sector> get_next_sector() { | ||||
| 			std::shared_ptr<Sector> sector(new Sector); | ||||
| 			auto sector = std::make_shared<Sector>(); | ||||
| 			const int max_index_count = index_count_ + 2; | ||||
|  | ||||
| 			while(index_count_ < max_index_count) { | ||||
| @@ -136,12 +138,12 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 				} | ||||
|  | ||||
| 				// get sector details, skip if this looks malformed | ||||
| 				uint8_t checksum = static_cast<uint8_t>(get_next_byte()); | ||||
| 				sector->sector = static_cast<uint8_t>(get_next_byte()); | ||||
| 				sector->track = static_cast<uint8_t>(get_next_byte()); | ||||
| 				uint8_t checksum = uint8_t(get_next_byte()); | ||||
| 				sector->sector = uint8_t(get_next_byte()); | ||||
| 				sector->track = uint8_t(get_next_byte()); | ||||
| 				uint8_t disk_id[2]; | ||||
| 				disk_id[0] = static_cast<uint8_t>(get_next_byte()); | ||||
| 				disk_id[1] = static_cast<uint8_t>(get_next_byte()); | ||||
| 				disk_id[0] = uint8_t(get_next_byte()); | ||||
| 				disk_id[1] = uint8_t(get_next_byte()); | ||||
| 				if(checksum != (sector->sector ^ sector->track ^ disk_id[0] ^ disk_id[1])) continue; | ||||
|  | ||||
| 				// look for the following data | ||||
| @@ -152,12 +154,12 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
|  | ||||
| 				checksum = 0; | ||||
| 				for(std::size_t c = 0; c < 256; c++) { | ||||
| 					sector->data[c] = static_cast<uint8_t>(get_next_byte()); | ||||
| 					sector->data[c] = uint8_t(get_next_byte()); | ||||
| 					checksum ^= sector->data[c]; | ||||
| 				} | ||||
|  | ||||
| 				if(checksum == get_next_byte()) { | ||||
| 					uint16_t sector_address = static_cast<uint16_t>((sector->track << 8) | sector->sector); | ||||
| 					uint16_t sector_address = uint16_t((sector->track << 8) | sector->sector); | ||||
| 					sector_cache_[sector_address] = sector; | ||||
| 					return sector; | ||||
| 				} | ||||
| @@ -170,7 +172,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| std::vector<File> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<Storage::Disk::Disk> &disk) { | ||||
| 	std::vector<File> files; | ||||
| 	CommodoreGCRParser parser; | ||||
| 	parser.drive->set_disk(disk); | ||||
| 	parser.set_disk(disk); | ||||
|  | ||||
| 	// find any sector whatsoever to establish the current track | ||||
| 	std::shared_ptr<CommodoreGCRParser::Sector> sector; | ||||
| @@ -190,7 +192,7 @@ std::vector<File> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<St | ||||
| 	} | ||||
|  | ||||
| 	// parse directory | ||||
| 	std::size_t header_pointer = static_cast<std::size_t>(-32); | ||||
| 	std::size_t header_pointer = size_t(-32); | ||||
| 	while(header_pointer+32+31 < directory.size()) { | ||||
| 		header_pointer += 32; | ||||
|  | ||||
| @@ -214,7 +216,7 @@ std::vector<File> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<St | ||||
| 		} | ||||
| 		new_file.name = Storage::Data::Commodore::petscii_from_bytes(&new_file.raw_name[0], 16, false); | ||||
|  | ||||
| 		std::size_t number_of_sectors = static_cast<std::size_t>(directory[header_pointer + 0x1e]) + (static_cast<std::size_t>(directory[header_pointer + 0x1f]) << 8); | ||||
| 		std::size_t number_of_sectors = size_t(directory[header_pointer + 0x1e]) + (size_t(directory[header_pointer + 0x1f]) << 8); | ||||
| 		new_file.data.reserve((number_of_sectors - 1) * 254 + 252); | ||||
|  | ||||
| 		bool is_first_sector = true; | ||||
| @@ -225,7 +227,7 @@ std::vector<File> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<St | ||||
| 			next_track = sector->data[0]; | ||||
| 			next_sector = sector->data[1]; | ||||
|  | ||||
| 			if(is_first_sector) new_file.starting_address = static_cast<uint16_t>(sector->data[2]) | static_cast<uint16_t>(sector->data[3] << 8); | ||||
| 			if(is_first_sector) new_file.starting_address = uint16_t(sector->data[2]) | uint16_t(sector->data[3] << 8); | ||||
| 			if(next_track) | ||||
| 				new_file.data.insert(new_file.data.end(), sector->data.begin() + (is_first_sector ? 4 : 2), sector->data.end()); | ||||
| 			else | ||||
|   | ||||
| @@ -23,7 +23,7 @@ bool Analyser::Static::Commodore::File::is_basic() { | ||||
| 	//		... null-terminated code ... | ||||
| 	//	(with a next line address of 0000 indicating end of program) | ||||
| 	while(1) { | ||||
| 		if(static_cast<size_t>(line_address - starting_address) >= data.size() + 2) break; | ||||
| 		if(size_t(line_address - starting_address) >= data.size() + 2) break; | ||||
|  | ||||
| 		uint16_t next_line_address = data[line_address - starting_address]; | ||||
| 		next_line_address |= data[line_address - starting_address + 1] << 8; | ||||
| @@ -33,13 +33,13 @@ bool Analyser::Static::Commodore::File::is_basic() { | ||||
| 		} | ||||
| 		if(next_line_address < line_address + 5) break; | ||||
|  | ||||
| 		if(static_cast<size_t>(line_address - starting_address) >= data.size() + 5) break; | ||||
| 		if(size_t(line_address - starting_address) >= data.size() + 5) break; | ||||
| 		uint16_t next_line_number = data[line_address - starting_address + 2]; | ||||
| 		next_line_number |= data[line_address - starting_address + 3] << 8; | ||||
|  | ||||
| 		if(next_line_number <= line_number) break; | ||||
|  | ||||
| 		line_number = static_cast<uint16_t>(next_line_number); | ||||
| 		line_number = uint16_t(next_line_number); | ||||
| 		line_address = next_line_address; | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cstring> | ||||
| #include <sstream> | ||||
|  | ||||
| using namespace Analyser::Static::Commodore; | ||||
| @@ -44,7 +45,7 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	TargetList destination; | ||||
|  | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->machine = Machine::Vic20;	// TODO: machine estimation | ||||
| 	target->confidence = 0.5; // TODO: a proper estimation | ||||
|  | ||||
| @@ -78,7 +79,7 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media | ||||
| 	} | ||||
|  | ||||
| 	if(!files.empty()) { | ||||
| 		target->memory_model = Target::MemoryModel::Unexpanded; | ||||
| 		auto memory_model = Target::MemoryModel::Unexpanded; | ||||
| 		std::ostringstream string_stream; | ||||
| 		string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ","; | ||||
| 		if(files.front().is_basic()) { | ||||
| @@ -94,16 +95,18 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media | ||||
| 			default: | ||||
| 				LOG("Unrecognised loading address for Commodore program: " << PADHEX(4) <<  files.front().starting_address); | ||||
| 			case 0x1001: | ||||
| 				target->memory_model = Target::MemoryModel::Unexpanded; | ||||
| 				memory_model = Target::MemoryModel::Unexpanded; | ||||
| 			break; | ||||
| 			case 0x1201: | ||||
| 				target->memory_model = Target::MemoryModel::ThirtyTwoKB; | ||||
| 				memory_model = Target::MemoryModel::ThirtyTwoKB; | ||||
| 			break; | ||||
| 			case 0x0401: | ||||
| 				target->memory_model = Target::MemoryModel::EightKB; | ||||
| 				memory_model = Target::MemoryModel::EightKB; | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		target->set_memory_model(memory_model); | ||||
|  | ||||
| 		// General approach: increase memory size conservatively such that the largest file found will fit. | ||||
| //		for(File &file : files) { | ||||
| //			std::size_t file_size = file.data.size(); | ||||
| @@ -145,13 +148,52 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media | ||||
| 	} | ||||
|  | ||||
| 	if(!target->media.empty()) { | ||||
| 		// Inspect filename for a region hint. | ||||
| 		// Inspect filename for configuration hints. | ||||
| 		std::string lowercase_name = file_name; | ||||
| 		std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower); | ||||
|  | ||||
| 		// Hint 1: 'ntsc' anywhere in the name implies America. | ||||
| 		if(lowercase_name.find("ntsc") != std::string::npos) { | ||||
| 			target->region = Analyser::Static::Commodore::Target::Region::American; | ||||
| 		} | ||||
|  | ||||
| 		// Potential additional hints: check for TheC64 tags. | ||||
| 		auto final_underscore = lowercase_name.find_last_of('_'); | ||||
| 		if(final_underscore != std::string::npos) { | ||||
| 			auto iterator = lowercase_name.begin() + ssize_t(final_underscore) + 1; | ||||
|  | ||||
| 			while(iterator != lowercase_name.end()) { | ||||
| 				// Grab the next tag. | ||||
| 				char next_tag[3] = {0, 0, 0}; | ||||
| 				next_tag[0] = *iterator++; | ||||
| 				if(iterator == lowercase_name.end()) break; | ||||
| 				next_tag[1] = *iterator++; | ||||
|  | ||||
| 				// Exit early if attempting to read another tag has run over the file extension. | ||||
| 				if(next_tag[0] == '.' || next_tag[1] == '.') break; | ||||
|  | ||||
| 				// Check whether it's anything. | ||||
| 				target->enabled_ram.bank0 |= !strcmp(next_tag, "b0"); | ||||
| 				target->enabled_ram.bank1 |= !strcmp(next_tag, "b1"); | ||||
| 				target->enabled_ram.bank2 |= !strcmp(next_tag, "b2"); | ||||
| 				target->enabled_ram.bank3 |= !strcmp(next_tag, "b3"); | ||||
| 				target->enabled_ram.bank5 |= !strcmp(next_tag, "b5"); | ||||
| 				if(!strcmp(next_tag, "tn")) {	// i.e. NTSC. | ||||
| 					target->region = Analyser::Static::Commodore::Target::Region::American; | ||||
| 				} | ||||
| 				if(!strcmp(next_tag, "tp")) {	// i.e. PAL. | ||||
| 					target->region = Analyser::Static::Commodore::Target::Region::European; | ||||
| 				} | ||||
|  | ||||
| 				// Unhandled: | ||||
| 				// | ||||
| 				//	M6: 	this is a C64 file. | ||||
| 				//	MV: 	this is a Vic-20 file. | ||||
| 				//	J1/J2:	this C64 file should have the primary joystick in slot 1/2. | ||||
| 				//	RO:		this disk image should be treated as read-only. | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Attach a 1540 if there are any disks here. | ||||
| 		target->has_c1540 = !target->media.disks.empty(); | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,8 @@ | ||||
| #ifndef Analyser_Static_Commodore_Target_h | ||||
| #define Analyser_Static_Commodore_Target_h | ||||
|  | ||||
| #include "../../../Reflection/Enum.hpp" | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include <string> | ||||
|  | ||||
| @@ -16,25 +18,57 @@ namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Commodore { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	enum class MemoryModel { | ||||
| 		Unexpanded, | ||||
| 		EightKB, | ||||
| 		ThirtyTwoKB | ||||
| 	}; | ||||
|  | ||||
| 	enum class Region { | ||||
| 	ReflectableEnum(Region, | ||||
| 		American, | ||||
| 		Danish, | ||||
| 		Japanese, | ||||
| 		European, | ||||
| 		Swedish | ||||
| 	}; | ||||
| 	); | ||||
|  | ||||
| 	/// 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; | ||||
|  | ||||
| 	MemoryModel memory_model = MemoryModel::Unexpanded; | ||||
| 	Region region = Region::European; | ||||
| 	bool has_c1540 = false; | ||||
| 	std::string loading_command; | ||||
|  | ||||
| 	Target() : Analyser::Static::Target(Machine::Vic20) { | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(enabled_ram.bank0); | ||||
| 			DeclareField(enabled_ram.bank1); | ||||
| 			DeclareField(enabled_ram.bank2); | ||||
| 			DeclareField(enabled_ram.bank3); | ||||
| 			DeclareField(enabled_ram.bank5); | ||||
| 			DeclareField(region); | ||||
| 			DeclareField(has_c1540); | ||||
| 			AnnounceEnum(Region); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -26,12 +26,12 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
|  | ||||
| 		Instruction instruction; | ||||
| 		instruction.address = address; | ||||
| 		address++; | ||||
| 		++address; | ||||
|  | ||||
| 		// get operation | ||||
| 		uint8_t operation = memory[local_address]; | ||||
| 		// Get operation. | ||||
| 		const uint8_t operation = memory[local_address]; | ||||
|  | ||||
| 		// decode addressing mode | ||||
| 		// Decode addressing mode. | ||||
| 		switch(operation&0x1f) { | ||||
| 			case 0x00: | ||||
| 				if(operation >= 0x80) instruction.addressing_mode = Instruction::Immediate; | ||||
| @@ -74,7 +74,7 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		// decode operation | ||||
| 		// Decode operation. | ||||
| #define RM_INSTRUCTION(base, op)	\ | ||||
| 	case base+0x09: case base+0x05: case base+0x15: case base+0x01: case base+0x11: case base+0x0d: case base+0x1d: case base+0x19:	\ | ||||
| 		instruction.operation = op;	\ | ||||
| @@ -222,14 +222,14 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| #undef M_INSTRUCTION | ||||
| #undef IM_INSTRUCTION | ||||
|  | ||||
| 		// get operand | ||||
| 		// Get operand. | ||||
| 		switch(instruction.addressing_mode) { | ||||
| 			// zero-byte operands | ||||
| 			// Zero-byte operands. | ||||
| 			case Instruction::Implied: | ||||
| 				instruction.operand = 0; | ||||
| 			break; | ||||
|  | ||||
| 			// one-byte operands | ||||
| 			// One-byte operands. | ||||
| 			case Instruction::Immediate: | ||||
| 			case Instruction::ZeroPage: case Instruction::ZeroPageX: case Instruction::ZeroPageY: | ||||
| 			case Instruction::IndexedIndirectX: case Instruction::IndirectIndexedY: | ||||
| @@ -242,7 +242,7 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| 			} | ||||
| 			break; | ||||
|  | ||||
| 			// two-byte operands | ||||
| 			// Two-byte operands. | ||||
| 			case Instruction::Absolute: case Instruction::AbsoluteX: case Instruction::AbsoluteY: | ||||
| 			case Instruction::Indirect: { | ||||
| 				std::size_t low_operand_address = address_mapper(address); | ||||
| @@ -250,18 +250,18 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| 				if(low_operand_address >= memory.size() || high_operand_address >= memory.size()) return; | ||||
| 				address += 2; | ||||
|  | ||||
| 				instruction.operand = memory[low_operand_address] | static_cast<uint16_t>(memory[high_operand_address] << 8); | ||||
| 				instruction.operand = memory[low_operand_address] | uint16_t(memory[high_operand_address] << 8); | ||||
| 			} | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		// store the instruction away | ||||
| 		// Store the instruction. | ||||
| 		disassembly.disassembly.instructions_by_address[instruction.address] = instruction; | ||||
|  | ||||
| 		// TODO: something wider-ranging than this | ||||
| 		if(instruction.addressing_mode == Instruction::Absolute || instruction.addressing_mode == Instruction::ZeroPage) { | ||||
| 			std::size_t mapped_address = address_mapper(instruction.operand); | ||||
| 			bool is_external = mapped_address >= memory.size(); | ||||
| 			const size_t mapped_address = address_mapper(instruction.operand); | ||||
| 			const bool is_external = mapped_address >= memory.size(); | ||||
|  | ||||
| 			switch(instruction.operation) { | ||||
| 				default: break; | ||||
| @@ -290,7 +290,7 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// decide on overall flow control | ||||
| 		// Decide on overall flow control. | ||||
| 		if(instruction.operation == Instruction::RTS || instruction.operation == Instruction::RTI) return; | ||||
| 		if(instruction.operation == Instruction::BRK) return;	// TODO: check whether IRQ vector is within memory range | ||||
| 		if(instruction.operation == Instruction::JSR) { | ||||
| @@ -302,7 +302,7 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| 			return; | ||||
| 		} | ||||
| 		if(instruction.addressing_mode == Instruction::Relative) { | ||||
| 			uint16_t destination = static_cast<uint16_t>(address + (int8_t)instruction.operand); | ||||
| 			uint16_t destination = uint16_t(address + int8_t(instruction.operand)); | ||||
| 			disassembly.remaining_entry_points.push_back(destination); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| // | ||||
| //  AddressMapper.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 30/12/2017. | ||||
| //  Copyright 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "AddressMapper.hpp" | ||||
| @@ -21,7 +21,7 @@ namespace Disassembler { | ||||
| */ | ||||
| template <typename T> std::function<std::size_t(T)> OffsetMapper(T start_address) { | ||||
| 	return [start_address](T argument) { | ||||
| 		return static_cast<std::size_t>(argument - start_address); | ||||
| 		return size_t(argument - start_address); | ||||
| 	}; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class Accessor { | ||||
| 		uint16_t word() { | ||||
| 			uint8_t low = byte(); | ||||
| 			uint8_t high = byte(); | ||||
| 			return static_cast<uint16_t>(low | (high << 8)); | ||||
| 			return uint16_t(low | (high << 8)); | ||||
| 		} | ||||
|  | ||||
| 		bool overrun() { | ||||
| @@ -562,7 +562,7 @@ struct Z80Disassembler { | ||||
| 			int access_type = | ||||
| 				((instruction.source == Instruction::Location::Operand_Indirect) ? 1 : 0) | | ||||
| 				((instruction.destination == Instruction::Location::Operand_Indirect) ? 2 : 0); | ||||
| 			uint16_t address = static_cast<uint16_t>(instruction.operand); | ||||
| 			uint16_t address = uint16_t(instruction.operand); | ||||
| 			bool is_internal = address_mapper(address) < memory.size(); | ||||
| 			switch(access_type) { | ||||
| 				default: break; | ||||
| @@ -594,7 +594,7 @@ struct Z80Disassembler { | ||||
| 				instruction.operation == Instruction::Operation::JR || | ||||
| 				instruction.operation == Instruction::Operation::CALL || | ||||
| 				instruction.operation == Instruction::Operation::RST) { | ||||
| 				disassembly.remaining_entry_points.push_back(static_cast<uint16_t>(instruction.operand)); | ||||
| 				disassembly.remaining_entry_points.push_back(uint16_t(instruction.operand)); | ||||
| 			} | ||||
|  | ||||
| 			// This is it if: an unconditional RET, RETI, RETN, JP or JR is found. | ||||
|   | ||||
| @@ -20,8 +20,7 @@ namespace { | ||||
|  | ||||
| Analyser::Static::Target *AppleTarget(const Storage::Encodings::AppleGCR::Sector *sector_zero) { | ||||
| 	using Target = Analyser::Static::AppleII::Target; | ||||
| 	auto *target = new Target; | ||||
| 	target->machine = Analyser::Machine::AppleII; | ||||
| 	auto *const target = new Target; | ||||
|  | ||||
| 	if(sector_zero && sector_zero->encoding == Storage::Encodings::AppleGCR::Sector::Encoding::FiveAndThree) { | ||||
| 		target->disk_controller = Target::DiskController::ThirteenSector; | ||||
| @@ -34,8 +33,7 @@ Analyser::Static::Target *AppleTarget(const Storage::Encodings::AppleGCR::Sector | ||||
|  | ||||
| Analyser::Static::Target *OricTarget(const Storage::Encodings::AppleGCR::Sector *sector_zero) { | ||||
| 	using Target = Analyser::Static::Oric::Target; | ||||
| 	auto *target = new Target; | ||||
| 	target->machine = Analyser::Machine::Oric; | ||||
| 	auto *const target = new Target; | ||||
| 	target->rom = Target::ROM::Pravetz; | ||||
| 	target->disk_interface = Target::DiskInterface::Pravetz; | ||||
| 	target->loading_command = "CALL 800\n"; | ||||
| @@ -49,8 +47,8 @@ Analyser::Static::TargetList Analyser::Static::DiskII::GetTargets(const Media &m | ||||
| 	if(media.disks.empty()) return {}; | ||||
|  | ||||
| 	// Grab track 0, sector 0: the boot sector. | ||||
| 	auto track_zero = media.disks.front()->get_track_at_position(Storage::Disk::Track::Address(0, Storage::Disk::HeadPosition(0))); | ||||
| 	auto sector_map = Storage::Encodings::AppleGCR::sectors_from_segment( | ||||
| 	const auto track_zero = media.disks.front()->get_track_at_position(Storage::Disk::Track::Address(0, Storage::Disk::HeadPosition(0))); | ||||
| 	const auto sector_map = Storage::Encodings::AppleGCR::sectors_from_segment( | ||||
| 		Storage::Disk::track_serialisation(*track_zero, Storage::Time(1, 50000))); | ||||
|  | ||||
| 	const Storage::Encodings::AppleGCR::Sector *sector_zero = nullptr; | ||||
| @@ -77,7 +75,7 @@ Analyser::Static::TargetList Analyser::Static::DiskII::GetTargets(const Media &m | ||||
| 	// If the boot sector looks like it's intended for the Oric, create an Oric. | ||||
| 	// Otherwise go with the Apple II. | ||||
|  | ||||
| 	auto disassembly = Analyser::Static::MOS6502::Disassemble(sector_zero->data, Analyser::Static::Disassembler::OffsetMapper(0xb800), {0xb800}); | ||||
| 	const auto disassembly = Analyser::Static::MOS6502::Disassemble(sector_zero->data, Analyser::Static::Disassembler::OffsetMapper(0xb800), {0xb800}); | ||||
|  | ||||
| 	bool did_read_shift_register = false; | ||||
| 	bool is_oric = false; | ||||
|   | ||||
| @@ -27,15 +27,14 @@ static std::unique_ptr<Analyser::Static::Target> CartridgeTarget( | ||||
| 	std::vector<Storage::Cartridge::Cartridge::Segment> output_segments; | ||||
| 	if(segment.data.size() & 0x1fff) { | ||||
| 		std::vector<uint8_t> truncated_data; | ||||
| 		std::vector<uint8_t>::difference_type truncated_size = static_cast<std::vector<uint8_t>::difference_type>(segment.data.size()) & ~0x1fff; | ||||
| 		std::vector<uint8_t>::difference_type truncated_size = std::vector<uint8_t>::difference_type(segment.data.size()) & ~0x1fff; | ||||
| 		truncated_data.insert(truncated_data.begin(), segment.data.begin(), segment.data.begin() + truncated_size); | ||||
| 		output_segments.emplace_back(start_address, truncated_data); | ||||
| 	} else { | ||||
| 		output_segments.emplace_back(start_address, segment.data); | ||||
| 	} | ||||
|  | ||||
| 	std::unique_ptr<Analyser::Static::MSX::Target> target(new Analyser::Static::MSX::Target); | ||||
| 	target->machine = Analyser::Machine::MSX; | ||||
| 	auto target = std::make_unique<Analyser::Static::MSX::Target>(); | ||||
| 	target->confidence = confidence; | ||||
|  | ||||
| 	if(type == Analyser::Static::MSX::Cartridge::Type::None) { | ||||
| @@ -97,7 +96,7 @@ static Analyser::Static::TargetList CartridgeTargetsFrom( | ||||
| 		// Reject cartridge if the ROM header wasn't found. | ||||
| 		if(!found_start) continue; | ||||
|  | ||||
| 		uint16_t init_address = static_cast<uint16_t>(segment.data[2] | (segment.data[3] << 8)); | ||||
| 		uint16_t init_address = uint16_t(segment.data[2] | (segment.data[3] << 8)); | ||||
| 		// TODO: check for a rational init address? | ||||
|  | ||||
| 		// If this ROM is less than 48kb in size then it's an ordinary ROM. Just emplace it and move on. | ||||
| @@ -147,7 +146,7 @@ static Analyser::Static::TargetList CartridgeTargetsFrom( | ||||
| //				) && | ||||
| //				((next_iterator->second.operand >> 13) != (0x4000 >> 13)) | ||||
| //			) { | ||||
| //				const uint16_t address = static_cast<uint16_t>(next_iterator->second.operand); | ||||
| //				const uint16_t address = uint16_t(next_iterator->second.operand); | ||||
| //				switch(iterator->second.operand) { | ||||
| //					case 0x6000: | ||||
| //						if(address >= 0x6000 && address < 0x8000) { | ||||
| @@ -208,13 +207,13 @@ static Analyser::Static::TargetList CartridgeTargetsFrom( | ||||
| 			if(	instruction_pair.second.operation == Instruction::Operation::LD && | ||||
| 				instruction_pair.second.destination == Instruction::Location::Operand_Indirect && | ||||
| 				instruction_pair.second.source == Instruction::Location::A) { | ||||
| 				address_counts[static_cast<uint16_t>(instruction_pair.second.operand)]++; | ||||
| 				address_counts[uint16_t(instruction_pair.second.operand)]++; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Weight confidences by number of observed hits. | ||||
| 		float total_hits = | ||||
| 			static_cast<float>( | ||||
| 			float( | ||||
| 				address_counts[0x6000] + address_counts[0x6800] + | ||||
| 				address_counts[0x7000] + address_counts[0x7800] + | ||||
| 				address_counts[0x77ff] + address_counts[0x8000] + | ||||
| @@ -226,35 +225,35 @@ static Analyser::Static::TargetList CartridgeTargetsFrom( | ||||
| 			segment, | ||||
| 			start_address, | ||||
| 			Analyser::Static::MSX::Cartridge::ASCII8kb, | ||||
| 			static_cast<float>(	address_counts[0x6000] + | ||||
| 								address_counts[0x6800] + | ||||
| 								address_counts[0x7000] + | ||||
| 								address_counts[0x7800]) / total_hits)); | ||||
| 			float(	address_counts[0x6000] + | ||||
| 					address_counts[0x6800] + | ||||
| 					address_counts[0x7000] + | ||||
| 					address_counts[0x7800]) / total_hits)); | ||||
| 		targets.push_back(CartridgeTarget( | ||||
| 			segment, | ||||
| 			start_address, | ||||
| 			Analyser::Static::MSX::Cartridge::ASCII16kb, | ||||
| 			static_cast<float>(	address_counts[0x6000] + | ||||
| 								address_counts[0x7000] + | ||||
| 								address_counts[0x77ff]) / total_hits)); | ||||
| 			float(	address_counts[0x6000] + | ||||
| 					address_counts[0x7000] + | ||||
| 					address_counts[0x77ff]) / total_hits)); | ||||
| 		if(!is_ascii) { | ||||
| 			targets.push_back(CartridgeTarget( | ||||
| 				segment, | ||||
| 				start_address, | ||||
| 				Analyser::Static::MSX::Cartridge::Konami, | ||||
| 				static_cast<float>(	address_counts[0x6000] + | ||||
| 									address_counts[0x8000] + | ||||
| 									address_counts[0xa000]) / total_hits)); | ||||
| 				float(	address_counts[0x6000] + | ||||
| 						address_counts[0x8000] + | ||||
| 						address_counts[0xa000]) / total_hits)); | ||||
| 		} | ||||
| 		if(!is_ascii) { | ||||
| 			targets.push_back(CartridgeTarget( | ||||
| 				segment, | ||||
| 				start_address, | ||||
| 				Analyser::Static::MSX::Cartridge::KonamiWithSCC, | ||||
| 				static_cast<float>(	address_counts[0x5000] + | ||||
| 									address_counts[0x7000] + | ||||
| 									address_counts[0x9000] + | ||||
| 									address_counts[0xb000]) / total_hits)); | ||||
| 				float(	address_counts[0x5000] + | ||||
| 						address_counts[0x7000] + | ||||
| 						address_counts[0x9000] + | ||||
| 						address_counts[0xb000]) / total_hits)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -269,7 +268,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) { | ||||
| @@ -290,11 +289,11 @@ Analyser::Static::TargetList Analyser::Static::MSX::GetTargets(const Media &medi | ||||
| 	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(); | ||||
|  | ||||
| 	if(!target->media.empty()) { | ||||
| 		target->machine = Machine::MSX; | ||||
| 		target->confidence = 0.5; | ||||
| 		destination.push_back(std::move(target)); | ||||
| 	} | ||||
|   | ||||
| @@ -44,7 +44,7 @@ std::vector<File> Analyser::Static::MSX::GetFiles(const std::shared_ptr<Storage: | ||||
| 		for(std::size_t c = 0; c < sizeof(header); ++c) { | ||||
| 			int next_byte = Parser::get_byte(*file_speed, tape_player); | ||||
| 			if(next_byte == -1) break; | ||||
| 			header[c] = static_cast<uint8_t>(next_byte); | ||||
| 			header[c] = uint8_t(next_byte); | ||||
| 		} | ||||
|  | ||||
| 		bool bytes_are_same = true; | ||||
| @@ -67,7 +67,7 @@ std::vector<File> Analyser::Static::MSX::GetFiles(const std::shared_ptr<Storage: | ||||
| 		// Read file name. | ||||
| 		char name[7]; | ||||
| 		for(std::size_t c = 1; c < 6; ++c) | ||||
| 			name[c] = static_cast<char>(Parser::get_byte(*file_speed, tape_player)); | ||||
| 			name[c] = char(Parser::get_byte(*file_speed, tape_player)); | ||||
| 		name[6] = '\0'; | ||||
| 		file.name = name; | ||||
|  | ||||
| @@ -82,7 +82,7 @@ std::vector<File> Analyser::Static::MSX::GetFiles(const std::shared_ptr<Storage: | ||||
| 					int byte = Parser::get_byte(*file_speed, tape_player); | ||||
| 					if(byte == -1) break; | ||||
| 					contains_end_of_file |= (byte == 0x1a); | ||||
| 					file.data.push_back(static_cast<uint8_t>(byte)); | ||||
| 					file.data.push_back(uint8_t(byte)); | ||||
| 				} | ||||
| 				if(c != -1) break; | ||||
| 				if(contains_end_of_file) { | ||||
| @@ -105,13 +105,13 @@ std::vector<File> Analyser::Static::MSX::GetFiles(const std::shared_ptr<Storage: | ||||
| 			for(c = 0; c < sizeof(locations); ++c) { | ||||
| 				int byte = Parser::get_byte(*file_speed, tape_player); | ||||
| 				if(byte == -1) break; | ||||
| 				locations[c] = static_cast<uint8_t>(byte); | ||||
| 				locations[c] = uint8_t(byte); | ||||
| 			} | ||||
| 			if(c != sizeof(locations)) continue; | ||||
|  | ||||
| 			file.starting_address = static_cast<uint16_t>(locations[0] | (locations[1] << 8)); | ||||
| 			end_address = static_cast<uint16_t>(locations[2] | (locations[3] << 8)); | ||||
| 			file.entry_address = static_cast<uint16_t>(locations[4] | (locations[5] << 8)); | ||||
| 			file.starting_address = uint16_t(locations[0] | (locations[1] << 8)); | ||||
| 			end_address = uint16_t(locations[2] | (locations[3] << 8)); | ||||
| 			file.entry_address = uint16_t(locations[4] | (locations[5] << 8)); | ||||
|  | ||||
| 			if(end_address < file.starting_address) continue; | ||||
|  | ||||
| @@ -119,7 +119,7 @@ std::vector<File> Analyser::Static::MSX::GetFiles(const std::shared_ptr<Storage: | ||||
| 			while(length--) { | ||||
| 				int byte = Parser::get_byte(*file_speed, tape_player); | ||||
| 				if(byte == -1) continue; | ||||
| 				file.data.push_back(static_cast<uint8_t>(byte)); | ||||
| 				file.data.push_back(uint8_t(byte)); | ||||
| 			} | ||||
|  | ||||
| 			files.push_back(std::move(file)); | ||||
| @@ -135,10 +135,10 @@ std::vector<File> Analyser::Static::MSX::GetFiles(const std::shared_ptr<Storage: | ||||
| 			next_address_buffer[1] = Parser::get_byte(*file_speed, tape_player); | ||||
|  | ||||
| 			if(next_address_buffer[0] == -1 || next_address_buffer[1] == -1) break; | ||||
| 			file.data.push_back(static_cast<uint8_t>(next_address_buffer[0])); | ||||
| 			file.data.push_back(static_cast<uint8_t>(next_address_buffer[1])); | ||||
| 			file.data.push_back(uint8_t(next_address_buffer[0])); | ||||
| 			file.data.push_back(uint8_t(next_address_buffer[1])); | ||||
|  | ||||
| 			uint16_t next_address = static_cast<uint16_t>(next_address_buffer[0] | (next_address_buffer[1] << 8)); | ||||
| 			uint16_t next_address = uint16_t(next_address_buffer[0] | (next_address_buffer[1] << 8)); | ||||
| 			if(!next_address) { | ||||
| 				files.push_back(std::move(file)); | ||||
| 				break; | ||||
| @@ -155,7 +155,7 @@ std::vector<File> Analyser::Static::MSX::GetFiles(const std::shared_ptr<Storage: | ||||
| 					found_error = true; | ||||
| 					break; | ||||
| 				} | ||||
| 				file.data.push_back(static_cast<uint8_t>(byte)); | ||||
| 				file.data.push_back(uint8_t(byte)); | ||||
| 			} | ||||
| 			if(found_error) break; | ||||
| 		} | ||||
|   | ||||
| @@ -9,6 +9,8 @@ | ||||
| #ifndef Analyser_Static_MSX_Target_h | ||||
| #define Analyser_Static_MSX_Target_h | ||||
|  | ||||
| #include "../../../Reflection/Enum.hpp" | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include <string> | ||||
|  | ||||
| @@ -16,15 +18,24 @@ namespace Analyser { | ||||
| namespace Static { | ||||
| namespace MSX { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	bool has_disk_drive = false; | ||||
| 	std::string loading_command; | ||||
|  | ||||
| 	enum class Region { | ||||
| 	ReflectableEnum(Region, | ||||
| 		Japan, | ||||
| 		USA, | ||||
| 		Europe | ||||
| 	} region = Region::USA; | ||||
| 	); | ||||
| 	Region region = Region::USA; | ||||
|  | ||||
| 	Target(): Analyser::Static::Target(Machine::MSX) { | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(has_disk_drive); | ||||
| 			DeclareField(region); | ||||
| 			AnnounceEnum(Region); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										25
									
								
								Analyser/Static/Macintosh/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Analyser/Static/Macintosh/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // | ||||
| //  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 *const target = new Target; | ||||
| 	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 */ | ||||
							
								
								
									
										37
									
								
								Analyser/Static/Macintosh/Target.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								Analyser/Static/Macintosh/Target.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| // | ||||
| //  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 | ||||
|  | ||||
| #include "../../../Reflection/Enum.hpp" | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Macintosh { | ||||
|  | ||||
| struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	ReflectableEnum(Model, Mac128k, Mac512k, Mac512ke, MacPlus); | ||||
| 	Model model = Model::MacPlus; | ||||
|  | ||||
| 	Target() : Analyser::Static::Target(Machine::Macintosh) { | ||||
| 		// Boilerplate for declaring fields and potential values. | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(model); | ||||
| 			AnnounceEnum(Model); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #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,9 +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); | ||||
| 	target->machine = Machine::Oric; | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	target->confidence = 0.5; | ||||
|  | ||||
| 	int basic10_votes = 0; | ||||
| @@ -115,12 +159,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 +172,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; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -49,10 +49,10 @@ std::vector<File> Analyser::Static::Oric::GetFiles(const std::shared_ptr<Storage | ||||
| 		} | ||||
|  | ||||
| 		// read end and start addresses | ||||
| 		new_file.ending_address = static_cast<uint16_t>(parser.get_next_byte(tape, is_fast) << 8); | ||||
| 		new_file.ending_address |= static_cast<uint16_t>(parser.get_next_byte(tape, is_fast)); | ||||
| 		new_file.starting_address = static_cast<uint16_t>(parser.get_next_byte(tape, is_fast) << 8); | ||||
| 		new_file.starting_address |= static_cast<uint16_t>(parser.get_next_byte(tape, is_fast)); | ||||
| 		new_file.ending_address = uint16_t(parser.get_next_byte(tape, is_fast) << 8); | ||||
| 		new_file.ending_address |= uint16_t(parser.get_next_byte(tape, is_fast)); | ||||
| 		new_file.starting_address = uint16_t(parser.get_next_byte(tape, is_fast) << 8); | ||||
| 		new_file.starting_address |= uint16_t(parser.get_next_byte(tape, is_fast)); | ||||
|  | ||||
| 		// skip an empty byte | ||||
| 		parser.get_next_byte(tape, is_fast); | ||||
| @@ -61,7 +61,7 @@ std::vector<File> Analyser::Static::Oric::GetFiles(const std::shared_ptr<Storage | ||||
| 		char file_name[17]; | ||||
| 		int name_pos = 0; | ||||
| 		while(name_pos < 16) { | ||||
| 			file_name[name_pos] = (char)parser.get_next_byte(tape, is_fast); | ||||
| 			file_name[name_pos] = char(parser.get_next_byte(tape, is_fast)); | ||||
| 			if(!file_name[name_pos]) break; | ||||
| 			name_pos++; | ||||
| 		} | ||||
| @@ -72,7 +72,7 @@ std::vector<File> Analyser::Static::Oric::GetFiles(const std::shared_ptr<Storage | ||||
| 		std::size_t body_length = new_file.ending_address - new_file.starting_address + 1; | ||||
| 		new_file.data.reserve(body_length); | ||||
| 		for(std::size_t c = 0; c < body_length; c++) { | ||||
| 			new_file.data.push_back(static_cast<uint8_t>(parser.get_next_byte(tape, is_fast))); | ||||
| 			new_file.data.push_back(uint8_t(parser.get_next_byte(tape, is_fast))); | ||||
| 		} | ||||
|  | ||||
| 		// only one validation check: was there enough tape? | ||||
|   | ||||
| @@ -9,6 +9,8 @@ | ||||
| #ifndef Analyser_Static_Oric_Target_h | ||||
| #define Analyser_Static_Oric_Target_h | ||||
|  | ||||
| #include "../../../Reflection/Enum.hpp" | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include <string> | ||||
|  | ||||
| @@ -16,22 +18,34 @@ namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Oric { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| 	enum class ROM { | ||||
| struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	ReflectableEnum(ROM, | ||||
| 		BASIC10, | ||||
| 		BASIC11, | ||||
| 		Pravetz | ||||
| 	}; | ||||
| 	); | ||||
|  | ||||
| 	enum class DiskInterface { | ||||
| 	ReflectableEnum(DiskInterface, | ||||
| 		None, | ||||
| 		Microdisc, | ||||
| 		Pravetz, | ||||
| 		None | ||||
| 	}; | ||||
| 		Jasmin, | ||||
| 		BD500 | ||||
| 	); | ||||
|  | ||||
| 	ROM rom = ROM::BASIC11; | ||||
| 	DiskInterface disk_interface = DiskInterface::None; | ||||
| 	std::string loading_command; | ||||
| 	bool should_start_jasmin = false; | ||||
|  | ||||
| 	Target(): Analyser::Static::Target(Machine::Oric) { | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(rom); | ||||
| 			DeclareField(disk_interface); | ||||
| 			AnnounceEnum(ROM); | ||||
| 			AnnounceEnum(DiskInterface); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -18,9 +18,7 @@ Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &med | ||||
| 		return {}; | ||||
|  | ||||
| 	TargetList targets; | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
|  | ||||
| 	target->machine = Machine::MasterSystem; | ||||
| 	auto target = std::make_unique<Target>(); | ||||
|  | ||||
| 	// Files named .sg are treated as for the SG1000; otherwise assume a Master System. | ||||
| 	if(file_name.size() >= 2 && *(file_name.end() - 2) == 's' && *(file_name.end() - 1) == 'g') { | ||||
|   | ||||
| @@ -9,23 +9,27 @@ | ||||
| #ifndef Analyser_Static_Sega_Target_h | ||||
| #define Analyser_Static_Sega_Target_h | ||||
|  | ||||
| #include "../../../Reflection/Enum.hpp" | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Sega { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	enum class Model { | ||||
| 		SG1000, | ||||
| 		MasterSystem, | ||||
| 		MasterSystem2, | ||||
| 	}; | ||||
|  | ||||
| 	enum class Region { | ||||
| 	ReflectableEnum(Region, | ||||
| 		Japan, | ||||
| 		USA, | ||||
| 		Europe, | ||||
| 		Brazil | ||||
| 	}; | ||||
| 	); | ||||
|  | ||||
| 	enum class PagingScheme { | ||||
| 		Sega, | ||||
| @@ -35,6 +39,13 @@ struct Target: public ::Analyser::Static::Target { | ||||
| 	Model model = Model::MasterSystem; | ||||
| 	Region region = Region::Japan; | ||||
| 	PagingScheme paging_scheme = PagingScheme::Sega; | ||||
|  | ||||
| 	Target() : Analyser::Static::Target(Machine::MasterSystem) { | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(region); | ||||
| 			AnnounceEnum(Region); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| #define is_master_system(v) v >= Analyser::Static::Sega::Target::Model::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" | ||||
| @@ -85,34 +94,39 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | ||||
| 		TryInsert(list, class, platforms)	\ | ||||
| 	} | ||||
|  | ||||
| 	Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// 80 | ||||
| 	Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// 81 | ||||
| 	Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600)						// A26 | ||||
| 	Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn)		// ADF | ||||
| 	Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge)					// BIN | ||||
| 	Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX)												// CAS | ||||
| 	Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC)										// CDT | ||||
| 	Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision)					// COL | ||||
| 	Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape)											// CSW | ||||
| 	Format("d64", result.disks, Disk::DiskImageHolder<Storage::Disk::D64>, TargetPlatform::Commodore)		// D64 | ||||
| 	Format("dmk", result.disks, Disk::DiskImageHolder<Storage::Disk::DMK>, TargetPlatform::MSX)				// DMK | ||||
| 	Format("do", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)		// DO | ||||
| 	Format("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)			// DSD | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC)	// DSK (Amstrad CPC) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)		// DSK (Apple) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX)			// DSK (MSX) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric)		// DSK (Oric) | ||||
| 	Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore)		// G64 | ||||
| 	Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// 80 | ||||
| 	Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// 81 | ||||
| 	Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600)							// A26 | ||||
| 	Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn)			// ADF | ||||
| 	Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge)						// BIN (cartridge dump) | ||||
| 	Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX)													// CAS | ||||
| 	Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC)											// CDT | ||||
| 	Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision)						// COL | ||||
| 	Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape)												// CSW | ||||
| 	Format("d64", result.disks, Disk::DiskImageHolder<Storage::Disk::D64>, TargetPlatform::Commodore)			// D64 | ||||
| 	Format("dmk", result.disks, Disk::DiskImageHolder<Storage::Disk::DMK>, TargetPlatform::MSX)					// DMK | ||||
| 	Format("do", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)			// DO | ||||
| 	Format("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)				// DSD | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC)		// DSK (Amstrad CPC) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)			// DSK (Apple II) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// DSK (Macintosh, floppy disk) | ||||
| 	Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh)						// DSK (Macintosh, hard disk) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX)				// DSK (MSX) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric)			// DSK (Oric) | ||||
| 	Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore)			// G64 | ||||
| 	Format(	"hfe", | ||||
| 			result.disks, | ||||
| 			Disk::DiskImageHolder<Storage::Disk::HFE>, | ||||
| 			TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric) | ||||
| 			// HFE (TODO: switch to AllDisk once the MSX stops being so greedy) | ||||
| 	Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII)			// NIB | ||||
| 	Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// O | ||||
| 	Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// P | ||||
| 	Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)		// PO | ||||
| 	Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// P81 | ||||
| 	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)		// IMG (DiskCopy 4.2) | ||||
| 	Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// IMG (DiskCopy 4.2) | ||||
| 	Format("msa", result.disks, Disk::DiskImageHolder<Storage::Disk::MSA>, TargetPlatform::AtariST)				// MSA | ||||
| 	Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII)				// NIB | ||||
| 	Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// O | ||||
| 	Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// P | ||||
| 	Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)			// PO | ||||
| 	Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// P81 | ||||
|  | ||||
| 	// PRG | ||||
| 	if(extension == "prg") { | ||||
| @@ -129,16 +143,18 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | ||||
| 	Format(	"rom", | ||||
| 			result.cartridges, | ||||
| 			Cartridge::BinaryDump, | ||||
| 			TargetPlatform::AcornElectron | TargetPlatform::ColecoVision | TargetPlatform::MSX)				// ROM | ||||
| 	Format("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("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore)								// TAP (Commodore) | ||||
| 	Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric)										// TAP (Oric) | ||||
| 	Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX)												// TSX | ||||
| 	Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081)											// TZX | ||||
| 	Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn)											// UEF (tape) | ||||
| 	Format("woz", result.disks, Disk::DiskImageHolder<Storage::Disk::WOZ>, TargetPlatform::DiskII)			// WOZ | ||||
| 			TargetPlatform::AcornElectron | TargetPlatform::ColecoVision | TargetPlatform::MSX)					// ROM | ||||
| 	Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega)								// SG | ||||
| 	Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega)								// SMS | ||||
| 	Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)				// SSD | ||||
| 	Format("st", result.disks, Disk::DiskImageHolder<Storage::Disk::ST>, TargetPlatform::AtariST)				// ST | ||||
| 	Format("stx", result.disks, Disk::DiskImageHolder<Storage::Disk::STX>, TargetPlatform::AtariST)				// STX | ||||
| 	Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore)									// TAP (Commodore) | ||||
| 	Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric)											// TAP (Oric) | ||||
| 	Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX)													// TSX | ||||
| 	Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081)												// TZX | ||||
| 	Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn)												// UEF (tape) | ||||
| 	Format("woz", result.disks, Disk::DiskImageHolder<Storage::Disk::WOZ>, TargetPlatform::DiskII)				// WOZ | ||||
|  | ||||
| #undef Format | ||||
| #undef Insert | ||||
| @@ -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,20 @@ 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(); | ||||
| 	} | ||||
|  | ||||
| 	Media &operator +=(const Media &rhs) { | ||||
| #define append(name)	name.insert(name.end(), rhs.name.begin(), rhs.name.end()); | ||||
| 		append(disks); | ||||
| 		append(tapes); | ||||
| 		append(cartridges); | ||||
| 		append(mass_storage_devices); | ||||
| #undef append | ||||
| 		return *this; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| @@ -40,11 +52,12 @@ struct Media { | ||||
| 	and instructions on how to launch the software attached, plus a measure of confidence in this target's correctness. | ||||
| */ | ||||
| struct Target { | ||||
| 	Target(Machine machine) : machine(machine) {} | ||||
| 	virtual ~Target() {} | ||||
|  | ||||
| 	Machine machine; | ||||
| 	Media media; | ||||
| 	float confidence; | ||||
| 	float confidence = 0.0f; | ||||
| }; | ||||
| typedef std::vector<std::unique_ptr<Target>> TargetList; | ||||
|  | ||||
|   | ||||
| @@ -34,7 +34,7 @@ Analyser::Static::TargetList Analyser::Static::ZX8081::GetTargets(const Media &m | ||||
| 		std::vector<Storage::Data::ZX8081::File> files = GetFiles(media.tapes.front()); | ||||
| 		media.tapes.front()->reset(); | ||||
| 		if(!files.empty()) { | ||||
| 			Target *target = new Target; | ||||
| 			Target *const target = new Target; | ||||
| 			destination.push_back(std::unique_ptr<::Analyser::Static::Target>(target)); | ||||
| 			target->machine = Machine::ZX8081; | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,8 @@ | ||||
| #ifndef Analyser_Static_ZX8081_Target_h | ||||
| #define Analyser_Static_ZX8081_Target_h | ||||
|  | ||||
| #include "../../../Reflection/Enum.hpp" | ||||
| #include "../../../Reflection/Struct.hpp" | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include <string> | ||||
|  | ||||
| @@ -16,17 +18,26 @@ namespace Analyser { | ||||
| namespace Static { | ||||
| namespace ZX8081 { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| 	enum class MemoryModel { | ||||
| struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> { | ||||
| 	ReflectableEnum(MemoryModel, | ||||
| 		Unexpanded, | ||||
| 		SixteenKB, | ||||
| 		SixtyFourKB | ||||
| 	}; | ||||
| 	); | ||||
|  | ||||
| 	MemoryModel memory_model = MemoryModel::Unexpanded; | ||||
| 	bool is_ZX81 = false; | ||||
| 	bool ZX80_uses_ZX81_ROM = false; | ||||
| 	std::string loading_command; | ||||
|  | ||||
| 	Target(): Analyser::Static::Target(Machine::ZX8081) { | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(memory_model); | ||||
| 			DeclareField(is_ZX81); | ||||
| 			DeclareField(ZX80_uses_ZX81_ROM); | ||||
| 			AnnounceEnum(MemoryModel); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,82 +0,0 @@ | ||||
| // | ||||
| //  ClockDeferrer.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 23/08/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef ClockDeferrer_h | ||||
| #define ClockDeferrer_h | ||||
|  | ||||
| #include <functional> | ||||
| #include <vector> | ||||
|  | ||||
| /*! | ||||
| 	A ClockDeferrer 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 { | ||||
| 	public: | ||||
| 		/// Constructs a ClockDeferrer that will call target(period) in between deferred actions. | ||||
| 		ClockDeferrer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {} | ||||
|  | ||||
| 		/*! | ||||
| 			Schedules @c action to occur in @c delay units of time. | ||||
|  | ||||
| 			Actions must be scheduled in the order they will occur. It is undefined behaviour | ||||
| 			to schedule them out of order. | ||||
| 		*/ | ||||
| 		void defer(TimeUnit delay, const std::function<void(void)> &action) { | ||||
| 			pending_actions_.emplace_back(delay, action); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Runs for @c length units of time. | ||||
|  | ||||
| 			The constructor-supplied target will be called with one or more periods that add up to @c length; | ||||
| 			any scheduled actions will be called between periods. | ||||
| 		*/ | ||||
| 		void run_for(TimeUnit length) { | ||||
| 			// If there are no pending actions, just run for the entire length. | ||||
| 			// This should be the normal branch. | ||||
| 			if(pending_actions_.empty()) { | ||||
| 				target_(length); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			// Divide the time to run according to the pending actions. | ||||
| 			while(length > TimeUnit(0)) { | ||||
| 				TimeUnit next_period = pending_actions_.empty() ? length : std::min(length, pending_actions_[0].delay); | ||||
| 				target_(next_period); | ||||
| 				length -= next_period; | ||||
|  | ||||
| 				off_t performances = 0; | ||||
| 				for(auto &action: pending_actions_) { | ||||
| 					action.delay -= next_period; | ||||
| 					if(!action.delay) { | ||||
| 						action.action(); | ||||
| 						++performances; | ||||
| 					} | ||||
| 				} | ||||
| 				if(performances) { | ||||
| 					pending_actions_.erase(pending_actions_.begin(), pending_actions_.begin() + performances); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		std::function<void(TimeUnit)> target_; | ||||
|  | ||||
| 		// The list of deferred actions. | ||||
| 		struct DeferredAction { | ||||
| 			TimeUnit delay; | ||||
| 			std::function<void(void)> action; | ||||
|  | ||||
| 			DeferredAction(TimeUnit delay, const std::function<void(void)> &action) : delay(delay), action(std::move(action)) {} | ||||
| 		}; | ||||
| 		std::vector<DeferredAction> pending_actions_; | ||||
| }; | ||||
|  | ||||
| #endif /* ClockDeferrer_h */ | ||||
| @@ -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: | ||||
|   | ||||
| @@ -67,7 +67,7 @@ class Source { | ||||
| 		} | ||||
|  | ||||
| 		/// @returns the current preferred clocking strategy. | ||||
| 		virtual Preference preferred_clocking() = 0; | ||||
| 		virtual Preference preferred_clocking() const = 0; | ||||
|  | ||||
| 	private: | ||||
| 		Observer *observer_ = nullptr; | ||||
|   | ||||
							
								
								
									
										124
									
								
								ClockReceiver/DeferredQueue.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								ClockReceiver/DeferredQueue.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| // | ||||
| //  DeferredQueue.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 23/08/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef DeferredQueue_h | ||||
| #define DeferredQueue_h | ||||
|  | ||||
| #include <functional> | ||||
| #include <vector> | ||||
|  | ||||
| /*! | ||||
| 	Provides the logic to insert into and traverse a list of future scheduled items. | ||||
| */ | ||||
| template <typename TimeUnit> class DeferredQueue { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			Schedules @c action to occur in @c delay units of time. | ||||
| 		*/ | ||||
| 		void defer(TimeUnit delay, const std::function<void(void)> &action) { | ||||
| 			// Apply immediately if there's no delay (or a negative delay). | ||||
| 			if(delay <= TimeUnit(0)) { | ||||
| 				action(); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			if(!pending_actions_.empty()) { | ||||
| 				// Otherwise enqueue, having subtracted the delay for any preceding events, | ||||
| 				// and subtracting from the subsequent, if any. | ||||
| 				auto insertion_point = pending_actions_.begin(); | ||||
| 				while(insertion_point != pending_actions_.end() && insertion_point->delay < delay) { | ||||
| 					delay -= insertion_point->delay; | ||||
| 					++insertion_point; | ||||
| 				} | ||||
| 				if(insertion_point != pending_actions_.end()) { | ||||
| 					insertion_point->delay -= delay; | ||||
| 				} | ||||
|  | ||||
| 				pending_actions_.emplace(insertion_point, delay, action); | ||||
| 			} else { | ||||
| 				pending_actions_.emplace_back(delay, action); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			@returns The amount of time until the next enqueued action will occur, | ||||
| 				or TimeUnit(-1) if the queue is empty. | ||||
| 		*/ | ||||
| 		TimeUnit time_until_next_action() { | ||||
| 			if(pending_actions_.empty()) return TimeUnit(-1); | ||||
| 			return pending_actions_.front().delay; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Advances the queue the specified amount of time, performing any actions it reaches. | ||||
| 		*/ | ||||
| 		void advance(TimeUnit time) { | ||||
| 			auto erase_iterator = pending_actions_.begin(); | ||||
| 			while(erase_iterator != pending_actions_.end()) { | ||||
| 				erase_iterator->delay -= time; | ||||
| 				if(erase_iterator->delay <= TimeUnit(0)) { | ||||
| 					time = -erase_iterator->delay; | ||||
| 					erase_iterator->action(); | ||||
| 					++erase_iterator; | ||||
| 				} else { | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 			if(erase_iterator != pending_actions_.begin()) { | ||||
| 				pending_actions_.erase(pending_actions_.begin(), erase_iterator); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		// The list of deferred actions. | ||||
| 		struct DeferredAction { | ||||
| 			TimeUnit delay; | ||||
| 			std::function<void(void)> action; | ||||
|  | ||||
| 			DeferredAction(TimeUnit delay, const std::function<void(void)> &action) : delay(delay), action(std::move(action)) {} | ||||
| 		}; | ||||
| 		std::vector<DeferredAction> pending_actions_; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	A DeferredQueue maintains a list of ordered actions and the times at which | ||||
| 	they should happen, and divides a total execution period up into the portions | ||||
| 	that occur between those actions, triggering each action when it is reached. | ||||
|  | ||||
| 	This list is efficient only for short queues. | ||||
| */ | ||||
| template <typename TimeUnit> class DeferredQueuePerformer: public DeferredQueue<TimeUnit> { | ||||
| 	public: | ||||
| 		/// Constructs a DeferredQueue that will call target(period) in between deferred actions. | ||||
| 		DeferredQueuePerformer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {} | ||||
|  | ||||
| 		/*! | ||||
| 			Runs for @c length units of time. | ||||
|  | ||||
| 			The constructor-supplied target will be called with one or more periods that add up to @c length; | ||||
| 			any scheduled actions will be called between periods. | ||||
| 		*/ | ||||
| 		void run_for(TimeUnit length) { | ||||
| 			auto time_to_next = DeferredQueue<TimeUnit>::time_until_next_action(); | ||||
| 			while(time_to_next != TimeUnit(-1) && time_to_next <= length) { | ||||
| 				target_(time_to_next); | ||||
| 				length -= time_to_next; | ||||
| 				DeferredQueue<TimeUnit>::advance(time_to_next); | ||||
| 			} | ||||
|  | ||||
| 			DeferredQueue<TimeUnit>::advance(length); | ||||
| 			target_(length); | ||||
|  | ||||
| 			// TODO: optimise this to avoid the multiple std::vector deletes. Find a neat way to expose that solution, maybe? | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		std::function<void(TimeUnit)> target_; | ||||
| }; | ||||
|  | ||||
| #endif /* DeferredQueue_h */ | ||||
| @@ -9,9 +9,9 @@ | ||||
| #ifndef ForceInline_hpp | ||||
| #define ForceInline_hpp | ||||
|  | ||||
| #ifdef DEBUG | ||||
| #ifndef NDEBUG | ||||
|  | ||||
| #define forceinline | ||||
| #define forceinline inline | ||||
|  | ||||
| #else | ||||
|  | ||||
|   | ||||
							
								
								
									
										173
									
								
								ClockReceiver/JustInTime.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								ClockReceiver/JustInTime.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| // | ||||
| //  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_; | ||||
| 		} | ||||
|  | ||||
| 		/// Acts exactly as per the standard ->, but preserves constness. | ||||
| 		forceinline const T *operator->() const { | ||||
| 			auto non_const_this = const_cast<JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *>(this); | ||||
| 			non_const_this->flush(); | ||||
| 			return &object_; | ||||
| 		} | ||||
|  | ||||
| 		/// Returns a pointer to the included object without flushing time. | ||||
| 		forceinline T *last_valid() { | ||||
| 			return &object_; | ||||
| 		} | ||||
|  | ||||
| 		/// Flushes all accumulated time. | ||||
| 		forceinline void flush() { | ||||
| 			if(!is_flushed_) { | ||||
| 				is_flushed_ = true; | ||||
| 				if constexpr (divider == 1) { | ||||
| 					const auto duration = time_since_update_.template flush<TargetTimeScale>(); | ||||
| 					object_.run_for(duration); | ||||
| 				} else { | ||||
| 					const auto duration = time_since_update_.template divide<TargetTimeScale>(LocalTimeScale(divider)); | ||||
| 					if(duration > TargetTimeScale(0)) | ||||
| 						object_.run_for(duration); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		T object_; | ||||
| 		LocalTimeScale time_since_update_; | ||||
| 		bool is_flushed_ = true; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	A RealTimeActor presents the same interface as a JustInTimeActor but doesn't defer work. | ||||
| 	Time added will be performed immediately. | ||||
|  | ||||
| 	Its primary purpose is to allow consumers to remain flexible in their scheduling. | ||||
| */ | ||||
| template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class RealTimeActor { | ||||
| 	public: | ||||
| 		template<typename... Args> RealTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {} | ||||
|  | ||||
| 		forceinline void operator += (const LocalTimeScale &rhs) { | ||||
| 			if constexpr (multiplier == 1 && divider == 1) { | ||||
| 				object_.run_for(TargetTimeScale(rhs)); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			if constexpr (multiplier == 1) { | ||||
| 				accumulated_time_ += rhs; | ||||
| 			} else { | ||||
| 				accumulated_time_ += rhs * multiplier; | ||||
| 			} | ||||
|  | ||||
| 			if constexpr (divider == 1) { | ||||
| 				const auto duration = accumulated_time_.template flush<TargetTimeScale>(); | ||||
| 				object_.run_for(duration); | ||||
| 			} else { | ||||
| 				const auto duration = accumulated_time_.template divide<TargetTimeScale>(LocalTimeScale(divider)); | ||||
| 				if(duration > TargetTimeScale(0)) | ||||
| 					object_.run_for(duration); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		forceinline T *operator->()				{	return &object_;	} | ||||
| 		forceinline const T *operator->() const	{	return &object_;	} | ||||
| 		forceinline T *last_valid() 			{	return &object_;	} | ||||
| 		forceinline void flush()				{} | ||||
|  | ||||
| 	private: | ||||
| 		T object_; | ||||
| 		LocalTimeScale accumulated_time_; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue. | ||||
| 	Any time the amount of accumulated time crosses a threshold provided at construction time, | ||||
| 	the object will be updated on the AsyncTaskQueue. | ||||
| */ | ||||
| template <class T, class LocalTimeScale = 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 */ | ||||
							
								
								
									
										88
									
								
								ClockReceiver/ScanSynchroniser.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								ClockReceiver/ScanSynchroniser.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| // | ||||
| //  ScanSynchroniser.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 09/02/2020. | ||||
| //  Copyright © 2020 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef ScanSynchroniser_h | ||||
| #define ScanSynchroniser_h | ||||
|  | ||||
| #include "../Outputs/ScanTarget.hpp" | ||||
|  | ||||
| #include <cmath> | ||||
|  | ||||
| namespace Time { | ||||
|  | ||||
| /*! | ||||
| 	Where an emulated machine is sufficiently close to a host machine's frame rate that a small nudge in | ||||
| 	its speed multiplier will bring it into frame synchronisation, the ScanSynchroniser provides a sequence of | ||||
| 	speed multipliers designed both to adjust the machine to the proper speed and, in a reasonable amount | ||||
| 	of time, to bring it into phase. | ||||
| */ | ||||
| class ScanSynchroniser { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			@returns @c true if the emulated machine can be synchronised with the host frame output based on its | ||||
| 				current @c [scan]status and the host machine's @c frame_duration; @c false otherwise. | ||||
| 		*/ | ||||
| 		bool can_synchronise(const Outputs::Display::ScanStatus &scan_status, double frame_duration) { | ||||
| 			ratio_ = 1.0; | ||||
| 			if(scan_status.field_duration_gradient < 0.00001) { | ||||
| 				// Check out the machine's current frame time. | ||||
| 				// If it's within 3% of a non-zero integer multiple of the | ||||
| 				// display rate, mark this time window to be split over the sync. | ||||
| 				ratio_ = (frame_duration * base_multiplier_) / scan_status.field_duration; | ||||
| 				const double integer_ratio = round(ratio_); | ||||
| 				if(integer_ratio > 0.0) { | ||||
| 					ratio_ /= integer_ratio; | ||||
| 					return ratio_ <= maximum_rate_adjustment && ratio_ >= 1.0 / maximum_rate_adjustment; | ||||
| 				} | ||||
| 			} | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			@returns The appropriate speed multiplier for the next frame based on the inputs previously supplied to @c can_synchronise. | ||||
| 				Results are undefined if @c can_synchroise returned @c false. | ||||
| 		*/ | ||||
| 		double next_speed_multiplier(const Outputs::Display::ScanStatus &scan_status) { | ||||
| 			// The host versus emulated ratio is calculated based on the current perceived frame duration of the machine. | ||||
| 			// Either that number is exactly correct or it's already the result of some sort of low-pass filter. So there's | ||||
| 			// no benefit to second guessing it here — just take it to be correct. | ||||
| 			// | ||||
| 			// ... with one slight caveat, which is that it is desireable to adjust phase here, to align vertical sync points. | ||||
| 			// So the set speed multiplier may be adjusted slightly to aim for that. | ||||
| 			double speed_multiplier = 1.0 / (ratio_ / base_multiplier_); | ||||
| 			if(scan_status.current_position > 0.0) { | ||||
| 				if(scan_status.current_position < 0.5) speed_multiplier /= phase_adjustment_ratio; | ||||
| 				else speed_multiplier *= phase_adjustment_ratio; | ||||
| 			} | ||||
| 			speed_multiplier_ = (speed_multiplier_ * 0.95) + (speed_multiplier * 0.05); | ||||
| 			return speed_multiplier_ * base_multiplier_; | ||||
| 		} | ||||
|  | ||||
| 		void set_base_speed_multiplier(double multiplier) { | ||||
| 			base_multiplier_ = multiplier; | ||||
| 		} | ||||
|  | ||||
| 		double get_base_speed_multiplier() { | ||||
| 			return base_multiplier_; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		static constexpr double maximum_rate_adjustment = 1.03; | ||||
| 		static constexpr double phase_adjustment_ratio = 1.005; | ||||
|  | ||||
| 		// Managed local state. | ||||
| 		double speed_multiplier_ = 1.0; | ||||
| 		double base_multiplier_ = 1.0; | ||||
|  | ||||
| 		// Temporary storage to bridge the can_synchronise -> next_speed_multiplier gap. | ||||
| 		double ratio_ = 1.0; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* ScanSynchroniser_h */ | ||||
| @@ -9,9 +9,16 @@ | ||||
| #ifndef TimeTypes_h | ||||
| #define TimeTypes_h | ||||
|  | ||||
| #include <chrono> | ||||
|  | ||||
| namespace Time { | ||||
|  | ||||
| typedef double Seconds; | ||||
| typedef int64_t Nanos; | ||||
|  | ||||
| inline Nanos nanos_now() { | ||||
| 	return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,8 @@ | ||||
| #include "1770.hpp" | ||||
|  | ||||
| #include "../../Storage/Disk/Encodings/MFM/Constants.hpp" | ||||
|  | ||||
| #define LOG_PREFIX "[WD FDC] " | ||||
| #include "../../Outputs/Log.hpp" | ||||
|  | ||||
| using namespace WD; | ||||
| @@ -16,19 +18,19 @@ using namespace WD; | ||||
| WD1770::WD1770(Personality p) : | ||||
| 		Storage::Disk::MFMController(8000000), | ||||
| 		personality_(p), | ||||
| 		interesting_event_mask_(static_cast<int>(Event1770::Command)) { | ||||
| 		interesting_event_mask_(int(Event1770::Command)) { | ||||
| 	set_is_double_density(false); | ||||
| 	posit_event(static_cast<int>(Event1770::Command)); | ||||
| 	posit_event(int(Event1770::Command)); | ||||
| } | ||||
|  | ||||
| void WD1770::set_register(int address, uint8_t value) { | ||||
| void WD1770::write(int address, uint8_t value) { | ||||
| 	switch(address&3) { | ||||
| 		case 0: { | ||||
| 			if((value&0xf0) == 0xd0) { | ||||
| 				if(value == 0xd0) { | ||||
| 					// Force interrupt **immediately**. | ||||
| 					LOG("Force interrupt immediately"); | ||||
| 					posit_event(static_cast<int>(Event1770::ForceInterrupt)); | ||||
| 					posit_event(int(Event1770::ForceInterrupt)); | ||||
| 				} else { | ||||
| 					ERROR("!!!TODO: force interrupt!!!"); | ||||
| 					update_status([] (Status &status) { | ||||
| @@ -37,7 +39,7 @@ void WD1770::set_register(int address, uint8_t value) { | ||||
| 				} | ||||
| 			} else { | ||||
| 				command_ = value; | ||||
| 				posit_event(static_cast<int>(Event1770::Command)); | ||||
| 				posit_event(int(Event1770::Command)); | ||||
| 			} | ||||
| 		} | ||||
| 		break; | ||||
| @@ -52,27 +54,35 @@ void WD1770::set_register(int address, uint8_t value) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t WD1770::get_register(int address) { | ||||
| uint8_t WD1770::read(int address) { | ||||
| 	switch(address&3) { | ||||
| 		default: { | ||||
| 			update_status([] (Status &status) { | ||||
| 				status.interrupt_request = false; | ||||
| 			}); | ||||
| 			uint8_t status = | ||||
| 					(status_.write_protect ? Flag::WriteProtect : 0) | | ||||
| 					(status_.crc_error ? Flag::CRCError : 0) | | ||||
| 					(status_.busy ? Flag::Busy : 0); | ||||
| 				(status_.crc_error ? Flag::CRCError : 0) | | ||||
| 				(status_.busy ? Flag::Busy : 0); | ||||
|  | ||||
| 			// Per Jean Louis-Guérin's documentation: | ||||
| 			// | ||||
| 			//	* 	the write-protect bit is locked into place by a type 2 or type 3 command, but is | ||||
| 			//		read live after a type 1. | ||||
| 			//	*	the track 0 bit is captured during a type 1 instruction and lost upon any other type, | ||||
| 			//		it is not live sampled. | ||||
| 			switch(status_.type) { | ||||
| 				case Status::One: | ||||
| 					status |= | ||||
| 						(get_drive().get_is_track_zero() ? Flag::TrackZero : 0) | | ||||
| 						(status_.seek_error ? Flag::SeekError : 0); | ||||
| 						// TODO: index hole | ||||
| 						(status_.track_zero ? Flag::TrackZero : 0) | | ||||
| 						(status_.seek_error ? Flag::SeekError : 0) | | ||||
| 						(get_drive().get_is_read_only() ? Flag::WriteProtect : 0) | | ||||
| 						(get_drive().get_index_pulse() ? Flag::Index : 0); | ||||
| 				break; | ||||
|  | ||||
| 				case Status::Two: | ||||
| 				case Status::Three: | ||||
| 					status |= | ||||
| 						(status_.write_protect ? Flag::WriteProtect : 0) | | ||||
| 						(status_.record_type ? Flag::RecordType : 0) | | ||||
| 						(status_.lost_data ? Flag::LostData : 0) | | ||||
| 						(status_.data_request ? Flag::DataRequest : 0) | | ||||
| @@ -89,10 +99,15 @@ uint8_t WD1770::get_register(int address) { | ||||
| 				if(status_.type == Status::One) | ||||
| 					status |= (status_.spin_up ? Flag::SpinUp : 0); | ||||
| 			} | ||||
| //			LOG("Returned status " << PADHEX(2) << int(status) << " of type " << 1+int(status_.type)); | ||||
| 			return status; | ||||
| 		} | ||||
| 		case 1:		return track_; | ||||
| 		case 2:		return sector_; | ||||
| 		case 1: | ||||
| 			LOG("Returned track " << int(track_)); | ||||
| 			return track_; | ||||
| 		case 2: | ||||
| 			LOG("Returned sector " << int(sector_)); | ||||
| 			return sector_; | ||||
| 		case 3: | ||||
| 			update_status([] (Status &status) { | ||||
| 				status.data_request = false; | ||||
| @@ -105,28 +120,30 @@ void WD1770::run_for(const Cycles cycles) { | ||||
| 	Storage::Disk::Controller::run_for(cycles); | ||||
|  | ||||
| 	if(delay_time_) { | ||||
| 		unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_int()); | ||||
| 		const auto number_of_cycles = cycles.as_integral(); | ||||
| 		if(delay_time_ <= number_of_cycles) { | ||||
| 			delay_time_ = 0; | ||||
| 			posit_event(static_cast<int>(Event1770::Timer)); | ||||
| 			posit_event(int(Event1770::Timer)); | ||||
| 		} else { | ||||
| 			delay_time_ -= number_of_cycles; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(mask); return; case __LINE__: | ||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = int(mask); return; case __LINE__: | ||||
| #define WAIT_FOR_TIME(ms)		resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer); | ||||
| #define WAIT_FOR_BYTES(count)	resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = static_cast<int>(Event::Token); return; } | ||||
| #define WAIT_FOR_BYTES(count)	resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = int(Event::Token); return; } | ||||
| #define BEGIN_SECTION()	switch(resume_point_) { default: | ||||
| #define END_SECTION()	(void)0; } | ||||
|  | ||||
| #define READ_ID()	\ | ||||
| 		if(new_event_type == static_cast<int>(Event::Token)) {	\ | ||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; }	\ | ||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {	\ | ||||
| 		if(new_event_type == int(Event::Token)) {	\ | ||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {\ | ||||
| 				set_data_mode(DataMode::Reading);	\ | ||||
| 				++distance_into_section_;	\ | ||||
| 			} else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {	\ | ||||
| 				header_[distance_into_section_ - 1] = get_latest_token().byte_value;	\ | ||||
| 				distance_into_section_++;	\ | ||||
| 				++distance_into_section_;	\ | ||||
| 			}	\ | ||||
| 		} | ||||
|  | ||||
| @@ -159,10 +176,10 @@ void WD1770::run_for(const Cycles cycles) { | ||||
| // +--------+----------+-------------------------+ | ||||
|  | ||||
| void WD1770::posit_event(int new_event_type) { | ||||
| 	if(new_event_type == static_cast<int>(Event::IndexHole)) { | ||||
| 	if(new_event_type == int(Event::IndexHole)) { | ||||
| 		index_hole_count_++; | ||||
| 		if(index_hole_count_target_ == index_hole_count_) { | ||||
| 			posit_event(static_cast<int>(Event1770::IndexHoleTarget)); | ||||
| 			posit_event(int(Event1770::IndexHoleTarget)); | ||||
| 			index_hole_count_target_ = -1; | ||||
| 		} | ||||
|  | ||||
| @@ -177,15 +194,16 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if(new_event_type == static_cast<int>(Event1770::ForceInterrupt)) { | ||||
| 	if(new_event_type == int(Event1770::ForceInterrupt)) { | ||||
| 		interesting_event_mask_ = 0; | ||||
| 		resume_point_ = 0; | ||||
| 		update_status([] (Status &status) { | ||||
| 			status.type = Status::One; | ||||
| 			status.data_request = false; | ||||
| 			status.spin_up = false; | ||||
| 		}); | ||||
| 	} else { | ||||
| 		if(!(interesting_event_mask_ & static_cast<int>(new_event_type))) return; | ||||
| 		if(!(interesting_event_mask_ & int(new_event_type))) return; | ||||
| 		interesting_event_mask_ &= ~new_event_type; | ||||
| 	} | ||||
|  | ||||
| @@ -208,6 +226,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		update_status([] (Status &status) { | ||||
| 			status.busy = true; | ||||
| 			status.interrupt_request = false; | ||||
| 			status.track_zero = false;	// Always reset by a non-type 1; so reset regardless and set properly later. | ||||
| 		}); | ||||
|  | ||||
| 		LOG("Starting " << PADHEX(2) << int(command_)); | ||||
| @@ -240,6 +259,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			status.data_request = false; | ||||
| 		}); | ||||
|  | ||||
| 		LOG("Step/Seek/Restore with track " << int(track_) << " data " << int(data_)); | ||||
| 		if(!has_motor_on_line() && !has_head_load_line()) goto test_type1_type; | ||||
|  | ||||
| 		if(has_motor_on_line()) goto begin_type1_spin_up; | ||||
| @@ -272,19 +292,19 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		} | ||||
|  | ||||
| 	perform_seek_or_restore_command: | ||||
| 		if(track_ == data_) goto verify; | ||||
| 		if(track_ == data_) goto verify_seek; | ||||
| 		step_direction_ = (data_ > track_); | ||||
|  | ||||
| 	adjust_track: | ||||
| 		if(step_direction_) track_++; else track_--; | ||||
| 		if(step_direction_) ++track_; else --track_; | ||||
|  | ||||
| 	perform_step: | ||||
| 		if(!step_direction_ && get_drive().get_is_track_zero()) { | ||||
| 			track_ = 0; | ||||
| 			goto verify; | ||||
| 			goto verify_seek; | ||||
| 		} | ||||
| 		get_drive().step(Storage::Disk::HeadPosition(step_direction_ ? 1 : -1)); | ||||
| 		unsigned int time_to_wait; | ||||
| 		Cycles::IntType time_to_wait; | ||||
| 		switch(command_ & 3) { | ||||
| 			default: | ||||
| 			case 0: time_to_wait = 6;	break; | ||||
| @@ -293,14 +313,17 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			case 3: time_to_wait = (personality_ == P1772) ? 3 : 30;	break; | ||||
| 		} | ||||
| 		WAIT_FOR_TIME(time_to_wait); | ||||
| 		if(command_ >> 5) goto verify; | ||||
| 		if(command_ >> 5) goto verify_seek; | ||||
| 		goto perform_seek_or_restore_command; | ||||
|  | ||||
| 	perform_step_command: | ||||
| 		if(command_ & 0x10) goto adjust_track; | ||||
| 		goto perform_step; | ||||
|  | ||||
| 	verify: | ||||
| 	verify_seek: | ||||
| 		update_status([this] (Status &status) { | ||||
| 			status.track_zero = get_drive().get_is_track_zero(); | ||||
| 		}); | ||||
| 		if(!(command_ & 0x04)) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| @@ -309,17 +332,20 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		distance_into_section_ = 0; | ||||
|  | ||||
| 	verify_read_data: | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | ||||
| 		WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token)); | ||||
| 		READ_ID(); | ||||
|  | ||||
| 		if(index_hole_count_ == 6) { | ||||
| 			LOG("Nothing found to verify"); | ||||
| 			update_status([] (Status &status) { | ||||
| 				status.seek_error = true; | ||||
| 			}); | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		if(distance_into_section_ == 7) { | ||||
| 			distance_into_section_ = 0; | ||||
| 			set_data_mode(DataMode::Scanning); | ||||
|  | ||||
| 			if(get_crc_generator().get_value()) { | ||||
| 				update_status([] (Status &status) { | ||||
| 					status.crc_error = true; | ||||
| @@ -334,8 +360,6 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 				}); | ||||
| 				goto wait_for_command; | ||||
| 			} | ||||
|  | ||||
| 			distance_into_section_ = 0; | ||||
| 		} | ||||
| 		goto verify_read_data; | ||||
|  | ||||
| @@ -392,8 +416,11 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
|  | ||||
| 		distance_into_section_ = 0; | ||||
| 		set_data_mode(DataMode::Scanning); | ||||
|  | ||||
| 	type2_get_header: | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | ||||
| 		WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token)); | ||||
| 		READ_ID(); | ||||
|  | ||||
| 		if(index_hole_count_ == 5) { | ||||
| @@ -404,8 +431,10 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		if(distance_into_section_ == 7) { | ||||
| 			LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2])); | ||||
| 			distance_into_section_ = 0; | ||||
| 			set_data_mode(DataMode::Scanning); | ||||
|  | ||||
| 			LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2])); | ||||
| 			if(		header_[0] == track_ && header_[2] == sector_ && | ||||
| 					(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) { | ||||
| 				LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2])); | ||||
| @@ -422,7 +451,6 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 				}); | ||||
| 				goto type2_read_or_write_data; | ||||
| 			} | ||||
| 			distance_into_section_ = 0; | ||||
| 		} | ||||
| 		goto type2_get_header; | ||||
|  | ||||
| @@ -453,7 +481,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 			status.data_request = true; | ||||
| 		}); | ||||
| 		distance_into_section_++; | ||||
| 		if(distance_into_section_ == 128 << header_[3]) { | ||||
| 		if(distance_into_section_ == 128 << (header_[3]&3)) { | ||||
| 			distance_into_section_ = 0; | ||||
| 			goto type2_check_crc; | ||||
| 		} | ||||
| @@ -465,6 +493,9 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		header_[distance_into_section_] = get_latest_token().byte_value; | ||||
| 		distance_into_section_++; | ||||
| 		if(distance_into_section_ == 2) { | ||||
| 			distance_into_section_ = 0; | ||||
| 			set_data_mode(DataMode::Scanning); | ||||
|  | ||||
| 			if(get_crc_generator().get_value()) { | ||||
| 				LOG("CRC error; terminating"); | ||||
| 				update_status([this] (Status &status) { | ||||
| @@ -473,11 +504,13 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 				goto wait_for_command; | ||||
| 			} | ||||
|  | ||||
| 			LOG("Finished reading sector " << std::dec << int(sector_)); | ||||
|  | ||||
| 			if(command_ & 0x10) { | ||||
| 				sector_++; | ||||
| 				LOG("Advancing to search for sector " << std::dec << int(sector_)); | ||||
| 				goto test_type2_write_protection; | ||||
| 			} | ||||
| 			LOG("Finished reading sector " << std::dec << int(sector_)); | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		goto type2_check_crc; | ||||
| @@ -531,7 +564,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		*/ | ||||
| 		write_byte(data_); | ||||
| 		distance_into_section_++; | ||||
| 		if(distance_into_section_ == 128 << header_[3]) { | ||||
| 		if(distance_into_section_ == 128 << (header_[3]&3)) { | ||||
| 			goto type2_write_crc; | ||||
| 		} | ||||
|  | ||||
| @@ -610,8 +643,8 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		distance_into_section_ = 0; | ||||
|  | ||||
| 	read_address_get_header: | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | ||||
| 		if(new_event_type == static_cast<int>(Event::Token)) { | ||||
| 		WAIT_FOR_EVENT(int(Event::IndexHole) | int(Event::Token)); | ||||
| 		if(new_event_type == int(Event::Token)) { | ||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; } | ||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { | ||||
| 				if(status_.data_request) { | ||||
| @@ -625,9 +658,11 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 				update_status([] (Status &status) { | ||||
| 					status.data_request = true; | ||||
| 				}); | ||||
| 				distance_into_section_++; | ||||
| 				++distance_into_section_; | ||||
|  | ||||
| 				if(distance_into_section_ == 7) { | ||||
| 					distance_into_section_ = 0; | ||||
|  | ||||
| 					if(get_crc_generator().get_value()) { | ||||
| 						update_status([] (Status &status) { | ||||
| 							status.crc_error = true; | ||||
| @@ -651,7 +686,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 		index_hole_count_ = 0; | ||||
|  | ||||
| 	read_track_read_byte: | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); | ||||
| 		WAIT_FOR_EVENT(int(Event::Token) | int(Event::IndexHole)); | ||||
| 		if(index_hole_count_) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| @@ -718,7 +753,7 @@ void WD1770::posit_event(int new_event_type) { | ||||
| 				case 0xfd: case 0xfe: | ||||
| 					// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022 | ||||
| 					write_raw_short( | ||||
| 						static_cast<uint16_t>( | ||||
| 						uint16_t( | ||||
| 							0xa022 | | ||||
| 							((data_ & 0x80) << 7) | | ||||
| 							((data_ & 0x40) << 6) | | ||||
| @@ -767,15 +802,18 @@ void WD1770::posit_event(int new_event_type) { | ||||
| } | ||||
|  | ||||
| void WD1770::update_status(std::function<void(Status &)> updater) { | ||||
| 	const Status old_status = status_; | ||||
|  | ||||
| 	if(delegate_) { | ||||
| 		Status old_status = status_; | ||||
| 		updater(status_); | ||||
| 		bool did_change = | ||||
| 		const bool did_change = | ||||
| 			(status_.busy != old_status.busy) || | ||||
| 			(status_.data_request != old_status.data_request); | ||||
| 			(status_.data_request != old_status.data_request) || | ||||
| 			(status_.interrupt_request != old_status.interrupt_request); | ||||
| 		if(did_change) delegate_->wd1770_did_change_output(this); | ||||
| 	} | ||||
| 	else updater(status_); | ||||
| 	} else updater(status_); | ||||
|  | ||||
| 	if(status_.busy != old_status.busy) update_clocking_observer(); | ||||
| } | ||||
|  | ||||
| void WD1770::set_head_load_request(bool head_load) {} | ||||
| @@ -783,5 +821,14 @@ void WD1770::set_motor_on(bool motor_on) {} | ||||
|  | ||||
| void WD1770::set_head_loaded(bool head_loaded) { | ||||
| 	head_is_loaded_ = head_loaded; | ||||
| 	if(head_loaded) posit_event(static_cast<int>(Event1770::HeadLoad)); | ||||
| 	if(head_loaded) posit_event(int(Event1770::HeadLoad)); | ||||
| } | ||||
|  | ||||
| bool WD1770::get_head_loaded() const { | ||||
| 	return head_is_loaded_; | ||||
| } | ||||
|  | ||||
| ClockingHint::Preference WD1770::preferred_clocking() const { | ||||
| 	if(status_.busy) return ClockingHint::Preference::RealTime; | ||||
| 	return Storage::Disk::MFMController::preferred_clocking(); | ||||
| } | ||||
|   | ||||
| @@ -36,52 +36,57 @@ class WD1770: public Storage::Disk::MFMController { | ||||
| 		using Storage::Disk::MFMController::set_is_double_density; | ||||
|  | ||||
| 		/// Writes @c value to the register at @c address. Only the low two bits of the address are decoded. | ||||
| 		void set_register(int address, uint8_t value); | ||||
| 		void write(int address, uint8_t value); | ||||
|  | ||||
| 		/// Fetches the value of the register @c address. Only the low two bits of the address are decoded. | ||||
| 		uint8_t get_register(int address); | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		/// Runs the controller for @c number_of_cycles cycles. | ||||
| 		void run_for(const Cycles cycles); | ||||
|  | ||||
| 		enum Flag: uint8_t { | ||||
| 			NotReady		= 0x80, | ||||
| 			NotReady		= 0x80,		// 0x80 | ||||
| 			MotorOn			= 0x80, | ||||
| 			WriteProtect	= 0x40, | ||||
| 			RecordType		= 0x20, | ||||
| 			WriteProtect	= 0x40,		// 0x40 | ||||
| 			RecordType		= 0x20,		// 0x20 | ||||
| 			SpinUp			= 0x20, | ||||
| 			HeadLoaded		= 0x20, | ||||
| 			RecordNotFound	= 0x10, | ||||
| 			RecordNotFound	= 0x10,		// 0x10 | ||||
| 			SeekError		= 0x10, | ||||
| 			CRCError		= 0x08, | ||||
| 			LostData		= 0x04, | ||||
| 			CRCError		= 0x08,		// 0x08 | ||||
| 			LostData		= 0x04,		// 0x04 | ||||
| 			TrackZero		= 0x04, | ||||
| 			DataRequest		= 0x02, | ||||
| 			DataRequest		= 0x02,		// 0x02 | ||||
| 			Index			= 0x02, | ||||
| 			Busy			= 0x01 | ||||
| 			Busy			= 0x01		// 0x01 | ||||
| 		}; | ||||
|  | ||||
| 		/// @returns The current value of the IRQ line output. | ||||
| 		inline bool get_interrupt_request_line()		{	return status_.interrupt_request;	} | ||||
| 		inline bool get_interrupt_request_line() const	{	return status_.interrupt_request;	} | ||||
|  | ||||
| 		/// @returns The current value of the DRQ line output. | ||||
| 		inline bool get_data_request_line()				{	return status_.data_request;		} | ||||
| 		inline bool get_data_request_line() const		{	return status_.data_request;		} | ||||
|  | ||||
| 		class Delegate { | ||||
| 			public: | ||||
| 				virtual void wd1770_did_change_output(WD1770 *wd1770) = 0; | ||||
| 		}; | ||||
| 		inline void set_delegate(Delegate *delegate)	{	delegate_ = delegate;			} | ||||
| 		inline void set_delegate(Delegate *delegate)	{	delegate_ = delegate;				} | ||||
|  | ||||
| 		ClockingHint::Preference preferred_clocking() const 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() const; | ||||
|  | ||||
| 	private: | ||||
| 		Personality personality_; | ||||
| 		inline bool has_motor_on_line() { return (personality_ != P1793 ) && (personality_ != P1773); } | ||||
| 		inline bool has_head_load_line() { return (personality_ == P1793 ); } | ||||
| 		const Personality personality_; | ||||
| 		bool has_motor_on_line() const { return (personality_ != P1793 ) && (personality_ != P1773); } | ||||
| 		bool has_head_load_line() const { return (personality_ == P1793 ); } | ||||
|  | ||||
| 		struct Status { | ||||
| 			bool write_protect = false; | ||||
| @@ -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,26 +77,6 @@ class IRQDelegatePortHandler: public PortHandler { | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| }; | ||||
|  | ||||
| class MOS6522Base: public MOS6522Storage { | ||||
| 	public: | ||||
| 		/// Sets the input value of line @c line on port @c port. | ||||
| 		void set_control_line_input(Port port, Line line, bool value); | ||||
|  | ||||
| 		/// Runs for a specified number of half cycles. | ||||
| 		void run_for(const HalfCycles half_cycles); | ||||
|  | ||||
| 		/// Runs for a specified number of cycles. | ||||
| 		void run_for(const Cycles cycles); | ||||
|  | ||||
| 		/// @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'). | ||||
|  | ||||
| @@ -102,25 +88,53 @@ class MOS6522Base: public MOS6522Storage { | ||||
| 	Consumers should derive their own curiously-recurring-template-pattern subclass, | ||||
| 	implementing bus communications as required. | ||||
| */ | ||||
| template <class T> class MOS6522: public MOS6522Base { | ||||
| 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 set_register(int address, uint8_t value); | ||||
| 		void write(int address, uint8_t value); | ||||
|  | ||||
| 		/*! Gets a register value. */ | ||||
| 		uint8_t get_register(int address); | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		/*! @returns the bus handler. */ | ||||
| 		T &bus_handler(); | ||||
|  | ||||
| 		/// Sets the input value of line @c line on port @c port. | ||||
| 		void set_control_line_input(Port port, Line line, bool value); | ||||
|  | ||||
| 		/// Runs for a specified number of half cycles. | ||||
| 		void run_for(const HalfCycles half_cycles); | ||||
|  | ||||
| 		/// Runs for a specified number of cycles. | ||||
| 		void run_for(const Cycles cycles); | ||||
|  | ||||
| 		/// @returns @c true if the IRQ line is currently active; @c false otherwise. | ||||
| 		bool get_interrupt_line() const; | ||||
|  | ||||
| 		/// 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(); | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -11,36 +11,65 @@ | ||||
| namespace MOS { | ||||
| namespace MOS6522 { | ||||
|  | ||||
| template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) { | ||||
| 	address &= 0xf; | ||||
| 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; | ||||
|  | ||||
| 		// Timer 1 | ||||
| 		case 0x6:	case 0x4:	registers_.timer_latch[0] = (registers_.timer_latch[0]&0xff00) | value;	break; | ||||
| 		case 0x5:	case 0x7: | ||||
| 			registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | static_cast<uint16_t>(value << 8); | ||||
| 			registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | uint16_t(value << 8); | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer1; | ||||
| 			if(address == 0x05) { | ||||
| 				registers_.next_timer[0] = registers_.timer_latch[0]; | ||||
| @@ -53,37 +82,63 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) | ||||
| 		case 0x8:	registers_.timer_latch[1] = value;	break; | ||||
| 		case 0x9: | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer2; | ||||
| 			registers_.next_timer[1] = registers_.timer_latch[1] | static_cast<uint16_t>(value << 8); | ||||
| 			registers_.next_timer[1] = registers_.timer_latch[1] | uint16_t(value << 8); | ||||
| 			timer_is_running_[1] = true; | ||||
| 			reevaluate_interrupts(); | ||||
| 		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: | ||||
| 		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: 	LOG("Unimplemented control line mode " << int((value >> 1)&7));		break; | ||||
| 					case 0x0c:	bus_handler_.set_control_line_output(Port::A, Line::Two, false);	break; | ||||
| 					case 0x0e:	bus_handler_.set_control_line_output(Port::A, Line::Two, true);		break; | ||||
| 			int shift = 0; | ||||
| 			for(int port = 0; port < 2; ++port) { | ||||
| 				handshake_modes_[port] = HandshakeMode::None; | ||||
| 				switch((value >> shift) & 0x0e) { | ||||
| 					default: break; | ||||
|  | ||||
| 					case 0x00:	// Negative interrupt input; set Cx2 interrupt on negative Cx2 transition, clear on access to Port x register. | ||||
| 					case 0x02:	// Independent negative interrupt input; set Cx2 interrupt on negative transition, don't clear automatically. | ||||
| 					case 0x04:	// Positive interrupt input; set Cx2 interrupt on positive Cx2 transition, clear on access to Port x register. | ||||
| 					case 0x06:	// Independent positive interrupt input; set Cx2 interrupt on positive transition, don't clear automatically. | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::Input); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x08:	// Handshake: set Cx2 to low on any read or write of Port x; set to high on an active transition of Cx1. | ||||
| 						handshake_modes_[port] = HandshakeMode::Handshake; | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::Off);	// At a guess. | ||||
| 					break; | ||||
|  | ||||
| 					case 0x0a:	// Pulse output: Cx2 is low for one cycle following a read or write of Port x. | ||||
| 						handshake_modes_[port] = HandshakeMode::Pulse; | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::On); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x0c:	// Manual output: Cx2 low. | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::Off); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x0e:	// Manual output: Cx2 high. | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::On); | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				shift += 4; | ||||
| 			} | ||||
| 			if(value & 0x80) { | ||||
| 				switch(value & 0xe0) { | ||||
| 					default: 	LOG("Unimplemented control line mode " << int((value >> 5)&7));		break; | ||||
| 					case 0xc0:	bus_handler_.set_control_line_output(Port::B, Line::Two, false);	break; | ||||
| 					case 0xe0:	bus_handler_.set_control_line_output(Port::B, Line::Two, true);		break; | ||||
| 				} | ||||
| 			} | ||||
| 		break; | ||||
| 		} break; | ||||
|  | ||||
| 		// Interrupt control | ||||
| 		case 0xd: | ||||
| @@ -100,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(); | ||||
| @@ -132,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; | ||||
| @@ -145,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); | ||||
| } | ||||
|  | ||||
| @@ -158,9 +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] = uint16_t(registers_.next_timer[0]); | ||||
| 		registers_.next_timer[0] = -1; | ||||
| 	} | ||||
| 	if(registers_.next_timer[1] >= 0) { | ||||
| 		registers_.timer[1] = 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() const { | ||||
| 	uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f; | ||||
| 	return interrupt_status; | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::evaluate_cb2_output() { | ||||
| 	// CB2 is a special case, being both the line the shift register can output to, | ||||
| 	// and one that can be used as an input or handshaking output according to the | ||||
| 	// peripheral control register. | ||||
|  | ||||
| 	// My guess: other CB2 functions work only if the shift register is disabled (?). | ||||
| 	if(shift_mode() != ShiftMode::Disabled) { | ||||
| 		// Shift register is enabled, one way or the other; but announce only output. | ||||
| 		if(is_shifting_out()) { | ||||
| 			// Output mode; set the level according to the current top of the shift register. | ||||
| 			bus_handler_.set_control_line_output(Port::B, Line::Two, !!(registers_.shift & 0x80)); | ||||
| 		} else { | ||||
| 			// Input mode. | ||||
| 			bus_handler_.set_control_line_output(Port::B, Line::Two, true); | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Shift register is disabled. | ||||
| 		bus_handler_.set_control_line_output(Port::B, Line::Two, control_outputs_[1].lines[1] != LineState::Off); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::set_control_line_output(Port port, Line line, LineState value) { | ||||
| 	if(port == Port::B && line == Line::Two) { | ||||
| 		control_outputs_[port].lines[line] = value; | ||||
| 		evaluate_cb2_output(); | ||||
| 	} else { | ||||
| 		// Do nothing if unchanged. | ||||
| 		if(value == control_outputs_[port].lines[line]) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		control_outputs_[port].lines[line] = value; | ||||
|  | ||||
| 		if(value != LineState::Input) { | ||||
| 			bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 			bus_handler_.set_control_line_output(port, line, value != LineState::Off); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::shift_in() { | ||||
| 	registers_.shift = uint8_t((registers_.shift << 1) | (control_inputs_[1].lines[1] ? 1 : 0)); | ||||
| 	--shift_bits_remaining_; | ||||
| 	if(!shift_bits_remaining_) { | ||||
| 		registers_.interrupt_flags |= InterruptFlag::ShiftRegister; | ||||
| 		reevaluate_interrupts(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::shift_out() { | ||||
| 	// When shifting out, the shift register rotates rather than strictly shifts. | ||||
| 	// TODO: is that true for all modes? | ||||
| 	if(shift_mode() == ShiftMode::OutUnderT2FreeRunning || shift_bits_remaining_) { | ||||
| 		registers_.shift = uint8_t((registers_.shift << 1) | (registers_.shift >> 7)); | ||||
| 		evaluate_cb2_output(); | ||||
|  | ||||
| 		--shift_bits_remaining_; | ||||
| 		if(!shift_bits_remaining_) { | ||||
| 			registers_.interrupt_flags |= InterruptFlag::ShiftRegister; | ||||
| 			reevaluate_interrupts(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class MOS6522Storage { | ||||
|  | ||||
| 		// The registers | ||||
| 		struct Registers { | ||||
| 			// "A  low  reset  (RES)  input  clears  all  R6522  internal registers to logic 0" | ||||
| 			// "A low reset (RES) input clears all R6522 internal registers to logic 0" | ||||
| 			uint8_t output[2] = {0, 0}; | ||||
| 			uint8_t input[2] = {0, 0}; | ||||
| 			uint8_t data_direction[2] = {0, 0}; | ||||
| @@ -37,14 +37,27 @@ class MOS6522Storage { | ||||
| 			bool timer_needs_reload = false; | ||||
| 		} registers_; | ||||
|  | ||||
| 		// control state | ||||
| 		// Control state. | ||||
| 		struct { | ||||
| 			bool line_one = false; | ||||
| 			bool line_two = false; | ||||
| 			bool lines[2] = {false, false}; | ||||
| 		} control_inputs_[2]; | ||||
|  | ||||
| 		enum class LineState { | ||||
| 			On, Off, Input | ||||
| 		}; | ||||
| 		struct { | ||||
| 			LineState lines[2] = {LineState::Input, LineState::Input}; | ||||
| 		} control_outputs_[2]; | ||||
|  | ||||
| 		enum class HandshakeMode { | ||||
| 			None, | ||||
| 			Handshake, | ||||
| 			Pulse | ||||
| 		} handshake_modes_[2] = { HandshakeMode::None, HandshakeMode::None }; | ||||
|  | ||||
| 		bool timer_is_running_[2] = {false, false}; | ||||
| 		bool last_posted_interrupt_status_ = false; | ||||
| 		int shift_bits_remaining_ = 8; | ||||
|  | ||||
| 		enum InterruptFlag: uint8_t { | ||||
| 			CA2ActiveEdge	= 1 << 0, | ||||
| @@ -55,6 +68,23 @@ class MOS6522Storage { | ||||
| 			Timer2			= 1 << 5, | ||||
| 			Timer1			= 1 << 6, | ||||
| 		}; | ||||
|  | ||||
| 		enum class ShiftMode { | ||||
| 			Disabled = 0, | ||||
| 			InUnderT2 = 1, | ||||
| 			InUnderPhase2 = 2, | ||||
| 			InUnderCB1 = 3, | ||||
| 			OutUnderT2FreeRunning = 4, | ||||
| 			OutUnderT2 = 5, | ||||
| 			OutUnderPhase2 = 6, | ||||
| 			OutUnderCB1 = 7 | ||||
| 		}; | ||||
| 		ShiftMode shift_mode() const { | ||||
| 			return ShiftMode((registers_.auxiliary_control >> 2) & 7); | ||||
| 		} | ||||
| 		bool is_shifting_out() const { | ||||
| 			return registers_.auxiliary_control & 0x10; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -32,7 +32,7 @@ template <class T> class MOS6532 { | ||||
| 		inline void set_ram(uint16_t address, uint8_t value)	{	ram_[address&0x7f] = value;		} | ||||
| 		inline uint8_t get_ram(uint16_t address)				{	return ram_[address & 0x7f];	} | ||||
|  | ||||
| 		inline void set_register(int address, uint8_t value) { | ||||
| 		inline void write(int address, uint8_t value) { | ||||
| 			const uint8_t decodedAddress = address & 0x07; | ||||
| 			switch(decodedAddress) { | ||||
| 				// Port output | ||||
| @@ -51,7 +51,7 @@ template <class T> class MOS6532 { | ||||
| 				case 0x04: case 0x05: case 0x06: case 0x07: | ||||
| 					if(address & 0x10) { | ||||
| 						timer_.writtenShift = timer_.activeShift = (decodedAddress - 0x04) * 3 + (decodedAddress / 0x07);	// i.e. 0, 3, 6, 10 | ||||
| 						timer_.value = (static_cast<unsigned int>(value) << timer_.activeShift) ; | ||||
| 						timer_.value = (unsigned(value) << timer_.activeShift) ; | ||||
| 						timer_.interrupt_enabled = !!(address&0x08); | ||||
| 						interrupt_status_ &= ~InterruptFlag::Timer; | ||||
| 						evaluate_interrupts(); | ||||
| @@ -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 | ||||
| @@ -79,7 +79,7 @@ template <class T> class MOS6532 { | ||||
|  | ||||
| 				// Timer and interrupt control | ||||
| 				case 0x04: case 0x06: { | ||||
| 					uint8_t value = static_cast<uint8_t>(timer_.value >> timer_.activeShift); | ||||
| 					uint8_t value = uint8_t(timer_.value >> timer_.activeShift); | ||||
| 					timer_.interrupt_enabled = !!(address&0x08); | ||||
| 					interrupt_status_ &= ~InterruptFlag::Timer; | ||||
| 					evaluate_interrupts(); | ||||
| @@ -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 = unsigned(cycles.as_integral()); | ||||
|  | ||||
| 			// permit counting _to_ zero; counting _through_ zero initiates the other behaviour | ||||
| 			if(timer_.value >= number_of_cycles) { | ||||
| @@ -122,7 +122,7 @@ template <class T> class MOS6532 { | ||||
| 		} | ||||
|  | ||||
| 		MOS6532() { | ||||
| 			timer_.value = static_cast<unsigned int>((rand() & 0xff) << 10); | ||||
| 			timer_.value = unsigned((rand() & 0xff) << 10); | ||||
| 		} | ||||
|  | ||||
| 		inline void set_port_did_change(int port) { | ||||
| @@ -142,7 +142,7 @@ template <class T> class MOS6532 { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		inline bool get_inerrupt_line() { | ||||
| 		inline bool get_inerrupt_line() const { | ||||
| 			return interrupt_line_; | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -17,13 +17,13 @@ AudioGenerator::AudioGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue | ||||
|  | ||||
|  | ||||
| void AudioGenerator::set_volume(uint8_t volume) { | ||||
| 	audio_queue_.defer([=]() { | ||||
| 		volume_ = static_cast<int16_t>(volume) * range_multiplier_; | ||||
| 	audio_queue_.defer([this, volume]() { | ||||
| 		volume_ = int16_t(volume) * range_multiplier_; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void AudioGenerator::set_control(int channel, uint8_t value) { | ||||
| 	audio_queue_.defer([=]() { | ||||
| 	audio_queue_.defer([this, channel, value]() { | ||||
| 		control_registers_[channel] = value; | ||||
| 	}); | ||||
| } | ||||
| @@ -98,7 +98,7 @@ static uint8_t noise_pattern[] = { | ||||
|  | ||||
| #define shift(r) shift_registers_[r] = (shift_registers_[r] << 1) | (((shift_registers_[r]^0x80)&control_registers_[r]) >> 7) | ||||
| #define increment(r) shift_registers_[r] = (shift_registers_[r]+1)%8191 | ||||
| #define update(r, m, up) counters_[r]++; if((counters_[r] >> m) == 0x80) { up(r); counters_[r] = static_cast<unsigned int>(control_registers_[r]&0x7f) << m; } | ||||
| #define update(r, m, up) counters_[r]++; if((counters_[r] >> m) == 0x80) { up(r); counters_[r] = unsigned(control_registers_[r]&0x7f) << m; } | ||||
| // Note on slightly askew test: as far as I can make out, if the value in the register is 0x7f then what's supposed to happen | ||||
| // is that the 0x7f is loaded, on the next clocked cycle the Vic spots a 0x7f, pumps the output, reloads, etc. No increment | ||||
| // ever occurs. It's conditional. I don't really want two conditionals if I can avoid it so I'm incrementing regardless and | ||||
| @@ -114,7 +114,7 @@ void AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target) | ||||
|  | ||||
| 		// this sums the output of all three sounds channels plus a DC offset for volume; | ||||
| 		// TODO: what's the real ratio of this stuff? | ||||
| 		target[c] = static_cast<int16_t>( | ||||
| 		target[c] = int16_t( | ||||
| 			(shift_registers_[0]&1) + | ||||
| 			(shift_registers_[1]&1) + | ||||
| 			(shift_registers_[2]&1) + | ||||
| @@ -133,7 +133,7 @@ void AudioGenerator::skip_samples(std::size_t number_of_samples) { | ||||
| } | ||||
|  | ||||
| void AudioGenerator::set_sample_volume_range(std::int16_t range) { | ||||
| 	range_multiplier_ = static_cast<int16_t>(range / 64); | ||||
| 	range_multiplier_ = int16_t(range / 64); | ||||
| } | ||||
|  | ||||
| #undef shift | ||||
|   | ||||
| @@ -30,6 +30,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource { | ||||
| 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||
| 		void skip_samples(std::size_t number_of_samples); | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| 		static constexpr bool get_is_stereo() { return false; } | ||||
|  | ||||
| 	private: | ||||
| 		Concurrency::DeferringAsyncTaskQueue &audio_queue_; | ||||
| @@ -58,7 +59,7 @@ enum class OutputMode { | ||||
| 	To run the VIC for a cycle, the caller should call @c get_address, make the requested bus access | ||||
| 	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: | ||||
| @@ -80,12 +81,14 @@ template <class BusHandler> class MOS6560 { | ||||
| 		} | ||||
|  | ||||
| 		void set_clock_rate(double clock_rate) { | ||||
| 			speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0)); | ||||
| 			speaker_.set_input_rate(float(clock_rate / 4.0)); | ||||
| 		} | ||||
|  | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target)		{ crt_.set_scan_target(scan_target); 	} | ||||
| 		void set_display_type(Outputs::Display::DisplayType display_type)	{ crt_.set_display_type(display_type); 	} | ||||
| 		Outputs::Speaker::Speaker *get_speaker() { return &speaker_; } | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target)		{ crt_.set_scan_target(scan_target); 			} | ||||
| 		Outputs::Display::ScanStatus get_scaled_scan_status() const			{ return crt_.get_scaled_scan_status() / 4.0f;	} | ||||
| 		void set_display_type(Outputs::Display::DisplayType display_type)	{ crt_.set_display_type(display_type); 			} | ||||
| 		Outputs::Display::DisplayType get_display_type() const				{ return crt_.get_display_type(); 				} | ||||
| 		Outputs::Speaker::Speaker *get_speaker()	 						{ return &speaker_; 							} | ||||
|  | ||||
| 		void set_high_frequency_cutoff(float cutoff) { | ||||
| 			speaker_.set_high_frequency_cutoff(cutoff); | ||||
| @@ -170,7 +173,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_; | ||||
| @@ -232,7 +235,7 @@ template <class BusHandler> class MOS6560 { | ||||
| 					if(column_counter_&1) { | ||||
| 						fetch_address = registers_.character_cell_start_address + (character_code_*(registers_.tall_characters ? 16 : 8)) + current_character_row_; | ||||
| 					} else { | ||||
| 						fetch_address = static_cast<uint16_t>(registers_.video_matrix_start_address + video_matrix_address_counter_); | ||||
| 						fetch_address = uint16_t(registers_.video_matrix_start_address + video_matrix_address_counter_); | ||||
| 						video_matrix_address_counter_++; | ||||
| 						if( | ||||
| 							(current_character_row_ == 15) || | ||||
| @@ -353,7 +356,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) { | ||||
| @@ -368,7 +371,7 @@ template <class BusHandler> class MOS6560 { | ||||
|  | ||||
| 				case 0x2: | ||||
| 					registers_.number_of_columns = value & 0x7f; | ||||
| 					registers_.video_matrix_start_address = static_cast<uint16_t>((registers_.video_matrix_start_address & 0x3c00) | ((value & 0x80) << 2)); | ||||
| 					registers_.video_matrix_start_address = uint16_t((registers_.video_matrix_start_address & 0x3c00) | ((value & 0x80) << 2)); | ||||
| 				break; | ||||
|  | ||||
| 				case 0x3: | ||||
| @@ -377,8 +380,8 @@ template <class BusHandler> class MOS6560 { | ||||
| 				break; | ||||
|  | ||||
| 				case 0x5: | ||||
| 					registers_.character_cell_start_address = static_cast<uint16_t>((value & 0x0f) << 10); | ||||
| 					registers_.video_matrix_start_address = static_cast<uint16_t>((registers_.video_matrix_start_address & 0x0200) | ((value & 0xf0) << 6)); | ||||
| 					registers_.character_cell_start_address = uint16_t((value & 0x0f) << 10); | ||||
| 					registers_.video_matrix_start_address = uint16_t((registers_.video_matrix_start_address & 0x0200) | ((value & 0xf0) << 6)); | ||||
| 				break; | ||||
|  | ||||
| 				case 0xa: | ||||
| @@ -417,11 +420,11 @@ template <class BusHandler> class MOS6560 { | ||||
| 		/* | ||||
| 			Reads from a 6560 register. | ||||
| 		*/ | ||||
| 		uint8_t get_register(int address) { | ||||
| 		uint8_t read(int address) const { | ||||
| 			address &= 0xf; | ||||
| 			switch(address) { | ||||
| 				default: return registers_.direct_values[address]; | ||||
| 				case 0x03: return static_cast<uint8_t>(raster_value() << 7) | (registers_.direct_values[3] & 0x7f); | ||||
| 				case 0x03: return uint8_t(raster_value() << 7) | (registers_.direct_values[3] & 0x7f); | ||||
| 				case 0x04: return (raster_value() >> 1) & 0xff; | ||||
| 			} | ||||
| 		} | ||||
| @@ -459,11 +462,11 @@ template <class BusHandler> class MOS6560 { | ||||
|  | ||||
| 		// counters that cover an entire field | ||||
| 		int horizontal_counter_ = 0, vertical_counter_ = 0; | ||||
| 		const int lines_this_field() { | ||||
| 		int lines_this_field() const { | ||||
| 			// Necessary knowledge here: only the NTSC 6560 supports interlaced video. | ||||
| 			return registers_.interlaced ? (is_odd_frame_ ? 262 : 263) : timing_.lines_per_progressive_field; | ||||
| 		} | ||||
| 		const int raster_value() { | ||||
| 		int raster_value() const { | ||||
| 			const int bonus_line = (horizontal_counter_ + timing_.line_counter_increment_offset) / timing_.cycles_per_line; | ||||
| 			const int line = vertical_counter_ + bonus_line; | ||||
| 			const int final_line = lines_this_field(); | ||||
| @@ -478,7 +481,7 @@ template <class BusHandler> class MOS6560 { | ||||
| 			} | ||||
| 			// Cf. http://www.sleepingelephant.com/ipw-web/bulletin/bb/viewtopic.php?f=14&t=7237&start=15#p80737 | ||||
| 		} | ||||
| 		bool is_odd_frame() { | ||||
| 		bool is_odd_frame() const { | ||||
| 			return is_odd_frame_ || !registers_.interlaced; | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -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]) { | ||||
| @@ -167,8 +167,8 @@ template <class T> class CRTC6845 { | ||||
| 	private: | ||||
| 		inline void perform_bus_cycle_phase1() { | ||||
| 			// Skew theory of operation: keep a history of the last three states, and apply whichever is selected. | ||||
| 			character_is_visible_shifter_ = (character_is_visible_shifter_ << 1) | static_cast<unsigned int>(character_is_visible_); | ||||
| 			bus_state_.display_enable = (static_cast<int>(character_is_visible_shifter_) & display_skew_mask_) && line_is_visible_; | ||||
| 			character_is_visible_shifter_ = (character_is_visible_shifter_ << 1) | unsigned(character_is_visible_); | ||||
| 			bus_state_.display_enable = (int(character_is_visible_shifter_) & display_skew_mask_) && line_is_visible_; | ||||
| 			bus_handler_.perform_bus_cycle_phase1(bus_state_); | ||||
| 		} | ||||
|  | ||||
| @@ -240,7 +240,7 @@ template <class T> class CRTC6845 { | ||||
| 		inline void do_end_of_frame() { | ||||
| 			line_counter_ = 0; | ||||
| 			line_is_visible_ = true; | ||||
| 			line_address_ = static_cast<uint16_t>((registers_[12] << 8) | registers_[13]); | ||||
| 			line_address_ = uint16_t((registers_[12] << 8) | registers_[13]); | ||||
| 			bus_state_.refresh_address = line_address_; | ||||
| 		} | ||||
|  | ||||
|   | ||||
							
								
								
									
										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() const { | ||||
| 	// 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() const 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() const { | ||||
| 	// 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() const 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]; | ||||
|   | ||||
| @@ -79,10 +79,10 @@ namespace { | ||||
| i8272::i8272(BusHandler &bus_handler, Cycles clock_rate) : | ||||
| 	Storage::Disk::MFMController(clock_rate), | ||||
| 	bus_handler_(bus_handler) { | ||||
| 	posit_event(static_cast<int>(Event8272::CommandByte)); | ||||
| 	posit_event(int(Event8272::CommandByte)); | ||||
| } | ||||
|  | ||||
| ClockingHint::Preference i8272::preferred_clocking() { | ||||
| ClockingHint::Preference i8272::preferred_clocking() const { | ||||
| 	const auto mfm_controller_preferred_clocking = Storage::Disk::MFMController::preferred_clocking(); | ||||
| 	if(mfm_controller_preferred_clocking != ClockingHint::Preference::None) return mfm_controller_preferred_clocking; | ||||
| 	return is_sleeping_ ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime; | ||||
| @@ -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)); | ||||
| 			posit_event(int(Event8272::Timer)); | ||||
| 		} else { | ||||
| 			delay_time_ -= cycles.as_int(); | ||||
| 			delay_time_ -= cycles.as_integral(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -108,13 +108,13 @@ 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. | ||||
| 					int direction = (drives_[c].target_head_position < drives_[c].head_position) ? -1 : 1; | ||||
| 					LOG("Target " << PADDEC(0) << drives_[c].target_head_position << " versus believed " << static_cast<int>(drives_[c].head_position)); | ||||
| 					LOG("Target " << PADDEC(0) << drives_[c].target_head_position << " versus believed " << int(drives_[c].head_position)); | ||||
| 					select_drive(c); | ||||
| 					get_drive().step(Storage::Disk::HeadPosition(direction)); | ||||
| 					if(drives_[c].target_head_position >= 0) drives_[c].head_position += direction; | ||||
| @@ -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; | ||||
| @@ -156,14 +156,14 @@ void i8272::run_for(Cycles cycles) { | ||||
|  | ||||
| 	// check for busy plus ready disabled | ||||
| 	if(is_executing_ && !get_drive().get_is_ready()) { | ||||
| 		posit_event(static_cast<int>(Event8272::NoLongerReady)); | ||||
| 		posit_event(int(Event8272::NoLongerReady)); | ||||
| 	} | ||||
|  | ||||
| 	is_sleeping_ = !delay_time_ && !drives_seeking_ && !head_timers_running_; | ||||
| 	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; | ||||
|  | ||||
| @@ -177,16 +177,16 @@ void i8272::set_register(int address, uint8_t value) { | ||||
| 	} else { | ||||
| 		// accumulate latest byte in the command byte sequence | ||||
| 		command_.push_back(value); | ||||
| 		posit_event(static_cast<int>(Event8272::CommandByte)); | ||||
| 		posit_event(int(Event8272::CommandByte)); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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(); | ||||
| 		result_stack_.pop_back(); | ||||
| 		if(result_stack_.empty()) posit_event(static_cast<int>(Event8272::ResultEmpty)); | ||||
| 		if(result_stack_.empty()) posit_event(int(Event8272::ResultEmpty)); | ||||
|  | ||||
| 		return result; | ||||
| 	} else { | ||||
| @@ -198,16 +198,16 @@ uint8_t i8272::get_register(int address) { | ||||
| #define END_SECTION()	} | ||||
|  | ||||
| #define MS_TO_CYCLES(x)			x * 8000 | ||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(mask); return; case __LINE__: | ||||
| #define WAIT_FOR_TIME(ms)		resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(Event8272::Timer); delay_time_ = MS_TO_CYCLES(ms); is_sleeping_ = false; update_clocking_observer(); case __LINE__: if(delay_time_) return; | ||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = int(mask); return; case __LINE__: | ||||
| #define WAIT_FOR_TIME(ms)		resume_point_ = __LINE__; interesting_event_mask_ = int(Event8272::Timer); delay_time_ = MS_TO_CYCLES(ms); is_sleeping_ = false; update_clocking_observer(); case __LINE__: if(delay_time_) return; | ||||
|  | ||||
| #define PASTE(x, y) x##y | ||||
| #define CONCAT(x, y) PASTE(x, y) | ||||
|  | ||||
| #define FIND_HEADER()	\ | ||||
| 	set_data_mode(DataMode::Scanning);	\ | ||||
| 	CONCAT(find_header, __LINE__): WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); \ | ||||
| 	if(event_type == static_cast<int>(Event::IndexHole)) { index_hole_limit_--; }	\ | ||||
| 	CONCAT(find_header, __LINE__): WAIT_FOR_EVENT(int(Event::Token) | int(Event::IndexHole)); \ | ||||
| 	if(event_type == int(Event::IndexHole)) { index_hole_limit_--; }	\ | ||||
| 	else if(get_latest_token().type == Token::ID) goto CONCAT(header_found, __LINE__);	\ | ||||
| 	\ | ||||
| 	if(index_hole_limit_) goto CONCAT(find_header, __LINE__);	\ | ||||
| @@ -215,8 +215,8 @@ uint8_t i8272::get_register(int address) { | ||||
|  | ||||
| #define FIND_DATA()	\ | ||||
| 	set_data_mode(DataMode::Scanning);	\ | ||||
| 	CONCAT(find_data, __LINE__): WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); \ | ||||
| 	if(event_type == static_cast<int>(Event::Token)) { \ | ||||
| 	CONCAT(find_data, __LINE__): WAIT_FOR_EVENT(int(Event::Token) | int(Event::IndexHole)); \ | ||||
| 	if(event_type == int(Event::Token)) { \ | ||||
| 		if(get_latest_token().type == Token::Byte || get_latest_token().type == Token::Sync) goto CONCAT(find_data, __LINE__);	\ | ||||
| 	} | ||||
|  | ||||
| @@ -264,8 +264,8 @@ uint8_t i8272::get_register(int address) { | ||||
| 	} | ||||
|  | ||||
| void i8272::posit_event(int event_type) { | ||||
| 	if(event_type == static_cast<int>(Event::IndexHole)) index_hole_count_++; | ||||
| 	if(event_type == static_cast<int>(Event8272::NoLongerReady)) { | ||||
| 	if(event_type == int(Event::IndexHole)) index_hole_count_++; | ||||
| 	if(event_type == int(Event8272::NoLongerReady)) { | ||||
| 		SetNotReady(); | ||||
| 		goto abort; | ||||
| 	} | ||||
| @@ -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, | ||||
| @@ -425,12 +425,12 @@ void i8272::posit_event(int event_type) { | ||||
| 	// Performs the read data or read deleted data command. | ||||
| 	read_data: | ||||
| 			LOG(PADHEX(2) << "Read [deleted] data [" | ||||
| 				<< static_cast<int>(command_[2]) << " " | ||||
| 				<< static_cast<int>(command_[3]) << " " | ||||
| 				<< static_cast<int>(command_[4]) << " " | ||||
| 				<< static_cast<int>(command_[5]) << " ... " | ||||
| 				<< static_cast<int>(command_[6]) << " " | ||||
| 				<< static_cast<int>(command_[8]) << "]"); | ||||
| 				<< int(command_[2]) << " " | ||||
| 				<< int(command_[3]) << " " | ||||
| 				<< int(command_[4]) << " " | ||||
| 				<< int(command_[5]) << " ... " | ||||
| 				<< int(command_[6]) << " " | ||||
| 				<< int(command_[8]) << "]"); | ||||
| 		read_next_data: | ||||
| 			goto read_write_find_header; | ||||
|  | ||||
| @@ -439,7 +439,7 @@ void i8272::posit_event(int event_type) { | ||||
| 		read_data_found_header: | ||||
| 			FIND_DATA(); | ||||
| 			ClearControlMark(); | ||||
| 			if(event_type == static_cast<int>(Event::Token)) { | ||||
| 			if(event_type == int(Event::Token)) { | ||||
| 				if(get_latest_token().type != Token::Data && get_latest_token().type != Token::DeletedData) { | ||||
| 					// Something other than a data mark came next, impliedly an ID or index mark. | ||||
| 					SetMissingAddressMark(); | ||||
| @@ -470,24 +470,24 @@ void i8272::posit_event(int event_type) { | ||||
| 		// | ||||
| 		// TODO: consider DTL. | ||||
| 		read_data_get_byte: | ||||
| 			WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); | ||||
| 			if(event_type == static_cast<int>(Event::Token)) { | ||||
| 			WAIT_FOR_EVENT(int(Event::Token) | int(Event::IndexHole)); | ||||
| 			if(event_type == int(Event::Token)) { | ||||
| 				result_stack_.push_back(get_latest_token().byte_value); | ||||
| 				distance_into_section_++; | ||||
| 				SetDataRequest(); | ||||
| 				SetDataDirectionToProcessor(); | ||||
| 				WAIT_FOR_EVENT(static_cast<int>(Event8272::ResultEmpty) | static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); | ||||
| 				WAIT_FOR_EVENT(int(Event8272::ResultEmpty) | int(Event::Token) | int(Event::IndexHole)); | ||||
| 			} | ||||
| 			switch(event_type) { | ||||
| 				case static_cast<int>(Event8272::ResultEmpty):	// The caller read the byte in time; proceed as normal. | ||||
| 				case int(Event8272::ResultEmpty):	// The caller read the byte in time; proceed as normal. | ||||
| 					ResetDataRequest(); | ||||
| 					if(distance_into_section_ < (128 << size_)) goto read_data_get_byte; | ||||
| 				break; | ||||
| 				case static_cast<int>(Event::Token):				// The caller hasn't read the old byte yet and a new one has arrived | ||||
| 				case int(Event::Token):				// The caller hasn't read the old byte yet and a new one has arrived | ||||
| 					SetOverrun(); | ||||
| 					goto abort; | ||||
| 				break; | ||||
| 				case static_cast<int>(Event::IndexHole): | ||||
| 				case int(Event::IndexHole): | ||||
| 					SetEndOfCylinder(); | ||||
| 					goto abort; | ||||
| 				break; | ||||
| @@ -515,12 +515,12 @@ void i8272::posit_event(int event_type) { | ||||
|  | ||||
| 	write_data: | ||||
| 			LOG(PADHEX(2) << "Write [deleted] data [" | ||||
| 				<< static_cast<int>(command_[2]) << " " | ||||
| 				<< static_cast<int>(command_[3]) << " " | ||||
| 				<< static_cast<int>(command_[4]) << " " | ||||
| 				<< static_cast<int>(command_[5]) << " ... " | ||||
| 				<< static_cast<int>(command_[6]) << " " | ||||
| 				<< static_cast<int>(command_[8]) << "]"); | ||||
| 				<< int(command_[2]) << " " | ||||
| 				<< int(command_[3]) << " " | ||||
| 				<< int(command_[4]) << " " | ||||
| 				<< int(command_[5]) << " ... " | ||||
| 				<< int(command_[6]) << " " | ||||
| 				<< int(command_[8]) << "]"); | ||||
|  | ||||
| 			if(get_drive().get_is_read_only()) { | ||||
| 				SetNotWriteable(); | ||||
| @@ -571,7 +571,7 @@ void i8272::posit_event(int event_type) { | ||||
| 	// Performs the read ID command. | ||||
| 	read_id: | ||||
| 		// Establishes the drive and head being addressed, and whether in double density mode. | ||||
| 			LOG(PADHEX(2) << "Read ID [" << static_cast<int>(command_[0]) << " " << static_cast<int>(command_[1]) << "]"); | ||||
| 			LOG(PADHEX(2) << "Read ID [" << int(command_[0]) << " " << int(command_[1]) << "]"); | ||||
|  | ||||
| 		// Sets a maximum index hole limit of 2 then waits either until it finds a header mark or sees too many index holes. | ||||
| 		// If a header mark is found, reads in the following bytes that produce a header. Otherwise branches to data not found. | ||||
| @@ -594,10 +594,10 @@ void i8272::posit_event(int event_type) { | ||||
| 	// Performs read track. | ||||
| 	read_track: | ||||
| 			LOG(PADHEX(2) << "Read track [" | ||||
| 				<< static_cast<int>(command_[2]) << " " | ||||
| 				<< static_cast<int>(command_[3]) << " " | ||||
| 				<< static_cast<int>(command_[4]) << " " | ||||
| 				<< static_cast<int>(command_[5]) << "]"); | ||||
| 				<< int(command_[2]) << " " | ||||
| 				<< int(command_[3]) << " " | ||||
| 				<< int(command_[4]) << " " | ||||
| 				<< int(command_[5]) << "]"); | ||||
|  | ||||
| 			// Wait for the index hole. | ||||
| 			WAIT_FOR_EVENT(Event::IndexHole); | ||||
| @@ -627,7 +627,7 @@ void i8272::posit_event(int event_type) { | ||||
| 			distance_into_section_++; | ||||
| 			SetDataRequest(); | ||||
| 			// TODO: other possible exit conditions; find a way to merge with the read_data version of this. | ||||
| 			WAIT_FOR_EVENT(static_cast<int>(Event8272::ResultEmpty)); | ||||
| 			WAIT_FOR_EVENT(int(Event8272::ResultEmpty)); | ||||
| 			ResetDataRequest(); | ||||
| 			if(distance_into_section_ < (128 << header_[2])) goto read_track_get_byte; | ||||
|  | ||||
| @@ -664,13 +664,13 @@ void i8272::posit_event(int event_type) { | ||||
| 			expects_input_ = true; | ||||
| 			distance_into_section_ = 0; | ||||
| 		format_track_write_header: | ||||
| 			WAIT_FOR_EVENT(static_cast<int>(Event::DataWritten) | static_cast<int>(Event::IndexHole)); | ||||
| 			WAIT_FOR_EVENT(int(Event::DataWritten) | int(Event::IndexHole)); | ||||
| 			switch(event_type) { | ||||
| 				case static_cast<int>(Event::IndexHole): | ||||
| 				case int(Event::IndexHole): | ||||
| 					SetOverrun(); | ||||
| 					goto abort; | ||||
| 				break; | ||||
| 				case static_cast<int>(Event::DataWritten): | ||||
| 				case int(Event::DataWritten): | ||||
| 					header_[distance_into_section_] = input_; | ||||
| 					write_byte(input_); | ||||
| 					has_input_ = false; | ||||
| @@ -683,10 +683,10 @@ void i8272::posit_event(int event_type) { | ||||
| 			} | ||||
|  | ||||
| 			LOG(PADHEX(2) << "W:" | ||||
| 				<< static_cast<int>(header_[0]) << " " | ||||
| 				<< static_cast<int>(header_[1]) << " " | ||||
| 				<< static_cast<int>(header_[2]) << " " | ||||
| 				<< static_cast<int>(header_[3]) << ", " | ||||
| 				<< int(header_[0]) << " " | ||||
| 				<< int(header_[1]) << " " | ||||
| 				<< int(header_[2]) << " " | ||||
| 				<< int(header_[3]) << ", " | ||||
| 				<< get_crc_generator().get_value()); | ||||
| 			write_crc(); | ||||
|  | ||||
| @@ -706,8 +706,8 @@ void i8272::posit_event(int event_type) { | ||||
| 			// Otherwise, pad out to the index hole. | ||||
| 		format_track_pad: | ||||
| 			write_byte(get_is_double_density() ? 0x4e : 0xff); | ||||
| 			WAIT_FOR_EVENT(static_cast<int>(Event::DataWritten) | static_cast<int>(Event::IndexHole)); | ||||
| 			if(event_type != static_cast<int>(Event::IndexHole)) goto format_track_pad; | ||||
| 			WAIT_FOR_EVENT(int(Event::DataWritten) | int(Event::IndexHole)); | ||||
| 			if(event_type != int(Event::IndexHole)) goto format_track_pad; | ||||
|  | ||||
| 			end_writing(); | ||||
|  | ||||
| @@ -758,7 +758,7 @@ void i8272::posit_event(int event_type) { | ||||
| 				// up in run_for understands to mean 'keep going until track 0 is active'). | ||||
| 				if(command_.size() > 2) { | ||||
| 					drives_[drive].target_head_position = command_[2]; | ||||
| 					LOG(PADHEX(2) << "Seek to " << static_cast<int>(command_[2])); | ||||
| 					LOG(PADHEX(2) << "Seek to " << int(command_[2])); | ||||
| 				} else { | ||||
| 					drives_[drive].target_head_position = -1; | ||||
| 					drives_[drive].head_position = 0; | ||||
| @@ -789,7 +789,7 @@ void i8272::posit_event(int event_type) { | ||||
| 				// If a drive was found, return its results. Otherwise return a single 0x80. | ||||
| 				if(found_drive != -1) { | ||||
| 					drives_[found_drive].phase = Drive::NotSeeking; | ||||
| 					status_[0] = static_cast<uint8_t>(found_drive); | ||||
| 					status_[0] = uint8_t(found_drive); | ||||
| 					main_status_ &= ~(1 << found_drive); | ||||
| 					SetSeekEnd(); | ||||
|  | ||||
| @@ -819,7 +819,7 @@ void i8272::posit_event(int event_type) { | ||||
| 				int drive = command_[1] & 3; | ||||
| 				select_drive(drive); | ||||
| 				result_stack_= { | ||||
| 					static_cast<uint8_t>( | ||||
| 					uint8_t( | ||||
| 						(command_[1] & 7) |	// drive and head number | ||||
| 						0x08 |				// single sided | ||||
| 						(get_drive().get_is_track_zero() ? 0x10 : 0x00)	| | ||||
| @@ -853,9 +853,9 @@ void i8272::posit_event(int event_type) { | ||||
| 	// Posts whatever is in result_stack_ as a result phase. Be aware that it is a stack, so the | ||||
| 	// last thing in it will be returned first. | ||||
| 	post_result: | ||||
| 			LOGNBR(PADHEX(2) << "Result to " << static_cast<int>(command_[0] & 0x1f) << ", main " << static_cast<int>(main_status_) << "; "); | ||||
| 			LOGNBR(PADHEX(2) << "Result to " << int(command_[0] & 0x1f) << ", main " << int(main_status_) << "; "); | ||||
| 			for(std::size_t c = 0; c < result_stack_.size(); c++) { | ||||
| 				LOGNBR(" " << static_cast<int>(result_stack_[result_stack_.size() - 1 - c])); | ||||
| 				LOGNBR(" " << int(result_stack_[result_stack_.size() - 1 - c])); | ||||
| 			} | ||||
| 			LOGNBR(std::endl); | ||||
|  | ||||
| @@ -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() const final; | ||||
|  | ||||
| 	protected: | ||||
| 		virtual void select_drive(int number) = 0; | ||||
| @@ -67,13 +67,13 @@ class i8272: public Storage::Disk::MFMController { | ||||
| 			ResultEmpty = (1 << 5), | ||||
| 			NoLongerReady = (1 << 6) | ||||
| 		}; | ||||
| 		void posit_event(int type) override; | ||||
| 		int interesting_event_mask_ = static_cast<int>(Event8272::CommandByte); | ||||
| 		void posit_event(int type) final; | ||||
| 		int interesting_event_mask_ = int(Event8272::CommandByte); | ||||
| 		int resume_point_ = 0; | ||||
| 		bool is_access_command_ = false; | ||||
|  | ||||
| 		// The counter used for ::Timer events. | ||||
| 		int delay_time_ = 0; | ||||
| 		Cycles::IntType delay_time_ = 0; | ||||
|  | ||||
| 		// The connected drives. | ||||
| 		struct Drive { | ||||
| @@ -89,12 +89,12 @@ class i8272: public Storage::Disk::MFMController { | ||||
| 			bool seek_failed = false; | ||||
|  | ||||
| 			// Seeking: transient state. | ||||
| 			int step_rate_counter = 0; | ||||
| 			Cycles::IntType step_rate_counter = 0; | ||||
| 			int steps_taken = 0; | ||||
| 			int target_head_position = 0;	// either an actual number, or -1 to indicate to step until track zero | ||||
|  | ||||
| 			// Head state. | ||||
| 			int head_unload_delay[2] = {0, 0}; | ||||
| 			Cycles::IntType head_unload_delay[2] = {0, 0}; | ||||
| 			bool head_is_loaded[2] = {false, false}; | ||||
|  | ||||
| 		} drives_[4]; | ||||
|   | ||||
							
								
								
									
										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() const { | ||||
| 	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() const { | ||||
| 	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() const; | ||||
|  | ||||
| 		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() const; | ||||
|  | ||||
| 			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 */ | ||||
| @@ -17,23 +17,23 @@ 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]; | ||||
|  | ||||
| 	ReverseTable() { | ||||
| 		for(int c = 0; c < 256; ++c) { | ||||
| 			map[c] = static_cast<uint8_t>( | ||||
| 			map[c] = uint8_t( | ||||
| 				((c & 0x80) >> 7) | | ||||
| 				((c & 0x40) >> 5) | | ||||
| 				((c & 0x20) >> 3) | | ||||
| @@ -117,10 +117,22 @@ void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||
| 	crt_.set_scan_target(scan_target); | ||||
| } | ||||
|  | ||||
| Outputs::Display::ScanStatus TMS9918::get_scaled_scan_status() const { | ||||
| 	// The input was scaled by 3/4 to convert half cycles to internal ticks, | ||||
| 	// so undo that and also allow for: (i) the multiply by 4 that it takes | ||||
| 	// to reach the CRT; and (ii) the fact that the half-cycles value was scaled, | ||||
| 	// and this should really reply in whole cycles. | ||||
| 	return crt_.get_scaled_scan_status() * (4.0f / (3.0f * 8.0f)); | ||||
| } | ||||
|  | ||||
| void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) { | ||||
| 	crt_.set_display_type(display_type); | ||||
| } | ||||
|  | ||||
| Outputs::Display::DisplayType TMS9918::get_display_type() { | ||||
| 	return crt_.get_display_type(); | ||||
| } | ||||
|  | ||||
| void Base::LineBuffer::reset_sprite_collection() { | ||||
| 	sprites_stopped = false; | ||||
| 	active_sprite_slot = 0; | ||||
| @@ -132,7 +144,7 @@ void Base::LineBuffer::reset_sprite_collection() { | ||||
|  | ||||
| void Base::posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_position, int screen_row) { | ||||
| 	if(!(status_ & StatusSpriteOverflow)) { | ||||
| 		status_ = static_cast<uint8_t>((status_ & ~0x1f) | (sprite_number & 0x1f)); | ||||
| 		status_ = uint8_t((status_ & ~0x1f) | (sprite_number & 0x1f)); | ||||
| 	} | ||||
| 	if(buffer.sprites_stopped) | ||||
| 		return; | ||||
| @@ -166,7 +178,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 +364,7 @@ void TMS9918::run_for(const HalfCycles cycles) { | ||||
| 				// Output video stream. | ||||
| 				// -------------------- | ||||
|  | ||||
| #define intersect(left, right, code)	\ | ||||
| 	{	\ | ||||
| #define intersect(left, right, code)	{	\ | ||||
| 		const int start = std::max(read_pointer_.column, left);	\ | ||||
| 		const int end = std::min(end_column, right);	\ | ||||
| 		if(end > start) {\ | ||||
| @@ -493,7 +504,7 @@ void Base::output_border(int cycles, uint32_t cram_dot) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void TMS9918::set_register(int address, uint8_t value) { | ||||
| void TMS9918::write(int address, uint8_t value) { | ||||
| 	// Writes to address 0 are writes to the video RAM. Store | ||||
| 	// the value and return. | ||||
| 	if(!(address & 1)) { | ||||
| @@ -520,7 +531,7 @@ void TMS9918::set_register(int address, uint8_t value) { | ||||
|  | ||||
| 	// The RAM pointer is always set on a second write, regardless of | ||||
| 	// whether the caller is intending to enqueue a VDP operation. | ||||
| 	ram_pointer_ = (ram_pointer_ & 0x00ff) | static_cast<uint16_t>(value << 8); | ||||
| 	ram_pointer_ = (ram_pointer_ & 0x00ff) | uint16_t(value << 8); | ||||
|  | ||||
| 	write_phase_ = false; | ||||
| 	if(value & 0x80) { | ||||
| @@ -625,7 +636,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 | ||||
| @@ -654,7 +665,7 @@ uint8_t TMS9918::get_current_line() { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return static_cast<uint8_t>(source_row); | ||||
| 	return uint8_t(source_row); | ||||
| } | ||||
|  | ||||
| uint8_t TMS9918::get_latched_horizontal_counter() { | ||||
| @@ -671,7 +682,7 @@ void TMS9918::latch_horizontal_counter() { | ||||
| 	latched_column_ = write_pointer_.column; | ||||
| } | ||||
|  | ||||
| uint8_t TMS9918::get_register(int address) { | ||||
| uint8_t TMS9918::read(int address) { | ||||
| 	write_phase_ = false; | ||||
|  | ||||
| 	// Reads from address 0 read video RAM, via the read-ahead buffer. | ||||
| @@ -830,8 +841,8 @@ void Base::draw_tms_character(int start, int end) { | ||||
| 		int sprite_collision = 0; | ||||
| 		memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); | ||||
|  | ||||
| 		static const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; | ||||
| 		static const int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; | ||||
| 		constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; | ||||
| 		constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; | ||||
|  | ||||
| 		// Draw all sprites into the sprite buffer. | ||||
| 		const int shifter_target = sprites_16x16_ ? 32 : 16; | ||||
|   | ||||
| @@ -44,9 +44,15 @@ class TMS9918: public Base { | ||||
| 		/*! Sets the scan target this TMS will post content to. */ | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *); | ||||
|  | ||||
| 		/// Gets the current scan status. | ||||
| 		Outputs::Display::ScanStatus get_scaled_scan_status() const; | ||||
|  | ||||
| 		/*! Sets the type of display the CRT will request. */ | ||||
| 		void set_display_type(Outputs::Display::DisplayType); | ||||
|  | ||||
| 		/*! Gets the type of display the CRT will request. */ | ||||
| 		Outputs::Display::DisplayType get_display_type(); | ||||
|  | ||||
| 		/*! | ||||
| 			Runs the VCP for the number of cycles indicate; it is an implicit assumption of the code | ||||
| 			that the input clock rate is 3579545 Hz, the NTSC colour clock rate. | ||||
| @@ -54,10 +60,10 @@ class TMS9918: public Base { | ||||
| 		void run_for(const HalfCycles cycles); | ||||
|  | ||||
| 		/*! Sets a register value. */ | ||||
| 		void set_register(int address, uint8_t value); | ||||
| 		void write(int address, uint8_t value); | ||||
|  | ||||
| 		/*! Gets a register value. */ | ||||
| 		uint8_t get_register(int address); | ||||
| 		uint8_t read(int address); | ||||
|  | ||||
| 		/*! Gets the current scan line; provided by the Master System only. */ | ||||
| 		uint8_t get_current_line(); | ||||
| @@ -69,8 +75,8 @@ class TMS9918: public Base { | ||||
| 		void latch_horizontal_counter(); | ||||
|  | ||||
| 		/*! | ||||
| 			Returns the amount of time until get_interrupt_line would next return true if | ||||
| 			there are no interceding calls to set_register or get_register. | ||||
| 			Returns the amount of time until @c get_interrupt_line would next return true if | ||||
| 			there are no interceding calls to @c write or to @c read. | ||||
|  | ||||
| 			If get_interrupt_line is true now, returns zero. If get_interrupt_line would | ||||
| 			never return true, returns -1. | ||||
|   | ||||
| @@ -100,7 +100,7 @@ class Base { | ||||
| 			// (though, in practice, it won't happen until the next | ||||
| 			// external slot after this number of cycles after the | ||||
| 			// device has requested the read or write). | ||||
| 			return 7; | ||||
| 			return 6; | ||||
| 		} | ||||
|  | ||||
| 		// Holds the main status register. | ||||
| @@ -352,9 +352,9 @@ class Base { | ||||
| 					if(master_system_.cram_is_selected) { | ||||
| 						// Adjust the palette. | ||||
| 						master_system_.colour_ram[ram_pointer_ & 0x1f] = palette_pack( | ||||
| 							static_cast<uint8_t>(((read_ahead_buffer_ >> 0) & 3) * 255 / 3), | ||||
| 							static_cast<uint8_t>(((read_ahead_buffer_ >> 2) & 3) * 255 / 3), | ||||
| 							static_cast<uint8_t>(((read_ahead_buffer_ >> 4) & 3) * 255 / 3) | ||||
| 							uint8_t(((read_ahead_buffer_ >> 0) & 3) * 255 / 3), | ||||
| 							uint8_t(((read_ahead_buffer_ >> 2) & 3) * 255 / 3), | ||||
| 							uint8_t(((read_ahead_buffer_ >> 4) & 3) * 255 / 3) | ||||
| 						); | ||||
|  | ||||
| 						// Schedule a CRAM dot; this is scheduled for wherever it should appear | ||||
| @@ -518,7 +518,7 @@ class Base { | ||||
| 	fetch_columns_4(location+12, column+4); | ||||
|  | ||||
| 			LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; | ||||
| 			const size_t row_base = pattern_name_address_ & (0x3c00 | static_cast<size_t>(write_pointer_.row >> 3) * 40); | ||||
| 			const size_t row_base = pattern_name_address_ & (0x3c00 | size_t(write_pointer_.row >> 3) * 40); | ||||
| 			const size_t row_offset = pattern_generator_table_address_ & (0x3800 | (write_pointer_.row & 7)); | ||||
|  | ||||
| 			switch(start) { | ||||
| @@ -731,7 +731,7 @@ class Base { | ||||
| 		const size_t scrolled_column = (column - horizontal_offset) & 0x1f;\ | ||||
| 		const size_t address = row_info.pattern_address_base + (scrolled_column << 1);	\ | ||||
| 		line_buffer.names[column].flags = ram_[address+1];	\ | ||||
| 		line_buffer.names[column].offset = static_cast<size_t>(	\ | ||||
| 		line_buffer.names[column].offset = size_t(	\ | ||||
| 			(((line_buffer.names[column].flags&1) << 8) | ram_[address]) << 5	\ | ||||
| 		) + row_info.sub_row[(line_buffer.names[column].flags&4) >> 2];	\ | ||||
| 	} | ||||
| @@ -785,7 +785,7 @@ class Base { | ||||
| 			}; | ||||
| 			const RowInfo scrolled_row_info = { | ||||
| 				(pattern_name_address & size_t(((scrolled_row & ~7) << 3) | 0x3800)) - pattern_name_offset, | ||||
| 				{static_cast<size_t>((scrolled_row & 7) << 2), 28 ^ static_cast<size_t>((scrolled_row & 7) << 2)} | ||||
| 				{size_t((scrolled_row & 7) << 2), 28 ^ size_t((scrolled_row & 7) << 2)} | ||||
| 			}; | ||||
| 			RowInfo row_info; | ||||
| 			if(master_system_.vertical_scroll_lock) { | ||||
|   | ||||
| @@ -6,51 +6,66 @@ | ||||
| //  Copyright 2016 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include <cmath> | ||||
|  | ||||
| #include "AY38910.hpp" | ||||
|  | ||||
| #include <cmath> | ||||
| //namespace GI { | ||||
| //namespace AY38910 { | ||||
|  | ||||
| using namespace GI::AY38910; | ||||
|  | ||||
| AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) { | ||||
| 	// set up envelope lookup tables | ||||
| template <bool is_stereo> | ||||
| AY38910<is_stereo>::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) { | ||||
| 	// Don't use the low bit of the envelope position if this is an AY. | ||||
| 	envelope_position_mask_ |= personality == Personality::AY38910; | ||||
|  | ||||
| 	// Set up envelope lookup tables. | ||||
| 	for(int c = 0; c < 16; c++) { | ||||
| 		for(int 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; | ||||
| 			} | ||||
| 		} | ||||
| @@ -59,21 +74,50 @@ AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_ | ||||
| 	set_sample_volume_range(0); | ||||
| } | ||||
|  | ||||
| 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))); | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::int16_t range) { | ||||
| 	// Set up volume lookup table; the function below is based on a combination of the graph | ||||
| 	// from the YM's datasheet, showing a clear power curve, and fitting that to observed | ||||
| 	// values reported elsewhere. | ||||
| 	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) / 3.18f)); | ||||
| 	} | ||||
| 	volumes_[0] = 0; | ||||
|  | ||||
| 	// Tie level 0 to silence. | ||||
| 	for(int v = 31; v >= 0; --v) { | ||||
| 		volumes_[v] -= volumes_[0]; | ||||
| 	} | ||||
|  | ||||
| 	evaluate_output_volume(); | ||||
| } | ||||
|  | ||||
| void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) { | ||||
| 	a_left_ = uint8_t(a_left * 255.0f); | ||||
| 	b_left_ = uint8_t(b_left * 255.0f); | ||||
| 	c_left_ = uint8_t(c_left * 255.0f); | ||||
| 	a_right_ = uint8_t(a_right * 255.0f); | ||||
| 	b_right_ = uint8_t(b_right * 255.0f); | ||||
| 	c_right_ = uint8_t(c_right * 255.0f); | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| 	// Note on structure below: the real AY has a built-in divider of 8 | ||||
| 	// prior to applying its tone and noise dividers. But the YM fills the | ||||
| 	// same total periods for noise and tone with double-precision envelopes. | ||||
| 	// Therefore this class implements a divider of 4 and doubles the tone | ||||
| 	// and noise periods. The envelope ticks along at the divide-by-four rate, | ||||
| 	// but if this is an AY rather than a YM then its lowest bit is forced to 1, | ||||
| 	// matching the YM datasheet's depiction of envelope level 31 as equal to | ||||
| 	// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc. | ||||
|  | ||||
| 	std::size_t c = 0; | ||||
| 	while((master_divider_&7) && c < number_of_samples) { | ||||
| 		target[c] = output_volume_; | ||||
| 	while((master_divider_&3) && c < number_of_samples) { | ||||
| 		if constexpr (is_stereo) { | ||||
| 			reinterpret_cast<uint32_t *>(target)[c] = output_volume_; | ||||
| 		} else { | ||||
| 			target[c] = int16_t(output_volume_); | ||||
| 		} | ||||
| 		master_divider_++; | ||||
| 		c++; | ||||
| 	} | ||||
| @@ -83,49 +127,53 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| 	if(tone_counters_[c]) tone_counters_[c]--;\ | ||||
| 	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++) { | ||||
| 			target[c] = output_volume_; | ||||
| 		for(int ic = 0; ic < 4 && c < number_of_samples; ic++) { | ||||
| 			if constexpr (is_stereo) { | ||||
| 				reinterpret_cast<uint32_t *>(target)[c] = output_volume_; | ||||
| 			} else { | ||||
| 				target[c] = int16_t(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_]; | ||||
| template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() { | ||||
| 	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 +190,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), | ||||
| @@ -153,34 +212,47 @@ void AY38910::evaluate_output_volume() { | ||||
| 	}; | ||||
| #undef channel_volume | ||||
|  | ||||
| 	// Mix additively. | ||||
| 	output_volume_ = static_cast<int16_t>( | ||||
| 		volumes_[volumes[0]] * channel_levels[0] + | ||||
| 		volumes_[volumes[1]] * channel_levels[1] + | ||||
| 		volumes_[volumes[2]] * channel_levels[2] | ||||
| 	); | ||||
| 	// Mix additively, weighting if in stereo. | ||||
| 	if constexpr (is_stereo) { | ||||
| 		int16_t *const output_volumes = reinterpret_cast<int16_t *>(&output_volume_); | ||||
| 		output_volumes[0] = int16_t(( | ||||
| 			volumes_[volumes[0]] * channel_levels[0] * a_left_ + | ||||
| 			volumes_[volumes[1]] * channel_levels[1] * b_left_ + | ||||
| 			volumes_[volumes[2]] * channel_levels[2] * c_left_ | ||||
| 		) >> 8); | ||||
| 		output_volumes[1] = int16_t(( | ||||
| 			volumes_[volumes[0]] * channel_levels[0] * a_right_ + | ||||
| 			volumes_[volumes[1]] * channel_levels[1] * b_right_ + | ||||
| 			volumes_[volumes[2]] * channel_levels[2] * c_right_ | ||||
| 		) >> 8); | ||||
| 	} else { | ||||
| 		output_volume_ = uint32_t( | ||||
| 			volumes_[volumes[0]] * channel_levels[0] + | ||||
| 			volumes_[volumes[1]] * channel_levels[1] + | ||||
| 			volumes_[volumes[2]] * channel_levels[2] | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool AY38910::is_zero_level() { | ||||
| template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() const { | ||||
| 	// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero. | ||||
| 	return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0; | ||||
| } | ||||
|  | ||||
| // MARK: - Register manipulation | ||||
|  | ||||
| void AY38910::select_register(uint8_t r) { | ||||
| template <bool is_stereo> void AY38910<is_stereo>::select_register(uint8_t r) { | ||||
| 	selected_register_ = r; | ||||
| } | ||||
|  | ||||
| void AY38910::set_register_value(uint8_t value) { | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t value) { | ||||
| 	// There are only 16 registers. | ||||
| 	if(selected_register_ > 15) return; | ||||
|  | ||||
| 	// If this is a register that affects audio output, enqueue a mutation onto the | ||||
| 	// audio generation thread. | ||||
| 	if(selected_register_ < 14) { | ||||
| 		const int selected_register = selected_register_; | ||||
| 		task_queue_.defer([=] () { | ||||
| 		task_queue_.defer([this, selected_register = selected_register_, value] () { | ||||
| 			// Perform any register-specific mutation to output generation. | ||||
| 			uint8_t masked_value = value; | ||||
| 			switch(selected_register) { | ||||
| @@ -189,7 +261,7 @@ void AY38910::set_register_value(uint8_t value) { | ||||
| 					int channel = selected_register >> 1; | ||||
|  | ||||
| 					if(selected_register & 1) | ||||
| 						tone_periods_[channel] = (tone_periods_[channel] & 0xff) | static_cast<uint16_t>((value&0xf) << 8); | ||||
| 						tone_periods_[channel] = (tone_periods_[channel] & 0xff) | uint16_t((value&0xf) << 8); | ||||
| 					else | ||||
| 						tone_periods_[channel] = (tone_periods_[channel] & ~0xff) | value; | ||||
| 				} | ||||
| @@ -204,7 +276,7 @@ void AY38910::set_register_value(uint8_t value) { | ||||
| 				break; | ||||
|  | ||||
| 				case 12: | ||||
| 					envelope_period_ = (envelope_period_ & 0xff) | static_cast<int>(value << 8); | ||||
| 					envelope_period_ = (envelope_period_ & 0xff) | int(value << 8); | ||||
| 				break; | ||||
|  | ||||
| 				case 13: | ||||
| @@ -242,7 +314,7 @@ void AY38910::set_register_value(uint8_t value) { | ||||
| 	if(update_port_a) set_port_output(false); | ||||
| } | ||||
|  | ||||
| uint8_t AY38910::get_register_value() { | ||||
| template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() { | ||||
| 	// This table ensures that bits that aren't defined within the AY are returned as 0s | ||||
| 	// when read, conforming to CPC-sourced unit tests. | ||||
| 	const uint8_t register_masks[16] = { | ||||
| @@ -256,24 +328,24 @@ uint8_t AY38910::get_register_value() { | ||||
|  | ||||
| // MARK: - Port querying | ||||
|  | ||||
| uint8_t AY38910::get_port_output(bool port_b) { | ||||
| template <bool is_stereo> uint8_t AY38910<is_stereo>::get_port_output(bool port_b) { | ||||
| 	return registers_[port_b ? 15 : 14]; | ||||
| } | ||||
|  | ||||
| // MARK: - Bus handling | ||||
|  | ||||
| void AY38910::set_port_handler(PortHandler *handler) { | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_port_handler(PortHandler *handler) { | ||||
| 	port_handler_ = handler; | ||||
| 	set_port_output(true); | ||||
| 	set_port_output(false); | ||||
| } | ||||
|  | ||||
| void AY38910::set_data_input(uint8_t r) { | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_data_input(uint8_t r) { | ||||
| 	data_input_ = r; | ||||
| 	update_bus(); | ||||
| } | ||||
|  | ||||
| void AY38910::set_port_output(bool port_b) { | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b) { | ||||
| 	// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor, | ||||
| 	// 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. | ||||
| @@ -283,7 +355,7 @@ void AY38910::set_port_output(bool port_b) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t AY38910::get_data_output() { | ||||
| template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() { | ||||
| 	if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) { | ||||
| 		// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the | ||||
| 		// value returned to the CPU when reading it is the and of the output value and any input. | ||||
| @@ -299,22 +371,22 @@ uint8_t AY38910::get_data_output() { | ||||
| 	return data_output_; | ||||
| } | ||||
|  | ||||
| void AY38910::set_control_lines(ControlLines control_lines) { | ||||
| 	switch(static_cast<int>(control_lines)) { | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) { | ||||
| 	switch(int(control_lines)) { | ||||
| 		default:					control_state_ = Inactive;		break; | ||||
|  | ||||
| 		case static_cast<int>(BDIR | BC2 | BC1): | ||||
| 		case int(BDIR | BC2 | BC1): | ||||
| 		case BDIR: | ||||
| 		case BC1:					control_state_ = LatchAddress;	break; | ||||
|  | ||||
| 		case static_cast<int>(BC2 | BC1):		control_state_ = Read;			break; | ||||
| 		case static_cast<int>(BDIR | BC2):		control_state_ = Write;			break; | ||||
| 		case int(BC2 | BC1):		control_state_ = Read;			break; | ||||
| 		case int(BDIR | BC2):		control_state_ = Write;			break; | ||||
| 	} | ||||
|  | ||||
| 	update_bus(); | ||||
| } | ||||
|  | ||||
| void AY38910::update_bus() { | ||||
| template <bool is_stereo> void AY38910<is_stereo>::update_bus() { | ||||
| 	// Assume no output, unless this turns out to be a read. | ||||
| 	data_output_ = 0xff; | ||||
| 	switch(control_state_) { | ||||
| @@ -324,3 +396,7 @@ void AY38910::update_bus() { | ||||
| 		case Read:			data_output_ = get_register_value();	break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Ensure both mono and stereo versions of the AY are built. | ||||
| template class GI::AY38910::AY38910<true>; | ||||
| template class GI::AY38910::AY38910<false>; | ||||
|   | ||||
| @@ -35,9 +35,10 @@ class PortHandler { | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Requests the current input on an AY port. | ||||
| 			Sets the current output on an AY port. | ||||
|  | ||||
| 			@param port_b @c true if the input being queried is Port B. @c false if it is Port A. | ||||
| 			@param port_b @c true if the output being posted is Port B. @c false if it is Port A. | ||||
| 			@param value the value now being output. | ||||
| 		*/ | ||||
| 		virtual void set_port_output(bool port_b, uint8_t value) {} | ||||
| }; | ||||
| @@ -51,15 +52,24 @@ 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 | ||||
| 	interface ports. | ||||
|  | ||||
| 	This AY has an attached mono or stereo mixer. | ||||
| */ | ||||
| class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
| template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
| 	public: | ||||
| 		/// 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,10 +93,23 @@ class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
| 		*/ | ||||
| 		void set_port_handler(PortHandler *); | ||||
|  | ||||
| 		/*! | ||||
| 			Enables or disables stereo output; if stereo output is enabled then also sets the weight of each of the AY's | ||||
| 			channels in each of the output channels. | ||||
|  | ||||
| 			If a_left_ = b_left = c_left = a_right = b_right = c_right = 1.0 then you'll get output that's effectively mono. | ||||
|  | ||||
| 			a_left = 0.0, a_right = 1.0 will make A full volume on the right output, and silent on the left. | ||||
|  | ||||
| 			a_left = 0.5, a_right = 0.5 will make A half volume on both outputs. | ||||
| 		*/ | ||||
| 		void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0); | ||||
|  | ||||
| 		// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter. | ||||
| 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||
| 		bool is_zero_level(); | ||||
| 		bool is_zero_level() const; | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| 		static constexpr bool get_is_stereo() { return is_stereo; } | ||||
|  | ||||
| 	private: | ||||
| 		Concurrency::DeferringAsyncTaskQueue &task_queue_; | ||||
| @@ -108,11 +131,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, | ||||
| @@ -127,14 +150,21 @@ class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
|  | ||||
| 		uint8_t data_input_, data_output_; | ||||
|  | ||||
| 		int16_t output_volume_; | ||||
| 		void evaluate_output_volume(); | ||||
| 		uint32_t output_volume_; | ||||
|  | ||||
| 		void update_bus(); | ||||
| 		PortHandler *port_handler_ = nullptr; | ||||
| 		void set_port_output(bool port_b); | ||||
|  | ||||
| 		void evaluate_output_volume(); | ||||
|  | ||||
| 		// Output mixing control. | ||||
| 		uint8_t a_left_ = 255, a_right_ = 255; | ||||
| 		uint8_t b_left_ = 255, b_right_ = 255; | ||||
| 		uint8_t c_left_ = 255, c_right_ = 255; | ||||
| }; | ||||
|  | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -28,11 +28,11 @@ void Toggle::skip_samples(const std::size_t number_of_samples) {} | ||||
| void Toggle::set_output(bool enabled) { | ||||
| 	if(is_enabled_ == enabled) return; | ||||
| 	is_enabled_ = enabled; | ||||
| 	audio_queue_.defer([=] { | ||||
| 	audio_queue_.defer([this, enabled] { | ||||
| 		level_ = enabled ? volume_ : 0; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| bool Toggle::get_output() { | ||||
| bool Toggle::get_output() const { | ||||
| 	return is_enabled_; | ||||
| } | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class Toggle: public Outputs::Speaker::SampleSource { | ||||
| 		void skip_samples(const std::size_t number_of_samples); | ||||
|  | ||||
| 		void set_output(bool enabled); | ||||
| 		bool get_output(); | ||||
| 		bool get_output() const; | ||||
|  | ||||
| 	private: | ||||
| 		// Accessed on the calling thread. | ||||
|   | ||||
| @@ -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,20 +78,20 @@ 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_) { | ||||
| 			--flux_duration_; | ||||
| 			if(!flux_duration_) inputs_ |= input_flux; | ||||
| 		} | ||||
| 		state_ = state_machine_[static_cast<std::size_t>(address)]; | ||||
| 		state_ = state_machine_[size_t(address)]; | ||||
| 		switch(state_ & 0xf) { | ||||
| 			default:	shift_register_ = 0;													break;	// clear | ||||
| 			case 0x8:																			break;	// nop | ||||
| 			default:	shift_register_ = 0;										break;	// clear | ||||
| 			case 0x8:																break;	// nop | ||||
|  | ||||
| 			case 0x9:	shift_register_ = static_cast<uint8_t>(shift_register_ << 1);			break;	// shift left, bringing in a zero | ||||
| 			case 0xd:	shift_register_ = static_cast<uint8_t>((shift_register_ << 1) | 1);		break;	// shift left, bringing in a one | ||||
| 			case 0x9:	shift_register_ = uint8_t(shift_register_ << 1);			break;	// shift left, bringing in a zero | ||||
| 			case 0xd:	shift_register_ = uint8_t((shift_register_ << 1) | 1);		break;	// shift left, bringing in a one | ||||
|  | ||||
| 			case 0xa:	// shift right, bringing in write protected status | ||||
| 				shift_register_ = (shift_register_ >> 1) | (is_write_protected() ? 0x80 : 0x00); | ||||
| @@ -105,7 +105,7 @@ void DiskII::run_for(const Cycles cycles) { | ||||
| 					return; | ||||
| 				} | ||||
| 			break; | ||||
| 			case 0xb:	shift_register_ = data_input_;											break;	// load data register from data bus | ||||
| 			case 0xb:	shift_register_ = data_input_;								break;	// load data register from data bus | ||||
| 		} | ||||
|  | ||||
| 		// Currently writing? | ||||
| @@ -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. | ||||
| @@ -225,7 +225,7 @@ void DiskII::set_component_prefers_clocking(ClockingHint::Source *component, Clo | ||||
| 	decide_clocking_preference(); | ||||
| } | ||||
|  | ||||
| ClockingHint::Preference DiskII::preferred_clocking() { | ||||
| ClockingHint::Preference DiskII::preferred_clocking() const { | ||||
| 	return clocking_preference_; | ||||
| } | ||||
|  | ||||
| @@ -266,7 +266,7 @@ int DiskII::read_address(int address) { | ||||
| 		break; | ||||
| 		case 0xf: | ||||
| 			if(!(inputs_ & input_mode)) | ||||
| 				drives_[active_drive_].begin_writing(Storage::Time(1, clock_rate_), false); | ||||
| 				drives_[active_drive_].begin_writing(Storage::Time(1, int(clock_rate_)), false); | ||||
| 			inputs_ |= input_mode; | ||||
| 		break; | ||||
| 	} | ||||
|   | ||||
| @@ -26,7 +26,7 @@ namespace Apple { | ||||
| /*! | ||||
| 	Provides an emulation of the Apple Disk II. | ||||
| */ | ||||
| class DiskII: | ||||
| class DiskII : | ||||
| 	public Storage::Disk::Drive::EventDelegate, | ||||
| 	public ClockingHint::Source, | ||||
| 	public ClockingHint::Observer { | ||||
| @@ -48,7 +48,7 @@ class DiskII: | ||||
| 			The value returned by @c read_address if accessing that address | ||||
| 			didn't cause the disk II to place anything onto the bus. | ||||
| 		*/ | ||||
| 		const int DidNotLoad = -1; | ||||
| 		static constexpr int DidNotLoad = -1; | ||||
|  | ||||
| 		/// Advances the controller by @c cycles. | ||||
| 		void run_for(const Cycles cycles); | ||||
| @@ -76,7 +76,7 @@ class DiskII: | ||||
| 		void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive); | ||||
|  | ||||
| 		// As per Sleeper. | ||||
| 		ClockingHint::Preference preferred_clocking() override; | ||||
| 		ClockingHint::Preference preferred_clocking() const 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 set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) override; | ||||
| 		void process_event(const Storage::Disk::Drive::Event &event) final; | ||||
| 		void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) final; | ||||
|  | ||||
| 		const int clock_rate_ = 0; | ||||
| 		const Cycles::IntType clock_rate_ = 0; | ||||
|  | ||||
| 		uint8_t state_ = 0; | ||||
| 		uint8_t inputs_ = 0; | ||||
| @@ -109,7 +109,7 @@ class DiskII: | ||||
|  | ||||
| 		int stepper_mask_ = 0; | ||||
| 		int stepper_position_ = 0; | ||||
| 		int motor_off_time_ = -1; | ||||
| 		Cycles::IntType motor_off_time_ = -1; | ||||
|  | ||||
| 		bool is_write_protected(); | ||||
| 		std::array<uint8_t, 256> state_machine_; | ||||
|   | ||||
							
								
								
									
										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) final; | ||||
|  | ||||
| 		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) final; | ||||
|  | ||||
| 		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 */ | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user