mirror of
				https://github.com/TomHarte/CLK.git
				synced 2025-10-31 05:16:08 +00:00 
			
		
		
		
	Compare commits
	
		
			645 Commits
		
	
	
		
			2019-03-10
			...
			2019-08-01
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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 | 
| @@ -59,6 +59,11 @@ KeyboardMachine::Machine *MultiMachine::keyboard_machine() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| MouseMachine::Machine *MultiMachine::mouse_machine() { | ||||
| 	// TODO. | ||||
| 	return nullptr; | ||||
| } | ||||
|  | ||||
| Configurable::Device *MultiMachine::configurable_device() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->configurable_device(); | ||||
|   | ||||
| @@ -54,6 +54,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De | ||||
| 		Configurable::Device *configurable_device() override; | ||||
| 		CRTMachine::Machine *crt_machine() override; | ||||
| 		JoystickMachine::Machine *joystick_machine() override; | ||||
| 		MouseMachine::Machine *mouse_machine() override; | ||||
| 		KeyboardMachine::Machine *keyboard_machine() override; | ||||
| 		MediaTarget::Machine *media_target() override; | ||||
| 		void *raw_pointer() override; | ||||
|   | ||||
| @@ -17,6 +17,7 @@ enum class Machine { | ||||
| 	Atari2600, | ||||
| 	ColecoVision, | ||||
| 	Electron, | ||||
| 	Macintosh, | ||||
| 	MasterSystem, | ||||
| 	MSX, | ||||
| 	Oric, | ||||
|   | ||||
| @@ -290,6 +290,7 @@ 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(); | ||||
|  | ||||
|   | ||||
							
								
								
									
										26
									
								
								Analyser/Static/Macintosh/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Analyser/Static/Macintosh/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| // | ||||
| //  StaticAnalyser.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 02/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "StaticAnalyser.hpp" | ||||
| #include "Target.hpp" | ||||
|  | ||||
| Analyser::Static::TargetList Analyser::Static::Macintosh::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { | ||||
| 	// This analyser can comprehend disks only. | ||||
| 	if(media.disks.empty()) return {}; | ||||
|  | ||||
| 	// If there is at least one disk, wave it through. | ||||
| 	Analyser::Static::TargetList targets; | ||||
|  | ||||
| 	using Target = Analyser::Static::Macintosh::Target; | ||||
| 	auto *target = new Target; | ||||
| 	target->machine = Analyser::Machine::Macintosh; | ||||
| 	target->media = media; | ||||
| 	targets.push_back(std::unique_ptr<Analyser::Static::Target>(target)); | ||||
|  | ||||
| 	return targets; | ||||
| } | ||||
							
								
								
									
										27
									
								
								Analyser/Static/Macintosh/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Analyser/Static/Macintosh/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  StaticAnalyser.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 02/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Analyser_Static_Macintosh_StaticAnalyser_hpp | ||||
| #define Analyser_Static_Macintosh_StaticAnalyser_hpp | ||||
|  | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include "../../../Storage/TargetPlatforms.hpp" | ||||
| #include <string> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Macintosh { | ||||
|  | ||||
| TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| #endif /* Analyser_Static_Macintosh_StaticAnalyser_hpp */ | ||||
							
								
								
									
										31
									
								
								Analyser/Static/Macintosh/Target.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								Analyser/Static/Macintosh/Target.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| // | ||||
| //  Target.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Analyser_Static_Macintosh_Target_h | ||||
| #define Analyser_Static_Macintosh_Target_h | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Macintosh { | ||||
|  | ||||
| struct Target: public ::Analyser::Static::Target { | ||||
| 	enum class Model { | ||||
| 		Mac128k, | ||||
| 		Mac512k, | ||||
| 		Mac512ke, | ||||
| 		MacPlus | ||||
| 	}; | ||||
|  | ||||
| 	Model model = Model::Mac512ke; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Analyser_Static_Macintosh_Target_h */ | ||||
| @@ -21,6 +21,7 @@ | ||||
| #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,6 +36,7 @@ | ||||
| #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" | ||||
| @@ -89,7 +91,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | ||||
| 	Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// 81 | ||||
| 	Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600)							// A26 | ||||
| 	Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn)			// ADF | ||||
| 	Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge)					// BIN | ||||
| 	Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge)						// BIN (cartridge dump) | ||||
| 	Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX)													// CAS | ||||
| 	Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC)											// CDT | ||||
| 	Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision)						// COL | ||||
| @@ -99,7 +101,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | ||||
| 	Format("do", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)			// DO | ||||
| 	Format("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)				// DSD | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC)		// DSK (Amstrad CPC) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)		// DSK (Apple) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII)			// DSK (Apple II) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// DSK (Macintosh) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX)				// DSK (MSX) | ||||
| 	Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric)			// DSK (Oric) | ||||
| 	Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore)			// G64 | ||||
| @@ -108,6 +111,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: | ||||
| 			Disk::DiskImageHolder<Storage::Disk::HFE>, | ||||
| 			TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric) | ||||
| 			// HFE (TODO: switch to AllDisk once the MSX stops being so greedy) | ||||
| 	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)		// IMG (DiskCopy 4.2) | ||||
| 	Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// IMG (DiskCopy 4.2) | ||||
| 	Format("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 | ||||
| @@ -173,9 +178,10 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) { | ||||
| 	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 | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,8 @@ | ||||
| #ifndef ClockReceiver_hpp | ||||
| #define ClockReceiver_hpp | ||||
|  | ||||
| #include "ForceInline.hpp" | ||||
|  | ||||
| /* | ||||
| 	Informal pattern for all classes that run from a clock cycle: | ||||
|  | ||||
| @@ -52,79 +54,92 @@ | ||||
| */ | ||||
| template <class T> class WrappedInt { | ||||
| 	public: | ||||
| 		constexpr WrappedInt(int l) : length_(l) {} | ||||
| 		constexpr WrappedInt() : length_(0) {} | ||||
| 		forceinline constexpr WrappedInt(int l) noexcept : length_(l) {} | ||||
| 		forceinline constexpr WrappedInt() noexcept : length_(0) {} | ||||
|  | ||||
| 		T &operator =(const T &rhs) { | ||||
| 		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_; } | ||||
| 		forceinline constexpr int as_int() 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) { | ||||
| 		forceinline T divide(const T &divisor) { | ||||
| 			T result(length_ / divisor.length_); | ||||
| 			length_ %= divisor.length_; | ||||
| 			return result; | ||||
| @@ -134,10 +149,12 @@ template <class T> class WrappedInt { | ||||
| 			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 | ||||
| @@ -150,51 +167,59 @@ template <class T> class WrappedInt { | ||||
| /// 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(int 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; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| /// 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(int 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_int() * 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; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| // 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 +228,9 @@ template <class T> class HalfClockReceiver: public T { | ||||
| 	public: | ||||
| 		using T::T; | ||||
|  | ||||
| 		inline void run_for(const HalfCycles half_cycles) { | ||||
| 		forceinline void run_for(const HalfCycles half_cycles) { | ||||
| 			half_cycles_ += half_cycles; | ||||
| 			T::run_for(half_cycles_.flush_cycles()); | ||||
| 			T::run_for(half_cycles_.flush<Cycles>()); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
|   | ||||
| @@ -1,26 +1,26 @@ | ||||
| //
 | ||||
| //  ClockDeferrer.hpp
 | ||||
| //  DeferredQueue.hpp
 | ||||
| //  Clock Signal
 | ||||
| //
 | ||||
| //  Created by Thomas Harte on 23/08/2018.
 | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved.
 | ||||
| //
 | ||||
| 
 | ||||
| #ifndef ClockDeferrer_h | ||||
| #define ClockDeferrer_h | ||||
| #ifndef DeferredQueue_h | ||||
| #define DeferredQueue_h | ||||
| 
 | ||||
| #include <functional> | ||||
| #include <vector> | ||||
| 
 | ||||
| /*!
 | ||||
| 	A ClockDeferrer maintains a list of ordered actions and the times at which | ||||
| 	A DeferredQueue maintains a list of ordered actions and the times at which | ||||
| 	they should happen, and divides a total execution period up into the portions | ||||
| 	that occur between those actions, triggering each action when it is reached. | ||||
| */ | ||||
| template <typename TimeUnit> class ClockDeferrer { | ||||
| template <typename TimeUnit> class DeferredQueue { | ||||
| 	public: | ||||
| 		/// Constructs a ClockDeferrer that will call target(period) in between deferred actions.
 | ||||
| 		ClockDeferrer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {} | ||||
| 		/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
 | ||||
| 		DeferredQueue(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {} | ||||
| 
 | ||||
| 		/*!
 | ||||
| 			Schedules @c action to occur in @c delay units of time. | ||||
| @@ -79,4 +79,4 @@ template <typename TimeUnit> class ClockDeferrer { | ||||
| 		std::vector<DeferredAction> pending_actions_; | ||||
| }; | ||||
| 
 | ||||
| #endif /* ClockDeferrer_h */ | ||||
| #endif /* DeferredQueue_h */ | ||||
							
								
								
									
										110
									
								
								ClockReceiver/JustInTime.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								ClockReceiver/JustInTime.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| // | ||||
| //  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" | ||||
|  | ||||
| /*! | ||||
| 	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, class LocalTimeScale, class TargetTimeScale = LocalTimeScale> class JustInTimeActor { | ||||
| 	public: | ||||
| 		/// Constructs a new JustInTimeActor using the same construction arguments as the included object. | ||||
| 		template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {} | ||||
|  | ||||
| 		/// Adds time to the actor. | ||||
| 		inline void operator += (const LocalTimeScale &rhs) { | ||||
| 			time_since_update_ += rhs; | ||||
| 			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_) object_.run_for(time_since_update_.template flush<TargetTimeScale>()); | ||||
| 			is_flushed_ = true; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		T object_; | ||||
| 		LocalTimeScale time_since_update_; | ||||
| 		bool is_flushed_ = true; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue. | ||||
| 	Any time the amount of accumulated time crosses a threshold provided at construction time, | ||||
| 	the object will be updated on the AsyncTaskQueue. | ||||
| */ | ||||
| template <class T, class LocalTimeScale, 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 */ | ||||
| @@ -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,7 +88,7 @@ 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; | ||||
| @@ -116,11 +102,39 @@ template <class T> class MOS6522: public MOS6522Base { | ||||
| 		/*! @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(); | ||||
|  | ||||
| 		/// 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,29 +11,58 @@ | ||||
| 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>::set_register(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; | ||||
|  | ||||
| @@ -59,32 +88,58 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) | ||||
| 		break; | ||||
|  | ||||
| 		// Shift | ||||
| 		case 0xa:	registers_.shift = value;				break; | ||||
| 		case 0xa: | ||||
| 			registers_.shift = value; | ||||
| 			shift_bits_remaining_ = 8; | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister; | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
|  | ||||
| 		// Control | ||||
| 		case 0xb: | ||||
| 			registers_.auxiliary_control = value; | ||||
| 			evaluate_cb2_output(); | ||||
| 		break; | ||||
| 		case 0xc: | ||||
| 		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; | ||||
| 				} | ||||
| 			} | ||||
| 			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; | ||||
| 				} | ||||
| 			} | ||||
| 			int shift = 0; | ||||
| 			for(int port = 0; port < 2; ++port) { | ||||
| 				handshake_modes_[port] = HandshakeMode::None; | ||||
| 				switch((value >> shift) & 0x0e) { | ||||
| 					default: break; | ||||
|  | ||||
| 					case 0x00:	// Negative interrupt input; set Cx2 interrupt on negative Cx2 transition, clear on access to Port x register. | ||||
| 					case 0x02:	// Independent negative interrupt input; set Cx2 interrupt on negative transition, don't clear automatically. | ||||
| 					case 0x04:	// Positive interrupt input; set Cx2 interrupt on positive Cx2 transition, clear on access to Port x register. | ||||
| 					case 0x06:	// Independent positive interrupt input; set Cx2 interrupt on positive transition, don't clear automatically. | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::Input); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x08:	// Handshake: set Cx2 to low on any read or write of Port x; set to high on an active transition of Cx1. | ||||
| 						handshake_modes_[port] = HandshakeMode::Handshake; | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::Off);	// At a guess. | ||||
| 					break; | ||||
|  | ||||
| 					case 0x0a:	// Pulse output: Cx2 is low for one cycle following a read or write of Port x. | ||||
| 						handshake_modes_[port] = HandshakeMode::Pulse; | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::On); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x0c:	// Manual output: Cx2 low. | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::Off); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x0e:	// Manual output: Cx2 high. | ||||
| 						set_control_line_output(Port(port), Line::Two, LineState::On); | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				shift += 4; | ||||
| 			} | ||||
| 		} break; | ||||
|  | ||||
| 		// Interrupt control | ||||
| 		case 0xd: | ||||
| 			registers_.interrupt_flags &= ~value; | ||||
| @@ -102,12 +157,13 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) | ||||
|  | ||||
| template <typename T> uint8_t MOS6522<T>::get_register(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] = static_cast<uint16_t>(registers_.next_timer[0]); | ||||
| 		registers_.next_timer[0] = -1; | ||||
| 	} | ||||
| 	if(registers_.next_timer[1] >= 0) { | ||||
| 		registers_.timer[1] = static_cast<uint16_t>(registers_.next_timer[1]); | ||||
| 		registers_.next_timer[1] = -1; | ||||
| 	} | ||||
|  | ||||
| 	// In pulse modes, CA2 and CB2 go high again on the next clock edge. | ||||
| 	if(handshake_modes_[1] == HandshakeMode::Pulse) { | ||||
| 		set_control_line_output(Port::B, Line::Two, LineState::On); | ||||
| 	} | ||||
| 	if(handshake_modes_[0] == HandshakeMode::Pulse) { | ||||
| 		set_control_line_output(Port::A, Line::Two, LineState::On); | ||||
| 	} | ||||
|  | ||||
| 	// If the shift register is shifting according to the input clock, do a shift. | ||||
| 	switch(shift_mode()) { | ||||
| 		default: 											break; | ||||
| 		case ShiftMode::InUnderPhase2:		shift_in();		break; | ||||
| 		case ShiftMode::OutUnderPhase2:		shift_out();	break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::do_phase1() { | ||||
| 	++ time_since_bus_handler_call_; | ||||
|  | ||||
| 	// IRQ is raised on the half cycle after overflow | ||||
| 	if((registers_.timer[1] == 0xffff) && !registers_.last_timer[1] && timer_is_running_[1]) { | ||||
| 		timer_is_running_[1] = false; | ||||
|  | ||||
| 		// If the shift register is shifting according to this timer, do a shift. | ||||
| 		// TODO: "shift register is driven by only the low order 8 bits of timer 2"? | ||||
| 		switch(shift_mode()) { | ||||
| 			default: 												break; | ||||
| 			case ShiftMode::InUnderT2:				shift_in();		break; | ||||
| 			case ShiftMode::OutUnderT2FreeRunning: 	shift_out();	break; | ||||
| 			case ShiftMode::OutUnderT2:				shift_out();	break;	// TODO: present a clock on CB1. | ||||
| 		} | ||||
|  | ||||
| 		registers_.interrupt_flags |= InterruptFlag::Timer2; | ||||
| 		reevaluate_interrupts(); | ||||
| 	} | ||||
|  | ||||
| 	if((registers_.timer[0] == 0xffff) && !registers_.last_timer[0] && timer_is_running_[0]) { | ||||
| 		registers_.interrupt_flags |= InterruptFlag::Timer1; | ||||
| 		reevaluate_interrupts(); | ||||
|  | ||||
| 		// Determine whether to reload. | ||||
| 		if(registers_.auxiliary_control&0x40) | ||||
| 			registers_.timer_needs_reload = true; | ||||
| 		else | ||||
| 			timer_is_running_[0] = false; | ||||
|  | ||||
| 		// Determine whether to toggle PB7. | ||||
| 		if(registers_.auxiliary_control&0x80) { | ||||
| 			registers_.output[1] ^= 0x80; | ||||
| 			bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 			bus_handler_.set_port_output(Port::B, registers_.output[1], registers_.data_direction[1]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /*! Runs for a specified number of half cycles. */ | ||||
| template <typename T> void MOS6522<T>::run_for(const HalfCycles half_cycles) { | ||||
| 	int number_of_half_cycles = half_cycles.as_int(); | ||||
| 	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) { | ||||
| 	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. */ | ||||
| template <typename T> bool MOS6522<T>::get_interrupt_line() { | ||||
| 	uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f; | ||||
| 	return !!interrupt_status; | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::evaluate_cb2_output() { | ||||
| 	// CB2 is a special case, being both the line the shift register can output to, | ||||
| 	// and one that can be used as an input or handshaking output according to the | ||||
| 	// peripheral control register. | ||||
|  | ||||
| 	// My guess: other CB2 functions work only if the shift register is disabled (?). | ||||
| 	if(shift_mode() != ShiftMode::Disabled) { | ||||
| 		// Shift register is enabled, one way or the other; but announce only output. | ||||
| 		if(is_shifting_out()) { | ||||
| 			// Output mode; set the level according to the current top of the shift register. | ||||
| 			bus_handler_.set_control_line_output(Port::B, Line::Two, !!(registers_.shift & 0x80)); | ||||
| 		} else { | ||||
| 			// Input mode. | ||||
| 			bus_handler_.set_control_line_output(Port::B, Line::Two, true); | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Shift register is disabled. | ||||
| 		bus_handler_.set_control_line_output(Port::B, Line::Two, control_outputs_[1].lines[1] != LineState::Off); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::set_control_line_output(Port port, Line line, LineState value) { | ||||
| 	if(port == Port::B && line == Line::Two) { | ||||
| 		control_outputs_[port].lines[line] = value; | ||||
| 		evaluate_cb2_output(); | ||||
| 	} else { | ||||
| 		// Do nothing if unchanged. | ||||
| 		if(value == control_outputs_[port].lines[line]) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		control_outputs_[port].lines[line] = value; | ||||
|  | ||||
| 		if(value != LineState::Input) { | ||||
| 			bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>()); | ||||
| 			bus_handler_.set_control_line_output(port, line, value != LineState::Off); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::shift_in() { | ||||
| 	registers_.shift = uint8_t((registers_.shift << 1) | (control_inputs_[1].lines[1] ? 1 : 0)); | ||||
| 	--shift_bits_remaining_; | ||||
| 	if(!shift_bits_remaining_) { | ||||
| 		registers_.interrupt_flags |= InterruptFlag::ShiftRegister; | ||||
| 		reevaluate_interrupts(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> void MOS6522<T>::shift_out() { | ||||
| 	// When shifting out, the shift register rotates rather than strictly shifts. | ||||
| 	// TODO: is that true for all modes? | ||||
| 	if(shift_mode() == ShiftMode::OutUnderT2FreeRunning || shift_bits_remaining_) { | ||||
| 		registers_.shift = uint8_t((registers_.shift << 1) | (registers_.shift >> 7)); | ||||
| 		evaluate_cb2_output(); | ||||
|  | ||||
| 		--shift_bits_remaining_; | ||||
| 		if(!shift_bits_remaining_) { | ||||
| 			registers_.interrupt_flags |= InterruptFlag::ShiftRegister; | ||||
| 			reevaluate_interrupts(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -37,14 +37,27 @@ class MOS6522Storage { | ||||
| 			bool timer_needs_reload = false; | ||||
| 		} registers_; | ||||
|  | ||||
| 		// control state | ||||
| 		// Control state. | ||||
| 		struct { | ||||
| 			bool line_one = false; | ||||
| 			bool line_two = false; | ||||
| 			bool lines[2] = {false, false}; | ||||
| 		} control_inputs_[2]; | ||||
|  | ||||
| 		enum class LineState { | ||||
| 			On, Off, Input | ||||
| 		}; | ||||
| 		struct { | ||||
| 			LineState lines[2] = {LineState::Input, LineState::Input}; | ||||
| 		} control_outputs_[2]; | ||||
|  | ||||
| 		enum class HandshakeMode { | ||||
| 			None, | ||||
| 			Handshake, | ||||
| 			Pulse | ||||
| 		} handshake_modes_[2] = { HandshakeMode::None, HandshakeMode::None }; | ||||
|  | ||||
| 		bool timer_is_running_[2] = {false, false}; | ||||
| 		bool last_posted_interrupt_status_ = false; | ||||
| 		int shift_bits_remaining_ = 8; | ||||
|  | ||||
| 		enum InterruptFlag: uint8_t { | ||||
| 			CA2ActiveEdge	= 1 << 0, | ||||
| @@ -55,6 +68,23 @@ class MOS6522Storage { | ||||
| 			Timer2			= 1 << 5, | ||||
| 			Timer1			= 1 << 6, | ||||
| 		}; | ||||
|  | ||||
| 		enum class ShiftMode { | ||||
| 			Disabled = 0, | ||||
| 			InUnderT2 = 1, | ||||
| 			InUnderPhase2 = 2, | ||||
| 			InUnderCB1 = 3, | ||||
| 			OutUnderT2FreeRunning = 4, | ||||
| 			OutUnderT2 = 5, | ||||
| 			OutUnderPhase2 = 6, | ||||
| 			OutUnderCB1 = 7 | ||||
| 		}; | ||||
| 		ShiftMode shift_mode() const { | ||||
| 			return ShiftMode((registers_.auxiliary_control >> 2) & 7); | ||||
| 		} | ||||
| 		bool is_shifting_out() const { | ||||
| 			return registers_.auxiliary_control & 0x10; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										268
									
								
								Components/8530/z8530.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								Components/8530/z8530.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | ||||
| // | ||||
| //  8530.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 07/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "z8530.hpp" | ||||
|  | ||||
| #include "../../Outputs/Log.hpp" | ||||
|  | ||||
| using namespace Zilog::SCC; | ||||
|  | ||||
| void z8530::reset() { | ||||
| 	// TODO. | ||||
| } | ||||
|  | ||||
| bool z8530::get_interrupt_line() { | ||||
| 	return | ||||
| 		(master_interrupt_control_ & 0x8) && | ||||
| 		( | ||||
| 			channels_[0].get_interrupt_line() || | ||||
| 			channels_[1].get_interrupt_line() | ||||
| 		); | ||||
| } | ||||
|  | ||||
| std::uint8_t z8530::read(int address) { | ||||
| 	if(address & 2) { | ||||
| 		// Read data register for channel | ||||
| 		return 0x00; | ||||
| 	} else { | ||||
| 		// Read control register for channel. | ||||
| 		uint8_t result = 0; | ||||
|  | ||||
| 		switch(pointer_) { | ||||
| 			default: | ||||
| 				result = channels_[address & 1].read(address & 2, pointer_); | ||||
| 			break; | ||||
|  | ||||
| 			case 2:		// Handled non-symmetrically between channels. | ||||
| 				if(address & 1) { | ||||
| 					LOG("[SCC] Unimplemented: register 2 status bits"); | ||||
| 				} else { | ||||
| 					result = interrupt_vector_; | ||||
|  | ||||
| 					// Modify the vector if permitted. | ||||
| //					if(master_interrupt_control_ & 1) { | ||||
| 						for(int port = 0; port < 2; ++port) { | ||||
| 							// TODO: the logic below assumes that DCD is the only implemented interrupt. Fix. | ||||
| 							if(channels_[port].get_interrupt_line()) { | ||||
| 								const uint8_t shift = 1 + 3*((master_interrupt_control_ & 0x10) >> 4); | ||||
| 								const uint8_t mask = uint8_t(~(7 << shift)); | ||||
| 								result = uint8_t( | ||||
| 									(result & mask) | | ||||
| 									((1 | ((port == 1) ? 4 : 0)) << shift) | ||||
| 								); | ||||
| 								break; | ||||
| 							} | ||||
| 						} | ||||
| //					} | ||||
| 				} | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		pointer_ = 0; | ||||
| 		update_delegate(); | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	return 0x00; | ||||
| } | ||||
|  | ||||
| void z8530::write(int address, std::uint8_t value) { | ||||
| 	if(address & 2) { | ||||
| 		// Write data register for channel. | ||||
| 	} else { | ||||
| 		// Write control register for channel. | ||||
|  | ||||
| 		// Most registers are per channel, but a couple are shared; sever | ||||
| 		// them here. | ||||
| 		switch(pointer_) { | ||||
| 			default: | ||||
| 				channels_[address & 1].write(address & 2, pointer_, value); | ||||
| 			break; | ||||
|  | ||||
| 			case 2:	// Interrupt vector register; shared between both channels. | ||||
| 				interrupt_vector_ = value; | ||||
| 				LOG("[SCC] Interrupt vector set to " << PADHEX(2) << int(value)); | ||||
| 			break; | ||||
|  | ||||
| 			case 9:	// Master interrupt and reset register; also shared between both channels. | ||||
| 				LOG("[SCC] Master interrupt and reset register: " << PADHEX(2) << int(value)); | ||||
| 				master_interrupt_control_ = value; | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		// The pointer number resets to 0 after every access, but if it is zero | ||||
| 		// then crib at least the next set of pointer bits (which, similarly, are shared | ||||
| 		// between the two channels). | ||||
| 		if(pointer_) { | ||||
| 			pointer_ = 0; | ||||
| 		} else { | ||||
| 			// The lowest three bits are the lowest three bits of the pointer. | ||||
| 			pointer_ = value & 7; | ||||
|  | ||||
| 			// If the command part of the byte is a 'point high', also set the | ||||
| 			// top bit of the pointer. | ||||
| 			if(((value >> 3)&7) == 1) { | ||||
| 				pointer_ |= 8; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	update_delegate(); | ||||
| } | ||||
|  | ||||
| void z8530::set_dcd(int port, bool level) { | ||||
| 	channels_[port].set_dcd(level); | ||||
| 	update_delegate(); | ||||
| } | ||||
|  | ||||
| // MARK: - Channel implementations | ||||
|  | ||||
| uint8_t z8530::Channel::read(bool data, uint8_t pointer) { | ||||
| 	// If this is a data read, just return it. | ||||
| 	if(data) { | ||||
| 		return data_; | ||||
| 	} else { | ||||
| 		// Otherwise, this is a control read... | ||||
| 		switch(pointer) { | ||||
| 			default: | ||||
| 				LOG("[SCC] Unrecognised control read from register " << int(pointer)); | ||||
| 			return 0x00; | ||||
|  | ||||
| 			case 0: | ||||
| 			return dcd_ ? 0x8 : 0x0; | ||||
|  | ||||
| 			case 0xf: | ||||
| 			return external_interrupt_status_; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return 0x00; | ||||
| } | ||||
|  | ||||
| void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) { | ||||
| 	if(data) { | ||||
| 		data_ = value; | ||||
| 		return; | ||||
| 	} else { | ||||
| 		switch(pointer) { | ||||
| 			default: | ||||
| 				LOG("[SCC] Unrecognised control write: " << PADHEX(2) << int(value) << " to register " << int(pointer)); | ||||
| 			break; | ||||
|  | ||||
| 			case 0x0:	// Write register 0 — CRC reset and other functions. | ||||
| 				// Decode CRC reset instructions. | ||||
| 				switch(value >> 6) { | ||||
| 					default:	/* Do nothing. */		break; | ||||
| 					case 1: | ||||
| 						LOG("[SCC] TODO: reset Rx CRC checker."); | ||||
| 					break; | ||||
| 					case 2: | ||||
| 						LOG("[SCC] TODO: reset Tx CRC checker."); | ||||
| 					break; | ||||
| 					case 3: | ||||
| 						LOG("[SCC] TODO: reset Tx underrun/EOM latch."); | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				// Decode command code. | ||||
| 				switch((value >> 3)&7) { | ||||
| 					default:	/* Do nothing. */		break; | ||||
| 					case 2: | ||||
| //						LOG("[SCC] reset ext/status interrupts."); | ||||
| 						external_status_interrupt_ = false; | ||||
| 						external_interrupt_status_ = 0; | ||||
| 					break; | ||||
| 					case 3: | ||||
| 						LOG("[SCC] TODO: send abort (SDLC)."); | ||||
| 					break; | ||||
| 					case 4: | ||||
| 						LOG("[SCC] TODO: enable interrupt on next Rx character."); | ||||
| 					break; | ||||
| 					case 5: | ||||
| 						LOG("[SCC] TODO: reset Tx interrupt pending."); | ||||
| 					break; | ||||
| 					case 6: | ||||
| 						LOG("[SCC] TODO: reset error."); | ||||
| 					break; | ||||
| 					case 7: | ||||
| 						LOG("[SCC] TODO: reset highest IUS."); | ||||
| 					break; | ||||
| 				} | ||||
| 			break; | ||||
|  | ||||
| 			case 0x1:	// Write register 1 — Transmit/Receive Interrupt and Data Transfer Mode Definition. | ||||
| 				interrupt_mask_ = value; | ||||
| 			break; | ||||
|  | ||||
| 			case 0x4:	// Write register 4 — Transmit/Receive Miscellaneous Parameters and Modes. | ||||
| 				// Bits 0 and 1 select parity mode. | ||||
| 				if(!(value&1)) { | ||||
| 					parity_ = Parity::Off; | ||||
| 				} else { | ||||
| 					parity_ = (value&2) ? Parity::Even : Parity::Odd; | ||||
| 				} | ||||
|  | ||||
| 				// Bits 2 and 3 select stop bits. | ||||
| 				switch((value >> 2)&3) { | ||||
| 					default:	stop_bits_ = StopBits::Synchronous;			break; | ||||
| 					case 1:		stop_bits_ = StopBits::OneBit;				break; | ||||
| 					case 2:		stop_bits_ = StopBits::OneAndAHalfBits;		break; | ||||
| 					case 3:		stop_bits_ = StopBits::TwoBits;				break; | ||||
| 				} | ||||
|  | ||||
| 				// Bits 4 and 5 pick a sync mode. | ||||
| 				switch((value >> 4)&3) { | ||||
| 					default:	sync_mode_ = Sync::Monosync;	break; | ||||
| 					case 1:		sync_mode_ = Sync::Bisync;		break; | ||||
| 					case 2:		sync_mode_ = Sync::SDLC;		break; | ||||
| 					case 3:		sync_mode_ = Sync::External;	break; | ||||
| 				} | ||||
|  | ||||
| 				// Bits 6 and 7 select a clock rate multiplier, unless synchronous | ||||
| 				// mode is enabled (and this is ignored if sync mode is external). | ||||
| 				if(stop_bits_ == StopBits::Synchronous) { | ||||
| 					clock_rate_multiplier_ = 1; | ||||
| 				} else { | ||||
| 					switch((value >> 6)&3) { | ||||
| 						default:	clock_rate_multiplier_ = 1;		break; | ||||
| 						case 1:		clock_rate_multiplier_ = 16;	break; | ||||
| 						case 2:		clock_rate_multiplier_ = 32;	break; | ||||
| 						case 3:		clock_rate_multiplier_ = 64;	break; | ||||
| 					} | ||||
| 				} | ||||
| 			break; | ||||
|  | ||||
| 			case 0xf:	// Write register 15 — External/Status Interrupt Control. | ||||
| 				external_interrupt_mask_ = value; | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void z8530::Channel::set_dcd(bool level) { | ||||
| 	if(dcd_ == level) return; | ||||
| 	dcd_ = level; | ||||
|  | ||||
| 	if(external_interrupt_mask_ & 0x8) { | ||||
| 		external_status_interrupt_ = true; | ||||
| 		external_interrupt_status_ |= 0x8; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool z8530::Channel::get_interrupt_line() { | ||||
| 	return | ||||
| 		(interrupt_mask_ & 1) && external_status_interrupt_; | ||||
| 	// TODO: other potential causes of an interrupt. | ||||
| } | ||||
|  | ||||
| void z8530::update_delegate() { | ||||
| 	const bool interrupt_line = get_interrupt_line(); | ||||
| 	if(interrupt_line != previous_interrupt_line_) { | ||||
| 		previous_interrupt_line_ = interrupt_line; | ||||
| 		if(delegate_) delegate_->did_change_interrupt_status(this, interrupt_line); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										99
									
								
								Components/8530/z8530.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								Components/8530/z8530.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| // | ||||
| //  z8530.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 07/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef z8530_hpp | ||||
| #define z8530_hpp | ||||
|  | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace Zilog { | ||||
| namespace SCC { | ||||
|  | ||||
| /*! | ||||
| 	Models the Zilog 8530 SCC, a serial adaptor. | ||||
| */ | ||||
| class z8530 { | ||||
| 	public: | ||||
| 		/* | ||||
| 			**Interface for emulated machine.** | ||||
|  | ||||
| 			Notes on addressing below: | ||||
|  | ||||
| 			There's no inherent ordering of the two 'address' lines, | ||||
| 			A/B and C/D, but the methods below assume: | ||||
|  | ||||
| 				A/B = A0 | ||||
| 				C/D = A1 | ||||
| 		*/ | ||||
| 		std::uint8_t read(int address); | ||||
| 		void write(int address, std::uint8_t value); | ||||
| 		void reset(); | ||||
| 		bool get_interrupt_line(); | ||||
|  | ||||
| 		struct Delegate { | ||||
| 			virtual void did_change_interrupt_status(z8530 *, bool new_status) = 0; | ||||
| 		}; | ||||
| 		void set_delegate(Delegate *delegate) { | ||||
| 			delegate_ = delegate; | ||||
| 		} | ||||
|  | ||||
| 		/* | ||||
| 			**Interface for serial port input.** | ||||
| 		*/ | ||||
| 		void set_dcd(int port, bool level); | ||||
|  | ||||
| 	private: | ||||
| 		class Channel { | ||||
| 			public: | ||||
| 				uint8_t read(bool data, uint8_t pointer); | ||||
| 				void write(bool data, uint8_t pointer, uint8_t value); | ||||
| 				void set_dcd(bool level); | ||||
| 				bool get_interrupt_line(); | ||||
|  | ||||
| 			private: | ||||
| 				uint8_t data_ = 0xff; | ||||
|  | ||||
| 				enum class Parity { | ||||
| 					Even, Odd, Off | ||||
| 				} parity_ = Parity::Off; | ||||
|  | ||||
| 				enum class StopBits { | ||||
| 					Synchronous, OneBit, OneAndAHalfBits, TwoBits | ||||
| 				} stop_bits_ = StopBits::Synchronous; | ||||
|  | ||||
| 				enum class Sync { | ||||
| 					Monosync, Bisync, SDLC, External | ||||
| 				} sync_mode_ = Sync::Monosync; | ||||
|  | ||||
| 				int clock_rate_multiplier_ = 1; | ||||
|  | ||||
| 				uint8_t interrupt_mask_ = 0;			// i.e. Write Register 0x1. | ||||
|  | ||||
| 				uint8_t external_interrupt_mask_ = 0;	// i.e. Write Register 0xf. | ||||
| 				bool external_status_interrupt_ = false; | ||||
| 				uint8_t external_interrupt_status_ = 0; | ||||
|  | ||||
| 				bool dcd_ = false; | ||||
| 		} channels_[2]; | ||||
|  | ||||
| 		uint8_t pointer_ = 0; | ||||
|  | ||||
| 		uint8_t interrupt_vector_ = 0; | ||||
|  | ||||
| 		uint8_t master_interrupt_control_ = 0; | ||||
|  | ||||
| 		bool previous_interrupt_line_ = false; | ||||
| 		void update_delegate(); | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| #endif /* z8530_hpp */ | ||||
| @@ -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); | ||||
| @@ -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. | ||||
|   | ||||
| @@ -98,7 +98,7 @@ class DiskII: | ||||
| 		void select_drive(int drive); | ||||
|  | ||||
| 		uint8_t trigger_address(int address, uint8_t value); | ||||
| 		void process_event(const Storage::Disk::Track::Event &event) override; | ||||
| 		void process_event(const Storage::Disk::Drive::Event &event) override; | ||||
| 		void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) override; | ||||
|  | ||||
| 		const int clock_rate_ = 0; | ||||
|   | ||||
							
								
								
									
										376
									
								
								Components/DiskII/IWM.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										376
									
								
								Components/DiskII/IWM.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,376 @@ | ||||
| // | ||||
| //  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  { | ||||
| 	const int CA0		= 1 << 0; | ||||
| 	const int CA1		= 1 << 1; | ||||
| 	const int CA2		= 1 << 2; | ||||
| 	const int LSTRB		= 1 << 3; | ||||
| 	const int ENABLE	= 1 << 4; | ||||
| 	const int DRIVESEL	= 1 << 5;	/* This means drive select, like on the original Disk II. */ | ||||
| 	const int Q6		= 1 << 6; | ||||
| 	const int Q7		= 1 << 7; | ||||
| 	const int SEL		= 1 << 8;	/* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */ | ||||
| } | ||||
|  | ||||
| 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. | ||||
| 	int integer_cycles = cycles.as_int(); | ||||
| 	switch(shift_mode_) { | ||||
| 		case ShiftMode::Reading: | ||||
| 			if(drive_is_rotating_[active_drive_]) { | ||||
| 				while(integer_cycles--) { | ||||
| 					drives_[active_drive_]->run_for(Cycles(1)); | ||||
| 					++cycles_since_shift_; | ||||
| 					if(cycles_since_shift_ == bit_length_ + Cycles(2)) { | ||||
| 						propose_shift(0); | ||||
| 					} | ||||
| 				} | ||||
| 			} else { | ||||
| 				while(cycles_since_shift_ + integer_cycles >= bit_length_ + Cycles(2)) { | ||||
| 					propose_shift(0); | ||||
| 					integer_cycles -= bit_length_.as_int() + 2 - cycles_since_shift_.as_int(); | ||||
| 				} | ||||
| 				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_int(); | ||||
| 					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_int()), 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"); | ||||
| 	shift_register_ = uint8_t((shift_register_ << 1) | bit); | ||||
| 	if(shift_register_ & 0x80) { | ||||
| 		data_register_ = shift_register_; | ||||
| 		shift_register_ = 0; | ||||
| 	} | ||||
| 	cycles_since_shift_ = Cycles(0); | ||||
| } | ||||
|  | ||||
| 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; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										117
									
								
								Components/DiskII/IWM.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								Components/DiskII/IWM.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| // | ||||
| //  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 "../../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); | ||||
|  | ||||
| 	private: | ||||
| 		// Storage::Disk::Drive::EventDelegate. | ||||
| 		void process_event(const Storage::Disk::Drive::Event &event) override; | ||||
|  | ||||
| 		const int clock_rate_; | ||||
|  | ||||
| 		uint8_t data_register_ = 0; | ||||
| 		uint8_t mode_ = 0; | ||||
| 		bool read_write_ready_ = true; | ||||
| 		bool write_overran_ = false; | ||||
|  | ||||
| 		int state_ = 0; | ||||
|  | ||||
| 		int active_drive_ = 0; | ||||
| 		IWMDrive *drives_[2] = {nullptr, nullptr}; | ||||
| 		bool drive_is_rotating_[2] = {false, false}; | ||||
|  | ||||
| 		void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override; | ||||
|  | ||||
| 		Cycles cycles_until_disable_; | ||||
| 		uint8_t write_handshake_ = 0x80; | ||||
|  | ||||
| 		void access(int address); | ||||
|  | ||||
| 		uint8_t shift_register_ = 0; | ||||
| 		uint8_t next_output_ = 0; | ||||
| 		int output_bits_remaining_ = 0; | ||||
|  | ||||
| 		void propose_shift(uint8_t bit); | ||||
| 		Cycles cycles_since_shift_; | ||||
| 		Cycles bit_length_ = Cycles(16); | ||||
|  | ||||
| 		void push_drive_state(); | ||||
|  | ||||
| 		enum class ShiftMode { | ||||
| 			Reading, | ||||
| 			Writing, | ||||
| 			CheckingWriteProtect | ||||
| 		} shift_mode_; | ||||
|  | ||||
| 		uint8_t sense(); | ||||
| 		void select_shift_mode(); | ||||
| }; | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* IWM_hpp */ | ||||
							
								
								
									
										179
									
								
								Components/DiskII/MacintoshDoubleDensityDrive.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								Components/DiskII/MacintoshDoubleDensityDrive.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| // | ||||
| //  MacintoshDoubleDensityDrive.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/07/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MacintoshDoubleDensityDrive.hpp" | ||||
|  | ||||
| /* | ||||
| 	Sources used pervasively: | ||||
|  | ||||
| 	http://members.iinet.net.au/~kalandi/apple/AUG/1991/11%20NOV.DEC/DISK.STUFF.html | ||||
| 	Apple Guide to the Macintosh Family Hardware | ||||
| 	Inside Macintosh III | ||||
| */ | ||||
|  | ||||
| using namespace Apple::Macintosh; | ||||
|  | ||||
| DoubleDensityDrive::DoubleDensityDrive(int input_clock_rate, bool is_800k) : | ||||
| 	IWMDrive(input_clock_rate, is_800k ? 2 : 1),	// Only 800kb drives are double sided. | ||||
| 	is_800k_(is_800k) { | ||||
| 	// Start with a valid rotation speed. | ||||
| 	if(is_800k) { | ||||
| 		Drive::set_rotation_speed(393.3807f); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - Speed Selection | ||||
|  | ||||
| void DoubleDensityDrive::did_step(Storage::Disk::HeadPosition to_position) { | ||||
| //	printf("At track %d\n", to_position.as_int()); | ||||
| 	// The 800kb drive automatically selects rotation speed as a function of | ||||
| 	// head position; the 400kb drive doesn't do so. | ||||
| 	if(is_800k_) { | ||||
| 		/* | ||||
| 			Numbers below cribbed from the Kryoflux forums; specifically: | ||||
| 			https://forum.kryoflux.com/viewtopic.php?t=1090 | ||||
|  | ||||
| 			They can almost be worked out algorithmically, since the point is to | ||||
| 			produce an almost-constant value for speed*(number of sectors), and: | ||||
|  | ||||
| 			393.3807 * 12 = 4720.5684 | ||||
| 			429.1723 * 11 = 4720.895421 | ||||
| 			472.1435 * 10 = 4721.435 | ||||
| 			524.5672 * 9 = 4721.1048 | ||||
| 			590.1098 * 8 = 4720.8784 | ||||
|  | ||||
| 			So 4721 / (number of sectors per track in zone) would give essentially | ||||
| 			the same results. | ||||
| 		*/ | ||||
| 		const int zone = to_position.as_int() >> 4; | ||||
| 		switch(zone) { | ||||
| 			case 0:		Drive::set_rotation_speed(393.3807f);	break; | ||||
| 			case 1:		Drive::set_rotation_speed(429.1723f);	break; | ||||
| 			case 2:		Drive::set_rotation_speed(472.1435f);	break; | ||||
| 			case 3:		Drive::set_rotation_speed(524.5672f);	break; | ||||
| 			default:	Drive::set_rotation_speed(590.1098f);	break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void DoubleDensityDrive::set_rotation_speed(float revolutions_per_minute) { | ||||
| 	if(!is_800k_) { | ||||
| 		// Don't allow drive speeds to drop below 10 RPM, as a temporary sop | ||||
| 		// to sanity. | ||||
| 		Drive::set_rotation_speed(std::max(10.0f, revolutions_per_minute)); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - Control input/output. | ||||
|  | ||||
| void DoubleDensityDrive::set_enabled(bool) { | ||||
| } | ||||
|  | ||||
| void DoubleDensityDrive::set_control_lines(int lines) { | ||||
| 	const auto old_state = control_state_; | ||||
| 	control_state_ = lines; | ||||
|  | ||||
| 	// Catch low-to-high LSTRB transitions. | ||||
| 	if((old_state ^ control_state_) & control_state_ & Line::LSTRB) { | ||||
| 		switch(control_state_ & (Line::CA2 | Line::CA1 | Line::CA0 | Line::SEL)) { | ||||
| 			default: | ||||
| 			break; | ||||
|  | ||||
| 			case 0:						// Set step direction — CA2 set => step outward. | ||||
| 			case Line::CA2: | ||||
| 				step_direction_ = (control_state_ & Line::CA2) ? -1 : 1; | ||||
| 			break; | ||||
|  | ||||
| 			case Line::CA1:				// Set drive motor — CA2 set => motor off. | ||||
| 			case Line::CA1|Line::CA2: | ||||
| 				set_motor_on(!(control_state_ & Line::CA2)); | ||||
| 			break; | ||||
|  | ||||
| 			case Line::CA0:				// Initiate a step. | ||||
| 				step(Storage::Disk::HeadPosition(step_direction_)); | ||||
| 			break; | ||||
|  | ||||
| 			case Line::SEL|Line::CA2:	// Reset new disk flag. | ||||
| 				has_new_disk_ = false; | ||||
| 			break; | ||||
|  | ||||
| 			case Line::CA2 | Line::CA1 | Line::CA0:	// Eject the disk. | ||||
| 				set_disk(nullptr); | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool DoubleDensityDrive::read() { | ||||
| 	switch(control_state_ & (CA2 | CA1 | CA0 | SEL)) { | ||||
| 		default: | ||||
| 		return false; | ||||
|  | ||||
| 		case 0:					// Head step direction. | ||||
| 								// (0 = inward) | ||||
| 		return step_direction_ <= 0; | ||||
|  | ||||
| 		case SEL:				// Disk in place. | ||||
| 								// (0 = disk present) | ||||
| 		return !has_disk(); | ||||
|  | ||||
| 		case CA0:				// Disk head step completed. | ||||
| 								// (0 = still stepping) | ||||
| 		return true;	// TODO: stepping delay. But at the main Drive level. | ||||
|  | ||||
| 		case CA0|SEL:			// Disk locked. | ||||
| 								// (0 = write protected) | ||||
| 		return !get_is_read_only(); | ||||
|  | ||||
| 		case CA1:				// Disk motor running. | ||||
| 								// (0 = motor on) | ||||
| 		return !get_motor_on(); | ||||
|  | ||||
| 		case CA1|SEL:			// Head at track 0. | ||||
| 								// (0 = at track 0) | ||||
| 								// "This bit becomes valid beginning 12 msec after the step that places the head at track 0." | ||||
| 		return !get_is_track_zero(); | ||||
|  | ||||
| 		case CA1|CA0:			// Disk has been ejected. | ||||
| 								// (0 = user has ejected disk) | ||||
| 		return !has_new_disk_; | ||||
|  | ||||
| 		case CA1|CA0|SEL:		// Tachometer. | ||||
| 								// (arbitrary) | ||||
| 		return get_tachometer(); | ||||
|  | ||||
| 		case CA2:				// Read data, lower head. | ||||
| 			set_head(0); | ||||
| 		return false; | ||||
|  | ||||
| 		case CA2|SEL:			// Read data, upper head. | ||||
| 			set_head(1); | ||||
| 		return false; | ||||
|  | ||||
| 		case CA2|CA1:			// Single- or double-sided drive. | ||||
| 								// (0 = single sided) | ||||
| 		return get_head_count() != 1; | ||||
|  | ||||
| 		case CA2|CA1|CA0:		// "Present/HD" (per the Mac Plus ROM) | ||||
| 								// (0 = ??HD??) | ||||
| 								// | ||||
| 								// Alternative explanation: "Disk ready for reading?" | ||||
| 								// (0 = ready) | ||||
| 		return false; | ||||
|  | ||||
| 		case CA2|CA1|CA0|SEL:	// Drive installed. | ||||
| 								// (0 = present, 1 = missing) | ||||
| 								// | ||||
| 								// TODO: why do I need to return this the wrong way around for the Mac Plus? | ||||
| 		return true; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void DoubleDensityDrive::did_set_disk() { | ||||
| 	has_new_disk_ = true; | ||||
| } | ||||
							
								
								
									
										53
									
								
								Components/DiskII/MacintoshDoubleDensityDrive.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								Components/DiskII/MacintoshDoubleDensityDrive.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| // | ||||
| //  MacintoshDoubleDensityDrive.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/07/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MacintoshDoubleDensityDrive_hpp | ||||
| #define MacintoshDoubleDensityDrive_hpp | ||||
|  | ||||
| #include "IWM.hpp" | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| class DoubleDensityDrive: public IWMDrive { | ||||
| 	public: | ||||
| 		DoubleDensityDrive(int input_clock_rate, bool is_800k); | ||||
|  | ||||
| 		/*! | ||||
| 			@returns @c true if this is an 800kb drive; @c false otherwise. | ||||
| 		*/ | ||||
| 		bool is_800k() { | ||||
| 			return is_800k_; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Sets the current rotation speed of this drive only if it is a 400kb drive. | ||||
| 			800kb drives select their own rotation speed based on head position, | ||||
| 			and ignore this input. | ||||
| 		*/ | ||||
| 		void set_rotation_speed(float revolutions_per_minute); | ||||
|  | ||||
| 		void set_enabled(bool) override; | ||||
| 		void set_control_lines(int) override; | ||||
| 		bool read() override; | ||||
|  | ||||
| 	private: | ||||
| 		// To receive the proper notifications from Storage::Disk::Drive. | ||||
| 		void did_step(Storage::Disk::HeadPosition to_position) override; | ||||
| 		void did_set_disk() override; | ||||
|  | ||||
| 		const bool is_800k_; | ||||
| 		bool has_new_disk_ = false; | ||||
| 		int control_state_ = 0; | ||||
| 		int step_direction_ = 1; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* MacintoshDoubleDensityDrive_hpp */ | ||||
| @@ -6,8 +6,8 @@ | ||||
| //  Copyright 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Keyboard_hpp | ||||
| #define Keyboard_hpp | ||||
| #ifndef Inputs_Keyboard_hpp | ||||
| #define Inputs_Keyboard_hpp | ||||
|  | ||||
| #include <vector> | ||||
| #include <set> | ||||
| @@ -75,4 +75,4 @@ class Keyboard { | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* Keyboard_hpp */ | ||||
| #endif /* Inputs_Keyboard_hpp */ | ||||
|   | ||||
							
								
								
									
										47
									
								
								Inputs/Mouse.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								Inputs/Mouse.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| // | ||||
| //  Mouse.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 11/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Mouse_h | ||||
| #define Mouse_h | ||||
|  | ||||
| namespace Inputs { | ||||
|  | ||||
| /*! | ||||
| 	Models a classic-era mouse: something that provides 2d relative motion plus | ||||
| 	some quantity of buttons. | ||||
| */ | ||||
| class Mouse { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			Indicates a movement of the mouse. | ||||
| 		*/ | ||||
| 		virtual void move(int x, int y) {} | ||||
|  | ||||
| 		/*! | ||||
| 			@returns the number of buttons on this mouse. | ||||
| 		*/ | ||||
| 		virtual int get_number_of_buttons() { | ||||
| 			return 1; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Indicates that button @c index is now either pressed or unpressed. | ||||
| 			The intention is that @c index be semantic, not positional: | ||||
| 			0 for the primary button, 1 for the secondary, 2 for the tertiary, etc. | ||||
| 		*/ | ||||
| 		virtual void set_button_pressed(int index, bool is_pressed) {} | ||||
|  | ||||
| 		/*! | ||||
| 			Releases all depressed buttons. | ||||
| 		*/ | ||||
| 		virtual void reset_all_buttons() {} | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* Mouse_h */ | ||||
							
								
								
									
										123
									
								
								Inputs/QuadratureMouse/QuadratureMouse.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								Inputs/QuadratureMouse/QuadratureMouse.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| // | ||||
| //  QuadratureMouse.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 11/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef QuadratureMouse_hpp | ||||
| #define QuadratureMouse_hpp | ||||
|  | ||||
| #include "../Mouse.hpp" | ||||
| #include <atomic> | ||||
|  | ||||
| namespace Inputs { | ||||
|  | ||||
| /*! | ||||
| 	Provides a simple implementation of a Mouse, designed for simple | ||||
| 	thread-safe feeding to a machine that accepts quadrature-encoded input. | ||||
|  | ||||
| 	TEMPORARY SIMPLIFICATION: it is assumed that the caller will be interested | ||||
| 	in observing a signal that dictates velocity, sampling the other to | ||||
| 	obtain direction only on transitions in the velocity signal. | ||||
|  | ||||
| 	Or, more concretely, of the two channels per axis, one is accurate only when | ||||
| 	the other transitions. Hence the discussion of 'primary' and 'secondary' | ||||
| 	channels below. This is intended to be fixed. | ||||
| */ | ||||
| class QuadratureMouse: public Mouse { | ||||
| 	public: | ||||
| 		QuadratureMouse(int number_of_buttons) : | ||||
| 			number_of_buttons_(number_of_buttons) {} | ||||
|  | ||||
| 		/* | ||||
| 			Inputs, to satisfy the Mouse interface. | ||||
| 		*/ | ||||
| 		void move(int x, int y) override { | ||||
| 			// Accumulate all provided motion. | ||||
| 			axes_[0] += x; | ||||
| 			axes_[1] += y; | ||||
| 		} | ||||
|  | ||||
| 		int get_number_of_buttons() override { | ||||
| 			return number_of_buttons_; | ||||
| 		} | ||||
|  | ||||
| 		void set_button_pressed(int index, bool is_pressed) override { | ||||
| 			if(is_pressed) | ||||
| 				button_flags_ |= (1 << index); | ||||
| 			else | ||||
| 				button_flags_ &= ~(1 << index); | ||||
| 		} | ||||
|  | ||||
| 		void reset_all_buttons() override { | ||||
| 			button_flags_ = 0; | ||||
| 		} | ||||
|  | ||||
| 		/* | ||||
| 			Outputs. | ||||
| 		*/ | ||||
|  | ||||
| 		/*! | ||||
| 			Applies a single step from the current accumulated mouse movement, which | ||||
| 			might involve the mouse moving right, or left, or not at all. | ||||
| 		*/ | ||||
| 		void prepare_step() { | ||||
| 			for(int axis = 0; axis < 2; ++axis) { | ||||
| 				// Do nothing if there's no motion to communicate. | ||||
| 				const int axis_value = axes_[axis]; | ||||
| 				if(!axis_value) continue; | ||||
|  | ||||
| 				// Toggle the primary channel and set the secondary for | ||||
| 				// negative motion. At present the y axis signals the | ||||
| 				// secondary channel the opposite way around from the | ||||
| 				// primary. | ||||
| 				primaries_[axis] ^= 1; | ||||
| 				secondaries_[axis] = primaries_[axis] ^ axis; | ||||
| 				if(axis_value > 0) { | ||||
| 					-- axes_[axis]; | ||||
| 					secondaries_[axis] ^= 1;	// Switch to positive motion. | ||||
| 				} else { | ||||
| 					++ axes_[axis]; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			@returns the two quadrature channels — bit 0 is the 'primary' channel | ||||
| 				(i.e. the one that can be monitored to observe velocity) and | ||||
| 				bit 1 is the 'secondary' (i.e. that which can be queried to | ||||
| 				observe direction). | ||||
| 		*/ | ||||
| 		int get_channel(int axis) { | ||||
| 			return primaries_[axis] | (secondaries_[axis] << 1); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			@returns a bit mask of the currently pressed buttons. | ||||
| 		*/ | ||||
| 		int get_button_mask() { | ||||
| 			return button_flags_; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			@returns @c true if any mouse motion is waiting to be communicated; | ||||
| 				@c false otherwise. | ||||
| 		*/ | ||||
| 		bool has_steps() { | ||||
| 			return axes_[0] || axes_[1]; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		int number_of_buttons_ = 0; | ||||
| 		std::atomic<int> button_flags_; | ||||
| 		std::atomic<int> axes_[2]; | ||||
|  | ||||
| 		int primaries_[2] = {0, 0}; | ||||
| 		int secondaries_[2] = {0, 0}; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* QuadratureMouse_hpp */ | ||||
| @@ -781,27 +781,37 @@ template <bool has_fdc> class ConcreteMachine: | ||||
| 			ay_.ay().set_port_handler(&key_state_); | ||||
|  | ||||
| 			// construct the list of necessary ROMs | ||||
| 			std::vector<std::string> required_roms = {"amsdos.rom"}; | ||||
| 			const std::string machine_name = "AmstradCPC"; | ||||
| 			std::vector<ROMMachine::ROM> required_roms = { | ||||
| 				ROMMachine::ROM(machine_name, "the Amstrad Disk Operating System", "amsdos.rom", 16*1024, 0x1fe22ecd) | ||||
| 			}; | ||||
| 			std::string model_number; | ||||
| 			uint32_t crcs[2]; | ||||
| 			switch(target.model) { | ||||
| 				default: | ||||
| 					model_number = "6128"; | ||||
| 					has_128k_ = true; | ||||
| 					crcs[0] = 0x0219bb74; | ||||
| 					crcs[1] = 0xca6af63d; | ||||
| 				break; | ||||
| 				case Analyser::Static::AmstradCPC::Target::Model::CPC464: | ||||
| 					model_number = "464"; | ||||
| 					has_128k_ = false; | ||||
| 					crcs[0] = 0x815752df; | ||||
| 					crcs[1] = 0x7d9a3bac; | ||||
| 				break; | ||||
| 				case Analyser::Static::AmstradCPC::Target::Model::CPC664: | ||||
| 					model_number = "664"; | ||||
| 					has_128k_ = false; | ||||
| 					crcs[0] = 0x3f5a6dc4; | ||||
| 					crcs[1] = 0x32fee492; | ||||
| 				break; | ||||
| 			} | ||||
| 			required_roms.push_back("os" + model_number + ".rom"); | ||||
| 			required_roms.push_back("basic" + model_number + ".rom"); | ||||
| 			required_roms.emplace_back(machine_name, "the CPC " + model_number + " firmware", "os" + model_number + ".rom", 16*1024, crcs[0]); | ||||
| 			required_roms.emplace_back(machine_name, "the CPC " + model_number + " BASIC ROM", "basic" + model_number + ".rom", 16*1024, crcs[1]); | ||||
|  | ||||
| 			// fetch and verify the ROMs | ||||
| 			const auto roms = rom_fetcher("AmstradCPC", required_roms); | ||||
| 			const auto roms = rom_fetcher(required_roms); | ||||
|  | ||||
| 			for(std::size_t index = 0; index < roms.size(); ++index) { | ||||
| 				auto &data = roms[index]; | ||||
|   | ||||
| @@ -8,33 +8,34 @@ | ||||
| 
 | ||||
| #include "AppleII.hpp" | ||||
| 
 | ||||
| #include "../../Activity/Source.hpp" | ||||
| #include "../MediaTarget.hpp" | ||||
| #include "../CRTMachine.hpp" | ||||
| #include "../JoystickMachine.hpp" | ||||
| #include "../KeyboardMachine.hpp" | ||||
| #include "../Utility/MemoryFuzzer.hpp" | ||||
| #include "../Utility/StringSerialiser.hpp" | ||||
| #include "../../../Activity/Source.hpp" | ||||
| #include "../../MediaTarget.hpp" | ||||
| #include "../../CRTMachine.hpp" | ||||
| #include "../../JoystickMachine.hpp" | ||||
| #include "../../KeyboardMachine.hpp" | ||||
| #include "../../Utility/MemoryFuzzer.hpp" | ||||
| #include "../../Utility/StringSerialiser.hpp" | ||||
| 
 | ||||
| #include "../../Processors/6502/6502.hpp" | ||||
| #include "../../Components/AudioToggle/AudioToggle.hpp" | ||||
| #include "../../../Processors/6502/6502.hpp" | ||||
| #include "../../../Components/AudioToggle/AudioToggle.hpp" | ||||
| 
 | ||||
| #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../Outputs/Log.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
| 
 | ||||
| #include "Card.hpp" | ||||
| #include "DiskIICard.hpp" | ||||
| #include "Video.hpp" | ||||
| 
 | ||||
| #include "../../Analyser/Static/AppleII/Target.hpp" | ||||
| #include "../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../Configurable/StandardOptions.hpp" | ||||
| #include "../../../Analyser/Static/AppleII/Target.hpp" | ||||
| #include "../../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../../Configurable/StandardOptions.hpp" | ||||
| 
 | ||||
| #include <algorithm> | ||||
| #include <array> | ||||
| #include <memory> | ||||
| 
 | ||||
| namespace AppleII { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| 
 | ||||
| std::vector<std::unique_ptr<Configurable::Option>> get_options() { | ||||
| 	return Configurable::standard_options( | ||||
| @@ -51,12 +52,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 	public CPU::MOS6502::BusHandler, | ||||
| 	public Inputs::Keyboard, | ||||
| 	public Configurable::Device, | ||||
| 	public AppleII::Machine, | ||||
| 	public Apple::II::Machine, | ||||
| 	public Activity::Source, | ||||
| 	public JoystickMachine::Machine, | ||||
| 	public AppleII::Card::Delegate { | ||||
| 	public Apple::II::Card::Delegate { | ||||
| 	private: | ||||
| 		struct VideoBusHandler : public AppleII::Video::BusHandler { | ||||
| 		struct VideoBusHandler : public Apple::II::Video::BusHandler { | ||||
| 			public: | ||||
| 				VideoBusHandler(uint8_t *ram, uint8_t *aux_ram) : ram_(ram), aux_ram_(aux_ram) {} | ||||
| 
 | ||||
| @@ -71,12 +72,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 
 | ||||
| 		CPU::MOS6502::Processor<(model == Analyser::Static::AppleII::Target::Model::EnhancedIIe) ? CPU::MOS6502::Personality::PSynertek65C02 : CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_; | ||||
| 		VideoBusHandler video_bus_handler_; | ||||
| 		AppleII::Video::Video<VideoBusHandler, is_iie()> video_; | ||||
| 		Apple::II::Video::Video<VideoBusHandler, is_iie()> video_; | ||||
| 		int cycles_into_current_line_ = 0; | ||||
| 		Cycles cycles_since_video_update_; | ||||
| 
 | ||||
| 		void update_video() { | ||||
| 			video_.run_for(cycles_since_video_update_.flush()); | ||||
| 			video_.run_for(cycles_since_video_update_.flush<Cycles>()); | ||||
| 		} | ||||
| 		static const int audio_divider = 8; | ||||
| 		void update_audio() { | ||||
| @@ -109,28 +110,28 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 		Cycles cycles_since_audio_update_; | ||||
| 
 | ||||
| 		// MARK: - Cards
 | ||||
| 		std::array<std::unique_ptr<AppleII::Card>, 7> cards_; | ||||
| 		std::array<std::unique_ptr<Apple::II::Card>, 7> cards_; | ||||
| 		Cycles cycles_since_card_update_; | ||||
| 		std::vector<AppleII::Card *> every_cycle_cards_; | ||||
| 		std::vector<AppleII::Card *> just_in_time_cards_; | ||||
| 		std::vector<Apple::II::Card *> every_cycle_cards_; | ||||
| 		std::vector<Apple::II::Card *> just_in_time_cards_; | ||||
| 
 | ||||
| 		int stretched_cycles_since_card_update_ = 0; | ||||
| 
 | ||||
| 		void install_card(std::size_t slot, AppleII::Card *card) { | ||||
| 		void install_card(std::size_t slot, Apple::II::Card *card) { | ||||
| 			assert(slot >= 1 && slot < 8); | ||||
| 			cards_[slot - 1].reset(card); | ||||
| 			card->set_delegate(this); | ||||
| 			pick_card_messaging_group(card); | ||||
| 		} | ||||
| 
 | ||||
| 		bool is_every_cycle_card(AppleII::Card *card) { | ||||
| 		bool is_every_cycle_card(Apple::II::Card *card) { | ||||
| 			return !card->get_select_constraints(); | ||||
| 		} | ||||
| 
 | ||||
| 		void pick_card_messaging_group(AppleII::Card *card) { | ||||
| 		void pick_card_messaging_group(Apple::II::Card *card) { | ||||
| 			const bool is_every_cycle = is_every_cycle_card(card); | ||||
| 			std::vector<AppleII::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_; | ||||
| 			std::vector<AppleII::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_; | ||||
| 			std::vector<Apple::II::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_; | ||||
| 			std::vector<Apple::II::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_; | ||||
| 
 | ||||
| 			if(std::find(intended.begin(), intended.end(), card) != intended.end()) return; | ||||
| 			auto old_membership = std::find(undesired.begin(), undesired.end(), card); | ||||
| @@ -138,12 +139,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 			intended.push_back(card); | ||||
| 		} | ||||
| 
 | ||||
| 		void card_did_change_select_constraints(AppleII::Card *card) override { | ||||
| 		void card_did_change_select_constraints(Apple::II::Card *card) override { | ||||
| 			pick_card_messaging_group(card); | ||||
| 		} | ||||
| 
 | ||||
| 		AppleII::DiskIICard *diskii_card() { | ||||
| 			return dynamic_cast<AppleII::DiskIICard *>(cards_[5].get()); | ||||
| 		Apple::II::DiskIICard *diskii_card() { | ||||
| 			return dynamic_cast<Apple::II::DiskIICard *>(cards_[5].get()); | ||||
| 		} | ||||
| 
 | ||||
| 		// MARK: - Memory Map.
 | ||||
| @@ -346,30 +347,39 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 
 | ||||
| 			// Pick the required ROMs.
 | ||||
| 			using Target = Analyser::Static::AppleII::Target; | ||||
| 			std::vector<std::string> rom_names; | ||||
| 			const std::string machine_name = "AppleII"; | ||||
| 			std::vector<ROMMachine::ROM> rom_descriptions; | ||||
| 			size_t rom_size = 12*1024; | ||||
| 			switch(target.model) { | ||||
| 				default: | ||||
| 					rom_names.push_back("apple2-character.rom"); | ||||
| 					rom_names.push_back("apple2o.rom"); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588); | ||||
| 				break; | ||||
| 				case Target::Model::IIplus: | ||||
| 					rom_names.push_back("apple2-character.rom"); | ||||
| 					rom_names.push_back("apple2.rom"); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26); | ||||
| 				break; | ||||
| 				case Target::Model::IIe: | ||||
| 					rom_size += 3840; | ||||
| 					rom_names.push_back("apple2eu-character.rom"); | ||||
| 					rom_names.push_back("apple2eu.rom"); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d); | ||||
| 				break; | ||||
| 				case Target::Model::EnhancedIIe: | ||||
| 					rom_size += 3840; | ||||
| 					rom_names.push_back("apple2e-character.rom"); | ||||
| 					rom_names.push_back("apple2e.rom"); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d); | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942); | ||||
| 				break; | ||||
| 			} | ||||
| 			const auto roms = rom_fetcher("AppleII", rom_names); | ||||
| 			const auto roms = rom_fetcher(rom_descriptions); | ||||
| 
 | ||||
| 			// Try to install a Disk II card now, before checking the ROM list,
 | ||||
| 			// to make sure that Disk II dependencies have been communicated.
 | ||||
| 			if(target.disk_controller != Target::DiskController::None) { | ||||
| 				// Apple recommended slot 6 for the (first) Disk II.
 | ||||
| 				install_card(6, new Apple::II::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector)); | ||||
| 			} | ||||
| 
 | ||||
| 			// Now, check and move the ROMs.
 | ||||
| 			if(!roms[0] || !roms[1]) { | ||||
| 				throw ROMMachine::Error::MissingROMs; | ||||
| 			} | ||||
| @@ -381,11 +391,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 
 | ||||
| 			video_.set_character_rom(*roms[0]); | ||||
| 
 | ||||
| 			if(target.disk_controller != Target::DiskController::None) { | ||||
| 				// Apple recommended slot 6 for the (first) Disk II.
 | ||||
| 				install_card(6, new AppleII::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector)); | ||||
| 			} | ||||
| 
 | ||||
| 			// Set up the default memory blocks. On a II or II+ these values will never change.
 | ||||
| 			// On a IIe they'll be affected by selection of auxiliary RAM.
 | ||||
| 			set_main_paging(); | ||||
| @@ -700,7 +705,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					// If this is a card access, figure out which card is at play before determining
 | ||||
| 					// the totality of who needs messaging.
 | ||||
| 					size_t card_number = 0; | ||||
| 					AppleII::Card::Select select = AppleII::Card::None; | ||||
| 					Apple::II::Card::Select select = Apple::II::Card::None; | ||||
| 
 | ||||
| 					if(address >= 0xc100) { | ||||
| 						/*
 | ||||
| @@ -708,20 +713,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 								0xCn00 to 0xCnff: card n. | ||||
| 						*/ | ||||
| 						card_number = (address - 0xc100) >> 8; | ||||
| 						select = AppleII::Card::Device; | ||||
| 						select = Apple::II::Card::Device; | ||||
| 					} else { | ||||
| 						/*
 | ||||
| 							Decode the area conventionally used by cards for registers: | ||||
| 								C0n0 to C0nF: card n - 8. | ||||
| 						*/ | ||||
| 						card_number = (address - 0xc090) >> 4; | ||||
| 						select = AppleII::Card::IO; | ||||
| 						select = Apple::II::Card::IO; | ||||
| 					} | ||||
| 
 | ||||
| 					// If the selected card is a just-in-time card, update the just-in-time cards,
 | ||||
| 					// and then message it specifically.
 | ||||
| 					const bool is_read = isReadOperation(operation); | ||||
| 					AppleII::Card *const target = cards_[static_cast<size_t>(card_number)].get(); | ||||
| 					Apple::II::Card *const target = cards_[static_cast<size_t>(card_number)].get(); | ||||
| 					if(target && !is_every_cycle_card(target)) { | ||||
| 						update_just_in_time_cards(); | ||||
| 						target->perform_bus_operation(select, is_read, address, value); | ||||
| @@ -732,7 +737,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					for(const auto &card: every_cycle_cards_) { | ||||
| 						card->run_for(Cycles(1), is_stretched_cycle); | ||||
| 						card->perform_bus_operation( | ||||
| 							(card == target) ? select : AppleII::Card::None, | ||||
| 							(card == target) ? select : Apple::II::Card::None, | ||||
| 							is_read, address, value); | ||||
| 					} | ||||
| 					has_updated_cards = true; | ||||
| @@ -744,7 +749,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 				const bool is_read = isReadOperation(operation); | ||||
| 				for(const auto &card: every_cycle_cards_) { | ||||
| 					card->run_for(Cycles(1), is_stretched_cycle); | ||||
| 					card->perform_bus_operation(AppleII::Card::None, is_read, address, value); | ||||
| 					card->perform_bus_operation(Apple::II::Card::None, is_read, address, value); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| @@ -818,7 +823,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 
 | ||||
| 		// MARK:: Configuration options.
 | ||||
| 		std::vector<std::unique_ptr<Configurable::Option>> get_options() override { | ||||
| 			return AppleII::get_options(); | ||||
| 			return Apple::II::get_options(); | ||||
| 		} | ||||
| 
 | ||||
| 		void set_selections(const Configurable::SelectionSet &selections_by_option) override { | ||||
| @@ -860,9 +865,10 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 		} | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| using namespace AppleII; | ||||
| using namespace Apple::II; | ||||
| 
 | ||||
| Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { | ||||
| 	using Target = Analyser::Static::AppleII::Target; | ||||
| @@ -9,14 +9,15 @@ | ||||
| #ifndef AppleII_hpp | ||||
| #define AppleII_hpp | ||||
| 
 | ||||
| #include "../../Configurable/Configurable.hpp" | ||||
| #include "../../Analyser/Static/StaticAnalyser.hpp" | ||||
| #include "../ROMMachine.hpp" | ||||
| #include "../../../Configurable/Configurable.hpp" | ||||
| #include "../../../Analyser/Static/StaticAnalyser.hpp" | ||||
| #include "../../ROMMachine.hpp" | ||||
| 
 | ||||
| #include <memory> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace AppleII { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| 
 | ||||
| /// @returns The options available for an Apple II.
 | ||||
| std::vector<std::unique_ptr<Configurable::Option>> get_options(); | ||||
| @@ -29,6 +30,7 @@ class Machine { | ||||
| 		static Machine *AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||
| }; | ||||
| 
 | ||||
| }; | ||||
| } | ||||
| } | ||||
| 
 | ||||
| #endif /* AppleII_hpp */ | ||||
| @@ -9,11 +9,12 @@ | ||||
| #ifndef Card_h | ||||
| #define Card_h | ||||
| 
 | ||||
| #include "../../Processors/6502/6502.hpp" | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../Activity/Observer.hpp" | ||||
| #include "../../../Processors/6502/6502.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../Activity/Observer.hpp" | ||||
| 
 | ||||
| namespace AppleII { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| 
 | ||||
| /*!
 | ||||
| 	This provides a small subset of the interface offered to cards installed in | ||||
| @@ -109,6 +110,7 @@ class Card { | ||||
| 		} | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| #endif /* Card_h */ | ||||
| @@ -8,15 +8,25 @@ | ||||
| 
 | ||||
| #include "DiskIICard.hpp" | ||||
| 
 | ||||
| using namespace AppleII; | ||||
| using namespace Apple::II; | ||||
| 
 | ||||
| DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector) : diskii_(2045454) { | ||||
| 	const auto roms = rom_fetcher( | ||||
| 		"DiskII", | ||||
| 		{ | ||||
| 			is_16_sector ? "boot-16.rom" : "boot-13.rom", | ||||
| 			is_16_sector ? "state-machine-16.rom" : "state-machine-13.rom" | ||||
| 	std::vector<std::unique_ptr<std::vector<uint8_t>>> roms; | ||||
| 	if(is_16_sector) { | ||||
| 		roms = rom_fetcher({ | ||||
| 			{"DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6}, | ||||
| 			{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } } | ||||
| 		}); | ||||
| 	} else { | ||||
| 		roms = rom_fetcher({ | ||||
| 			{"DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ff}, | ||||
| 			{"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 } | ||||
| 		}); | ||||
| 	} | ||||
| 	if(!roms[0] || !roms[1]) { | ||||
| 		throw ROMMachine::Error::MissingROMs; | ||||
| 	} | ||||
| 
 | ||||
| 	boot_ = std::move(*roms[0]); | ||||
| 	diskii_.set_state_machine(*roms[1]); | ||||
| 	set_select_constraints(None); | ||||
| @@ -10,17 +10,18 @@ | ||||
| #define DiskIICard_hpp | ||||
| 
 | ||||
| #include "Card.hpp" | ||||
| #include "../ROMMachine.hpp" | ||||
| #include "../../ROMMachine.hpp" | ||||
| 
 | ||||
| #include "../../Components/DiskII/DiskII.hpp" | ||||
| #include "../../Storage/Disk/Disk.hpp" | ||||
| #include "../../ClockReceiver/ClockingHintSource.hpp" | ||||
| #include "../../../Components/DiskII/DiskII.hpp" | ||||
| #include "../../../Storage/Disk/Disk.hpp" | ||||
| #include "../../../ClockReceiver/ClockingHintSource.hpp" | ||||
| 
 | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace AppleII { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| 
 | ||||
| class DiskIICard: public Card, public ClockingHint::Observer { | ||||
| 	public: | ||||
| @@ -41,6 +42,7 @@ class DiskIICard: public Card, public ClockingHint::Observer { | ||||
| 		ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| #endif /* DiskIICard_hpp */ | ||||
| @@ -8,7 +8,7 @@ | ||||
| 
 | ||||
| #include "Video.hpp" | ||||
| 
 | ||||
| using namespace AppleII::Video; | ||||
| using namespace Apple::II::Video; | ||||
| 
 | ||||
| VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) : | ||||
| 	crt_(910, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance1), | ||||
| @@ -9,14 +9,15 @@ | ||||
| #ifndef Video_hpp | ||||
| #define Video_hpp | ||||
| 
 | ||||
| #include "../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../ClockReceiver/ClockDeferrer.hpp" | ||||
| #include "../../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../ClockReceiver/DeferredQueue.hpp" | ||||
| 
 | ||||
| #include <array> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace AppleII { | ||||
| namespace Apple { | ||||
| namespace II { | ||||
| namespace Video { | ||||
| 
 | ||||
| class BusHandler { | ||||
| @@ -250,8 +251,8 @@ class VideoBase { | ||||
| 		*/ | ||||
| 		void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const; | ||||
| 
 | ||||
| 		// Maintain a ClockDeferrer for delayed mode switches.
 | ||||
| 		ClockDeferrer<Cycles> deferrer_; | ||||
| 		// Maintain a DeferredQueue for delayed mode switches.
 | ||||
| 		DeferredQueue<Cycles> deferrer_; | ||||
| }; | ||||
| 
 | ||||
| template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| @@ -600,6 +601,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 		BusHandler &bus_handler_; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										97
									
								
								Machines/Apple/Macintosh/Audio.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								Machines/Apple/Macintosh/Audio.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| // | ||||
| //  Audio.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 31/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Audio.hpp" | ||||
|  | ||||
| using namespace Apple::Macintosh; | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| // The sample_length is coupled with the clock rate selected within the Macintosh proper; | ||||
| // as per the header-declaration a divide-by-two clock is expected to arrive here. | ||||
| const std::size_t sample_length = 352 / 2; | ||||
|  | ||||
| } | ||||
|  | ||||
| Audio::Audio(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {} | ||||
|  | ||||
| // MARK: - Inputs | ||||
|  | ||||
| void Audio::post_sample(uint8_t sample) { | ||||
| 	// Store sample directly indexed by current write pointer; this ensures that collected samples | ||||
| 	// directly map to volume and enabled/disabled states. | ||||
| 	sample_queue_.buffer[sample_queue_.write_pointer] = sample; | ||||
| 	sample_queue_.write_pointer = (sample_queue_.write_pointer + 1) % sample_queue_.buffer.size(); | ||||
| } | ||||
|  | ||||
| void Audio::set_volume(int volume) { | ||||
| 	// Do nothing if the volume hasn't changed. | ||||
| 	if(posted_volume_ == volume) return; | ||||
| 	posted_volume_ = volume; | ||||
|  | ||||
| 	// Post the volume change as a deferred event. | ||||
| 	task_queue_.defer([=] () { | ||||
| 		volume_ = volume; | ||||
| 		set_volume_multiplier(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void Audio::set_enabled(bool on) { | ||||
| 	// Do nothing if the mask hasn't changed. | ||||
| 	if(posted_enable_mask_ == int(on)) return; | ||||
| 	posted_enable_mask_ = int(on); | ||||
|  | ||||
| 	// Post the enabled mask change as a deferred event. | ||||
| 	task_queue_.defer([=] () { | ||||
| 		enabled_mask_ = int(on); | ||||
| 		set_volume_multiplier(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| // MARK: - Output generation | ||||
|  | ||||
| bool Audio::is_zero_level() { | ||||
| 	return !volume_ || !enabled_mask_; | ||||
| } | ||||
|  | ||||
| void Audio::set_sample_volume_range(std::int16_t range) { | ||||
| 	// Some underflow here doesn't really matter. | ||||
| 	output_volume_ = range / (7 * 255); | ||||
| 	set_volume_multiplier(); | ||||
| } | ||||
|  | ||||
| void Audio::set_volume_multiplier() { | ||||
| 	volume_multiplier_ = int16_t(output_volume_ * volume_ * enabled_mask_); | ||||
| } | ||||
|  | ||||
| void Audio::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| 	// TODO: the implementation below acts as if the hardware uses pulse-amplitude modulation; | ||||
| 	// in fact it uses pulse-width modulation. But the scale for pulses isn't specified, so | ||||
| 	// that's something to return to. | ||||
|  | ||||
| 	while(number_of_samples) { | ||||
| 		// Determine how many output samples will be at the same level. | ||||
| 		const auto cycles_left_in_sample = std::min(number_of_samples, sample_length - subcycle_offset_); | ||||
|  | ||||
| 		// Determine the output level, and output that many samples. | ||||
| 		// (Hoping that the copiler substitutes an effective memset16-type operation here). | ||||
| 		const int16_t output_level = volume_multiplier_ * (int16_t(sample_queue_.buffer[sample_queue_.read_pointer]) - 128); | ||||
| 		for(size_t c = 0; c < cycles_left_in_sample; ++c) { | ||||
| 			target[c] = output_level; | ||||
| 		} | ||||
| 		target += cycles_left_in_sample; | ||||
|  | ||||
| 		// Advance the sample pointer. | ||||
| 		subcycle_offset_ += cycles_left_in_sample; | ||||
| 		sample_queue_.read_pointer = (sample_queue_.read_pointer + (subcycle_offset_ / sample_length)) % sample_queue_.buffer.size(); | ||||
| 		subcycle_offset_ %= sample_length; | ||||
|  | ||||
| 		// Decreate the number of samples left to write. | ||||
| 		number_of_samples -= cycles_left_in_sample; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										88
									
								
								Machines/Apple/Macintosh/Audio.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								Machines/Apple/Macintosh/Audio.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| // | ||||
| //  Audio.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 31/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Audio_hpp | ||||
| #define Audio_hpp | ||||
|  | ||||
| #include "../../../Concurrency/AsyncTaskQueue.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
|  | ||||
| #include <array> | ||||
| #include <atomic> | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| /*! | ||||
| 	Implements the Macintosh's audio output hardware. | ||||
|  | ||||
| 	Designed to be clocked at half the rate of the real hardware — i.e. | ||||
| 	a shade less than 4Mhz. | ||||
| */ | ||||
| class Audio: public ::Outputs::Speaker::SampleSource { | ||||
| 	public: | ||||
| 		Audio(Concurrency::DeferringAsyncTaskQueue &task_queue); | ||||
|  | ||||
| 		/*! | ||||
| 			Macintosh audio is (partly) sourced by the same scanning | ||||
| 			hardware as the video; each line it collects an additional | ||||
| 			word of memory, half of which is used for audio output. | ||||
|  | ||||
| 			Use this method to add a newly-collected sample to the queue. | ||||
| 		*/ | ||||
| 		void post_sample(uint8_t sample); | ||||
|  | ||||
| 		/*! | ||||
| 			Macintosh audio also separately receives an output volume | ||||
| 			level, in the range 0 to 7. | ||||
|  | ||||
| 			Use this method to set the current output volume. | ||||
| 		*/ | ||||
| 		void set_volume(int volume); | ||||
|  | ||||
| 		/*! | ||||
| 			A further factor in audio output is the on-off toggle. | ||||
| 		*/ | ||||
| 		void set_enabled(bool on); | ||||
|  | ||||
| 		// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter. | ||||
| 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||
| 		bool is_zero_level(); | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
|  | ||||
| 	private: | ||||
| 		Concurrency::DeferringAsyncTaskQueue &task_queue_; | ||||
|  | ||||
| 		// A queue of fetched samples; read from by one thread, | ||||
| 		// written to by another. | ||||
| 		struct { | ||||
| 			std::array<uint8_t, 740> buffer; | ||||
| 			size_t read_pointer = 0, write_pointer = 0; | ||||
| 		} sample_queue_; | ||||
|  | ||||
| 		// Emulator-thread stateful variables, to avoid work posting | ||||
| 		// deferral updates if possible. | ||||
| 		int posted_volume_ = 0; | ||||
| 		int posted_enable_mask_ = 0; | ||||
|  | ||||
| 		// Stateful variables, modified from the audio generation | ||||
| 		// thread only. | ||||
| 		int volume_ = 0; | ||||
| 		int enabled_mask_ = 0; | ||||
| 		std::int16_t output_volume_ = 0; | ||||
|  | ||||
| 		std::int16_t volume_multiplier_ = 0; | ||||
| 		std::size_t subcycle_offset_ = 0; | ||||
| 		void set_volume_multiplier(); | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Audio_hpp */ | ||||
							
								
								
									
										34
									
								
								Machines/Apple/Macintosh/DeferredAudio.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								Machines/Apple/Macintosh/DeferredAudio.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| // | ||||
| //  DeferredAudio.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 01/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef DeferredAudio_h | ||||
| #define DeferredAudio_h | ||||
|  | ||||
| #include "Audio.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| struct DeferredAudio { | ||||
| 	Concurrency::DeferringAsyncTaskQueue queue; | ||||
| 	Audio audio; | ||||
| 	Outputs::Speaker::LowpassSpeaker<Audio> speaker; | ||||
| 	HalfCycles time_since_update; | ||||
|  | ||||
| 	DeferredAudio() : audio(queue), speaker(audio) {} | ||||
|  | ||||
| 	void flush() { | ||||
| 		speaker.run_for(queue, time_since_update.flush<Cycles>()); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* DeferredAudio_h */ | ||||
							
								
								
									
										63
									
								
								Machines/Apple/Macintosh/DriveSpeedAccumulator.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Machines/Apple/Macintosh/DriveSpeedAccumulator.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| // | ||||
| //  DriveSpeedAccumulator.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 01/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "DriveSpeedAccumulator.hpp" | ||||
|  | ||||
| using namespace Apple::Macintosh; | ||||
|  | ||||
| void DriveSpeedAccumulator::post_sample(uint8_t sample) { | ||||
| 	if(!number_of_drives_) return; | ||||
|  | ||||
| 	// An Euler-esque approximation is used here: just collect all | ||||
| 	// the samples until there is a certain small quantity of them, | ||||
| 	// then produce a new estimate of rotation speed and start the | ||||
| 	// buffer afresh. | ||||
| 	samples_[sample_pointer_] = sample; | ||||
| 	++sample_pointer_; | ||||
|  | ||||
| 	if(sample_pointer_ == samples_.size()) { | ||||
| 		sample_pointer_ = 0; | ||||
|  | ||||
| 		// The below fits for a function like `a + bc`; it encapsultes the following | ||||
| 		// beliefs: | ||||
| 		// | ||||
| 		//	(i) motor speed is proportional to voltage supplied; | ||||
| 		//	(ii) with pulse-width modulation it's therefore proportional to the duty cycle; | ||||
| 		//	(iii) the Mac pulse-width modulates whatever it reads from the disk speed buffer; | ||||
| 		//	(iv) ... subject to software pulse-width modulation of that pulse-width modulation. | ||||
| 		// | ||||
| 		// So, I believe current motor speed is proportional to a low-pass filtering of | ||||
| 		// the speed buffer. Which I've implemented very coarsely via 'large' bucketed-averages, | ||||
| 		// noting also that exact disk motor speed is always a little approximate. | ||||
|  | ||||
| 		// Sum all samples. | ||||
| 		// TODO: if the above is the correct test, do it on sample receipt rather than | ||||
| 		// bothering with an intermediate buffer. | ||||
| 		int sum = 0; | ||||
| 		for(auto s: samples_) { | ||||
| 			sum += s; | ||||
| 		} | ||||
|  | ||||
| 		// The formula below was derived from observing values the Mac wrote into its | ||||
| 		// disk-speed buffer. Given that it runs a calibration loop before doing so, | ||||
| 		// I cannot guarantee the accuracy of these numbers beyond being within the | ||||
| 		// range that the computer would accept. | ||||
| 		const float normalised_sum = float(sum) / float(samples_.size()); | ||||
| 		const float rotation_speed = (normalised_sum * 27.08f) - 259.0f; | ||||
|  | ||||
| 		for(int c = 0; c < number_of_drives_; ++c) { | ||||
| 			drives_[c]->set_rotation_speed(rotation_speed); | ||||
| 		} | ||||
| //		printf("RPM: %0.2f (%d sum)\n", rotation_speed, sum); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void DriveSpeedAccumulator::add_drive(Apple::Macintosh::DoubleDensityDrive *drive) { | ||||
| 	drives_[number_of_drives_] = drive; | ||||
| 	++number_of_drives_; | ||||
| } | ||||
							
								
								
									
										45
									
								
								Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| // | ||||
| //  DriveSpeedAccumulator.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 01/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef DriveSpeedAccumulator_hpp | ||||
| #define DriveSpeedAccumulator_hpp | ||||
|  | ||||
| #include <array> | ||||
| #include <cstddef> | ||||
| #include <cstdint> | ||||
|  | ||||
| #include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp" | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| class DriveSpeedAccumulator { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			Accepts fetched motor control values. | ||||
| 		*/ | ||||
| 		void post_sample(uint8_t sample); | ||||
|  | ||||
| 		/*! | ||||
| 			Adds a connected drive. Up to two of these | ||||
| 			can be supplied. Only Macintosh DoubleDensityDrives | ||||
| 			are supported. | ||||
| 		*/ | ||||
| 		void add_drive(Apple::Macintosh::DoubleDensityDrive *drive); | ||||
|  | ||||
| 	private: | ||||
| 		std::array<uint8_t, 20> samples_; | ||||
| 		std::size_t sample_pointer_ = 0; | ||||
| 		Apple::Macintosh::DoubleDensityDrive *drives_[2] = {nullptr, nullptr}; | ||||
| 		int number_of_drives_ = 0; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* DriveSpeedAccumulator_hpp */ | ||||
							
								
								
									
										294
									
								
								Machines/Apple/Macintosh/Keyboard.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								Machines/Apple/Macintosh/Keyboard.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | ||||
| // | ||||
| //  Keyboard.h | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 08/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Apple_Macintosh_Keyboard_hpp | ||||
| #define Apple_Macintosh_Keyboard_hpp | ||||
|  | ||||
| #include "../../KeyboardMachine.hpp" | ||||
|  | ||||
| #include <mutex> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| class Keyboard { | ||||
| 	public: | ||||
| 		void set_input(bool data) { | ||||
| 			switch(mode_) { | ||||
| 				case Mode::Waiting: | ||||
| 					/* | ||||
| 						"Only the computer can initiate communication over the keyboard lines. When the computer and keyboard | ||||
| 						are turned on, the computer is in charge of the keyboard interface and the keyboard is passive. The | ||||
| 						computer signals that it is ready to begin communication by pulling the Keyboard Data line low." | ||||
| 					*/ | ||||
| 					if(!data) { | ||||
| 						mode_ = Mode::AcceptingCommand; | ||||
| 						phase_ = 0; | ||||
| 						command_ = 0; | ||||
| 					} | ||||
| 				break; | ||||
|  | ||||
| 				case Mode::AcceptingCommand: | ||||
| 					/* Note value, so that it can be latched upon a clock transition. */ | ||||
| 					data_input_ = data; | ||||
| 				break; | ||||
|  | ||||
| 				case Mode::AwaitingEndOfCommand: | ||||
| 					/* | ||||
| 						The last bit of the command leaves the Keyboard Data line low; the computer then indicates that it is ready | ||||
| 						to receive the keyboard's response by setting the Keyboard Data line high. | ||||
| 					*/ | ||||
| 					if(data) { | ||||
| 						mode_ = Mode::PerformingCommand; | ||||
| 						phase_ = 0; | ||||
| 					} | ||||
| 				break; | ||||
|  | ||||
| 				default: | ||||
| 				case Mode::SendingResponse: | ||||
| 					/* This line isn't currently an input; do nothing. */ | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		bool get_clock() { | ||||
| 			return clock_output_; | ||||
| 		} | ||||
|  | ||||
| 		bool get_data() { | ||||
| 			return !!(response_ & 0x80); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			The keyboard expects ~10 µs-frequency ticks, i.e. a clock rate of just around 100 kHz. | ||||
| 		*/ | ||||
| 		void run_for(HalfCycles cycle) { | ||||
| 			switch(mode_) { | ||||
| 				default: | ||||
| 				case Mode::Waiting: return; | ||||
|  | ||||
| 				case Mode::AcceptingCommand: { | ||||
| 					/* | ||||
| 						"When the computer is sending data to the keyboard, the keyboard transmits eight cycles of 400 µS each (180 µS low, | ||||
| 						220 µS high) on the Keyboard Clock line. On the falling edge of each keyboard clock cycle, the Macintosh Plus places | ||||
| 						a data bit on the data line and holds it there for 400 µS. The keyboard reads the data bit 80 µS after the rising edge | ||||
| 						of the Keyboard Clock signal." | ||||
| 					*/ | ||||
| 					const auto offset = phase_ % 40; | ||||
| 					clock_output_ = offset >= 18; | ||||
|  | ||||
| 					if(offset == 26) { | ||||
| 						command_ = (command_ << 1) | (data_input_ ? 1 : 0); | ||||
| 					} | ||||
|  | ||||
| 					++phase_; | ||||
| 					if(phase_ == 8*40) { | ||||
| 						mode_ = Mode::AwaitingEndOfCommand; | ||||
| 						phase_ = 0; | ||||
| 						clock_output_ = false; | ||||
| 					} | ||||
| 				} break; | ||||
|  | ||||
| 				case Mode::AwaitingEndOfCommand: | ||||
| 					// Time out if the end-of-command seems not to be forthcoming. | ||||
| 					// This is an elaboration on my part; a guess. | ||||
| 					++phase_; | ||||
| 					if(phase_ == 1000) { | ||||
| 						clock_output_ = false; | ||||
| 						mode_ = Mode::Waiting; | ||||
| 						phase_ = 0; | ||||
| 					} | ||||
| 				return; | ||||
|  | ||||
| 				case Mode::PerformingCommand: { | ||||
| 					response_ = perform_command(command_); | ||||
|  | ||||
| 					// Inquiry has a 0.25-second timeout; everything else is instant. | ||||
| 					++phase_; | ||||
| 					if(phase_ == 25000 || command_ != 0x10 || response_ != 0x7b) { | ||||
| 						mode_ = Mode::SendingResponse; | ||||
| 						phase_ = 0; | ||||
| 					} | ||||
| 				} break; | ||||
|  | ||||
| 				case Mode::SendingResponse: { | ||||
| 					/* | ||||
| 						"When sending data to the computer, the keyboard transmits eight cycles of 330 µS each (160 µS low, 170 µS high) | ||||
| 						on the normally high Keyboard Clock line. It places a data bit on the data line 40 µS before the falling edge of each | ||||
| 						clock cycle and maintains it for 330 µS. The VIA in the computer latches the data bit into its shift register on the | ||||
| 						rising edge of the Keyboard Clock signal." | ||||
| 					*/ | ||||
| 					const auto offset = phase_ % 33; | ||||
| 					clock_output_ = offset >= 16; | ||||
|  | ||||
| 					if(offset == 29) { | ||||
| 						response_ <<= 1; | ||||
| 					} | ||||
|  | ||||
| 					++phase_; | ||||
| 					if(phase_ == 8*33) { | ||||
| 						clock_output_ = false; | ||||
| 						mode_ = Mode::Waiting; | ||||
| 						phase_ = 0; | ||||
| 					} | ||||
| 				} break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		void enqueue_key_state(uint16_t key, bool is_pressed) { | ||||
| 			// Front insert; messages will be pop_back'd. | ||||
| 			std::lock_guard<decltype(key_queue_mutex_)> lock(key_queue_mutex_); | ||||
|  | ||||
| 			// Keys on the keypad are preceded by a $79 keycode; in the internal naming scheme | ||||
| 			// they are indicated by having bit 8 set. So add the $79 prefix if required. | ||||
| 			if(key & 0x100) { | ||||
| 				key_queue_.insert(key_queue_.begin(), 0x79); | ||||
| 			} | ||||
| 			key_queue_.insert(key_queue_.begin(), (is_pressed ? 0x00 : 0x80) | uint8_t(key)); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
|  | ||||
| 		int perform_command(int command) { | ||||
| 			switch(command) { | ||||
| 				case 0x10:		// Inquiry. | ||||
| 				case 0x14: {	// Instant. | ||||
| 					std::lock_guard<decltype(key_queue_mutex_)> lock(key_queue_mutex_); | ||||
| 					if(!key_queue_.empty()) { | ||||
| 						const auto new_message = key_queue_.back(); | ||||
| 						key_queue_.pop_back(); | ||||
| 						return new_message; | ||||
| 					} | ||||
| 				} break; | ||||
|  | ||||
| 				case 0x16:	// Model number. | ||||
| 				return | ||||
| 					0x01 |			// b0: always 1 | ||||
| 					(1 << 1) |		// keyboard model number | ||||
| 					(1 << 4);		// next device number | ||||
| 									// (b7 not set => no next device) | ||||
|  | ||||
| 				case 0x36:	// Test | ||||
| 				return 0x7d;		// 0x7d = ACK, 0x77 = not ACK. | ||||
| 			} | ||||
| 			return 0x7b;	// No key transition. | ||||
| 		} | ||||
|  | ||||
| 		enum class Mode { | ||||
| 			Waiting, | ||||
| 			AcceptingCommand, | ||||
| 			AwaitingEndOfCommand, | ||||
| 			SendingResponse, | ||||
| 			PerformingCommand | ||||
| 		} mode_ = Mode::Waiting; | ||||
| 		int phase_ = 0; | ||||
| 		int command_ = 0; | ||||
| 		int response_ = 0; | ||||
|  | ||||
| 		bool data_input_ = false; | ||||
| 		bool clock_output_ = false; | ||||
|  | ||||
| 		// TODO: improve this very, very simple implementation. | ||||
| 		std::mutex key_queue_mutex_; | ||||
| 		std::vector<uint8_t> key_queue_; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Provides a mapping from idiomatic PC keys to Macintosh keys. | ||||
| */ | ||||
| class KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper { | ||||
| 	uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) override { | ||||
| 		using Key = Inputs::Keyboard::Key; | ||||
| 		switch(key) { | ||||
| 			default: return KeyboardMachine::MappedMachine::KeyNotMapped; | ||||
|  | ||||
| 			/* | ||||
| 				See p284 of the Apple Guide to the Macintosh Family Hardware | ||||
| 				for documentation of the mapping below. | ||||
| 			*/ | ||||
|  | ||||
| 			case Key::BackTick:				return 0x65; | ||||
| 			case Key::k1:					return 0x25; | ||||
| 			case Key::k2:					return 0x27; | ||||
| 			case Key::k3:					return 0x29; | ||||
| 			case Key::k4:					return 0x2b; | ||||
| 			case Key::k5:					return 0x2f; | ||||
| 			case Key::k6:					return 0x2d; | ||||
| 			case Key::k7:					return 0x35; | ||||
| 			case Key::k8:					return 0x39; | ||||
| 			case Key::k9:					return 0x33; | ||||
| 			case Key::k0:					return 0x3b; | ||||
| 			case Key::Hyphen:				return 0x37; | ||||
| 			case Key::Equals:				return 0x31; | ||||
| 			case Key::BackSpace:			return 0x67; | ||||
|  | ||||
| 			case Key::Tab:					return 0x61; | ||||
| 			case Key::Q:					return 0x19; | ||||
| 			case Key::W:					return 0x1b; | ||||
| 			case Key::E:					return 0x1d; | ||||
| 			case Key::R:					return 0x1f; | ||||
| 			case Key::T:					return 0x23; | ||||
| 			case Key::Y:					return 0x21; | ||||
| 			case Key::U:					return 0x41; | ||||
| 			case Key::I:					return 0x45; | ||||
| 			case Key::O:					return 0x3f; | ||||
| 			case Key::P:					return 0x47; | ||||
| 			case Key::OpenSquareBracket:	return 0x43; | ||||
| 			case Key::CloseSquareBracket:	return 0x3d; | ||||
|  | ||||
| 			case Key::CapsLock:				return 0x73; | ||||
| 			case Key::A:					return 0x01; | ||||
| 			case Key::S:					return 0x03; | ||||
| 			case Key::D:					return 0x05; | ||||
| 			case Key::F:					return 0x07; | ||||
| 			case Key::G:					return 0x0b; | ||||
| 			case Key::H:					return 0x09; | ||||
| 			case Key::J:					return 0x4d; | ||||
| 			case Key::K:					return 0x51; | ||||
| 			case Key::L:					return 0x4b; | ||||
| 			case Key::Semicolon:			return 0x53; | ||||
| 			case Key::Quote:				return 0x4f; | ||||
| 			case Key::Enter:				return 0x49; | ||||
|  | ||||
| 			case Key::LeftShift:			return 0x71; | ||||
| 			case Key::Z:					return 0x0d; | ||||
| 			case Key::X:					return 0x0f; | ||||
| 			case Key::C:					return 0x11; | ||||
| 			case Key::V:					return 0x13; | ||||
| 			case Key::B:					return 0x17; | ||||
| 			case Key::N:					return 0x5b; | ||||
| 			case Key::M:					return 0x5d; | ||||
| 			case Key::Comma:				return 0x57; | ||||
| 			case Key::FullStop:				return 0x5f; | ||||
| 			case Key::ForwardSlash:			return 0x59; | ||||
| 			case Key::RightShift:			return 0x71; | ||||
|  | ||||
| 			case Key::Left:					return 0x100 | 0x0d; | ||||
| 			case Key::Right:				return 0x100 | 0x05; | ||||
| 			case Key::Up:					return 0x100 | 0x1b; | ||||
| 			case Key::Down:					return 0x100 | 0x11; | ||||
|  | ||||
| 			case Key::LeftOption: | ||||
| 			case Key::RightOption:			return 0x75; | ||||
| 			case Key::LeftMeta: | ||||
| 			case Key::RightMeta:			return 0x6f; | ||||
|  | ||||
| 			case Key::Space:				return 0x63; | ||||
| 			case Key::BackSlash:			return 0x55; | ||||
|  | ||||
| 			/* TODO: the numeric keypad. */ | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Apple_Macintosh_Keyboard_hpp */ | ||||
							
								
								
									
										653
									
								
								Machines/Apple/Macintosh/Macintosh.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										653
									
								
								Machines/Apple/Macintosh/Macintosh.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,653 @@ | ||||
| // | ||||
| //  Macintosh.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Macintosh.hpp" | ||||
|  | ||||
| #include <array> | ||||
|  | ||||
| #include "DeferredAudio.hpp" | ||||
| #include "DriveSpeedAccumulator.hpp" | ||||
| #include "Keyboard.hpp" | ||||
| #include "RealTimeClock.hpp" | ||||
| #include "Video.hpp" | ||||
|  | ||||
| #include "../../CRTMachine.hpp" | ||||
| #include "../../KeyboardMachine.hpp" | ||||
| #include "../../MediaTarget.hpp" | ||||
| #include "../../MouseMachine.hpp" | ||||
|  | ||||
| #include "../../../Inputs/QuadratureMouse/QuadratureMouse.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| #include "../../../ClockReceiver/JustInTime.hpp" | ||||
|  | ||||
| //#define LOG_TRACE | ||||
|  | ||||
| #include "../../../Components/6522/6522.hpp" | ||||
| #include "../../../Components/8530/z8530.hpp" | ||||
| #include "../../../Components/DiskII/IWM.hpp" | ||||
| #include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp" | ||||
| #include "../../../Processors/68000/68000.hpp" | ||||
|  | ||||
| #include "../../../Analyser/Static/Macintosh/Target.hpp" | ||||
|  | ||||
| #include "../../Utility/MemoryPacker.hpp" | ||||
| #include "../../Utility/MemoryFuzzer.hpp" | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| const int CLOCK_RATE = 7833600; | ||||
|  | ||||
| } | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachine: | ||||
| 	public Machine, | ||||
| 	public CRTMachine::Machine, | ||||
| 	public MediaTarget::Machine, | ||||
| 	public MouseMachine::Machine, | ||||
| 	public CPU::MC68000::BusHandler, | ||||
| 	public KeyboardMachine::MappedMachine, | ||||
| 	public Zilog::SCC::z8530::Delegate { | ||||
| 	public: | ||||
| 		using Target = Analyser::Static::Macintosh::Target; | ||||
|  | ||||
| 		ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : | ||||
| 		 	mc68000_(*this), | ||||
| 		 	iwm_(CLOCK_RATE), | ||||
| 		 	video_(ram_, audio_, drive_speed_accumulator_), | ||||
| 		 	via_(via_port_handler_), | ||||
| 		 	via_port_handler_(*this, clock_, keyboard_, video_, audio_, iwm_, mouse_), | ||||
| 		 	drives_{ | ||||
| 		 		{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke}, | ||||
| 		 		{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke} | ||||
| 			}, | ||||
| 			mouse_(1) { | ||||
|  | ||||
| 			// Select a ROM name and determine the proper ROM and RAM sizes | ||||
| 			// based on the machine model. | ||||
| 			using Model = Analyser::Static::Macintosh::Target::Model; | ||||
| 			const std::string machine_name = "Macintosh"; | ||||
| 			uint32_t ram_size, rom_size; | ||||
| 			std::vector<ROMMachine::ROM> rom_descriptions; | ||||
| 			switch(model) { | ||||
| 				default: | ||||
| 				case Model::Mac128k: | ||||
| 					ram_size = 128*1024; | ||||
| 					rom_size = 64*1024; | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28); | ||||
| 				break; | ||||
| 				case Model::Mac512k: | ||||
| 					ram_size = 512*1024; | ||||
| 					rom_size = 64*1024; | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d); | ||||
| 				break; | ||||
| 				case Model::Mac512ke: | ||||
| 				case Model::MacPlus: { | ||||
| 					ram_size = 512*1024; | ||||
| 					rom_size = 128*1024; | ||||
| 					const std::initializer_list<uint32_t> crc32s = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e }; | ||||
| 					rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s); | ||||
| 				} break; | ||||
| 			} | ||||
| 			ram_mask_ = (ram_size >> 1) - 1; | ||||
| 			rom_mask_ = (rom_size >> 1) - 1; | ||||
| 			video_.set_ram_mask(ram_mask_); | ||||
|  | ||||
| 			// Grab a copy of the ROM and convert it into big-endian data. | ||||
| 			const auto roms = rom_fetcher(rom_descriptions); | ||||
| 			if(!roms[0]) { | ||||
| 				throw ROMMachine::Error::MissingROMs; | ||||
| 			} | ||||
| 			roms[0]->resize(rom_size); | ||||
| 			Memory::PackBigEndian16(*roms[0], rom_); | ||||
|  | ||||
| 			// Randomise memory contents. | ||||
| 			Memory::Fuzz(ram_, sizeof(ram_) / sizeof(*ram_)); | ||||
|  | ||||
| 			// Attach the drives to the IWM. | ||||
| 			iwm_.iwm.set_drive(0, &drives_[0]); | ||||
| 			iwm_.iwm.set_drive(1, &drives_[1]); | ||||
|  | ||||
| 			// If they are 400kb drives, also attach them to the drive-speed accumulator. | ||||
| 			if(!drives_[0].is_800k()) drive_speed_accumulator_.add_drive(&drives_[0]); | ||||
| 			if(!drives_[1].is_800k()) drive_speed_accumulator_.add_drive(&drives_[1]); | ||||
|  | ||||
| 			// Make sure interrupt changes from the SCC are observed. | ||||
| 			scc_.set_delegate(this); | ||||
|  | ||||
| 			// The Mac runs at 7.8336mHz. | ||||
| 			set_clock_rate(double(CLOCK_RATE)); | ||||
| 			audio_.speaker.set_input_rate(float(CLOCK_RATE) / 2.0f); | ||||
|  | ||||
| 			// Insert any supplied media. | ||||
| 			insert_media(target.media); | ||||
| 		} | ||||
|  | ||||
| 		~ConcreteMachine() { | ||||
| 			audio_.queue.flush(); | ||||
| 		} | ||||
|  | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { | ||||
| 			video_.set_scan_target(scan_target); | ||||
| 		} | ||||
|  | ||||
| 		Outputs::Speaker::Speaker *get_speaker() override { | ||||
| 			return &audio_.speaker; | ||||
| 		} | ||||
|  | ||||
| 		void run_for(const Cycles cycles) override { | ||||
| 			mc68000_.run_for(cycles); | ||||
| 		} | ||||
|  | ||||
| 		using Microcycle = CPU::MC68000::Microcycle; | ||||
|  | ||||
| 		HalfCycles perform_bus_operation(const Microcycle &cycle, int is_supervisor) { | ||||
| 			// TODO: pick a delay if this is a video-clashing memory fetch. | ||||
| 			HalfCycles delay(0); | ||||
|  | ||||
| 			time_since_video_update_ += cycle.length; | ||||
| 			iwm_.time_since_update += cycle.length; | ||||
|  | ||||
| 			// The VIA runs at one-tenth of the 68000's clock speed, in sync with the E clock. | ||||
| 			// See: Guide to the Macintosh Hardware Family p149 (PDF p188). Some extra division | ||||
| 			// may occur here in order to provide VSYNC at a proper moment. | ||||
| 			// Possibly route vsync. | ||||
| 			if(time_since_video_update_ < time_until_video_event_) { | ||||
| 				via_clock_ += cycle.length; | ||||
| 				via_.run_for(via_clock_.divide(HalfCycles(10))); | ||||
| 			} else { | ||||
| 				auto via_time_base = time_since_video_update_ - cycle.length; | ||||
| 				auto via_cycles_outstanding = cycle.length; | ||||
| 				while(time_until_video_event_ < time_since_video_update_) { | ||||
| 					const auto via_cycles = time_until_video_event_ - via_time_base; | ||||
| 					via_time_base = HalfCycles(0); | ||||
| 					via_cycles_outstanding -= via_cycles; | ||||
|  | ||||
| 					via_clock_ += via_cycles; | ||||
| 					via_.run_for(via_clock_.divide(HalfCycles(10))); | ||||
|  | ||||
| 					video_.run_for(time_until_video_event_); | ||||
| 					time_since_video_update_ -= time_until_video_event_; | ||||
| 					time_until_video_event_ = video_.get_next_sequence_point(); | ||||
|  | ||||
| 					via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !video_.vsync()); | ||||
| 				} | ||||
|  | ||||
| 				via_clock_ += via_cycles_outstanding; | ||||
| 				via_.run_for(via_clock_.divide(HalfCycles(10))); | ||||
| 			} | ||||
|  | ||||
| 			// The keyboard also has a clock, albeit a very slow one — 100,000 cycles/second. | ||||
| 			// Its clock and data lines are connected to the VIA. | ||||
| 			keyboard_clock_ += cycle.length; | ||||
| 			const auto keyboard_ticks = keyboard_clock_.divide(HalfCycles(CLOCK_RATE / 100000)); | ||||
| 			if(keyboard_ticks > HalfCycles(0)) { | ||||
| 				keyboard_.run_for(keyboard_ticks); | ||||
| 				via_.set_control_line_input(MOS::MOS6522::Port::B, MOS::MOS6522::Line::Two, keyboard_.get_data()); | ||||
| 				via_.set_control_line_input(MOS::MOS6522::Port::B, MOS::MOS6522::Line::One, keyboard_.get_clock()); | ||||
| 			} | ||||
|  | ||||
| 			// Feed mouse inputs within at most 1250 cycles of each other. | ||||
| 			if(mouse_.has_steps()) { | ||||
| 				time_since_mouse_update_ += cycle.length; | ||||
| 				const auto mouse_ticks = time_since_mouse_update_.divide(HalfCycles(2500)); | ||||
| 				if(mouse_ticks > HalfCycles(0)) { | ||||
| 					mouse_.prepare_step(); | ||||
| 					scc_.set_dcd(0, mouse_.get_channel(1) & 1); | ||||
| 					scc_.set_dcd(1, mouse_.get_channel(0) & 1); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// TODO: SCC should be clocked at a divide-by-two, if and when it actually has | ||||
| 			// anything connected. | ||||
|  | ||||
| 			// Consider updating the real-time clock. | ||||
| 			real_time_clock_ += cycle.length; | ||||
| 			auto ticks = real_time_clock_.divide_cycles(Cycles(CLOCK_RATE)).as_int(); | ||||
| 			while(ticks--) { | ||||
| 				clock_.update(); | ||||
| 				// TODO: leave a delay between toggling the input rather than using this coupled hack. | ||||
| 				via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two, true); | ||||
| 				via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two, false); | ||||
| 			} | ||||
|  | ||||
| 			// A null cycle leaves nothing else to do. | ||||
| 			if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return delay; | ||||
|  | ||||
| 			auto word_address = cycle.active_operation_word_address(); | ||||
|  | ||||
| 			// Everything above E0 0000 is signalled as being on the peripheral bus. | ||||
| 			mc68000_.set_is_peripheral_address(word_address >= 0x700000); | ||||
|  | ||||
| 			// All code below deals only with reads and writes — cycles in which a | ||||
| 			// data select is active. So quit now if this is not the active part of | ||||
| 			//  a read or write. | ||||
| 			if(!cycle.data_select_active()) return delay; | ||||
|  | ||||
| 			// Check whether this access maps into the IO area; if so then | ||||
| 			// apply more complicated decoding logic. | ||||
| 			if(word_address >= 0x400000) { | ||||
| 				const int register_address = word_address >> 8; | ||||
|  | ||||
| 				switch(word_address & 0x78f000) { | ||||
| 					case 0x70f000: | ||||
| 						// VIA accesses are via address 0xefe1fe + register*512, | ||||
| 						// which at word precision is 0x77f0ff + register*256. | ||||
| 						if(cycle.operation & Microcycle::Read) { | ||||
| 							cycle.value->halves.low = via_.get_register(register_address); | ||||
| 						} else { | ||||
| 							via_.set_register(register_address, cycle.value->halves.low); | ||||
| 						} | ||||
| 					break; | ||||
|  | ||||
| 					case 0x68f000: | ||||
| 						// The IWM; this is a purely polled device, so can be run on demand. | ||||
| 						iwm_.flush(); | ||||
| 						if(cycle.operation & Microcycle::Read) { | ||||
| 							cycle.value->halves.low = iwm_.iwm.read(register_address); | ||||
| 						} else { | ||||
| 							iwm_.iwm.write(register_address, cycle.value->halves.low); | ||||
| 						} | ||||
| 					break; | ||||
|  | ||||
| 					case 0x780000: | ||||
| 						// Phase read. | ||||
| 						if(cycle.operation & Microcycle::Read) { | ||||
| 							cycle.value->halves.low = phase_ & 7; | ||||
| 						} | ||||
| 					break; | ||||
|  | ||||
| 					case 0x480000: case 0x48f000: | ||||
| 					case 0x580000: case 0x58f000: | ||||
| 						// Any word access here adjusts phase. | ||||
| 						if(cycle.operation & Microcycle::SelectWord) { | ||||
| 							++phase_; | ||||
| 						} else { | ||||
| 							if(word_address < 0x500000) { | ||||
| 								// A0 = 1 => reset; A0 = 0 => read. | ||||
| 								if(*cycle.address & 1) { | ||||
| 									scc_.reset(); | ||||
| 								} else { | ||||
| 									const auto read = scc_.read(int(word_address)); | ||||
| 									if(cycle.operation & Microcycle::Read) { | ||||
| 										cycle.value->halves.low = read; | ||||
| 									} | ||||
| 								} | ||||
| 							} else { | ||||
| 								if(*cycle.address & 1) { | ||||
| 									if(cycle.operation & Microcycle::Read) { | ||||
| 										scc_.write(int(word_address), 0xff); | ||||
| 									} else { | ||||
| 										scc_.write(int(word_address), cycle.value->halves.low); | ||||
| 									} | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					break; | ||||
|  | ||||
| 					default: | ||||
| 						if(cycle.operation & Microcycle::Read) { | ||||
| 							LOG("Unrecognised read " << PADHEX(6) << (*cycle.address & 0xffffff)); | ||||
| 							cycle.value->halves.low = 0x00; | ||||
| 						} else { | ||||
| 							LOG("Unrecognised write %06x" << PADHEX(6) << (*cycle.address & 0xffffff)); | ||||
| 						} | ||||
| 					break; | ||||
| 				} | ||||
| 				if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff; | ||||
|  | ||||
| 				return delay; | ||||
| 			} | ||||
|  | ||||
| 			// Having reached here, this is a RAM or ROM access. | ||||
|  | ||||
| 			// When ROM overlay is enabled, the ROM begins at both $000000 and $400000, | ||||
| 			// and RAM is available at $600000. | ||||
| 			// | ||||
| 			// Otherwise RAM is mapped at $000000 and ROM from $400000. | ||||
| 			uint16_t *memory_base; | ||||
| 			if( | ||||
| 				(!ROM_is_overlay_ && word_address < 0x200000) || | ||||
| 				(ROM_is_overlay_ && word_address >= 0x300000) | ||||
| 			) { | ||||
| 				memory_base = ram_; | ||||
| 				word_address &= ram_mask_; | ||||
|  | ||||
| 				// This is coupled with the Macintosh implementation of video; the magic | ||||
| 				// constant should probably be factored into the Video class. | ||||
| 				// It embodies knowledge of the fact that video (and audio) will always | ||||
| 				// be fetched from the final $d900 bytes (i.e. $6c80 words) of memory. | ||||
| 				// (And that ram_mask_ = ram size - 1). | ||||
| //				if(word_address > ram_mask_ - 0x6c80) | ||||
| 					update_video(); | ||||
| 			} else { | ||||
| 				memory_base = rom_; | ||||
| 				word_address &= rom_mask_; | ||||
|  | ||||
| 				// Writes to ROM have no effect, and it doesn't mirror above 0x60000. | ||||
| 				if(!(cycle.operation & Microcycle::Read)) return delay; | ||||
| 				if(word_address >= 0x300000) { | ||||
| 					if(cycle.operation & Microcycle::SelectWord) { | ||||
| 						cycle.value->full = 0xffff; | ||||
| 					} else { | ||||
| 						cycle.value->halves.low = 0xff; | ||||
| 					} | ||||
| 					return delay; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			switch(cycle.operation & (Microcycle::SelectWord | Microcycle::SelectByte | Microcycle::Read | Microcycle::InterruptAcknowledge)) { | ||||
| 				default: | ||||
| 				break; | ||||
|  | ||||
| 				// Catches the deliberation set of operation to 0 above. | ||||
| 				case 0: break; | ||||
|  | ||||
| 				case Microcycle::InterruptAcknowledge | Microcycle::SelectByte: | ||||
| 					// The Macintosh uses autovectored interrupts. | ||||
| 					mc68000_.set_is_peripheral_address(true); | ||||
| 				break; | ||||
|  | ||||
| 				case Microcycle::SelectWord | Microcycle::Read: | ||||
| 					cycle.value->full = memory_base[word_address]; | ||||
| 				break; | ||||
| 				case Microcycle::SelectByte | Microcycle::Read: | ||||
| 					cycle.value->halves.low = uint8_t(memory_base[word_address] >> cycle.byte_shift()); | ||||
| 				break; | ||||
| 				case Microcycle::SelectWord: | ||||
| 					memory_base[word_address] = cycle.value->full; | ||||
| 				break; | ||||
| 				case Microcycle::SelectByte: | ||||
| 					memory_base[word_address] = uint16_t( | ||||
| 						(cycle.value->halves.low << cycle.byte_shift()) | | ||||
| 						(memory_base[word_address] & cycle.untouched_byte_mask()) | ||||
| 					); | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			/* | ||||
| 				Normal memory map: | ||||
|  | ||||
| 				000000: 	RAM | ||||
| 				400000: 	ROM | ||||
| 				9FFFF8+:	SCC read operations | ||||
| 				BFFFF8+:	SCC write operations | ||||
| 				DFE1FF+:	IWM | ||||
| 				EFE1FE+:	VIA | ||||
| 			*/ | ||||
|  | ||||
| 			return delay; | ||||
| 		} | ||||
|  | ||||
| 		void flush() { | ||||
| 			// Flush the video before the audio queue; in a Mac the | ||||
| 			// video is responsible for providing part of the | ||||
| 			// audio signal, so the two aren't as distinct as in | ||||
| 			// most machines. | ||||
| 			update_video(); | ||||
|  | ||||
| 			// As above: flush audio after video. | ||||
| 			via_.flush(); | ||||
| 			audio_.queue.perform(); | ||||
|  | ||||
| 			// Experimental? | ||||
| 			iwm_.flush(); | ||||
| 		} | ||||
|  | ||||
| 		void set_rom_is_overlay(bool rom_is_overlay) { | ||||
| 			ROM_is_overlay_ = rom_is_overlay; | ||||
| 		} | ||||
|  | ||||
| 		bool video_is_outputting() { | ||||
| 			return video_.is_outputting(time_since_video_update_); | ||||
| 		} | ||||
|  | ||||
| 		void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) { | ||||
| 			update_video(); | ||||
| 			video_.set_use_alternate_buffers(use_alternate_screen_buffer, use_alternate_audio_buffer); | ||||
| 		} | ||||
|  | ||||
| 		bool insert_media(const Analyser::Static::Media &media) override { | ||||
| 			if(media.disks.empty()) | ||||
| 				return false; | ||||
|  | ||||
| 			// TODO: shouldn't allow disks to be replaced like this, as the Mac | ||||
| 			// uses software eject. Will need to expand messaging ability of | ||||
| 			// insert_media. | ||||
| 			if(drives_[0].has_disk()) | ||||
| 				drives_[1].set_disk(media.disks[0]); | ||||
| 			else | ||||
| 				drives_[0].set_disk(media.disks[0]); | ||||
|  | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		// MARK: Keyboard input. | ||||
|  | ||||
| 		KeyboardMapper *get_keyboard_mapper() override { | ||||
| 			return &keyboard_mapper_; | ||||
| 		} | ||||
|  | ||||
| 		void set_key_state(uint16_t key, bool is_pressed) override { | ||||
| 			keyboard_.enqueue_key_state(key, is_pressed); | ||||
| 		} | ||||
|  | ||||
| 		// TODO: clear all keys. | ||||
|  | ||||
| 		// MARK: Interrupt updates. | ||||
|  | ||||
| 		void did_change_interrupt_status(Zilog::SCC::z8530 *sender, bool new_status) override { | ||||
| 			update_interrupt_input(); | ||||
| 		} | ||||
|  | ||||
| 		void update_interrupt_input() { | ||||
| 			// Update interrupt input. | ||||
| 			// TODO: does this really cascade like this? | ||||
| 			if(scc_.get_interrupt_line()) { | ||||
| 				mc68000_.set_interrupt_level(2); | ||||
| 			} else if(via_.get_interrupt_line()) { | ||||
| 				mc68000_.set_interrupt_level(1); | ||||
| 			} else { | ||||
| 				mc68000_.set_interrupt_level(0); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		void update_video() { | ||||
| 			video_.run_for(time_since_video_update_.flush<HalfCycles>()); | ||||
| 			time_until_video_event_ = video_.get_next_sequence_point(); | ||||
| 		} | ||||
|  | ||||
| 		Inputs::Mouse &get_mouse() override { | ||||
| 			return mouse_; | ||||
| 		} | ||||
|  | ||||
| 		struct IWM { | ||||
| 			IWM(int clock_rate) : iwm(clock_rate) {} | ||||
|  | ||||
| 			HalfCycles time_since_update; | ||||
| 			Apple::IWM iwm; | ||||
|  | ||||
| 			void flush() { | ||||
| 				iwm.run_for(time_since_update.flush<Cycles>()); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		class VIAPortHandler: public MOS::MOS6522::PortHandler { | ||||
| 			public: | ||||
| 				VIAPortHandler(ConcreteMachine &machine, RealTimeClock &clock, Keyboard &keyboard, Video &video, DeferredAudio &audio, IWM &iwm, Inputs::QuadratureMouse &mouse) : | ||||
| 					machine_(machine), clock_(clock), keyboard_(keyboard), video_(video), audio_(audio), iwm_(iwm), mouse_(mouse) {} | ||||
|  | ||||
| 				using Port = MOS::MOS6522::Port; | ||||
| 				using Line = MOS::MOS6522::Line; | ||||
|  | ||||
| 				void set_port_output(Port port, uint8_t value, uint8_t direction_mask) { | ||||
| 					/* | ||||
| 						Peripheral lines: keyboard data, interrupt configuration. | ||||
| 						(See p176 [/215]) | ||||
| 					*/ | ||||
| 					switch(port) { | ||||
| 						case Port::A: | ||||
| 							/* | ||||
| 								Port A: | ||||
| 									b7:	[input] SCC wait/request (/W/REQA and /W/REQB wired together for a logical OR) | ||||
| 									b6:	0 = alternate screen buffer, 1 = main screen buffer | ||||
| 									b5:	floppy disk SEL state control (upper/lower head "among other things") | ||||
| 									b4:	1 = use ROM overlay memory map, 0 = use ordinary memory map | ||||
| 									b3:	0 = use alternate sound buffer, 1 = use ordinary sound buffer | ||||
| 									b2–b0:	audio output volume | ||||
| 							*/ | ||||
| 							iwm_.flush(); | ||||
| 							iwm_.iwm.set_select(!!(value & 0x20)); | ||||
|  | ||||
| 							machine_.set_use_alternate_buffers(!(value & 0x40), !(value&0x08)); | ||||
| 							machine_.set_rom_is_overlay(!!(value & 0x10)); | ||||
|  | ||||
| 							audio_.flush(); | ||||
| 							audio_.audio.set_volume(value & 7); | ||||
| 						break; | ||||
|  | ||||
| 						case Port::B: | ||||
| 							/* | ||||
| 								Port B: | ||||
| 									b7:	0 = sound enabled, 1 = sound disabled | ||||
| 									b6:	[input] 0 = video beam in visible portion of line, 1 = outside | ||||
| 									b5:	[input] mouse y2 | ||||
| 									b4:	[input] mouse x2 | ||||
| 									b3:	[input] 0 = mouse button down, 1 = up | ||||
| 									b2:	0 = real-time clock enabled, 1 = disabled | ||||
| 									b1:	clock's data-clock line | ||||
| 									b0:	clock's serial data line | ||||
| 							*/ | ||||
| 							if(value & 0x4) clock_.abort(); | ||||
| 							else clock_.set_input(!!(value & 0x2), !!(value & 0x1)); | ||||
|  | ||||
| 							audio_.flush(); | ||||
| 							audio_.audio.set_enabled(!(value & 0x80)); | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				uint8_t get_port_input(Port port) { | ||||
| 					switch(port) { | ||||
| 						case Port::A: | ||||
| //							printf("6522 r A\n"); | ||||
| 						return 0x00;	// TODO: b7 = SCC wait/request | ||||
|  | ||||
| 						case Port::B: | ||||
| 						return uint8_t( | ||||
| 							((mouse_.get_button_mask() & 1) ? 0x00 : 0x08) | | ||||
| 							((mouse_.get_channel(0) & 2) << 3) | | ||||
| 							((mouse_.get_channel(1) & 2) << 4) | | ||||
| 							(clock_.get_data() ? 0x02 : 0x00) | | ||||
| 							(machine_.video_is_outputting() ? 0x00 : 0x40) | ||||
| 						); | ||||
| 					} | ||||
|  | ||||
| 					// Should be unreachable. | ||||
| 					return 0xff; | ||||
| 				} | ||||
|  | ||||
| 				void set_control_line_output(Port port, Line line, bool value) { | ||||
| 					/* | ||||
| 						Keyboard wiring (I believe): | ||||
| 						CB2 = data		(input/output) | ||||
| 						CB1 = clock		(input) | ||||
|  | ||||
| 						CA2 is used for receiving RTC interrupts. | ||||
| 						CA1 is used for receiving vsync. | ||||
| 					*/ | ||||
| 					if(port == Port::B && line == Line::Two) { | ||||
| 						keyboard_.set_input(value); | ||||
| 					} | ||||
| 					else LOG("Unhandled control line output: " << (port ? 'B' : 'A') << int(line)); | ||||
| 				} | ||||
|  | ||||
| 				void run_for(HalfCycles duration) { | ||||
| 					// The 6522 enjoys a divide-by-ten, so multiply back up here to make the | ||||
| 					// divided-by-two clock the audio works on. | ||||
| 					audio_.time_since_update += HalfCycles(duration.as_int() * 5); | ||||
| 				} | ||||
|  | ||||
| 				void flush() { | ||||
| 					audio_.flush(); | ||||
| 				} | ||||
|  | ||||
| 				void set_interrupt_status(bool status) { | ||||
| 					machine_.update_interrupt_input(); | ||||
| 				} | ||||
|  | ||||
| 			private: | ||||
| 				ConcreteMachine &machine_; | ||||
| 				RealTimeClock &clock_; | ||||
| 				Keyboard &keyboard_; | ||||
| 				Video &video_; | ||||
| 				DeferredAudio &audio_; | ||||
| 				IWM &iwm_; | ||||
| 				Inputs::QuadratureMouse &mouse_; | ||||
| 		}; | ||||
|  | ||||
| 		CPU::MC68000::Processor<ConcreteMachine, true> mc68000_; | ||||
|  | ||||
| 		DriveSpeedAccumulator drive_speed_accumulator_; | ||||
| 		IWM iwm_; | ||||
|  | ||||
| 		DeferredAudio audio_; | ||||
| 		Video video_; | ||||
|  | ||||
| 		RealTimeClock clock_; | ||||
| 		Keyboard keyboard_; | ||||
|  | ||||
| 		MOS::MOS6522::MOS6522<VIAPortHandler> via_; | ||||
|  		VIAPortHandler via_port_handler_; | ||||
|  | ||||
|  		Zilog::SCC::z8530 scc_; | ||||
|  | ||||
|  		HalfCycles via_clock_; | ||||
|  		HalfCycles real_time_clock_; | ||||
|  		HalfCycles keyboard_clock_; | ||||
|  		HalfCycles time_since_video_update_; | ||||
|  		HalfCycles time_until_video_event_; | ||||
|  		HalfCycles time_since_mouse_update_; | ||||
|  | ||||
| 		bool ROM_is_overlay_ = true; | ||||
| 		int phase_ = 1; | ||||
|  | ||||
| 		DoubleDensityDrive drives_[2]; | ||||
| 		Inputs::QuadratureMouse mouse_; | ||||
|  | ||||
| 		Apple::Macintosh::KeyboardMapper keyboard_mapper_; | ||||
|  | ||||
| 		uint32_t ram_mask_ = 0; | ||||
| 		uint32_t rom_mask_ = 0; | ||||
| 		uint16_t rom_[64*1024]; | ||||
| 		uint16_t ram_[256*1024]; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| using namespace Apple::Macintosh; | ||||
|  | ||||
| Machine *Machine::Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { | ||||
| 	auto *const mac_target = dynamic_cast<const Analyser::Static::Macintosh::Target *>(target); | ||||
|  | ||||
| 	using Model = Analyser::Static::Macintosh::Target::Model; | ||||
| 	switch(mac_target->model) { | ||||
| 		default: | ||||
| 		case Model::Mac128k:	return new ConcreteMachine<Model::Mac128k>(*mac_target, rom_fetcher); | ||||
| 		case Model::Mac512k:	return new ConcreteMachine<Model::Mac512k>(*mac_target, rom_fetcher); | ||||
| 		case Model::Mac512ke:	return new ConcreteMachine<Model::Mac512ke>(*mac_target, rom_fetcher); | ||||
| 		case Model::MacPlus:	return new ConcreteMachine<Model::MacPlus>(*mac_target, rom_fetcher); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Machine::~Machine() {} | ||||
							
								
								
									
										30
									
								
								Machines/Apple/Macintosh/Macintosh.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								Machines/Apple/Macintosh/Macintosh.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| // | ||||
| //  Macintosh.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Macintosh_hpp | ||||
| #define Macintosh_hpp | ||||
|  | ||||
| #include "../../../Analyser/Static/StaticAnalyser.hpp" | ||||
| #include "../../ROMMachine.hpp" | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| class Machine { | ||||
| 	public: | ||||
| 		virtual ~Machine(); | ||||
|  | ||||
| 		/// Creates and returns a Macintosh. | ||||
| 		static Machine *Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||
| }; | ||||
|  | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Macintosh_hpp */ | ||||
							
								
								
									
										166
									
								
								Machines/Apple/Macintosh/RealTimeClock.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								Machines/Apple/Macintosh/RealTimeClock.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| // | ||||
| //  RealTimeClock.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 07/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef RealTimeClock_hpp | ||||
| #define RealTimeClock_hpp | ||||
|  | ||||
| #include "../../Utility/MemoryFuzzer.hpp" | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| /*! | ||||
| 	Models the storage component of Apple's real-time clock. | ||||
|  | ||||
| 	Since tracking of time is pushed to this class, it is assumed | ||||
| 	that whomever is translating real time into emulated time | ||||
| 	will notify the VIA of a potential interrupt. | ||||
| */ | ||||
| class RealTimeClock { | ||||
| 	public: | ||||
| 		RealTimeClock() { | ||||
| 			// TODO: this should persist, if possible, rather than | ||||
| 			// being randomly initialised. | ||||
| 			Memory::Fuzz(data_, sizeof(data_)); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Advances the clock by 1 second. | ||||
|  | ||||
| 			The caller should also notify the VIA. | ||||
| 		*/ | ||||
| 		void update() { | ||||
| 			for(int c = 0; c < 4; ++c) { | ||||
| 				++seconds_[c]; | ||||
| 				if(seconds_[c]) break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Sets the current clock and data inputs to the clock. | ||||
| 		*/ | ||||
| 		void set_input(bool clock, bool data) { | ||||
| 			/* | ||||
| 				Documented commands: | ||||
|  | ||||
| 					z0000001		Seconds register 0 (lowest order byte) | ||||
| 					z0000101		Seconds register 1 | ||||
| 					z0001001		Seconds register 2 | ||||
| 					z0001101		Seconds register 3 | ||||
| 					00110001		Test register (write only) | ||||
| 					00110101		Write-protect register (write only) | ||||
| 					z010aa01		RAM addresses 0x10 - 0x13 | ||||
| 					z1aaaa01		RAM addresses 0x00 – 0x0f | ||||
|  | ||||
| 					z = 1 => a read; z = 0 => a write. | ||||
|  | ||||
| 				The top bit of the write-protect register enables (0) or disables (1) | ||||
| 				writes to other locations. | ||||
|  | ||||
| 				All the documentation says about the test register is to set the top | ||||
| 				two bits to 0 for normal operation. Abnormal operation is undefined. | ||||
|  | ||||
| 				The data line is valid when the clock transitions to level 0. | ||||
| 			*/ | ||||
|  | ||||
| 			if(clock && !previous_clock_) { | ||||
| 				// Shift into the command_ register, no matter what. | ||||
| 				command_ = uint16_t((command_ << 1) | (data ? 1 : 0)); | ||||
| 				result_ <<= 1; | ||||
|  | ||||
| 				// Increment phase. | ||||
| 				++phase_; | ||||
|  | ||||
| 				// When phase hits 8, inspect the command. | ||||
| 				// If it's a read, prepare a result. | ||||
| 				if(phase_ == 8) { | ||||
| 					if(command_ & 0x80) { | ||||
| 						// A read. | ||||
| 						const auto address = (command_ >> 2) & 0x1f; | ||||
|  | ||||
| 						// Begin pessimistically. | ||||
| 						result_ = 0xff; | ||||
|  | ||||
| 						if(address < 4) { | ||||
| 							result_ = seconds_[address]; | ||||
| 						} else if(address >= 0x10) { | ||||
| 							result_ = data_[address & 0xf]; | ||||
| 						} else if(address >= 0x8 && address <= 0xb) { | ||||
| 							result_ = data_[0x10 + (address & 0x3)]; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				// If phase hits 16 and this was a read command, | ||||
| 				// just stop. If it was a write command, do the | ||||
| 				// actual write. | ||||
| 				if(phase_ == 16) { | ||||
| 					if(!(command_ & 0x8000)) { | ||||
| 						// A write. | ||||
|  | ||||
| 						const auto address = (command_ >> 10) & 0x1f; | ||||
| 						const uint8_t value = uint8_t(command_ & 0xff); | ||||
|  | ||||
| 						// First test: is this to the write-protect register? | ||||
| 						if(address == 0xd) { | ||||
| 							write_protect_ = value; | ||||
| 						} | ||||
|  | ||||
| 						// No other writing is permitted if the write protect | ||||
| 						// register won't allow it. | ||||
| 						if(!(write_protect_ & 0x80)) { | ||||
| 							if(address < 4) { | ||||
| 								seconds_[address] = value; | ||||
| 							} else if(address >= 0x10) { | ||||
| 								data_[address & 0xf] = value; | ||||
| 							} else if(address >= 0x8 && address <= 0xb) { | ||||
| 								data_[0x10 + (address & 0x3)] = value; | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					// A phase of 16 always ends the command, so reset here. | ||||
| 					abort(); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			previous_clock_ = clock; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Reads the current data output level from the clock. | ||||
| 		*/ | ||||
| 		bool get_data() { | ||||
| 			return !!(result_ & 0x80); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Announces that a serial command has been aborted. | ||||
| 		*/ | ||||
| 		void abort() { | ||||
| 			result_ = 0; | ||||
| 			phase_ = 0; | ||||
| 			command_ = 0; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		uint8_t data_[0x14]; | ||||
| 		uint8_t seconds_[4]; | ||||
| 		uint8_t write_protect_; | ||||
|  | ||||
| 		int phase_ = 0; | ||||
| 		uint16_t command_; | ||||
| 		uint8_t result_ = 0; | ||||
|  | ||||
| 		bool previous_clock_ = false; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* RealTimeClock_hpp */ | ||||
							
								
								
									
										201
									
								
								Machines/Apple/Macintosh/Video.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								Machines/Apple/Macintosh/Video.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,201 @@ | ||||
| // | ||||
| //  Video.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Video.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| using namespace Apple::Macintosh; | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| const HalfCycles line_length(704); | ||||
| const int number_of_lines = 370; | ||||
| const HalfCycles frame_length(line_length * HalfCycles(number_of_lines)); | ||||
| const int sync_start = 36; | ||||
| const int sync_end = 38; | ||||
|  | ||||
| } | ||||
|  | ||||
| // Re: CRT timings, see the Apple Guide to the Macintosh Hardware Family, | ||||
| // bottom of page 400: | ||||
| // | ||||
| //	"For each scan line, 512 pixels are drawn on the screen ... | ||||
| //	The horizontal blanking interval takes the time of an additional 192 pixels" | ||||
| // | ||||
| // And, at the top of 401: | ||||
| // | ||||
| //	"The visible portion of a full-screen display consists of 342 horizontal scan lines... | ||||
| //	During the vertical blanking interval, the turned-off beam ... traces out an additional 28 scan lines," | ||||
| // | ||||
| Video::Video(uint16_t *ram, DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) : | ||||
| 	audio_(audio), | ||||
| 	drive_speed_accumulator_(drive_speed_accumulator), | ||||
|  	crt_(704, 1, 370, Outputs::Display::ColourSpace::YIQ, 1, 1, 6, false, Outputs::Display::InputDataType::Luminance1), | ||||
|  	ram_(ram) { | ||||
|  | ||||
|  	crt_.set_display_type(Outputs::Display::DisplayType::RGB); | ||||
| 	crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f)); | ||||
| 	crt_.set_aspect_ratio(1.73f);	// The Mac uses a non-standard scanning area. | ||||
| } | ||||
|  | ||||
| void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) { | ||||
| 	crt_.set_scan_target(scan_target); | ||||
| } | ||||
|  | ||||
| void Video::run_for(HalfCycles duration) { | ||||
| 	// Determine the current video and audio bases. These values don't appear to be latched, they apply immediately. | ||||
| 	const size_t video_base = (use_alternate_screen_buffer_ ? (0xffff2700 >> 1) : (0xffffa700 >> 1)) & ram_mask_; | ||||
| 	const size_t audio_base = (use_alternate_audio_buffer_ ? (0xffffa100 >> 1) : (0xfffffd00 >> 1)) & ram_mask_; | ||||
|  | ||||
| 	// The number of HalfCycles is literally the number of pixel clocks to move through, | ||||
| 	// since pixel output occurs at twice the processor clock. So divide by 16 to get | ||||
| 	// the number of fetches. | ||||
| 	while(duration > HalfCycles(0)) { | ||||
| 		const auto pixel_start = frame_position_ % line_length; | ||||
| 		const int line = (frame_position_ / line_length).as_int(); | ||||
|  | ||||
| 		const auto cycles_left_in_line = std::min(line_length - pixel_start, duration); | ||||
|  | ||||
| 		// Line timing, entirely invented as I can find exactly zero words of documentation: | ||||
| 		// | ||||
| 		//	First 342 lines: | ||||
| 		// | ||||
| 		//	First 32 words = pixels; | ||||
| 		//	next 5 words = right border; | ||||
| 		//	next 2 words = sync level; | ||||
| 		//	final 5 words = left border. | ||||
| 		// | ||||
| 		//	Then 12 lines of border, 3 of sync, 11 more of border. | ||||
|  | ||||
| 		const int first_word = pixel_start.as_int() >> 4; | ||||
| 		const int final_word = (pixel_start + cycles_left_in_line).as_int() >> 4; | ||||
|  | ||||
| 		if(first_word != final_word) { | ||||
| 			if(line < 342) { | ||||
| 				// If there are any pixels left to output, do so. | ||||
| 				if(first_word < 32) { | ||||
| 					const int final_pixel_word = std::min(final_word, 32); | ||||
|  | ||||
| 					if(!first_word) { | ||||
| 						pixel_buffer_ = crt_.begin_data(512); | ||||
| 					} | ||||
|  | ||||
| 					if(pixel_buffer_) { | ||||
| 						for(int c = first_word; c < final_pixel_word; ++c) { | ||||
| 							uint16_t pixels = ram_[video_base + video_address_] ^ 0xffff; | ||||
| 							++video_address_; | ||||
|  | ||||
| 							pixel_buffer_[15] = pixels & 0x01; | ||||
| 							pixel_buffer_[14] = pixels & 0x02; | ||||
| 							pixel_buffer_[13] = pixels & 0x04; | ||||
| 							pixel_buffer_[12] = pixels & 0x08; | ||||
| 							pixel_buffer_[11] = pixels & 0x10; | ||||
| 							pixel_buffer_[10] = pixels & 0x20; | ||||
| 							pixel_buffer_[9] = pixels & 0x40; | ||||
| 							pixel_buffer_[8] = pixels & 0x80; | ||||
|  | ||||
| 							pixels >>= 8; | ||||
| 							pixel_buffer_[7] = pixels & 0x01; | ||||
| 							pixel_buffer_[6] = pixels & 0x02; | ||||
| 							pixel_buffer_[5] = pixels & 0x04; | ||||
| 							pixel_buffer_[4] = pixels & 0x08; | ||||
| 							pixel_buffer_[3] = pixels & 0x10; | ||||
| 							pixel_buffer_[2] = pixels & 0x20; | ||||
| 							pixel_buffer_[1] = pixels & 0x40; | ||||
| 							pixel_buffer_[0] = pixels & 0x80; | ||||
|  | ||||
| 							pixel_buffer_ += 16; | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					if(final_pixel_word == 32) { | ||||
| 						crt_.output_data(512); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if(first_word < sync_start && final_word >= sync_start)	crt_.output_blank((sync_start - 32) * 16); | ||||
| 				if(first_word < sync_end && final_word >= sync_end)		crt_.output_sync((sync_end - sync_start) * 16); | ||||
| 				if(final_word == 44)									crt_.output_blank((44 - sync_end) * 16); | ||||
| 			} else if(final_word == 44) { | ||||
| 				if(line >= 353 && line < 356) { | ||||
| 					/* Output a sync line. */ | ||||
| 					crt_.output_sync(sync_start * 16); | ||||
| 					crt_.output_blank((sync_end - sync_start) * 16); | ||||
| 					crt_.output_sync((44 - sync_end) * 16); | ||||
| 				} else { | ||||
| 					/* Output a blank line. */ | ||||
| 					crt_.output_blank(sync_start * 16); | ||||
| 					crt_.output_sync((sync_end - sync_start) * 16); | ||||
| 					crt_.output_blank((44 - sync_end) * 16); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Audio and disk fetches occur "just before video data". | ||||
| 			if(final_word == 44) { | ||||
| 				const uint16_t audio_word = ram_[audio_address_ + audio_base]; | ||||
| 				++audio_address_; | ||||
| 				audio_.audio.post_sample(audio_word >> 8); | ||||
| 				drive_speed_accumulator_.post_sample(audio_word & 0xff); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		duration -= cycles_left_in_line; | ||||
| 		frame_position_ = frame_position_ + cycles_left_in_line; | ||||
| 		if(frame_position_ == frame_length) { | ||||
| 			frame_position_ = HalfCycles(0); | ||||
| 			/* | ||||
| 				Video: $1A700 and the alternate buffer starts at $12700; for a 512K Macintosh, add $60000 to these numbers. | ||||
| 			*/ | ||||
| 			video_address_ = 0; | ||||
|  | ||||
| 			/* | ||||
| 				"The main sound buffer is at $1FD00 in a 128K Macintosh, and the alternate buffer is at $1A100; | ||||
| 				for a 512K Macintosh, add $60000 to these values." | ||||
| 			*/ | ||||
| 			audio_address_ = 0; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool Video::vsync() { | ||||
| 	const int line = (frame_position_ / line_length).as_int(); | ||||
| 	return line >= 353 && line < 356; | ||||
| } | ||||
|  | ||||
| HalfCycles Video::get_next_sequence_point() { | ||||
| 	const int line = (frame_position_ / line_length).as_int(); | ||||
| 	if(line >= 353 && line < 356) { | ||||
| 		// Currently in vsync, so get time until start of line 357, | ||||
| 		// when vsync will end. | ||||
| 		return HalfCycles(356) * line_length - frame_position_; | ||||
| 	} else { | ||||
| 		// Not currently in vsync, so get time until start of line 353. | ||||
| 		const auto start_of_vsync = HalfCycles(353) * line_length; | ||||
| 		if(frame_position_ < start_of_vsync) | ||||
| 			return start_of_vsync - frame_position_; | ||||
| 		else | ||||
| 			return start_of_vsync + HalfCycles(number_of_lines) * line_length - frame_position_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool Video::is_outputting(HalfCycles offset) { | ||||
| 	const auto offset_position = frame_position_ + offset % frame_length; | ||||
| 	const int column = (offset_position % line_length).as_int() >> 4; | ||||
| 	const int line = (offset_position / line_length).as_int(); | ||||
| 	return line < 342 && column < 32; | ||||
| } | ||||
|  | ||||
| void Video::set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) { | ||||
| 	use_alternate_screen_buffer_ = use_alternate_screen_buffer; | ||||
| 	use_alternate_audio_buffer_ = use_alternate_audio_buffer; | ||||
| } | ||||
|  | ||||
| void Video::set_ram_mask(uint32_t mask) { | ||||
| 	ram_mask_ = mask; | ||||
| } | ||||
							
								
								
									
										94
									
								
								Machines/Apple/Macintosh/Video.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								Machines/Apple/Macintosh/Video.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| // | ||||
| //  Video.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Video_hpp | ||||
| #define Video_hpp | ||||
|  | ||||
| #include "../../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "DeferredAudio.hpp" | ||||
| #include "DriveSpeedAccumulator.hpp" | ||||
|  | ||||
| namespace Apple { | ||||
| namespace Macintosh { | ||||
|  | ||||
| /*! | ||||
| 	Models the 68000-era Macintosh video hardware, producing a 512x348 pixel image, | ||||
| 	within a total scanning area of 370 lines, at 352 cycles per line. | ||||
|  | ||||
| 	This class also collects audio and 400kb drive-speed data, forwarding those values. | ||||
| */ | ||||
| class Video { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			Constructs an instance of @c Video sourcing its pixel data from @c ram and | ||||
| 			providing audio and drive-speed bytes to @c audio and @c drive_speed_accumulator. | ||||
| 		*/ | ||||
| 		Video(uint16_t *ram, DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator); | ||||
|  | ||||
| 		/*! | ||||
| 			Sets the target device for video data. | ||||
| 		*/ | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target); | ||||
|  | ||||
| 		/*! | ||||
| 			Produces the next @c duration period of pixels. | ||||
| 		*/ | ||||
| 		void run_for(HalfCycles duration); | ||||
|  | ||||
| 		/*! | ||||
| 			Sets whether the alternate screen and/or audio buffers should be used to source data. | ||||
| 		*/ | ||||
| 		void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer); | ||||
|  | ||||
| 		/*! | ||||
| 			Provides a mask indicating which parts of the generated video and audio/drive addresses are | ||||
| 			actually decoded, accessing *word-sized memory*; e.g. for a 128kb Macintosh this should be (1 << 16) - 1 = 0xffff. | ||||
| 		*/ | ||||
| 		void set_ram_mask(uint32_t); | ||||
|  | ||||
| 		/*! | ||||
| 			@returns @c true if the video is currently outputting a vertical sync, @c false otherwise. | ||||
| 		*/ | ||||
| 		bool vsync(); | ||||
|  | ||||
| 		/* | ||||
| 			@returns @c true if in @c offset half cycles from now, the video will be outputting pixels; | ||||
| 				@c false otherwise. | ||||
| 		*/ | ||||
| 		bool is_outputting(HalfCycles offset = HalfCycles(0)); | ||||
|  | ||||
| 		/*! | ||||
| 			@returns the amount of time until there is next a transition on the | ||||
| 				vsync signal. | ||||
| 		*/ | ||||
| 		HalfCycles get_next_sequence_point(); | ||||
|  | ||||
| 	private: | ||||
| 		DeferredAudio &audio_; | ||||
| 		DriveSpeedAccumulator &drive_speed_accumulator_; | ||||
|  | ||||
| 		Outputs::CRT::CRT crt_; | ||||
| 		uint16_t *ram_ = nullptr; | ||||
| 		uint32_t ram_mask_ = 0; | ||||
|  | ||||
| 		HalfCycles frame_position_; | ||||
|  | ||||
| 		size_t video_address_ = 0; | ||||
| 		size_t audio_address_ = 0; | ||||
|  | ||||
| 		uint8_t *pixel_buffer_ = nullptr; | ||||
|  | ||||
| 		bool use_alternate_screen_buffer_ = false; | ||||
| 		bool use_alternate_audio_buffer_ = false; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Video_hpp */ | ||||
| @@ -55,13 +55,13 @@ class Bus { | ||||
| 		// video backlog accumulation counter | ||||
| 		Cycles cycles_since_video_update_; | ||||
| 		inline void update_video() { | ||||
| 			tia_.run_for(cycles_since_video_update_.flush()); | ||||
| 			tia_.run_for(cycles_since_video_update_.flush<Cycles>()); | ||||
| 		} | ||||
|  | ||||
| 		// RIOT backlog accumulation counter | ||||
| 		Cycles cycles_since_6532_update_; | ||||
| 		inline void update_6532() { | ||||
| 			mos6532_.run_for(cycles_since_6532_update_.flush()); | ||||
| 			mos6532_.run_for(cycles_since_6532_update_.flush<Cycles>()); | ||||
| 		} | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -19,6 +19,7 @@ | ||||
|  | ||||
| #include "../../Configurable/StandardOptions.hpp" | ||||
| #include "../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../ClockReceiver/JustInTime.hpp" | ||||
|  | ||||
| #include "../../Outputs/Speaker/Implementation/CompoundSource.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| @@ -131,8 +132,7 @@ class ConcreteMachine: | ||||
| 			joysticks_.emplace_back(new Joystick); | ||||
|  | ||||
| 			const auto roms = rom_fetcher( | ||||
| 				"ColecoVision", | ||||
| 				{ "coleco.rom" }); | ||||
| 				{ {"ColecoVision", "the ColecoVision BIOS", "coleco.rom", 8*1024, 0x3aa93ef3} }); | ||||
|  | ||||
| 			if(!roms[0]) { | ||||
| 				throw ROMMachine::Error::MissingROMs; | ||||
| @@ -170,7 +170,7 @@ class ConcreteMachine: | ||||
| 			} | ||||
|  | ||||
| 			// ColecoVisions have composite output only. | ||||
| 			vdp_.set_display_type(Outputs::Display::DisplayType::CompositeColour); | ||||
| 			vdp_->set_display_type(Outputs::Display::DisplayType::CompositeColour); | ||||
| 		} | ||||
|  | ||||
| 		~ConcreteMachine() { | ||||
| @@ -182,11 +182,11 @@ class ConcreteMachine: | ||||
| 		} | ||||
|  | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { | ||||
| 			vdp_.set_scan_target(scan_target); | ||||
| 			vdp_->set_scan_target(scan_target); | ||||
| 		} | ||||
|  | ||||
| 		void set_display_type(Outputs::Display::DisplayType display_type) override { | ||||
| 			vdp_.set_display_type(display_type); | ||||
| 			vdp_->set_display_type(display_type); | ||||
| 		} | ||||
|  | ||||
| 		Outputs::Speaker::Speaker *get_speaker() override { | ||||
| @@ -211,7 +211,7 @@ class ConcreteMachine: | ||||
| 			} | ||||
| 			const HalfCycles length = cycle.length + penalty; | ||||
|  | ||||
| 			time_since_vdp_update_ += length; | ||||
| 			vdp_ += length; | ||||
| 			time_since_sn76489_update_ += length; | ||||
|  | ||||
| 			// Act only if necessary. | ||||
| @@ -256,10 +256,9 @@ class ConcreteMachine: | ||||
| 					case CPU::Z80::PartialMachineCycle::Input: | ||||
| 						switch((address >> 5) & 7) { | ||||
| 							case 5: | ||||
| 								update_video(); | ||||
| 								*cycle.value = vdp_.get_register(address); | ||||
| 								z80_.set_non_maskable_interrupt_line(vdp_.get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_.get_time_until_interrupt(); | ||||
| 								*cycle.value = vdp_->get_register(address); | ||||
| 								z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_->get_time_until_interrupt(); | ||||
| 							break; | ||||
|  | ||||
| 							case 7: { | ||||
| @@ -300,10 +299,9 @@ class ConcreteMachine: | ||||
| 							break; | ||||
|  | ||||
| 							case 5: | ||||
| 								update_video(); | ||||
| 								vdp_.set_register(address, *cycle.value); | ||||
| 								z80_.set_non_maskable_interrupt_line(vdp_.get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_.get_time_until_interrupt(); | ||||
| 								vdp_->set_register(address, *cycle.value); | ||||
| 								z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_->get_time_until_interrupt(); | ||||
| 							break; | ||||
|  | ||||
| 							case 7: | ||||
| @@ -355,7 +353,7 @@ class ConcreteMachine: | ||||
| 		} | ||||
|  | ||||
| 		void flush() { | ||||
| 			update_video(); | ||||
| 			vdp_.flush(); | ||||
| 			update_audio(); | ||||
| 			audio_queue_.perform(); | ||||
| 		} | ||||
| @@ -397,12 +395,9 @@ class ConcreteMachine: | ||||
| 		inline void update_audio() { | ||||
| 			speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(sn76489_divider))); | ||||
| 		} | ||||
| 		inline void update_video() { | ||||
| 			vdp_.run_for(time_since_vdp_update_.flush()); | ||||
| 		} | ||||
|  | ||||
| 		CPU::Z80::Processor<ConcreteMachine, false, false> z80_; | ||||
| 		TI::TMS::TMS9918 vdp_; | ||||
| 		JustInTimeActor<TI::TMS::TMS9918, HalfCycles> vdp_; | ||||
|  | ||||
| 		Concurrency::DeferringAsyncTaskQueue audio_queue_; | ||||
| 		TI::SN76489 sn76489_; | ||||
| @@ -425,7 +420,6 @@ class ConcreteMachine: | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_; | ||||
| 		bool joysticks_in_keypad_mode_ = false; | ||||
|  | ||||
| 		HalfCycles time_since_vdp_update_; | ||||
| 		HalfCycles time_since_sn76489_update_; | ||||
| 		HalfCycles time_until_interrupt_; | ||||
|  | ||||
|   | ||||
| @@ -39,13 +39,20 @@ MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher & | ||||
| 	// attach the only drive there is | ||||
| 	set_drive(drive_); | ||||
|  | ||||
| 	std::string rom_name; | ||||
| 	std::string device_name; | ||||
| 	uint32_t crc = 0; | ||||
| 	switch(personality) { | ||||
| 		case Personality::C1540:	rom_name = "1540.bin";	break; | ||||
| 		case Personality::C1541:	rom_name = "1541.bin";	break; | ||||
| 		case Personality::C1540: | ||||
| 			device_name = "1540"; | ||||
| 			crc = 0x718d42b1; | ||||
| 		break; | ||||
| 		case Personality::C1541: | ||||
| 			device_name = "1541"; | ||||
| 			crc = 0xfb760019; | ||||
| 		break; | ||||
| 	} | ||||
|  | ||||
| 	auto roms = rom_fetcher("Commodore1540", {rom_name}); | ||||
| 	auto roms = rom_fetcher({ {"Commodore1540", "the " + device_name + " ROM", device_name + ".bin", 16*1024, crc} }); | ||||
| 	if(!roms[0]) { | ||||
| 		throw ROMMachine::Error::MissingROMs; | ||||
| 	} | ||||
|   | ||||
| @@ -323,31 +323,34 @@ class ConcreteMachine: | ||||
| 			// install a joystick | ||||
| 			joysticks_.emplace_back(new Joystick(*user_port_via_port_handler_, *keyboard_via_port_handler_)); | ||||
|  | ||||
| 			std::vector<std::string> rom_names = { "basic.bin" }; | ||||
| 			const std::string machine_name = "Vic20"; | ||||
| 			std::vector<ROMMachine::ROM> rom_names = { | ||||
| 				{machine_name, "the VIC-20 BASIC ROM", "basic.bin", 8*1024, 0xdb4c43c1} | ||||
| 			}; | ||||
| 			switch(target.region) { | ||||
| 				default: | ||||
| 					rom_names.push_back("characters-english.bin"); | ||||
| 					rom_names.push_back("kernel-pal.bin"); | ||||
| 					rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6); | ||||
| 					rom_names.emplace_back(machine_name, "the English-language PAL VIC-20 kernel ROM", "kernel-pal.bin", 8*1024, 0x4be07cb4); | ||||
| 				break; | ||||
| 				case Analyser::Static::Commodore::Target::Region::American: | ||||
| 					rom_names.push_back("characters-english.bin"); | ||||
| 					rom_names.push_back("kernel-ntsc.bin"); | ||||
| 					rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6); | ||||
| 					rom_names.emplace_back(machine_name, "the English-language NTSC VIC-20 kernel ROM", "kernel-ntsc.bin", 8*1024, 0xe5e7c174); | ||||
| 				break; | ||||
| 				case Analyser::Static::Commodore::Target::Region::Danish: | ||||
| 					rom_names.push_back("characters-danish.bin"); | ||||
| 					rom_names.push_back("kernel-danish.bin"); | ||||
| 					rom_names.emplace_back(machine_name, "the Danish VIC-20 character ROM", "characters-danish.bin", 4*1024, 0x7fc11454); | ||||
| 					rom_names.emplace_back(machine_name, "the Danish VIC-20 kernel ROM", "kernel-danish.bin", 8*1024, 0x02adaf16); | ||||
| 				break; | ||||
| 				case Analyser::Static::Commodore::Target::Region::Japanese: | ||||
| 					rom_names.push_back("characters-japanese.bin"); | ||||
| 					rom_names.push_back("kernel-japanese.bin"); | ||||
| 					rom_names.emplace_back(machine_name, "the Japanese VIC-20 character ROM", "characters-japanese.bin", 4*1024, 0xfcfd8a4b); | ||||
| 					rom_names.emplace_back(machine_name, "the Japanese VIC-20 kernel ROM", "kernel-japanese.bin", 8*1024, 0x336900d7); | ||||
| 				break; | ||||
| 				case Analyser::Static::Commodore::Target::Region::Swedish: | ||||
| 					rom_names.push_back("characters-swedish.bin"); | ||||
| 					rom_names.push_back("kernel-japanese.bin"); | ||||
| 					rom_names.emplace_back(machine_name, "the Swedish VIC-20 character ROM", "characters-swedish.bin", 4*1024, 0xd808551d); | ||||
| 					rom_names.emplace_back(machine_name, "the Swedish VIC-20 kernel ROM", "kernel-swedish.bin", 8*1024, 0xb2a60662); | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			const auto roms = rom_fetcher("Vic20", rom_names); | ||||
| 			const auto roms = rom_fetcher(rom_names); | ||||
|  | ||||
| 			for(const auto &rom: roms) { | ||||
| 				if(!rom) { | ||||
| @@ -699,7 +702,7 @@ class ConcreteMachine: | ||||
|  | ||||
| 	private: | ||||
| 		void update_video() { | ||||
| 			mos6560_.run_for(cycles_since_mos6560_update_.flush()); | ||||
| 			mos6560_.run_for(cycles_since_mos6560_update_.flush<Cycles>()); | ||||
| 		} | ||||
| 		CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_; | ||||
|  | ||||
|   | ||||
| @@ -11,10 +11,13 @@ | ||||
|  | ||||
| #include "../Configurable/Configurable.hpp" | ||||
| #include "../Activity/Source.hpp" | ||||
| #include "MediaTarget.hpp" | ||||
|  | ||||
| #include "CRTMachine.hpp" | ||||
| #include "JoystickMachine.hpp" | ||||
| #include "KeyboardMachine.hpp" | ||||
| #include "MediaTarget.hpp" | ||||
| #include "MouseMachine.hpp" | ||||
|  | ||||
| #include "Utility/Typer.hpp" | ||||
|  | ||||
| namespace Machine { | ||||
| @@ -31,6 +34,7 @@ struct DynamicMachine { | ||||
| 	virtual CRTMachine::Machine *crt_machine() = 0; | ||||
| 	virtual JoystickMachine::Machine *joystick_machine() = 0; | ||||
| 	virtual KeyboardMachine::Machine *keyboard_machine() = 0; | ||||
| 	virtual MouseMachine::Machine *mouse_machine() = 0; | ||||
| 	virtual MediaTarget::Machine *media_target() = 0; | ||||
|  | ||||
| 	/*! | ||||
|   | ||||
| @@ -62,17 +62,22 @@ class ConcreteMachine: | ||||
| 			set_clock_rate(2000000); | ||||
|  | ||||
| 			speaker_.set_input_rate(2000000 / SoundGenerator::clock_rate_divider); | ||||
| 			speaker_.set_high_frequency_cutoff(7000); | ||||
|  | ||||
| 			std::vector<std::string> rom_names = {"basic.rom", "os.rom"}; | ||||
| 			const std::string machine_name = "Electron"; | ||||
| 			std::vector<ROMMachine::ROM> required_roms = { | ||||
| 				{machine_name, "the Acorn BASIC II ROM", "basic.rom", 16*1024, 0x79434781}, | ||||
| 				{machine_name, "the Electron MOS ROM", "os.rom", 16*1024, 0xbf63fb1f} | ||||
| 			}; | ||||
| 			if(target.has_adfs) { | ||||
| 				rom_names.push_back("ADFS-E00_1.rom"); | ||||
| 				rom_names.push_back("ADFS-E00_2.rom"); | ||||
| 				required_roms.emplace_back(machine_name, "the E00 ADFS ROM, first slot", "ADFS-E00_1.rom", 16*1024, 0x51523993); | ||||
| 				required_roms.emplace_back(machine_name, "the E00 ADFS ROM, second slot", "ADFS-E00_2.rom", 16*1024, 0x8d17de0e); | ||||
| 			} | ||||
| 			const size_t dfs_rom_position = rom_names.size(); | ||||
| 			const size_t dfs_rom_position = required_roms.size(); | ||||
| 			if(target.has_dfs) { | ||||
| 				rom_names.push_back("DFS-1770-2.20.rom"); | ||||
| 				required_roms.emplace_back(machine_name, "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5); | ||||
| 			} | ||||
| 			const auto roms = rom_fetcher("Electron", rom_names); | ||||
| 			const auto roms = rom_fetcher(required_roms); | ||||
|  | ||||
| 			for(const auto &rom: roms) { | ||||
| 				if(!rom) { | ||||
| @@ -505,7 +510,7 @@ class ConcreteMachine: | ||||
| 		// MARK: - Work deferral updates. | ||||
| 		inline void update_display() { | ||||
| 			if(cycles_since_display_update_ > 0) { | ||||
| 				video_output_.run_for(cycles_since_display_update_.flush()); | ||||
| 				video_output_.run_for(cycles_since_display_update_.flush<Cycles>()); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -34,7 +34,7 @@ enum Key: uint16_t { | ||||
| }; | ||||
|  | ||||
| struct KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper { | ||||
| 	uint16_t mapped_key_for_key(Inputs::Keyboard::Key key); | ||||
| 	uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) override; | ||||
| }; | ||||
|  | ||||
| struct CharacterMapper: public ::Utility::CharacterMapper { | ||||
|   | ||||
| @@ -45,6 +45,7 @@ | ||||
|  | ||||
| #include "../../Configurable/StandardOptions.hpp" | ||||
| #include "../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../ClockReceiver/JustInTime.hpp" | ||||
|  | ||||
| #include "../../Analyser/Static/MSX/Target.hpp" | ||||
|  | ||||
| @@ -173,33 +174,37 @@ class ConcreteMachine: | ||||
| 			mixer_.set_relative_volumes({0.5f, 0.1f, 0.4f}); | ||||
|  | ||||
| 			// Install the proper TV standard and select an ideal BIOS name. | ||||
| 			std::vector<std::string> rom_names = {"msx.rom"}; | ||||
| 			const std::string machine_name = "MSX"; | ||||
| 			std::vector<ROMMachine::ROM> required_roms = { | ||||
| 				{machine_name, "any MSX BIOS", "msx.rom", 32*1024, 0x94ee12f3} | ||||
| 			}; | ||||
|  | ||||
| 			bool is_ntsc = true; | ||||
| 			uint8_t character_generator = 1;	/* 0 = Japan, 1 = USA, etc, 2 = USSR */ | ||||
| 			uint8_t date_format = 1;			/* 0 = Y/M/D, 1 = M/D/Y, 2 = D/M/Y */ | ||||
| 			uint8_t keyboard = 1;				/* 0 = Japan, 1 = USA, 2 = France, 3 = UK, 4 = Germany, 5 = USSR, 6 = Spain */ | ||||
|  | ||||
| 			// TODO: CRCs below are incomplete, at best. | ||||
| 			switch(target.region) { | ||||
| 				case Target::Region::Japan: | ||||
| 					rom_names.push_back("msx-japanese.rom"); | ||||
| 					vdp_.set_tv_standard(TI::TMS::TVStandard::NTSC); | ||||
| 					required_roms.emplace_back(machine_name, "a Japanese MSX BIOS", "msx-japanese.rom", 32*1024, 0xee229390); | ||||
| 					vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC); | ||||
|  | ||||
| 					is_ntsc = true; | ||||
| 					character_generator = 0; | ||||
| 					date_format = 0; | ||||
| 				break; | ||||
| 				case Target::Region::USA: | ||||
| 					rom_names.push_back("msx-american.rom"); | ||||
| 					vdp_.set_tv_standard(TI::TMS::TVStandard::NTSC); | ||||
| 					required_roms.emplace_back(machine_name, "an American MSX BIOS", "msx-american.rom", 32*1024, 0); | ||||
| 					vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC); | ||||
|  | ||||
| 					is_ntsc = true; | ||||
| 					character_generator = 1; | ||||
| 					date_format = 1; | ||||
| 				break; | ||||
| 				case Target::Region::Europe: | ||||
| 					rom_names.push_back("msx-european.rom"); | ||||
| 					vdp_.set_tv_standard(TI::TMS::TVStandard::PAL); | ||||
| 					required_roms.emplace_back(machine_name, "a European MSX BIOS", "msx-european.rom", 32*1024, 0); | ||||
| 					vdp_->set_tv_standard(TI::TMS::TVStandard::PAL); | ||||
|  | ||||
| 					is_ntsc = false; | ||||
| 					character_generator = 1; | ||||
| @@ -209,10 +214,12 @@ class ConcreteMachine: | ||||
|  | ||||
| 			// Fetch the necessary ROMs; try the region-specific ROM first, | ||||
| 			// but failing that fall back on patching the main one. | ||||
| 			size_t disk_index = 0; | ||||
| 			if(target.has_disk_drive) { | ||||
| 				rom_names.push_back("disk.rom"); | ||||
| 				disk_index = required_roms.size(); | ||||
| 				required_roms.emplace_back(machine_name, "the MSX-DOS ROM", "disk.rom", 16*1024, 0x721f61df); | ||||
| 			} | ||||
| 			const auto roms = rom_fetcher("MSX", rom_names); | ||||
| 			const auto roms = rom_fetcher(required_roms); | ||||
|  | ||||
| 			if((!roms[0] && !roms[1]) || (target.has_disk_drive && !roms[2])) { | ||||
| 				throw ROMMachine::Error::MissingROMs; | ||||
| @@ -227,7 +234,6 @@ class ConcreteMachine: | ||||
| 				memory_slots_[0].source = std::move(*roms[0]); | ||||
| 				memory_slots_[0].source.resize(32768); | ||||
|  | ||||
|  | ||||
| 				memory_slots_[0].source[0x2b] = uint8_t( | ||||
| 					(is_ntsc ? 0x00 : 0x80) | | ||||
| 					(date_format << 4) | | ||||
| @@ -252,7 +258,7 @@ class ConcreteMachine: | ||||
| 			// Add a disk cartridge if any disks were supplied. | ||||
| 			if(target.has_disk_drive) { | ||||
| 				memory_slots_[2].set_handler(new DiskROM(memory_slots_[2].source)); | ||||
| 				memory_slots_[2].source = std::move(*roms[1]); | ||||
| 				memory_slots_[2].source = std::move(*roms[disk_index]); | ||||
| 				memory_slots_[2].source.resize(16384); | ||||
|  | ||||
| 				map(2, 0, 0x4000, 0x2000); | ||||
| @@ -273,11 +279,11 @@ class ConcreteMachine: | ||||
| 		} | ||||
|  | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { | ||||
| 			vdp_.set_scan_target(scan_target); | ||||
| 			vdp_->set_scan_target(scan_target); | ||||
| 		} | ||||
|  | ||||
| 		void set_display_type(Outputs::Display::DisplayType display_type) override { | ||||
| 			vdp_.set_display_type(display_type); | ||||
| 			vdp_->set_display_type(display_type); | ||||
| 		} | ||||
|  | ||||
| 		Outputs::Speaker::Speaker *get_speaker() override { | ||||
| @@ -406,7 +412,7 @@ class ConcreteMachine: | ||||
| 			// but otherwise runs without pause. | ||||
| 			const HalfCycles addition((cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode) ? 2 : 0); | ||||
| 			const HalfCycles total_length = addition + cycle.length; | ||||
| 			time_since_vdp_update_ += total_length; | ||||
| 			vdp_ += total_length; | ||||
| 			time_since_ay_update_ += total_length; | ||||
| 			memory_slots_[0].cycles_since_update += total_length; | ||||
| 			memory_slots_[1].cycles_since_update += total_length; | ||||
| @@ -484,7 +490,7 @@ class ConcreteMachine: | ||||
| 							*cycle.value = read_pointers_[address >> 13][address & 8191]; | ||||
| 						} else { | ||||
| 							int slot_hit = (paged_memory_ >> ((address >> 14) * 2)) & 3; | ||||
| 							memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush()); | ||||
| 							memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush<HalfCycles>()); | ||||
| 							*cycle.value = memory_slots_[slot_hit].handler->read(address); | ||||
| 						} | ||||
| 					break; | ||||
| @@ -495,7 +501,7 @@ class ConcreteMachine: | ||||
| 						int slot_hit = (paged_memory_ >> ((address >> 14) * 2)) & 3; | ||||
| 						if(memory_slots_[slot_hit].handler) { | ||||
| 							update_audio(); | ||||
| 							memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush()); | ||||
| 							memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush<HalfCycles>()); | ||||
| 							memory_slots_[slot_hit].handler->write(address, *cycle.value, read_pointers_[pc_address_ >> 13] != memory_slots_[0].read_pointers[pc_address_ >> 13]); | ||||
| 						} | ||||
| 					} break; | ||||
| @@ -503,10 +509,9 @@ class ConcreteMachine: | ||||
| 					case CPU::Z80::PartialMachineCycle::Input: | ||||
| 						switch(address & 0xff) { | ||||
| 							case 0x98:	case 0x99: | ||||
| 								vdp_.run_for(time_since_vdp_update_.flush()); | ||||
| 								*cycle.value = vdp_.get_register(address); | ||||
| 								z80_.set_interrupt_line(vdp_.get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_.get_time_until_interrupt(); | ||||
| 								*cycle.value = vdp_->get_register(address); | ||||
| 								z80_.set_interrupt_line(vdp_->get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_->get_time_until_interrupt(); | ||||
| 							break; | ||||
|  | ||||
| 							case 0xa2: | ||||
| @@ -531,10 +536,9 @@ class ConcreteMachine: | ||||
| 						const int port = address & 0xff; | ||||
| 						switch(port) { | ||||
| 							case 0x98:	case 0x99: | ||||
| 								vdp_.run_for(time_since_vdp_update_.flush()); | ||||
| 								vdp_.set_register(address, *cycle.value); | ||||
| 								z80_.set_interrupt_line(vdp_.get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_.get_time_until_interrupt(); | ||||
| 								vdp_->set_register(address, *cycle.value); | ||||
| 								z80_.set_interrupt_line(vdp_->get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_->get_time_until_interrupt(); | ||||
| 							break; | ||||
|  | ||||
| 							case 0xa0:	case 0xa1: | ||||
| @@ -607,7 +611,7 @@ class ConcreteMachine: | ||||
| 		} | ||||
|  | ||||
| 		void flush() { | ||||
| 			vdp_.run_for(time_since_vdp_update_.flush()); | ||||
| 			vdp_.flush(); | ||||
| 			update_audio(); | ||||
| 			audio_queue_.perform(); | ||||
| 		} | ||||
| @@ -748,7 +752,7 @@ class ConcreteMachine: | ||||
| 		}; | ||||
|  | ||||
| 		CPU::Z80::Processor<ConcreteMachine, false, false> z80_; | ||||
| 		TI::TMS::TMS9918 vdp_; | ||||
| 		JustInTimeActor<TI::TMS::TMS9918, HalfCycles> vdp_; | ||||
| 		Intel::i8255::i8255<i8255PortHandler> i8255_; | ||||
|  | ||||
| 		Concurrency::DeferringAsyncTaskQueue audio_queue_; | ||||
| @@ -792,7 +796,6 @@ class ConcreteMachine: | ||||
| 		uint8_t scratch_[8192]; | ||||
| 		uint8_t unpopulated_[8192]; | ||||
|  | ||||
| 		HalfCycles time_since_vdp_update_; | ||||
| 		HalfCycles time_since_ay_update_; | ||||
| 		HalfCycles time_until_interrupt_; | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,7 @@ | ||||
| #include "../KeyboardMachine.hpp" | ||||
|  | ||||
| #include "../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../ClockReceiver/JustInTime.hpp" | ||||
|  | ||||
| #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../Outputs/Log.hpp" | ||||
| @@ -132,7 +133,12 @@ class ConcreteMachine: | ||||
|  | ||||
| 			// Load the BIOS if relevant. | ||||
| 			if(has_bios()) { | ||||
| 				const auto roms = rom_fetcher("MasterSystem", {"bios.sms"}); | ||||
| 				// TODO: there's probably a million other versions of the Master System BIOS; try to build a | ||||
| 				// CRC32 catalogue of those. So far: | ||||
| 				// | ||||
| 				//	0072ed54 = US/European BIOS 1.3 | ||||
| 				//	48d44a13 = Japanese BIOS 2.1 | ||||
| 				const auto roms = rom_fetcher({ {"MasterSystem", "the Master System BIOS", "bios.sms", 8*1024, { 0x0072ed54, 0x48d44a13 } } }); | ||||
| 				if(!roms[0]) { | ||||
| 					// No BIOS found; attempt to boot as though it has already disabled itself. | ||||
| 					memory_control_ |= 0x08; | ||||
| @@ -163,16 +169,16 @@ class ConcreteMachine: | ||||
| 		} | ||||
|  | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { | ||||
| 			vdp_.set_tv_standard( | ||||
| 			vdp_->set_tv_standard( | ||||
| 				(region_ == Target::Region::Europe) ? | ||||
| 					TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC); | ||||
| 			time_until_debounce_ = vdp_.get_time_until_line(-1); | ||||
| 			time_until_debounce_ = vdp_->get_time_until_line(-1); | ||||
|  | ||||
| 			vdp_.set_scan_target(scan_target); | ||||
| 			vdp_->set_scan_target(scan_target); | ||||
| 		} | ||||
|  | ||||
| 		void set_display_type(Outputs::Display::DisplayType display_type) override { | ||||
| 			vdp_.set_display_type(display_type); | ||||
| 			vdp_->set_display_type(display_type); | ||||
| 		} | ||||
|  | ||||
| 		Outputs::Speaker::Speaker *get_speaker() override { | ||||
| @@ -184,7 +190,7 @@ class ConcreteMachine: | ||||
| 		} | ||||
|  | ||||
| 		forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { | ||||
| 			time_since_vdp_update_ += cycle.length; | ||||
| 			vdp_ += cycle.length; | ||||
| 			time_since_sn76489_update_ += cycle.length; | ||||
|  | ||||
| 			if(cycle.is_terminal()) { | ||||
| @@ -228,17 +234,15 @@ class ConcreteMachine: | ||||
| 								*cycle.value = 0xff; | ||||
| 							break; | ||||
| 							case 0x40: | ||||
| 								update_video(); | ||||
| 								*cycle.value = vdp_.get_current_line(); | ||||
| 								*cycle.value = vdp_->get_current_line(); | ||||
| 							break; | ||||
| 							case 0x41: | ||||
| 								*cycle.value = vdp_.get_latched_horizontal_counter(); | ||||
| 								*cycle.value = vdp_.last_valid()->get_latched_horizontal_counter(); | ||||
| 							break; | ||||
| 							case 0x80: case 0x81: | ||||
| 								update_video(); | ||||
| 								*cycle.value = vdp_.get_register(address); | ||||
| 								z80_.set_interrupt_line(vdp_.get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_.get_time_until_interrupt(); | ||||
| 								*cycle.value = vdp_->get_register(address); | ||||
| 								z80_.set_interrupt_line(vdp_->get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_->get_time_until_interrupt(); | ||||
| 							break; | ||||
| 							case 0xc0: { | ||||
| 								Joystick *const joypad1 = static_cast<Joystick *>(joysticks_[0].get()); | ||||
| @@ -279,8 +283,7 @@ class ConcreteMachine: | ||||
|  | ||||
| 								// Latch if either TH has newly gone to 1. | ||||
| 								if((new_ths^previous_ths)&new_ths) { | ||||
| 									update_video(); | ||||
| 									vdp_.latch_horizontal_counter(); | ||||
| 									vdp_->latch_horizontal_counter(); | ||||
| 								} | ||||
| 							} break; | ||||
| 							case 0x40: case 0x41: | ||||
| @@ -288,10 +291,9 @@ class ConcreteMachine: | ||||
| 								sn76489_.set_register(*cycle.value); | ||||
| 							break; | ||||
| 							case 0x80: case 0x81: | ||||
| 								update_video(); | ||||
| 								vdp_.set_register(address, *cycle.value); | ||||
| 								z80_.set_interrupt_line(vdp_.get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_.get_time_until_interrupt(); | ||||
| 								vdp_->set_register(address, *cycle.value); | ||||
| 								z80_.set_interrupt_line(vdp_->get_interrupt_line()); | ||||
| 								time_until_interrupt_ = vdp_->get_time_until_interrupt(); | ||||
| 							break; | ||||
| 							case 0xc0: | ||||
| 								LOG("TODO: [output] I/O port A/N; " << int(*cycle.value)); | ||||
| @@ -326,15 +328,14 @@ class ConcreteMachine: | ||||
| 			time_until_debounce_ -= cycle.length; | ||||
| 			if(time_until_debounce_ <= HalfCycles(0)) { | ||||
| 				z80_.set_non_maskable_interrupt_line(pause_is_pressed_); | ||||
| 				update_video(); | ||||
| 				time_until_debounce_ = vdp_.get_time_until_line(-1); | ||||
| 				time_until_debounce_ = vdp_->get_time_until_line(-1); | ||||
| 			} | ||||
|  | ||||
| 			return HalfCycles(0); | ||||
| 		} | ||||
|  | ||||
| 		void flush() { | ||||
| 			update_video(); | ||||
| 			vdp_.flush(); | ||||
| 			update_audio(); | ||||
| 			audio_queue_.perform(); | ||||
| 		} | ||||
| @@ -407,16 +408,13 @@ class ConcreteMachine: | ||||
| 		inline void update_audio() { | ||||
| 			speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(sn76489_divider))); | ||||
| 		} | ||||
| 		inline void update_video() { | ||||
| 			vdp_.run_for(time_since_vdp_update_.flush()); | ||||
| 		} | ||||
|  | ||||
| 		using Target = Analyser::Static::Sega::Target; | ||||
| 		Target::Model model_; | ||||
| 		Target::Region region_; | ||||
| 		Target::PagingScheme paging_scheme_; | ||||
| 		CPU::Z80::Processor<ConcreteMachine, false, false> z80_; | ||||
| 		TI::TMS::TMS9918 vdp_; | ||||
| 		JustInTimeActor<TI::TMS::TMS9918, HalfCycles> vdp_; | ||||
|  | ||||
| 		Concurrency::DeferringAsyncTaskQueue audio_queue_; | ||||
| 		TI::SN76489 sn76489_; | ||||
| @@ -426,7 +424,6 @@ class ConcreteMachine: | ||||
| 		Inputs::Keyboard keyboard_; | ||||
| 		bool reset_is_pressed_ = false, pause_is_pressed_ = false; | ||||
|  | ||||
| 		HalfCycles time_since_vdp_update_; | ||||
| 		HalfCycles time_since_sn76489_update_; | ||||
| 		HalfCycles time_until_interrupt_; | ||||
| 		HalfCycles time_until_debounce_; | ||||
|   | ||||
							
								
								
									
										23
									
								
								Machines/MouseMachine.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								Machines/MouseMachine.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| // | ||||
| //  MouseMachine.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 11/06/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MouseMachine_hpp | ||||
| #define MouseMachine_hpp | ||||
|  | ||||
| #include "../Inputs/Mouse.hpp" | ||||
|  | ||||
| namespace MouseMachine { | ||||
|  | ||||
| class Machine { | ||||
| 	public: | ||||
| 		virtual Inputs::Mouse &get_mouse() = 0; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* MouseMachine_hpp */ | ||||
| @@ -170,7 +170,7 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler { | ||||
| 		/*! | ||||
| 			Advances time. This class manages the AY's concept of time to permit updating-on-demand. | ||||
| 		*/ | ||||
| 		inline void run_for(const Cycles cycles) { | ||||
| 		inline void run_for(const HalfCycles cycles) { | ||||
| 			cycles_since_ay_update_ += cycles; | ||||
| 		} | ||||
|  | ||||
| @@ -182,11 +182,11 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler { | ||||
|  | ||||
| 	private: | ||||
| 		void update_ay() { | ||||
| 			speaker_.run_for(audio_queue_, cycles_since_ay_update_.flush()); | ||||
| 			speaker_.run_for(audio_queue_, cycles_since_ay_update_.flush<Cycles>()); | ||||
| 		} | ||||
| 		bool ay_bdir_ = false; | ||||
| 		bool ay_bc1_ = false; | ||||
| 		Cycles cycles_since_ay_update_; | ||||
| 		HalfCycles cycles_since_ay_update_; | ||||
|  | ||||
| 		Concurrency::DeferringAsyncTaskQueue &audio_queue_; | ||||
| 		GI::AY38910::AY38910 &ay8910_; | ||||
| @@ -228,19 +228,34 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co | ||||
| 				diskii_.set_clocking_hint_observer(this); | ||||
| 			} | ||||
|  | ||||
| 			std::vector<std::string> rom_names = {"colour.rom"}; | ||||
| 			const std::string machine_name = "Oric"; | ||||
| 			std::vector<ROMMachine::ROM> rom_names = { {machine_name, "the Oric colour ROM", "colour.rom", 128, 0xd50fca65} }; | ||||
| 			switch(target.rom) { | ||||
| 				case Analyser::Static::Oric::Target::ROM::BASIC10: rom_names.push_back("basic10.rom");	break; | ||||
| 				case Analyser::Static::Oric::Target::ROM::BASIC11: rom_names.push_back("basic11.rom");	break; | ||||
| 				case Analyser::Static::Oric::Target::ROM::Pravetz: rom_names.push_back("pravetz.rom");	break; | ||||
| 				case Analyser::Static::Oric::Target::ROM::BASIC10: | ||||
| 					rom_names.emplace_back(machine_name, "Oric BASIC 1.0", "basic10.rom", 16*1024, 0xf18710b4); | ||||
| 				break; | ||||
| 				case Analyser::Static::Oric::Target::ROM::BASIC11: | ||||
| 					rom_names.emplace_back(machine_name, "Oric BASIC 1.1", "basic11.rom", 16*1024, 0xc3a92bef); | ||||
| 				break; | ||||
| 				case Analyser::Static::Oric::Target::ROM::Pravetz: | ||||
| 					rom_names.emplace_back(machine_name, "Pravetz BASIC", "pravetz.rom", 16*1024, 0x58079502); | ||||
| 				break; | ||||
| 			} | ||||
| 			size_t diskii_state_machine_index = 0; | ||||
| 			switch(disk_interface) { | ||||
| 				default: break; | ||||
| 				case Analyser::Static::Oric::Target::DiskInterface::Microdisc:	rom_names.push_back("microdisc.rom");	break; | ||||
| 				case Analyser::Static::Oric::Target::DiskInterface::Pravetz:	rom_names.push_back("8dos.rom");		break; | ||||
| 				case Analyser::Static::Oric::Target::DiskInterface::Microdisc: | ||||
| 					rom_names.emplace_back(machine_name, "the ORIC Microdisc ROM", "microdisc.rom", 8*1024, 0xa9664a9c); | ||||
| 				break; | ||||
| 				case Analyser::Static::Oric::Target::DiskInterface::Pravetz: | ||||
| 					rom_names.emplace_back(machine_name, "the 8DOS boot ROM", "8dos.rom", 512, 0x49a74c06); | ||||
| 					// These ROM details are coupled with those in the DiskIICard. | ||||
| 					diskii_state_machine_index = rom_names.size(); | ||||
| 					rom_names.push_back({"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 }}); | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			const auto roms = rom_fetcher("Oric", rom_names); | ||||
| 			const auto roms = rom_fetcher(rom_names); | ||||
|  | ||||
| 			for(std::size_t index = 0; index < roms.size(); ++index) { | ||||
| 				if(!roms[index]) { | ||||
| @@ -261,11 +276,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co | ||||
| 					pravetz_rom_ = std::move(*roms[2]); | ||||
| 					pravetz_rom_.resize(512); | ||||
|  | ||||
| 					auto state_machine_rom = rom_fetcher("DiskII", {"state-machine-16.rom"}); | ||||
| 					if(!state_machine_rom[0]) { | ||||
| 						throw ROMMachine::Error::MissingROMs; | ||||
| 					} | ||||
| 					diskii_.set_state_machine(*state_machine_rom[0]); | ||||
| 					diskii_.set_state_machine(*roms[diskii_state_machine_index]); | ||||
| 				} break; | ||||
| 			} | ||||
|  | ||||
| @@ -434,7 +445,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co | ||||
| 			} | ||||
|  | ||||
| 			via_.run_for(Cycles(1)); | ||||
| 			via_port_handler_.run_for(Cycles(1)); | ||||
| 			tape_player_.run_for(Cycles(1)); | ||||
| 			switch(disk_interface) { | ||||
| 				default: break; | ||||
| @@ -456,7 +466,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co | ||||
|  | ||||
| 		forceinline void flush() { | ||||
| 			update_video(); | ||||
| 			via_port_handler_.flush(); | ||||
| 			via_.flush(); | ||||
| 			flush_diskii(); | ||||
| 		} | ||||
|  | ||||
| @@ -575,7 +585,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co | ||||
| 		uint8_t ram_[65536]; | ||||
| 		Cycles cycles_since_video_update_; | ||||
| 		inline void update_video() { | ||||
| 			video_output_.run_for(cycles_since_video_update_.flush()); | ||||
| 			video_output_.run_for(cycles_since_video_update_.flush<Cycles>()); | ||||
| 		} | ||||
|  | ||||
| 		// ROM bookkeeping | ||||
| @@ -607,7 +617,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co | ||||
| 		Apple::DiskII diskii_; | ||||
| 		Cycles cycles_since_diskii_update_; | ||||
| 		void flush_diskii() { | ||||
| 			diskii_.run_for(cycles_since_diskii_update_.flush()); | ||||
| 			diskii_.run_for(cycles_since_diskii_update_.flush<Cycles>()); | ||||
| 		} | ||||
| 		std::vector<uint8_t> pravetz_rom_; | ||||
| 		std::size_t pravetz_rom_base_pointer_ = 0; | ||||
|   | ||||
| @@ -16,15 +16,41 @@ | ||||
|  | ||||
| namespace ROMMachine { | ||||
|  | ||||
| /*! | ||||
| 	Describes a ROM image; this term is used in this emulator strictly in the sense of firmware — | ||||
| 	system software that is an inherent part of a machine. | ||||
| */ | ||||
| struct ROM { | ||||
| 	/// The machine with which this ROM is associated, in a form that is safe for using as | ||||
| 	/// part of a file name. | ||||
| 	std::string machine_name; | ||||
| 	/// A descriptive name for this ROM, suitable for use in a bullet-point list, a bracket | ||||
| 	/// clause, etc, e.g. "the Electron MOS 1.0". | ||||
| 	std::string descriptive_name; | ||||
| 	/// An idiomatic file name for this ROM, e.g. "os10.rom". | ||||
| 	std::string file_name; | ||||
| 	/// The expected size of this ROM in bytes, e.g. 32768. | ||||
| 	size_t size = 0; | ||||
| 	/// CRC32s for all known acceptable copies of this ROM; intended to allow a host platform | ||||
| 	/// to test user-provided ROMs of unknown provenance. **Not** intended to be used | ||||
| 	/// to exclude ROMs where the user's intent is otherwise clear. | ||||
| 	std::vector<uint32_t> crc32s; | ||||
|  | ||||
| 	ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, uint32_t crc32) : | ||||
| 		machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s({crc32}) {} | ||||
| 	ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, std::initializer_list<uint32_t> crc32s) : | ||||
| 		machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s(crc32s) {} | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Defines the signature for a function that must be supplied by the host environment in order to give machines | ||||
| 	a route for fetching any system ROMs they might need. | ||||
|  | ||||
| 	The caller will supply the idiomatic name of the machine plus a vector of the names of ROM files that it expects | ||||
| 	to be present. The recevier should return a vector of unique_ptrs that either contain the contents of the | ||||
| 	ROM from @c names that corresponds by index, or else are the nullptr | ||||
| 	The caller will supply a vector of the names of ROM files that it would like to inspect. The recevier should | ||||
| 	return a vector of unique_ptrs that either contain the contents of the ROM from @c names that corresponds by | ||||
| 	index, or else are @c nullptr. | ||||
| */ | ||||
| typedef std::function<std::vector<std::unique_ptr<std::vector<uint8_t>>>(const std::string &machine, const std::vector<std::string> &names)> ROMFetcher; | ||||
| typedef std::function<std::vector<std::unique_ptr<std::vector<uint8_t>>>(const std::vector<ROM> &roms)> ROMFetcher; | ||||
|  | ||||
| enum class Error { | ||||
| 	MissingROMs | ||||
|   | ||||
| @@ -9,7 +9,8 @@ | ||||
| #include "MachineForTarget.hpp" | ||||
|  | ||||
| #include "../AmstradCPC/AmstradCPC.hpp" | ||||
| #include "../AppleII/AppleII.hpp" | ||||
| #include "../Apple/AppleII/AppleII.hpp" | ||||
| #include "../Apple/Macintosh/Macintosh.hpp" | ||||
| #include "../Atari2600/Atari2600.hpp" | ||||
| #include "../ColecoVision/ColecoVision.hpp" | ||||
| #include "../Commodore/Vic-20/Vic20.hpp" | ||||
| @@ -33,14 +34,15 @@ namespace { | ||||
| #define Bind(m)	BindD(m, m) | ||||
| 		switch(target->machine) { | ||||
| 			Bind(AmstradCPC) | ||||
| 			Bind(AppleII) | ||||
| 			BindD(Apple::II, AppleII) | ||||
| 			BindD(Apple::Macintosh, Macintosh) | ||||
| 			Bind(Atari2600) | ||||
| 			BindD(Coleco::Vision, ColecoVision) | ||||
| 			BindD(Commodore::Vic20, Vic20) | ||||
| 			Bind(Electron) | ||||
| 			BindD(Sega::MasterSystem, MasterSystem) | ||||
| 			Bind(MSX) | ||||
| 			Bind(Oric) | ||||
| 			BindD(Commodore::Vic20, Vic20) | ||||
| 			BindD(Sega::MasterSystem, MasterSystem) | ||||
| 			Bind(ZX8081) | ||||
|  | ||||
| 			default: | ||||
| @@ -103,6 +105,7 @@ std::string Machine::ShortNameForTargetMachine(const Analyser::Machine machine) | ||||
| 		case Analyser::Machine::Atari2600:		return "Atari2600"; | ||||
| 		case Analyser::Machine::ColecoVision:	return "ColecoVision"; | ||||
| 		case Analyser::Machine::Electron:		return "Electron"; | ||||
| 		case Analyser::Machine::Macintosh:		return "Macintosh"; | ||||
| 		case Analyser::Machine::MSX:			return "MSX"; | ||||
| 		case Analyser::Machine::Oric:			return "Oric"; | ||||
| 		case Analyser::Machine::Vic20:			return "Vic20"; | ||||
| @@ -119,6 +122,7 @@ std::string Machine::LongNameForTargetMachine(Analyser::Machine machine) { | ||||
| 		case Analyser::Machine::Atari2600:		return "Atari 2600"; | ||||
| 		case Analyser::Machine::ColecoVision:	return "ColecoVision"; | ||||
| 		case Analyser::Machine::Electron:		return "Acorn Electron"; | ||||
| 		case Analyser::Machine::Macintosh:		return "Apple Macintosh"; | ||||
| 		case Analyser::Machine::MSX:			return "MSX"; | ||||
| 		case Analyser::Machine::Oric:			return "Oric"; | ||||
| 		case Analyser::Machine::Vic20:			return "Vic 20"; | ||||
| @@ -132,7 +136,7 @@ std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> Machin | ||||
| 	std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> options; | ||||
|  | ||||
| 	options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::AmstradCPC), AmstradCPC::get_options())); | ||||
| 	options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::AppleII), AppleII::get_options())); | ||||
| 	options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::AppleII), Apple::II::get_options())); | ||||
| 	options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::ColecoVision), Coleco::Vision::get_options())); | ||||
| 	options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::Electron), Electron::get_options())); | ||||
| 	options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::MSX), MSX::get_options())); | ||||
|   | ||||
| @@ -23,6 +23,10 @@ void Memory::Fuzz(uint8_t *buffer, std::size_t size) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void Memory::Fuzz(uint16_t *buffer, std::size_t size) { | ||||
| 	Fuzz(reinterpret_cast<uint8_t *>(buffer), size * sizeof(uint16_t)); | ||||
| } | ||||
|  | ||||
| void Memory::Fuzz(std::vector<uint8_t> &buffer) { | ||||
| 	Fuzz(buffer.data(), buffer.size()); | ||||
| } | ||||
|   | ||||
| @@ -18,7 +18,10 @@ namespace Memory { | ||||
| /// Stores @c size random bytes from @c buffer onwards. | ||||
| void Fuzz(uint8_t *buffer, std::size_t size); | ||||
|  | ||||
| // Replaces all existing vector contents with random bytes. | ||||
| /// Stores @c size random 16-bit words from @c buffer onwards. | ||||
| void Fuzz(uint16_t *buffer, std::size_t size); | ||||
|  | ||||
| /// Replaces all existing vector contents with random bytes. | ||||
| void Fuzz(std::vector<uint8_t> &buffer); | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										15
									
								
								Machines/Utility/MemoryPacker.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Machines/Utility/MemoryPacker.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| // | ||||
| //  MemoryPacker.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MemoryPacker.hpp" | ||||
|  | ||||
| void Memory::PackBigEndian16(const std::vector<uint8_t> &source, uint16_t *target) { | ||||
| 	for(std::size_t c = 0; c < source.size(); c += 2) { | ||||
| 		target[c >> 1] = uint16_t(source[c] << 8) | uint16_t(source[c+1]); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										24
									
								
								Machines/Utility/MemoryPacker.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Machines/Utility/MemoryPacker.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // | ||||
| //  MemoryPacker.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/05/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MemoryPacker_hpp | ||||
| #define MemoryPacker_hpp | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Memory { | ||||
|  | ||||
| /*! | ||||
| 	Copies the bytes from @c source into @c target, interpreting them | ||||
| 	as big-endian 16-bit data. | ||||
| */ | ||||
| void PackBigEndian16(const std::vector<uint8_t> &source, uint16_t *target); | ||||
|  | ||||
| } | ||||
| #endif /* MemoryPacker_hpp */ | ||||
| @@ -45,6 +45,10 @@ template<typename T> class TypedDynamicMachine: public ::Machine::DynamicMachine | ||||
| 			return get<KeyboardMachine::Machine>(); | ||||
| 		} | ||||
|  | ||||
| 		MouseMachine::Machine *mouse_machine() override { | ||||
| 			return get<MouseMachine::Machine>(); | ||||
| 		} | ||||
|  | ||||
| 		Configurable::Device *configurable_device() override { | ||||
| 			return get<Configurable::Device>(); | ||||
| 		} | ||||
|   | ||||
| @@ -76,7 +76,11 @@ template<bool is_zx81> class ConcreteMachine: | ||||
| 			clear_all_keys(); | ||||
|  | ||||
| 			const bool use_zx81_rom = target.is_ZX81 || target.ZX80_uses_ZX81_ROM; | ||||
| 			const auto roms = rom_fetcher("ZX8081", { use_zx81_rom ? "zx81.rom" : "zx80.rom" }); | ||||
| 			const auto roms = | ||||
| 				use_zx81_rom ? | ||||
| 					rom_fetcher({ {"ZX8081", "the ZX81 BASIC ROM", "zx81.rom", 8 * 1024, 0x4b1dd6eb} }) : | ||||
| 					rom_fetcher({ {"ZX8081", "the ZX80 BASIC ROM", "zx80.rom", 4 * 1024, 0x4c7fc597} }); | ||||
|  | ||||
| 			if(!roms[0]) throw ROMMachine::Error::MissingROMs; | ||||
|  | ||||
| 			rom_ = std::move(*roms[0]); | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +1,9 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <Workspace | ||||
|    version = "1.0"> | ||||
|    <FileRef | ||||
|       location = "group:/Users/thomasharte/Projects/CLK/OSBindings/Mac/Clock SignalTests/68000ArithmeticTests.mm"> | ||||
|    </FileRef> | ||||
|    <FileRef | ||||
|       location = "self:Clock Signal.xcodeproj"> | ||||
|    </FileRef> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <Scheme | ||||
|    LastUpgradeVersion = "0930" | ||||
|    LastUpgradeVersion = "1030" | ||||
|    version = "1.3"> | ||||
|    <BuildAction | ||||
|       parallelizeBuildables = "YES" | ||||
| @@ -26,6 +26,7 @@ | ||||
|       buildConfiguration = "Debug" | ||||
|       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||||
|       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||||
|       disableMainThreadChecker = "YES" | ||||
|       codeCoverageEnabled = "YES" | ||||
|       shouldUseLaunchSchemeArgsEnv = "YES"> | ||||
|       <Testables> | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> | ||||
| <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> | ||||
|     <dependencies> | ||||
|         <deployment identifier="macosx"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/> | ||||
|         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||||
|     </dependencies> | ||||
|     <objects> | ||||
| @@ -14,7 +14,7 @@ | ||||
|         </customObject> | ||||
|         <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> | ||||
|         <customObject id="-3" userLabel="Application" customClass="NSObject"/> | ||||
|         <window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="xOd-HO-29H" userLabel="Window"> | ||||
|         <window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="xOd-HO-29H" userLabel="Window"> | ||||
|             <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/> | ||||
|             <windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="YES"/> | ||||
|             <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> | ||||
| @@ -25,7 +25,7 @@ | ||||
|                 <rect key="frame" x="0.0" y="0.0" width="600" height="450"/> | ||||
|                 <autoresizingMask key="autoresizingMask"/> | ||||
|                 <subviews> | ||||
|                     <openGLView hidden="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSOpenGLView"> | ||||
|                     <openGLView hidden="YES" wantsLayer="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSOpenGLView"> | ||||
|                         <rect key="frame" x="0.0" y="0.0" width="600" height="450"/> | ||||
|                     </openGLView> | ||||
|                 </subviews> | ||||
| @@ -36,6 +36,11 @@ | ||||
|                     <constraint firstItem="DEG-fq-cjd" firstAttribute="width" secondItem="gIp-Ho-8D9" secondAttribute="width" id="mYS-bH-DST"/> | ||||
|                 </constraints> | ||||
|             </view> | ||||
|             <userDefinedRuntimeAttributes> | ||||
|                 <userDefinedRuntimeAttribute type="color" keyPath="backgroundColor"> | ||||
|                     <color key="value" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | ||||
|                 </userDefinedRuntimeAttribute> | ||||
|             </userDefinedRuntimeAttributes> | ||||
|             <connections> | ||||
|                 <outlet property="delegate" destination="-2" id="0bl-1N-x8E"/> | ||||
|                 <outlet property="initialFirstResponder" destination="DEG-fq-cjd" id="9RI-Kx-QeN"/> | ||||
|   | ||||
| @@ -10,10 +10,13 @@ | ||||
|  | ||||
| #import "CSStaticAnalyser.h" | ||||
|  | ||||
| #import	"CSOpenGLView.h" | ||||
| #import "CSAudioQueue.h" | ||||
| #import	"CSOpenGLView.h" | ||||
| #import "CSROMReceiverView.h" | ||||
|  | ||||
| #import "CSBestEffortUpdater.h" | ||||
| #import "CSJoystickManager.h" | ||||
|  | ||||
| #import "NSData+CRC32.h" | ||||
|  | ||||
| #include "KeyCodes.h" | ||||
|   | ||||
| @@ -16,109 +16,76 @@ class MachineDocument: | ||||
| 	CSOpenGLViewDelegate, | ||||
| 	CSOpenGLViewResponderDelegate, | ||||
| 	CSBestEffortUpdaterDelegate, | ||||
| 	CSAudioQueueDelegate | ||||
| 	CSAudioQueueDelegate, | ||||
| 	CSROMReciverViewDelegate | ||||
| { | ||||
| 	fileprivate let actionLock = NSLock() | ||||
| 	fileprivate let drawLock = NSLock() | ||||
| 	fileprivate let bestEffortLock = NSLock() | ||||
| 	// MARK: - Mutual Exclusion. | ||||
|  | ||||
| 	var machine: CSMachine! | ||||
| 	var name: String! { | ||||
| 		get { | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| 	var optionsPanelNibName: String? | ||||
| 	/// Ensures exclusive access between calls to self.machine.run and close(). | ||||
| 	private let actionLock = NSLock() | ||||
| 	/// Ensures exclusive access between calls to machine.updateView and machine.drawView, and close(). | ||||
| 	private let drawLock = NSLock() | ||||
| 	/// Ensures exclusive access to the best-effort updater. | ||||
| 	private let bestEffortLock = NSLock() | ||||
|  | ||||
| 	func aspectRatio() -> NSSize { | ||||
| 	// MARK: - Machine details. | ||||
|  | ||||
| 	/// A description of the machine this document should represent once fully set up. | ||||
| 	private var machineDescription: CSStaticAnalyser? | ||||
|  | ||||
| 	/// The active machine, following its successful creation. | ||||
| 	private var machine: CSMachine! | ||||
|  | ||||
| 	/// @returns the appropriate window content aspect ratio for this @c self.machine. | ||||
| 	private func aspectRatio() -> NSSize { | ||||
| 		return NSSize(width: 4.0, height: 3.0) | ||||
| 	} | ||||
|  | ||||
| 	/// The output audio queue, if any. | ||||
| 	private var audioQueue: CSAudioQueue! | ||||
|  | ||||
| 	/// The best-effort updater. | ||||
| 	private var bestEffortUpdater: CSBestEffortUpdater? | ||||
|  | ||||
| 	// MARK: - Main NIB connections. | ||||
|  | ||||
| 	/// The OpenGL view to receive this machine's display. | ||||
| 	@IBOutlet weak var openGLView: CSOpenGLView! | ||||
|  | ||||
| 	/// The options panel, if any. | ||||
| 	@IBOutlet var optionsPanel: MachinePanel! | ||||
|  | ||||
| 	/// An action to display the options panel, if there is one. | ||||
| 	@IBAction func showOptions(_ sender: AnyObject!) { | ||||
| 		optionsPanel?.setIsVisible(true) | ||||
| 	} | ||||
|  | ||||
| 	/// The activity panel, if one is deemed appropriate. | ||||
| 	@IBOutlet var activityPanel: NSPanel! | ||||
|  | ||||
| 	/// An action to display the activity panel, if there is one. | ||||
| 	@IBAction func showActivity(_ sender: AnyObject!) { | ||||
| 		activityPanel.setIsVisible(true) | ||||
| 	} | ||||
|  | ||||
| 	fileprivate var audioQueue: CSAudioQueue! = nil | ||||
| 	fileprivate var bestEffortUpdater: CSBestEffortUpdater? | ||||
| 	// MARK: - NSDocument Overrides and NSWindowDelegate methods. | ||||
|  | ||||
| 	/// Links this class to the MachineDocument NIB. | ||||
| 	override var windowNibName: NSNib.Name? { | ||||
| 		return NSNib.Name(rawValue: "MachineDocument") | ||||
| 		return "MachineDocument" | ||||
| 	} | ||||
|  | ||||
| 	override func windowControllerDidLoadNib(_ aController: NSWindowController) { | ||||
| 		super.windowControllerDidLoadNib(aController) | ||||
| 		aController.window?.contentAspectRatio = self.aspectRatio() | ||||
| 		setupMachineOutput() | ||||
| 	convenience init(type typeName: String) throws { | ||||
| 		self.init() | ||||
| 		self.fileType = typeName | ||||
| 	} | ||||
|  | ||||
| 	// Attempting to show a sheet before the window is visible (such as when the NIB is loaded) results in | ||||
| 	// a sheet mysteriously floating on its own. For now, use windowDidUpdate as a proxy to know that the window | ||||
| 	// is visible, though it's a little premature. | ||||
| 	func windowDidUpdate(_ notification: Notification) { | ||||
| 		if self.shouldShowNewMachinePanel { | ||||
| 			self.shouldShowNewMachinePanel = false | ||||
| 			Bundle.main.loadNibNamed(NSNib.Name(rawValue: "MachinePicker"), owner: self, topLevelObjects: nil) | ||||
| 			self.machinePicker?.establishStoredOptions() | ||||
| 			self.windowControllers[0].window?.beginSheet(self.machinePickerPanel!, completionHandler: nil) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	fileprivate func setupMachineOutput() { | ||||
| 		if let machine = self.machine, let openGLView = self.openGLView { | ||||
| 			// establish the output aspect ratio and audio | ||||
| 			let aspectRatio = self.aspectRatio() | ||||
| 			openGLView.perform(glContext: { | ||||
| 				machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height)) | ||||
| 			}) | ||||
|  | ||||
| 			// attach an options panel if one is available | ||||
| 			if let optionsPanelNibName = self.optionsPanelNibName { | ||||
| 				Bundle.main.loadNibNamed(NSNib.Name(rawValue: optionsPanelNibName), owner: self, topLevelObjects: nil) | ||||
| 				self.optionsPanel.machine = machine | ||||
| 				self.optionsPanel?.establishStoredOptions() | ||||
| 				showOptions(self) | ||||
| 			} | ||||
|  | ||||
| 			machine.delegate = self | ||||
| 			self.bestEffortUpdater = CSBestEffortUpdater() | ||||
|  | ||||
| 			// callbacks from the OpenGL may come on a different thread, immediately following the .delegate set; | ||||
| 			// hence the full setup of the best-effort updater prior to setting self as a delegate | ||||
| 			openGLView.delegate = self | ||||
| 			openGLView.responderDelegate = self | ||||
|  | ||||
| 			setupAudioQueueClockRate() | ||||
|  | ||||
| 			// bring OpenGL view-holding window on top of the options panel and show the content | ||||
| 			openGLView.isHidden = false | ||||
| 			openGLView.window!.makeKeyAndOrderFront(self) | ||||
| 			openGLView.window!.makeFirstResponder(openGLView) | ||||
|  | ||||
| 			// start accepting best effort updates | ||||
| 			self.bestEffortUpdater!.delegate = self | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	func machineSpeakerDidChangeInputClock(_ machine: CSMachine) { | ||||
| 		setupAudioQueueClockRate() | ||||
| 	} | ||||
|  | ||||
| 	fileprivate func setupAudioQueueClockRate() { | ||||
| 		// establish and provide the audio queue, taking advice as to an appropriate sampling rate | ||||
| 		let maximumSamplingRate = CSAudioQueue.preferredSamplingRate() | ||||
| 		let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate))) | ||||
| 		if selectedSamplingRate > 0 { | ||||
| 			audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate)) | ||||
| 			audioQueue.delegate = self | ||||
| 			self.machine.audioQueue = self.audioQueue | ||||
| 			self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize) | ||||
| 	override func read(from url: URL, ofType typeName: String) throws { | ||||
| 		if let analyser = CSStaticAnalyser(fileAt: url) { | ||||
| 			self.displayName = analyser.displayName | ||||
| 			self.configureAs(analyser) | ||||
| 		} else { | ||||
| 			throw NSError(domain: "MachineDocument", code: -1, userInfo: nil) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -148,56 +115,135 @@ class MachineDocument: | ||||
| 		super.close() | ||||
| 	} | ||||
|  | ||||
| 	// MARK: configuring | ||||
| 	override func data(ofType typeName: String) throws -> Data { | ||||
| 		throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil) | ||||
| 	} | ||||
|  | ||||
| 	override func windowControllerDidLoadNib(_ aController: NSWindowController) { | ||||
| 		super.windowControllerDidLoadNib(aController) | ||||
| 		aController.window?.contentAspectRatio = self.aspectRatio() | ||||
| 	} | ||||
|  | ||||
| 	private var missingROMs: [CSMissingROM] = [] | ||||
| 	func configureAs(_ analysis: CSStaticAnalyser) { | ||||
| 		if let machine = CSMachine(analyser: analysis) { | ||||
| 		self.machineDescription = analysis | ||||
|  | ||||
| 		let missingROMs = NSMutableArray() | ||||
| 		if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) { | ||||
| 			self.machine = machine | ||||
| 			self.optionsPanelNibName = analysis.optionsPanelNibName | ||||
| 			setupMachineOutput() | ||||
| 			setupActivityDisplay() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	fileprivate var shouldShowNewMachinePanel = false | ||||
| 	override func read(from url: URL, ofType typeName: String) throws { | ||||
| 		if let analyser = CSStaticAnalyser(fileAt: url) { | ||||
| 			self.displayName = analyser.displayName | ||||
| 			self.configureAs(analyser) | ||||
| 		} else { | ||||
| 			throw NSError(domain: "MachineDocument", code: -1, userInfo: nil) | ||||
| 			// Store the selected machine and list of missing ROMs, and | ||||
| 			// show the missing ROMs dialogue. | ||||
| 			self.missingROMs = [] | ||||
| 			for untypedMissingROM in missingROMs { | ||||
| 				self.missingROMs.append(untypedMissingROM as! CSMissingROM) | ||||
| 			} | ||||
|  | ||||
| 			requestRoms() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	convenience init(type typeName: String) throws { | ||||
| 		self.init() | ||||
| 		self.fileType = typeName | ||||
| 		self.shouldShowNewMachinePanel = true | ||||
| 	enum InteractionMode { | ||||
| 		case notStarted, showingMachinePicker, showingROMRequester, showingMachine | ||||
| 	} | ||||
| 	private var interactionMode: InteractionMode = .notStarted | ||||
|  | ||||
| 	// Attempting to show a sheet before the window is visible (such as when the NIB is loaded) results in | ||||
| 	// a sheet mysteriously floating on its own. For now, use windowDidUpdate as a proxy to know that the window | ||||
| 	// is visible, though it's a little premature. | ||||
| 	func windowDidUpdate(_ notification: Notification) { | ||||
| 		// If an interaction mode is not yet in effect, pick the proper one and display the relevant thing. | ||||
| 		if self.interactionMode == .notStarted { | ||||
| 			// If a full machine exists, just continue showing it. | ||||
| 			if self.machine != nil { | ||||
| 				self.interactionMode = .showingMachine | ||||
| 				setupMachineOutput() | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 	// MARK: the pasteboard | ||||
| 	func paste(_ sender: Any) { | ||||
| 		let pasteboard = NSPasteboard.general | ||||
| 		if let string = pasteboard.string(forType: .string) { | ||||
| 			self.machine.paste(string) | ||||
| 			// If a machine has been picked but is not showing, there must be ROMs missing. | ||||
| 			if self.machineDescription != nil { | ||||
| 				self.interactionMode = .showingROMRequester | ||||
| 				requestRoms() | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// If a machine hasn't even been picked yet, show the machine picker. | ||||
| 			self.interactionMode = .showingMachinePicker | ||||
| 			Bundle.main.loadNibNamed("MachinePicker", owner: self, topLevelObjects: nil) | ||||
| 			self.machinePicker?.establishStoredOptions() | ||||
| 			self.windowControllers[0].window?.beginSheet(self.machinePickerPanel!, completionHandler: nil) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: CSBestEffortUpdaterDelegate | ||||
| 	final func bestEffortUpdater(_ bestEffortUpdater: CSBestEffortUpdater!, runForInterval duration: TimeInterval, didSkipPreviousUpdate: Bool) { | ||||
| 		if actionLock.try() { | ||||
| 			self.machine.run(forInterval: duration) | ||||
| 			actionLock.unlock() | ||||
| 	// MARK: - Connections Between Machine and the Outside World | ||||
|  | ||||
| 	private func setupMachineOutput() { | ||||
| 		if let machine = self.machine, let openGLView = self.openGLView { | ||||
| 			// Establish the output aspect ratio and audio. | ||||
| 			let aspectRatio = self.aspectRatio() | ||||
| 			openGLView.perform(glContext: { | ||||
| 				machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height)) | ||||
| 			}) | ||||
|  | ||||
| 			// Attach an options panel if one is available. | ||||
| 			if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName { | ||||
| 				Bundle.main.loadNibNamed(optionsPanelNibName, owner: self, topLevelObjects: nil) | ||||
| 				self.optionsPanel.machine = machine | ||||
| 				self.optionsPanel?.establishStoredOptions() | ||||
| 				showOptions(self) | ||||
| 			} | ||||
|  | ||||
| 			machine.delegate = self | ||||
| 			self.bestEffortUpdater = CSBestEffortUpdater() | ||||
|  | ||||
| 			// Callbacks from the OpenGL may come on a different thread, immediately following the .delegate set; | ||||
| 			// hence the full setup of the best-effort updater prior to setting self as a delegate. | ||||
| 			openGLView.delegate = self | ||||
| 			openGLView.responderDelegate = self | ||||
|  | ||||
| 			// If this machine has a mouse, enable mouse capture. | ||||
| 			openGLView.shouldCaptureMouse = machine.hasMouse | ||||
|  | ||||
| 			setupAudioQueueClockRate() | ||||
|  | ||||
| 			// Bring OpenGL view-holding window on top of the options panel and show the content. | ||||
| 			openGLView.isHidden = false | ||||
| 			openGLView.window!.makeKeyAndOrderFront(self) | ||||
| 			openGLView.window!.makeFirstResponder(openGLView) | ||||
|  | ||||
| 			// Start accepting best effort updates. | ||||
| 			self.bestEffortUpdater!.delegate = self | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: CSAudioQueueDelegate | ||||
| 	func machineSpeakerDidChangeInputClock(_ machine: CSMachine) { | ||||
| 		setupAudioQueueClockRate() | ||||
| 	} | ||||
|  | ||||
| 	private func setupAudioQueueClockRate() { | ||||
| 		// establish and provide the audio queue, taking advice as to an appropriate sampling rate | ||||
| 		let maximumSamplingRate = CSAudioQueue.preferredSamplingRate() | ||||
| 		let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate))) | ||||
| 		if selectedSamplingRate > 0 { | ||||
| 			audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate)) | ||||
| 			audioQueue.delegate = self | ||||
| 			self.machine.audioQueue = self.audioQueue | ||||
| 			self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/// Responds to the CSAudioQueueDelegate dry-queue warning message by requesting a machine update. | ||||
| 	final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) { | ||||
| 		bestEffortLock.lock() | ||||
| 		bestEffortUpdater?.update() | ||||
| 		bestEffortLock.unlock() | ||||
| 	} | ||||
|  | ||||
| 	// MARK: CSOpenGLViewDelegate | ||||
| 	/// Responds to the CSOpenGLViewDelegate redraw message by requesting a machine update if this is a timed | ||||
| 	/// request, and ordering a redraw regardless of the motivation. | ||||
| 	final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) { | ||||
| 		if redrawEvent == .timer { | ||||
| 			bestEffortLock.lock() | ||||
| @@ -218,7 +264,27 @@ class MachineDocument: | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: Runtime media insertion. | ||||
| 	/// Responds to CSBestEffortUpdaterDelegate update message by running the machine. | ||||
| 	final func bestEffortUpdater(_ bestEffortUpdater: CSBestEffortUpdater!, runForInterval duration: TimeInterval, didSkipPreviousUpdate: Bool) { | ||||
| 		if let machine = self.machine, actionLock.try() { | ||||
| 			machine.run(forInterval: duration) | ||||
| 			actionLock.unlock() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: - Pasteboard Forwarding. | ||||
|  | ||||
| 	/// Forwards any text currently on the pasteboard into the active machine. | ||||
| 	func paste(_ sender: Any) { | ||||
| 		let pasteboard = NSPasteboard.general | ||||
| 		if let string = pasteboard.string(forType: .string), let machine = self.machine { | ||||
| 			machine.paste(string) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: - Runtime Media Insertion. | ||||
|  | ||||
| 	/// Delegate message to receive drag and drop files. | ||||
| 	final func openGLView(_ view: CSOpenGLView, didReceiveFileAt URL: URL) { | ||||
| 		let mediaSet = CSMediaSet(fileAt: URL) | ||||
| 		if let mediaSet = mediaSet { | ||||
| @@ -226,6 +292,8 @@ class MachineDocument: | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/// Action for the insert menu command; displays an NSOpenPanel and then segues into the same process | ||||
| 	/// as if a file had been received via drag and drop. | ||||
| 	@IBAction final func insertMedia(_ sender: AnyObject!) { | ||||
| 		let openPanel = NSOpenPanel() | ||||
| 		openPanel.message = "Hint: you can also insert media by dragging and dropping it onto the machine's window." | ||||
| @@ -241,37 +309,40 @@ class MachineDocument: | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: NSDocument overrides | ||||
| 	override func data(ofType typeName: String) throws -> Data { | ||||
| 		throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil) | ||||
| 	} | ||||
| 	// MARK: - Input Management. | ||||
|  | ||||
| 	// MARK: Input management | ||||
| 	/// Upon a resign key, immediately releases all ongoing input mechanisms — any currently pressed keys, | ||||
| 	/// and joystick and mouse inputs. | ||||
| 	func windowDidResignKey(_ notification: Notification) { | ||||
| 		if let machine = self.machine { | ||||
| 			machine.clearAllKeys() | ||||
| 			machine.joystickManager = nil | ||||
| 		} | ||||
| 		self.openGLView.releaseMouse() | ||||
| 	} | ||||
|  | ||||
| 	/// Upon becoming key, attaches joystick input to the machine. | ||||
| 	func windowDidBecomeKey(_ notification: Notification) { | ||||
| 		if let machine = self.machine { | ||||
| 			machine.joystickManager = (DocumentController.shared as! DocumentController).joystickManager | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/// Forwards key down events directly to the machine. | ||||
| 	func keyDown(_ event: NSEvent) { | ||||
| 		if let machine = self.machine { | ||||
| 			machine.setKey(event.keyCode, characters: event.characters, isPressed: true) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/// Forwards key up events directly to the machine. | ||||
| 	func keyUp(_ event: NSEvent) { | ||||
| 		if let machine = self.machine { | ||||
| 			machine.setKey(event.keyCode, characters: event.characters, isPressed: false) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/// Synthesies appropriate key up and key down events upon any change in modifiers. | ||||
| 	func flagsChanged(_ newModifiers: NSEvent) { | ||||
| 		if let machine = self.machine { | ||||
| 			machine.setKey(VK_Shift, characters: nil, isPressed: newModifiers.modifierFlags.contains(.shift)) | ||||
| @@ -281,19 +352,199 @@ class MachineDocument: | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: New machine creation | ||||
| 	/// Forwards mouse movement events to the mouse. | ||||
| 	func mouseMoved(_ event: NSEvent) { | ||||
| 		if let machine = self.machine { | ||||
| 			machine.addMouseMotionX(event.deltaX, y: event.deltaY) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/// Forwards mouse button down events to the mouse. | ||||
| 	func mouseUp(_ event: NSEvent) { | ||||
| 		if let machine = self.machine { | ||||
| 			machine.setMouseButton(Int32(event.buttonNumber), isPressed: false) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/// Forwards mouse button up events to the mouse. | ||||
| 	func mouseDown(_ event: NSEvent) { | ||||
| 		if let machine = self.machine { | ||||
| 			machine.setMouseButton(Int32(event.buttonNumber), isPressed: true) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: - MachinePicker Outlets and Actions | ||||
| 	@IBOutlet var machinePicker: MachinePicker? | ||||
| 	@IBOutlet var machinePickerPanel: NSWindow? | ||||
| 	@IBAction func createMachine(_ sender: NSButton?) { | ||||
| 		self.configureAs(machinePicker!.selectedMachine()) | ||||
| 		machinePicker = nil | ||||
| 		let selectedMachine = machinePicker!.selectedMachine() | ||||
| 		self.windowControllers[0].window?.endSheet(self.machinePickerPanel!) | ||||
| 		self.machinePicker = nil | ||||
| 		self.configureAs(selectedMachine) | ||||
| 	} | ||||
|  | ||||
| 	@IBAction func cancelCreateMachine(_ sender: NSButton?) { | ||||
| 		close() | ||||
| 	} | ||||
|  | ||||
| 	// MARK: - ROMRequester Outlets and Actions | ||||
| 	@IBOutlet var romRequesterPanel: NSWindow? | ||||
| 	@IBOutlet var romRequesterText: NSTextField? | ||||
| 	@IBOutlet var romReceiverErrorField: NSTextField? | ||||
| 	@IBOutlet var romReceiverView: CSROMReceiverView? | ||||
| 	private var romRequestBaseText = "" | ||||
| 	func requestRoms() { | ||||
| 		// Don't act yet if there's no window controller yet. | ||||
| 		if self.windowControllers.count == 0 { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Load the ROM requester dialogue. | ||||
| 		Bundle.main.loadNibNamed("ROMRequester", owner: self, topLevelObjects: nil) | ||||
| 		self.romReceiverView!.delegate = self | ||||
| 		self.romRequestBaseText = romRequesterText!.stringValue | ||||
| 		romReceiverErrorField?.alphaValue = 0.0 | ||||
|  | ||||
| 		// Populate the current absentee list. | ||||
| 		populateMissingRomList() | ||||
|  | ||||
| 		// Show the thing. | ||||
| 		self.windowControllers[0].window?.beginSheet(self.romRequesterPanel!, completionHandler: nil) | ||||
| 	} | ||||
|  | ||||
| 	@IBAction func cancelRequestROMs(_ sender: NSButton?) { | ||||
| 		close() | ||||
| 	} | ||||
|  | ||||
| 	func populateMissingRomList() { | ||||
| 		// Fill in the missing details; first build a list of all the individual | ||||
| 		// line items. | ||||
| 		var requestLines: [String] = [] | ||||
| 		for missingROM in self.missingROMs { | ||||
| 			if let descriptiveName = missingROM.descriptiveName { | ||||
| 				requestLines.append("• " + descriptiveName) | ||||
| 			} else { | ||||
| 				requestLines.append("• " + missingROM.fileName) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Suffix everything up to the penultimate line with a semicolon; | ||||
| 		// the penultimate line with a semicolon and a conjunctive; the final | ||||
| 		// line with a full stop. | ||||
| 		for x in 0 ..< requestLines.count { | ||||
| 			if x < requestLines.count - 2 { | ||||
| 				requestLines[x].append(";") | ||||
| 			} else if x < requestLines.count - 1 { | ||||
| 				requestLines[x].append("; and") | ||||
| 			} else { | ||||
| 				requestLines[x].append(".") | ||||
| 			} | ||||
| 		} | ||||
| 		romRequesterText!.stringValue = self.romRequestBaseText + requestLines.joined(separator: "\n") | ||||
| 	} | ||||
|  | ||||
| 	func romReceiverView(_ view: CSROMReceiverView, didReceiveFileAt URL: URL) { | ||||
| 		// Test whether the file identified matches any of the currently missing ROMs. | ||||
| 		// If so then remove that ROM from the missing list and update the request screen. | ||||
| 		// If no ROMs are still missing, start the machine. | ||||
| 		do { | ||||
| 			let fileData = try Data(contentsOf: URL) | ||||
| 			var didInstallRom = false | ||||
|  | ||||
| 			// Try to match by size first, CRC second. Accept that some ROMs may have | ||||
| 			// some additional appended data. Arbitrarily allow them to be up to 10kb | ||||
| 			// too large. | ||||
| 			var index = 0 | ||||
| 			for missingROM in self.missingROMs { | ||||
| 				if fileData.count >= missingROM.size && fileData.count < missingROM.size + 10*1024 { | ||||
| 					// Trim to size. | ||||
| 					let trimmedData = fileData[0 ..< missingROM.size] | ||||
|  | ||||
| 					// Get CRC. | ||||
| 					if missingROM.crc32s.contains( (trimmedData as NSData).crc32 ) { | ||||
| 						// This ROM matches; copy it into the application library, | ||||
| 						// strike it from the missing ROM list and decide how to | ||||
| 						// proceed. | ||||
| 						let fileManager = FileManager.default | ||||
| 						let targetPath = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] | ||||
| 							.appendingPathComponent("ROMImages") | ||||
| 							.appendingPathComponent(missingROM.machineName) | ||||
| 						let targetFile = targetPath | ||||
| 							.appendingPathComponent(missingROM.fileName) | ||||
|  | ||||
| 						do { | ||||
| 							try fileManager.createDirectory(atPath: targetPath.path, withIntermediateDirectories: true, attributes: nil) | ||||
| 							try trimmedData.write(to: targetFile) | ||||
| 						} catch let error { | ||||
| 							showRomReceiverError(error: "Couldn't write to application support directory: \(error)") | ||||
| 						} | ||||
|  | ||||
| 						self.missingROMs.remove(at: index) | ||||
| 						didInstallRom = true | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				index = index + 1 | ||||
| 			} | ||||
|  | ||||
| 			if didInstallRom { | ||||
| 				if self.missingROMs.count == 0 { | ||||
| 					self.windowControllers[0].window?.endSheet(self.romRequesterPanel!) | ||||
| 					configureAs(self.machineDescription!) | ||||
| 				} else { | ||||
| 					populateMissingRomList() | ||||
| 				} | ||||
| 			} else { | ||||
| 				showRomReceiverError(error: "Didn't recognise contents of \(URL.lastPathComponent)") | ||||
| 			} | ||||
| 		} catch let error { | ||||
| 			showRomReceiverError(error: "Couldn't read file at \(URL.absoluteString): \(error)") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Yucky ugliness follows; my experience as an iOS developer intersects poorly with | ||||
| 	// NSAnimationContext hence the various stateful diplications below. isShowingError | ||||
| 	// should be essentially a duplicate of the current alphaValue, and animationCount | ||||
| 	// is to resolve my inability to figure out how to cancel scheduled animations. | ||||
| 	private var errorText = "" | ||||
| 	private var isShowingError = false | ||||
| 	private var animationCount = 0 | ||||
| 	private func showRomReceiverError(error: String) { | ||||
| 		// Set or append the new error. | ||||
| 		if self.errorText.count > 0 { | ||||
| 			self.errorText = self.errorText + "\n" + error | ||||
| 		} else { | ||||
| 			self.errorText = error | ||||
| 		} | ||||
|  | ||||
| 		// Apply the new complete text. | ||||
| 		romReceiverErrorField!.stringValue = self.errorText | ||||
|  | ||||
| 		if !isShowingError { | ||||
| 			// Schedule the box's appearance. | ||||
| 			NSAnimationContext.beginGrouping() | ||||
| 			NSAnimationContext.current.duration = 0.1 | ||||
| 			romReceiverErrorField?.animator().alphaValue = 1.0 | ||||
| 			NSAnimationContext.endGrouping() | ||||
| 			isShowingError = true | ||||
| 		} | ||||
|  | ||||
| 		// Schedule the box to disappear. | ||||
| 		self.animationCount = self.animationCount + 1 | ||||
| 		let capturedAnimationCount = animationCount | ||||
| 		DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(2)) { | ||||
| 			if self.animationCount == capturedAnimationCount { | ||||
| 				NSAnimationContext.beginGrouping() | ||||
| 				NSAnimationContext.current.duration = 1.0 | ||||
| 				self.romReceiverErrorField?.animator().alphaValue = 0.0 | ||||
| 				NSAnimationContext.endGrouping() | ||||
| 				self.isShowingError = false | ||||
| 				self.errorText = "" | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// MARK: Joystick-via-the-keyboard selection | ||||
| 	@IBAction func useKeyboardAsKeyboard(_ sender: NSMenuItem?) { | ||||
| 		machine.inputMode = .keyboard | ||||
| @@ -303,6 +554,9 @@ class MachineDocument: | ||||
| 		machine.inputMode = .joystick | ||||
| 	} | ||||
|  | ||||
| 	/// Determines which of the menu items to enable and disable based on the ability of the | ||||
| 	/// current machine to handle keyboard and joystick input, accept new media and whether | ||||
| 	/// it has an associted activity window. | ||||
| 	override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { | ||||
| 		if let menuItem = item as? NSMenuItem { | ||||
| 			switch item.action { | ||||
| @@ -336,7 +590,7 @@ class MachineDocument: | ||||
| 		return super.validateUserInterfaceItem(item) | ||||
| 	} | ||||
|  | ||||
| 	// Screenshot capture. | ||||
| 	/// Saves a screenshot of the | ||||
| 	@IBAction func saveScreenshot(_ sender: AnyObject!) { | ||||
| 		// Grab a date formatter and form a file name. | ||||
| 		let dateFormatter = DateFormatter() | ||||
| @@ -360,7 +614,8 @@ class MachineDocument: | ||||
| 	} | ||||
|  | ||||
| 	// MARK: Activity display. | ||||
| 	class LED { | ||||
|  | ||||
| 	private class LED { | ||||
| 		let levelIndicator: NSLevelIndicator | ||||
| 		init(levelIndicator: NSLevelIndicator) { | ||||
| 			self.levelIndicator = levelIndicator | ||||
| @@ -368,11 +623,12 @@ class MachineDocument: | ||||
| 		var isLit = false | ||||
| 		var isBlinking = false | ||||
| 	} | ||||
| 	fileprivate var leds: [String: LED] = [:] | ||||
| 	private var leds: [String: LED] = [:] | ||||
|  | ||||
| 	func setupActivityDisplay() { | ||||
| 		var leds = machine.leds | ||||
| 		if leds.count > 0 { | ||||
| 			Bundle.main.loadNibNamed(NSNib.Name(rawValue: "Activity"), owner: self, topLevelObjects: nil) | ||||
| 			Bundle.main.loadNibNamed("Activity", owner: self, topLevelObjects: nil) | ||||
| 			showActivity(nil) | ||||
|  | ||||
| 			// Inspect the activity panel for indicators. | ||||
|   | ||||
| @@ -14,42 +14,42 @@ | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cartridge.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Atari 2600 Cartridge</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Atari 2600 Cartridge</string> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>rom</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>chip.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>ROM Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| @@ -57,110 +57,110 @@ | ||||
| 				<string>uef</string> | ||||
| 				<string>uef.gz</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cassette.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Electron/BBC UEF Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>prg</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>floppy525.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Commodore Program</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>tap</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cassette.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Tape Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>g64</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>floppy525.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Commodore Disk</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Editor</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>d64</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>floppy525.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Commodore 1540/1 Disk</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Editor</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| @@ -171,44 +171,44 @@ | ||||
| 				<string>adl</string> | ||||
| 				<string>adm</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>floppy35.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Electron/BBC Disk Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Editor</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>dsk</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>floppy35.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Disk Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Editor</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| @@ -216,22 +216,22 @@ | ||||
| 				<string>o</string> | ||||
| 				<string>80</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cassette.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>ZX80 Tape Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| @@ -240,240 +240,240 @@ | ||||
| 				<string>81</string> | ||||
| 				<string>p81</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cassette.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>ZX81 Tape Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>csw</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cassette.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Tape Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>tzx</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cassette.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Tape Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>cdt</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cassette.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Amstrad CPC Tape Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>hfe</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>floppy35.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>HxC Disk Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>cas</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cassette.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>MSX Tape Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>dmk</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>floppy35.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Disk Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>tsx</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cassette.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>MSX Tape Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>col</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cartridge.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>ColecoVision Cartridge</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>sms</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cartridge.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Master System Cartridge</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>sg</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>cartridge.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>SG1000 Cartridge</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| @@ -483,22 +483,48 @@ | ||||
| 				<string>do</string> | ||||
| 				<string>po</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>floppy525.png</string> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>Apple II Disk Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<false/> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 			<key>LSHandlerRank</key> | ||||
| 			<string>Owner</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>CFBundleTypeExtensions</key> | ||||
| 			<array> | ||||
| 				<string>img</string> | ||||
| 				<string>hfv</string> | ||||
| 				<string>image</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeIconFile</key> | ||||
| 			<string>floppy35</string> | ||||
| 			<key>CFBundleTypeMIMETypes</key> | ||||
| 			<array> | ||||
| 				<string>application/x-apple-diskimage</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeName</key> | ||||
| 			<string>DiskCopy 4.2 Disk Image</string> | ||||
| 			<key>CFBundleTypeOSTypes</key> | ||||
| 			<array> | ||||
| 				<string>????</string> | ||||
| 			</array> | ||||
| 			<key>CFBundleTypeRole</key> | ||||
| 			<string>Viewer</string> | ||||
| 			<key>LSTypeIsPackage</key> | ||||
| 			<integer>0</integer> | ||||
| 			<key>NSDocumentClass</key> | ||||
| 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string> | ||||
| 		</dict> | ||||
| 	</array> | ||||
| 	<key>CFBundleExecutable</key> | ||||
| @@ -524,7 +550,7 @@ | ||||
| 	<key>LSMinimumSystemVersion</key> | ||||
| 	<string>$(MACOSX_DEPLOYMENT_TARGET)</string> | ||||
| 	<key>NSHumanReadableCopyright</key> | ||||
| 	<string>Copyright 2018 Thomas Harte. All rights reserved.</string> | ||||
| 	<string>Copyright 2019 Thomas Harte. All rights reserved.</string> | ||||
| 	<key>NSMainNibFile</key> | ||||
| 	<string>MainMenu</string> | ||||
| 	<key>NSPrincipalClass</key> | ||||
|   | ||||
| @@ -33,6 +33,14 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { | ||||
| 	CSMachineKeyboardInputModeJoystick | ||||
| }; | ||||
|  | ||||
| @interface CSMissingROM: NSObject | ||||
| @property (nonatomic, readonly, nonnull) NSString *machineName; | ||||
| @property (nonatomic, readonly, nonnull) NSString *fileName; | ||||
| @property (nonatomic, readonly, nullable) NSString *descriptiveName; | ||||
| @property (nonatomic, readonly) NSUInteger size; | ||||
| @property (nonatomic, readonly, nonnull) NSArray<NSNumber *> *crc32s; | ||||
| @end | ||||
|  | ||||
| // Deliberately low; to ensure CSMachine has been declared as an @class already. | ||||
| #import "CSAtari2600.h" | ||||
| #import "CSZX8081.h" | ||||
| @@ -45,8 +53,10 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { | ||||
| 	Initialises an instance of CSMachine. | ||||
|  | ||||
| 	@param result The CSStaticAnalyser result that describes the machine needed. | ||||
| 	@param missingROMs An array that is filled with a list of ROMs that the machine requested but which | ||||
| 		were not found; populated only if this `init` has failed. | ||||
| */ | ||||
| - (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result NS_DESIGNATED_INITIALIZER; | ||||
| - (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray<CSMissingROM *> *)missingROMs NS_DESIGNATED_INITIALIZER; | ||||
|  | ||||
| - (void)runForInterval:(NSTimeInterval)interval; | ||||
|  | ||||
| @@ -61,6 +71,9 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { | ||||
| - (void)setKey:(uint16_t)key characters:(nullable NSString *)characters isPressed:(BOOL)isPressed; | ||||
| - (void)clearAllKeys; | ||||
|  | ||||
| - (void)setMouseButton:(int)button isPressed:(BOOL)isPressed; | ||||
| - (void)addMouseMotionX:(CGFloat)deltaX y:(CGFloat)deltaY; | ||||
|  | ||||
| @property (nonatomic, strong, nullable) CSAudioQueue *audioQueue; | ||||
| @property (nonatomic, readonly, nonnull) CSOpenGLView *view; | ||||
| @property (nonatomic, weak, nullable) id<CSMachineDelegate> delegate; | ||||
| @@ -81,6 +94,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { | ||||
| // Input control. | ||||
| @property (nonatomic, readonly) BOOL hasExclusiveKeyboard; | ||||
| @property (nonatomic, readonly) BOOL hasJoystick; | ||||
| @property (nonatomic, readonly) BOOL hasMouse; | ||||
| @property (nonatomic, assign) CSMachineKeyboardInputMode inputMode; | ||||
| @property (nonatomic, nullable) CSJoystickManager *joystickManager; | ||||
|  | ||||
|   | ||||
| @@ -74,6 +74,68 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 	__unsafe_unretained CSMachine *machine; | ||||
| }; | ||||
|  | ||||
| @interface CSMissingROM (Mutability) | ||||
| @property (nonatomic, nonnull, copy) NSString *machineName; | ||||
| @property (nonatomic, nonnull, copy) NSString *fileName; | ||||
| @property (nonatomic, nullable, copy) NSString *descriptiveName; | ||||
| @property (nonatomic, readwrite) NSUInteger size; | ||||
| @property (nonatomic, copy) NSArray<NSNumber *> *crc32s; | ||||
| @end | ||||
|  | ||||
| @implementation CSMissingROM { | ||||
| 	NSString *_machineName; | ||||
| 	NSString *_fileName; | ||||
| 	NSString *_descriptiveName; | ||||
| 	NSUInteger _size; | ||||
| 	NSArray<NSNumber *> *_crc32s; | ||||
| } | ||||
|  | ||||
| - (NSString *)machineName { | ||||
| 	return _machineName; | ||||
| } | ||||
|  | ||||
| - (void)setMachineName:(NSString *)machineName { | ||||
| 	_machineName = [machineName copy]; | ||||
| } | ||||
|  | ||||
| - (NSString *)fileName { | ||||
| 	return _fileName; | ||||
| } | ||||
|  | ||||
| - (void)setFileName:(NSString *)fileName { | ||||
| 	_fileName = [fileName copy]; | ||||
| } | ||||
|  | ||||
| - (NSString *)descriptiveName { | ||||
| 	return _descriptiveName; | ||||
| } | ||||
|  | ||||
| - (void)setDescriptiveName:(NSString *)descriptiveName { | ||||
| 	_descriptiveName = [descriptiveName copy]; | ||||
| } | ||||
|  | ||||
| - (NSUInteger)size { | ||||
| 	return _size; | ||||
| } | ||||
|  | ||||
| - (void)setSize:(NSUInteger)size { | ||||
| 	_size = size; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSNumber *> *)crc32s { | ||||
| 	return _crc32s; | ||||
| } | ||||
|  | ||||
| - (void)setCrc32s:(NSArray<NSNumber *> *)crc32s { | ||||
| 	_crc32s = [crc32s copy]; | ||||
| } | ||||
|  | ||||
| - (NSString *)description { | ||||
| 	return [NSString stringWithFormat:@"%@/%@, %@ bytes, CRCs: %@", _fileName, _descriptiveName, @(_size), _crc32s]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation CSMachine { | ||||
| 	SpeakerDelegate _speakerDelegate; | ||||
| 	ActivityObserver _activityObserver; | ||||
| @@ -90,14 +152,37 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 	std::unique_ptr<Outputs::Display::OpenGL::ScanTarget> _scanTarget; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithAnalyser:(CSStaticAnalyser *)result { | ||||
| - (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableArray<CSMissingROM *> *)missingROMs { | ||||
| 	self = [super init]; | ||||
| 	if(self) { | ||||
| 		_analyser = result; | ||||
|  | ||||
| 		Machine::Error error; | ||||
| 		_machine.reset(Machine::MachineForTargets(_analyser.targets, CSROMFetcher(), error)); | ||||
| 		if(!_machine) return nil; | ||||
| 		std::vector<ROMMachine::ROM> missing_roms; | ||||
| 		_machine.reset(Machine::MachineForTargets(_analyser.targets, CSROMFetcher(&missing_roms), error)); | ||||
| 		if(!_machine) { | ||||
| 			for(const auto &missing_rom : missing_roms) { | ||||
| 				CSMissingROM *rom = [[CSMissingROM alloc] init]; | ||||
|  | ||||
| 				// Copy/convert the primitive fields. | ||||
| 				rom.machineName = [NSString stringWithUTF8String:missing_rom.machine_name.c_str()]; | ||||
| 				rom.fileName = [NSString stringWithUTF8String:missing_rom.file_name.c_str()]; | ||||
| 				rom.descriptiveName = missing_rom.descriptive_name.empty() ? nil : [NSString stringWithUTF8String:missing_rom.descriptive_name.c_str()]; | ||||
| 				rom.size = missing_rom.size; | ||||
|  | ||||
| 				// Convert the CRC list. | ||||
| 				NSMutableArray<NSNumber *> *crc32s = [[NSMutableArray alloc] init]; | ||||
| 				for(const auto &crc : missing_rom.crc32s) { | ||||
| 					[crc32s addObject:@(crc)]; | ||||
| 				} | ||||
| 				rom.crc32s = [crc32s copy]; | ||||
|  | ||||
| 				// Add to the missing list. | ||||
| 				[missingROMs addObject:rom]; | ||||
| 			} | ||||
|  | ||||
| 			return nil; | ||||
| 		} | ||||
|  | ||||
| 		_inputMode = | ||||
| 			(_machine->keyboard_machine() && _machine->keyboard_machine()->get_keyboard().is_exclusive()) | ||||
| @@ -410,14 +495,14 @@ struct ActivityObserver: public Activity::Observer { | ||||
| } | ||||
|  | ||||
| - (void)clearAllKeys { | ||||
| 	auto keyboard_machine = _machine->keyboard_machine(); | ||||
| 	const auto keyboard_machine = _machine->keyboard_machine(); | ||||
| 	if(keyboard_machine) { | ||||
| 		@synchronized(self) { | ||||
| 			keyboard_machine->get_keyboard().reset_all_keys(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	auto joystick_machine = _machine->joystick_machine(); | ||||
| 	const auto joystick_machine = _machine->joystick_machine(); | ||||
| 	if(joystick_machine) { | ||||
| 		@synchronized(self) { | ||||
| 			for(auto &joystick : joystick_machine->get_joysticks()) { | ||||
| @@ -425,6 +510,31 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	const auto mouse_machine = _machine->mouse_machine(); | ||||
| 	if(mouse_machine) { | ||||
| 		@synchronized(self) { | ||||
| 			mouse_machine->get_mouse().reset_all_buttons(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)setMouseButton:(int)button isPressed:(BOOL)isPressed { | ||||
| 	auto mouse_machine = _machine->mouse_machine(); | ||||
| 	if(mouse_machine) { | ||||
| 		@synchronized(self) { | ||||
| 			mouse_machine->get_mouse().set_button_pressed(button % mouse_machine->get_mouse().get_number_of_buttons(), isPressed); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)addMouseMotionX:(CGFloat)deltaX y:(CGFloat)deltaY { | ||||
| 	auto mouse_machine = _machine->mouse_machine(); | ||||
| 	if(mouse_machine) { | ||||
| 		@synchronized(self) { | ||||
| 			mouse_machine->get_mouse().move(int(deltaX), int(deltaY)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| #pragma mark - Options | ||||
| @@ -540,6 +650,10 @@ struct ActivityObserver: public Activity::Observer { | ||||
| 	return !!_machine->joystick_machine(); | ||||
| } | ||||
|  | ||||
| - (BOOL)hasMouse { | ||||
| 	return !!_machine->mouse_machine(); | ||||
| } | ||||
|  | ||||
| - (BOOL)hasExclusiveKeyboard { | ||||
| 	return !!_machine->keyboard_machine() && _machine->keyboard_machine()->get_keyboard().is_exclusive(); | ||||
| } | ||||
|   | ||||
| @@ -8,4 +8,4 @@ | ||||
|  | ||||
| #include "ROMMachine.hpp" | ||||
|  | ||||
| ROMMachine::ROMFetcher CSROMFetcher(); | ||||
| ROMMachine::ROMFetcher CSROMFetcher(std::vector<ROMMachine::ROM> *missing_roms = nullptr); | ||||
|   | ||||
| @@ -14,15 +14,39 @@ | ||||
|  | ||||
| #include <string> | ||||
|  | ||||
| ROMMachine::ROMFetcher CSROMFetcher() { | ||||
| 	return [] (const std::string &machine, const std::vector<std::string> &names) -> std::vector<std::unique_ptr<std::vector<std::uint8_t>>> { | ||||
| 		NSString *subDirectory = [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:machine.c_str()]]; | ||||
| 		std::vector<std::unique_ptr<std::vector<std::uint8_t>>> results; | ||||
| 		for(const auto &name: names) { | ||||
| 			NSData *fileData = [[NSBundle mainBundle] dataForResource:[NSString stringWithUTF8String:name.c_str()] withExtension:nil subdirectory:subDirectory]; | ||||
| ROMMachine::ROMFetcher CSROMFetcher(std::vector<ROMMachine::ROM> *missing_roms) { | ||||
| 	return [missing_roms] (const std::vector<ROMMachine::ROM> &roms) -> std::vector<std::unique_ptr<std::vector<std::uint8_t>>> { | ||||
| 		NSArray<NSURL *> *const supportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask]; | ||||
|  | ||||
| 			if(!fileData) | ||||
| 		std::vector<std::unique_ptr<std::vector<std::uint8_t>>> results; | ||||
| 		for(const auto &rom: roms) { | ||||
| 			NSData *fileData; | ||||
| 			NSString *const subdirectory = [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:rom.machine_name.c_str()]]; | ||||
|  | ||||
| 			// Check for this file first within the application support directories. | ||||
| 			for(NSURL *supportURL in supportURLs) { | ||||
| 				NSURL *const fullURL = [[supportURL URLByAppendingPathComponent:subdirectory] | ||||
| 							URLByAppendingPathComponent:[NSString stringWithUTF8String:rom.file_name.c_str()]]; | ||||
| 				fileData = [NSData dataWithContentsOfURL:fullURL]; | ||||
| 				if(fileData) break; | ||||
| 			} | ||||
|  | ||||
| 			// Failing that, check inside the application bundle. | ||||
| 			if(!fileData) { | ||||
| 				fileData = [[NSBundle mainBundle] | ||||
| 					dataForResource:[NSString stringWithUTF8String:rom.file_name.c_str()] | ||||
| 					withExtension:nil | ||||
| 					subdirectory:subdirectory]; | ||||
| 			} | ||||
|  | ||||
| 			// Store an appropriate result, accumulating a list of the missing if requested. | ||||
| 			if(!fileData) { | ||||
| 				results.emplace_back(nullptr); | ||||
|  | ||||
| 				if(missing_roms) { | ||||
| 					missing_roms->push_back(rom); | ||||
| 				} | ||||
| 			} | ||||
| 			else { | ||||
| 				std::unique_ptr<std::vector<std::uint8_t>> data(new std::vector<std::uint8_t>); | ||||
| 				*data = fileData.stdVector8; | ||||
|   | ||||
							
								
								
									
										17
									
								
								OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  NSData+CRC32.m | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 22/07/2019. | ||||
| //  Copyright 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| #include <stdint.h> | ||||
|  | ||||
| @interface NSData (CRC32) | ||||
|  | ||||
| @property(nonnull, nonatomic, readonly) NSNumber *crc32; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										19
									
								
								OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  NSData+CRC32.m | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 22/07/2019. | ||||
| //  Copyright 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "NSData+CRC32.h" | ||||
|  | ||||
| #include <zlib.h> | ||||
|  | ||||
| @implementation NSData (StdVector) | ||||
|  | ||||
| - (NSNumber *)crc32 { | ||||
|     return @(crc32(crc32(0, Z_NULL, 0), self.bytes, (uInt)self.length)); | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -29,6 +29,13 @@ typedef NS_ENUM(NSInteger, CSMachineCPCModel) { | ||||
| 	CSMachineCPCModel6128 | ||||
| }; | ||||
|  | ||||
| typedef NS_ENUM(NSInteger, CSMachineMacintoshModel) { | ||||
| 	CSMachineMacintoshModel128k, | ||||
| 	CSMachineMacintoshModel512k, | ||||
| 	CSMachineMacintoshModel512ke, | ||||
| 	CSMachineMacintoshModelPlus, | ||||
| }; | ||||
|  | ||||
| typedef NS_ENUM(NSInteger, CSMachineOricModel) { | ||||
| 	CSMachineOricModelOric1, | ||||
| 	CSMachineOricModelOricAtmos, | ||||
| @@ -69,6 +76,7 @@ typedef int Kilobytes; | ||||
| - (instancetype)initWithZX80MemorySize:(Kilobytes)memorySize useZX81ROM:(BOOL)useZX81ROM; | ||||
| - (instancetype)initWithZX81MemorySize:(Kilobytes)memorySize; | ||||
| - (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController; | ||||
| - (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model; | ||||
|  | ||||
| @property(nonatomic, readonly) NSString *optionsPanelNibName; | ||||
| @property(nonatomic, readonly) NSString *displayName; | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
| #include "../../../../../Analyser/Static/AmstradCPC/Target.hpp" | ||||
| #include "../../../../../Analyser/Static/AppleII/Target.hpp" | ||||
| #include "../../../../../Analyser/Static/Commodore/Target.hpp" | ||||
| #include "../../../../../Analyser/Static/Macintosh/Target.hpp" | ||||
| #include "../../../../../Analyser/Static/MSX/Target.hpp" | ||||
| #include "../../../../../Analyser/Static/Oric/Target.hpp" | ||||
| #include "../../../../../Analyser/Static/ZX8081/Target.hpp" | ||||
| @@ -188,7 +189,27 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K | ||||
| 		_targets.push_back(std::move(target)); | ||||
| 	} | ||||
| 	return self; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model { | ||||
| 	self = [super init]; | ||||
| 	if(self) { | ||||
| 		using Target = Analyser::Static::Macintosh::Target; | ||||
| 		std::unique_ptr<Target> target(new Target); | ||||
| 		target->machine = Analyser::Machine::Macintosh; | ||||
|  | ||||
| 		using Model = Target::Model; | ||||
| 		switch(model) { | ||||
| 			default: | ||||
| 			case CSMachineMacintoshModel128k:	target->model = Model::Mac128k;		break; | ||||
| 			case CSMachineMacintoshModel512k:	target->model = Model::Mac512k;		break; | ||||
| 			case CSMachineMacintoshModel512ke:	target->model = Model::Mac512ke;	break; | ||||
| 			case CSMachineMacintoshModelPlus:	target->model = Model::MacPlus;		break; | ||||
| 		} | ||||
|  | ||||
| 		_targets.push_back(std::move(target)); | ||||
| 	} | ||||
| 	return self; | ||||
| } | ||||
|  | ||||
| - (NSString *)optionsPanelNibName { | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> | ||||
| <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> | ||||
|     <dependencies> | ||||
|         <deployment identifier="macosx"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/> | ||||
|         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||||
|     </dependencies> | ||||
|     <objects> | ||||
| @@ -17,14 +17,14 @@ | ||||
|         <window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titleVisibility="hidden" id="QvC-M9-y7g"> | ||||
|             <windowStyleMask key="styleMask" titled="YES" documentModal="YES"/> | ||||
|             <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> | ||||
|             <rect key="contentRect" x="196" y="240" width="600" height="205"/> | ||||
|             <rect key="contentRect" x="196" y="240" width="650" height="205"/> | ||||
|             <rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/> | ||||
|             <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ"> | ||||
|                 <rect key="frame" x="0.0" y="0.0" width="600" height="205"/> | ||||
|                 <rect key="frame" x="0.0" y="0.0" width="650" height="205"/> | ||||
|                 <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                 <subviews> | ||||
|                     <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hKn-1l-OSN"> | ||||
|                         <rect key="frame" x="499" y="13" width="87" height="32"/> | ||||
|                         <rect key="frame" x="549" y="13" width="87" height="32"/> | ||||
|                         <buttonCell key="cell" type="push" title="Choose" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="MnM-xo-4Qa"> | ||||
|                             <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> | ||||
|                             <font key="font" metaFont="system"/> | ||||
| @@ -37,7 +37,7 @@ DQ | ||||
|                         </connections> | ||||
|                     </button> | ||||
|                     <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JQy-Cj-AOK"> | ||||
|                         <rect key="frame" x="418" y="13" width="82" height="32"/> | ||||
|                         <rect key="frame" x="468" y="13" width="82" height="32"/> | ||||
|                         <buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sub-rB-Req"> | ||||
|                             <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> | ||||
|                             <font key="font" metaFont="system"/> | ||||
| @@ -59,12 +59,12 @@ Gw | ||||
|                         </textFieldCell> | ||||
|                     </textField> | ||||
|                     <tabView translatesAutoresizingMaskIntoConstraints="NO" id="VUb-QG-x7c"> | ||||
|                         <rect key="frame" x="13" y="51" width="574" height="140"/> | ||||
|                         <rect key="frame" x="13" y="51" width="624" height="140"/> | ||||
|                         <font key="font" metaFont="system"/> | ||||
|                         <tabViewItems> | ||||
|                             <tabViewItem label="Apple II" identifier="appleii" id="P59-QG-LOa"> | ||||
|                                 <view key="view" id="dHz-Yv-GNq"> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="94"/> | ||||
|                                     <rect key="frame" x="10" y="33" width="604" height="94"/> | ||||
|                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                                     <subviews> | ||||
|                                         <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="V5Z-dX-Ns4"> | ||||
| @@ -130,7 +130,7 @@ Gw | ||||
|                             </tabViewItem> | ||||
|                             <tabViewItem label="Amstrad CPC" identifier="cpc" id="JmB-OF-xcM"> | ||||
|                                 <view key="view" id="5zS-Nj-Ynx"> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="94"/> | ||||
|                                     <rect key="frame" x="10" y="33" width="604" height="94"/> | ||||
|                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                                     <subviews> | ||||
|                                         <popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="00d-sg-Krh"> | ||||
| @@ -168,18 +168,18 @@ Gw | ||||
|                             </tabViewItem> | ||||
|                             <tabViewItem label="Electron" identifier="electron" id="muc-z9-Vqc"> | ||||
|                                 <view key="view" id="SRc-2D-95G"> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="93"/> | ||||
|                                     <rect key="frame" x="10" y="33" width="604" height="94"/> | ||||
|                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                                     <subviews> | ||||
|                                         <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JqM-IK-FMP"> | ||||
|                                             <rect key="frame" x="15" y="74" width="164" height="18"/> | ||||
|                                             <rect key="frame" x="15" y="75" width="164" height="18"/> | ||||
|                                             <buttonCell key="cell" type="check" title="With Disk Filing System" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="tpW-5C-xKp"> | ||||
|                                                 <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> | ||||
|                                                 <font key="font" metaFont="system"/> | ||||
|                                             </buttonCell> | ||||
|                                         </button> | ||||
|                                         <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="945-wU-JOH"> | ||||
|                                             <rect key="frame" x="15" y="54" width="228" height="18"/> | ||||
|                                             <rect key="frame" x="15" y="55" width="228" height="18"/> | ||||
|                                             <buttonCell key="cell" type="check" title="With Advanced Disk Filing System" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="S0c-Jg-7Pu"> | ||||
|                                                 <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> | ||||
|                                                 <font key="font" metaFont="system"/> | ||||
| @@ -197,9 +197,31 @@ Gw | ||||
|                                     </constraints> | ||||
|                                 </view> | ||||
|                             </tabViewItem> | ||||
|                             <tabViewItem label="Macintosh" identifier="mac" id="lmR-z3-xSm"> | ||||
|                                 <view key="view" id="7Yf-vi-Q0W"> | ||||
|                                     <rect key="frame" x="10" y="33" width="604" height="94"/> | ||||
|                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                                     <subviews> | ||||
|                                         <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="F6e-UC-25u"> | ||||
|                                             <rect key="frame" x="15" y="74" width="574" height="17"/> | ||||
|                                             <textFieldCell key="cell" lineBreakMode="clipping" title="At present Clock Signal emulates only the Macintosh 512ke." id="IGV-Yp-6Af"> | ||||
|                                                 <font key="font" usesAppearanceFont="YES"/> | ||||
|                                                 <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> | ||||
|                                                 <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> | ||||
|                                             </textFieldCell> | ||||
|                                         </textField> | ||||
|                                     </subviews> | ||||
|                                     <constraints> | ||||
|                                         <constraint firstAttribute="trailing" secondItem="F6e-UC-25u" secondAttribute="trailing" constant="17" id="42z-hS-aPq"/> | ||||
|                                         <constraint firstItem="F6e-UC-25u" firstAttribute="leading" secondItem="7Yf-vi-Q0W" secondAttribute="leading" constant="17" id="bIg-C3-xdz"/> | ||||
|                                         <constraint firstItem="F6e-UC-25u" firstAttribute="top" secondItem="7Yf-vi-Q0W" secondAttribute="top" constant="3" id="cxs-OP-oH5"/> | ||||
|                                         <constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="F6e-UC-25u" secondAttribute="bottom" constant="17" id="vpF-ER-pmD"/> | ||||
|                                     </constraints> | ||||
|                                 </view> | ||||
|                             </tabViewItem> | ||||
|                             <tabViewItem label="MSX" identifier="msx" id="6SR-DY-zdI"> | ||||
|                                 <view key="view" id="mWD-An-tR7"> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="94"/> | ||||
|                                     <rect key="frame" x="10" y="33" width="604" height="94"/> | ||||
|                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                                     <subviews> | ||||
|                                         <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8xT-Pr-8SE"> | ||||
| @@ -233,6 +255,7 @@ Gw | ||||
|                                         </textField> | ||||
|                                     </subviews> | ||||
|                                     <constraints> | ||||
|                                         <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="LG6-mP-SeG" secondAttribute="trailing" constant="17" id="0Oc-n7-gaM"/> | ||||
|                                         <constraint firstItem="8xT-Pr-8SE" firstAttribute="top" secondItem="LG6-mP-SeG" secondAttribute="bottom" constant="6" id="LBt-4m-GDc"/> | ||||
|                                         <constraint firstItem="LG6-mP-SeG" firstAttribute="top" secondItem="mWD-An-tR7" secondAttribute="top" constant="3" id="bcb-ZZ-VpQ"/> | ||||
|                                         <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="8xT-Pr-8SE" secondAttribute="trailing" constant="17" id="l8P-UW-8ig"/> | ||||
| @@ -246,7 +269,7 @@ Gw | ||||
|                             </tabViewItem> | ||||
|                             <tabViewItem label="Oric" identifier="oric" id="NSx-DC-p4M"> | ||||
|                                 <view key="view" id="sOR-e0-8iZ"> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="94"/> | ||||
|                                     <rect key="frame" x="10" y="33" width="604" height="94"/> | ||||
|                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                                     <subviews> | ||||
|                                         <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0ct-tf-uRH"> | ||||
| @@ -311,7 +334,7 @@ Gw | ||||
|                             </tabViewItem> | ||||
|                             <tabViewItem label="Vic-20" identifier="vic20" id="cyO-PU-hSU"> | ||||
|                                 <view key="view" id="fLI-XB-QCr"> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="94"/> | ||||
|                                     <rect key="frame" x="10" y="33" width="604" height="94"/> | ||||
|                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                                     <subviews> | ||||
|                                         <popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ueK-gq-gaF"> | ||||
| @@ -388,11 +411,11 @@ Gw | ||||
|                             </tabViewItem> | ||||
|                             <tabViewItem label="ZX80" identifier="zx80" id="tMH-kF-GUz"> | ||||
|                                 <view key="view" id="8hL-Vn-Hg0"> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="93"/> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="94"/> | ||||
|                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                                     <subviews> | ||||
|                                         <popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="I1a-Eu-5UB"> | ||||
|                                             <rect key="frame" x="106" y="66" width="115" height="25"/> | ||||
|                                             <rect key="frame" x="106" y="67" width="115" height="25"/> | ||||
|                                             <popUpButtonCell key="cell" type="push" title="Unexpanded" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="4Sa-jR-xOd" id="B8M-do-Yod"> | ||||
|                                                 <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> | ||||
|                                                 <font key="font" metaFont="menu"/> | ||||
| @@ -406,7 +429,7 @@ Gw | ||||
|                                             </popUpButtonCell> | ||||
|                                         </popUpButton> | ||||
|                                         <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="NCX-4e-lSu"> | ||||
|                                             <rect key="frame" x="15" y="71" width="87" height="17"/> | ||||
|                                             <rect key="frame" x="15" y="72" width="87" height="17"/> | ||||
|                                             <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Memory Size:" id="e6x-TE-OC5"> | ||||
|                                                 <font key="font" metaFont="system"/> | ||||
|                                                 <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> | ||||
| @@ -414,7 +437,7 @@ Gw | ||||
|                                             </textFieldCell> | ||||
|                                         </textField> | ||||
|                                         <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ReP-bV-Thu"> | ||||
|                                             <rect key="frame" x="15" y="47" width="114" height="18"/> | ||||
|                                             <rect key="frame" x="15" y="48" width="114" height="18"/> | ||||
|                                             <buttonCell key="cell" type="check" title="Use ZX81 ROM" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="VQH-nv-Pfm"> | ||||
|                                                 <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> | ||||
|                                                 <font key="font" metaFont="system"/> | ||||
| @@ -436,11 +459,11 @@ Gw | ||||
|                             </tabViewItem> | ||||
|                             <tabViewItem label="ZX81" identifier="zx81" id="Wnn-nQ-gZ6"> | ||||
|                                 <view key="view" id="bmd-gL-gzT"> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="93"/> | ||||
|                                     <rect key="frame" x="10" y="33" width="554" height="94"/> | ||||
|                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|                                     <subviews> | ||||
|                                         <popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5aO-UX-HnX"> | ||||
|                                             <rect key="frame" x="106" y="66" width="115" height="25"/> | ||||
|                                             <rect key="frame" x="106" y="67" width="115" height="25"/> | ||||
|                                             <popUpButtonCell key="cell" type="push" title="Unexpanded" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="7QC-Ij-hES" id="d3W-Gl-3Mf"> | ||||
|                                                 <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> | ||||
|                                                 <font key="font" metaFont="menu"/> | ||||
| @@ -454,7 +477,7 @@ Gw | ||||
|                                             </popUpButtonCell> | ||||
|                                         </popUpButton> | ||||
|                                         <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8tU-73-XEE"> | ||||
|                                             <rect key="frame" x="15" y="71" width="87" height="17"/> | ||||
|                                             <rect key="frame" x="15" y="72" width="87" height="17"/> | ||||
|                                             <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Memory Size:" id="z4b-oR-Yl2"> | ||||
|                                                 <font key="font" metaFont="system"/> | ||||
|                                                 <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> | ||||
| @@ -490,7 +513,7 @@ Gw | ||||
|                     <constraint firstItem="VUb-QG-x7c" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" id="zT3-Ea-QQJ"/> | ||||
|                 </constraints> | ||||
|             </view> | ||||
|             <point key="canvasLocation" x="34" y="89"/> | ||||
|             <point key="canvasLocation" x="34" y="88.5"/> | ||||
|         </window> | ||||
|         <customObject id="192-Eb-Rpg" customClass="MachinePicker" customModule="Clock_Signal" customModuleProvider="target"> | ||||
|             <connections> | ||||
|   | ||||
| @@ -157,6 +157,9 @@ class MachinePicker: NSObject { | ||||
| 					default:	return CSStaticAnalyser(amstradCPCModel: .model6128) | ||||
| 				} | ||||
|  | ||||
| 			case "mac": | ||||
| 				return CSStaticAnalyser(macintoshModel: .model512ke) | ||||
|  | ||||
| 			case "msx": | ||||
| 				let hasDiskDrive = msxHasDiskDriveButton!.state == .on | ||||
| 				switch msxRegionButton!.selectedItem?.tag { | ||||
|   | ||||
							
								
								
									
										27
									
								
								OSBindings/Mac/Clock Signal/ROMRequester/CSROMReceiverView.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								OSBindings/Mac/Clock Signal/ROMRequester/CSROMReceiverView.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  CSROMReceiverView.h | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 22/07/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Cocoa/Cocoa.h> | ||||
|  | ||||
| @class CSROMReceiverView; | ||||
|  | ||||
| @protocol CSROMReciverViewDelegate | ||||
| /*! | ||||
| 	Announces receipt of a file by drag and drop to the delegate. | ||||
| 	@param view The view making the request. | ||||
| 	@param URL The file URL of the received file. | ||||
| */ | ||||
| - (void)romReceiverView:(nonnull CSROMReceiverView *)view didReceiveFileAtURL:(nonnull NSURL *)URL; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @interface CSROMReceiverView : NSView | ||||
|  | ||||
| @property(nonatomic, weak, nullable) id <CSROMReciverViewDelegate> delegate; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										40
									
								
								OSBindings/Mac/Clock Signal/ROMRequester/CSROMReceiverView.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								OSBindings/Mac/Clock Signal/ROMRequester/CSROMReceiverView.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| // | ||||
| //  CSROMReceiverView.m | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 22/07/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "CSROMReceiverView.h" | ||||
|  | ||||
| @interface CSROMReceiverView () <NSDraggingDestination> | ||||
| @end | ||||
|  | ||||
|  | ||||
| @implementation CSROMReceiverView | ||||
|  | ||||
| - (void)awakeFromNib { | ||||
| 	[super awakeFromNib]; | ||||
|  | ||||
| 	// Accept file URLs by drag and drop. | ||||
| 	[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]]; | ||||
| } | ||||
|  | ||||
| #pragma mark - NSDraggingDestination | ||||
|  | ||||
| - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender { | ||||
| 	// Just forward the URLs. | ||||
| 	for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems]) { | ||||
| 		NSURL *URL = [NSURL URLWithString:[item stringForType:(__bridge NSString *)kUTTypeFileURL]]; | ||||
| 		[self.delegate romReceiverView:self didReceiveFileAtURL:URL]; | ||||
| 	} | ||||
| 	return YES; | ||||
| } | ||||
|  | ||||
| - (NSDragOperation)draggingEntered:(id < NSDraggingInfo >)sender { | ||||
| 	// The emulator will take a copy of any deposited ROM files. | ||||
| 	return NSDragOperationCopy; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										75
									
								
								OSBindings/Mac/Clock Signal/ROMRequester/ROMRequester.xib
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								OSBindings/Mac/Clock Signal/ROMRequester/ROMRequester.xib
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> | ||||
|     <dependencies> | ||||
|         <deployment identifier="macosx"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/> | ||||
|         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||||
|     </dependencies> | ||||
|     <objects> | ||||
|         <customObject id="-2" userLabel="File's Owner" customClass="MachineDocument" customModule="Clock_Signal" customModuleProvider="target"> | ||||
|             <connections> | ||||
|                 <outlet property="romReceiverErrorField" destination="Bia-0m-GxK" id="wnH-Kz-uXD"/> | ||||
|                 <outlet property="romReceiverView" destination="EiT-Mj-1SZ" id="Y2M-Qd-i7y"/> | ||||
|                 <outlet property="romRequesterPanel" destination="QvC-M9-y7g" id="saI-YH-9NP"/> | ||||
|                 <outlet property="romRequesterText" destination="5qG-I3-Qav" id="J9M-T8-Xyy"/> | ||||
|             </connections> | ||||
|         </customObject> | ||||
|         <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> | ||||
|         <customObject id="-3" userLabel="Application" customClass="NSObject"/> | ||||
|         <window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titleVisibility="hidden" id="QvC-M9-y7g"> | ||||
|             <windowStyleMask key="styleMask" titled="YES"/> | ||||
|             <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> | ||||
|             <rect key="contentRect" x="196" y="240" width="480" height="270"/> | ||||
|             <rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/> | ||||
|             <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ" customClass="CSROMReceiverView"> | ||||
|                 <rect key="frame" x="0.0" y="0.0" width="480" height="270"/> | ||||
|                 <autoresizingMask key="autoresizingMask"/> | ||||
|                 <subviews> | ||||
|                     <textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5qG-I3-Qav"> | ||||
|                         <rect key="frame" x="18" y="148" width="444" height="102"/> | ||||
|                         <textFieldCell key="cell" enabled="NO" allowsUndo="NO" id="itJ-2T-0ia"> | ||||
|                             <font key="font" usesAppearanceFont="YES"/> | ||||
|                             <string key="title">Clock Signal requires you to provide images of the system ROMs for this machine. They will be stored permanently; you need do this only once.
Please drag and drop the following over this view: | ||||
|  | ||||
| </string> | ||||
|                             <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> | ||||
|                             <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> | ||||
|                         </textFieldCell> | ||||
|                     </textField> | ||||
|                     <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nvl-dm-8bK"> | ||||
|                         <rect key="frame" x="384" y="13" width="82" height="32"/> | ||||
|                         <buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="oS1-Pk-LsO"> | ||||
|                             <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> | ||||
|                             <font key="font" metaFont="system"/> | ||||
|                             <string key="keyEquivalent" base64-UTF8="YES"> | ||||
| DQ | ||||
| </string> | ||||
|                         </buttonCell> | ||||
|                         <connections> | ||||
|                             <action selector="cancelRequestROMs:" target="-2" id="58q-yi-dlv"/> | ||||
|                         </connections> | ||||
|                     </button> | ||||
|                     <textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bia-0m-GxK"> | ||||
|                         <rect key="frame" x="20" y="61" width="442" height="17"/> | ||||
|                         <textFieldCell key="cell" allowsUndo="NO" alignment="center" title="Multiline Label" drawsBackground="YES" id="8jl-xs-LjP"> | ||||
|                             <font key="font" metaFont="message"/> | ||||
|                             <color key="textColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> | ||||
|                             <color key="backgroundColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/> | ||||
|                         </textFieldCell> | ||||
|                     </textField> | ||||
|                 </subviews> | ||||
|                 <constraints> | ||||
|                     <constraint firstItem="Bia-0m-GxK" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" id="9Lj-3p-7Vr"/> | ||||
|                     <constraint firstAttribute="trailing" secondItem="5qG-I3-Qav" secondAttribute="trailing" constant="20" id="AwT-WO-Nyf"/> | ||||
|                     <constraint firstAttribute="bottom" secondItem="nvl-dm-8bK" secondAttribute="bottom" constant="20" id="BU0-5T-YRH"/> | ||||
|                     <constraint firstAttribute="bottom" secondItem="Bia-0m-GxK" secondAttribute="bottom" constant="61" id="ENn-7a-NGa"/> | ||||
|                     <constraint firstAttribute="trailing" secondItem="Bia-0m-GxK" secondAttribute="trailing" constant="18" id="UUy-bf-87w"/> | ||||
|                     <constraint firstItem="5qG-I3-Qav" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" id="bRM-z1-cRf"/> | ||||
|                     <constraint firstAttribute="trailing" secondItem="nvl-dm-8bK" secondAttribute="trailing" constant="20" id="eQI-Nj-ane"/> | ||||
|                     <constraint firstItem="nvl-dm-8bK" firstAttribute="top" relation="greaterThanOrEqual" secondItem="5qG-I3-Qav" secondAttribute="bottom" constant="17" id="plB-x4-He5"/> | ||||
|                     <constraint firstItem="5qG-I3-Qav" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" id="qeC-Rh-hmr"/> | ||||
|                 </constraints> | ||||
|             </view> | ||||
|         </window> | ||||
|     </objects> | ||||
| </document> | ||||
| @@ -63,6 +63,29 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { | ||||
| */ | ||||
| - (void)paste:(nonnull id)sender; | ||||
|  | ||||
| @optional | ||||
|  | ||||
| /*! | ||||
| 	Supplies a mouse moved event to the delegate. This functions only if | ||||
| 	shouldCaptureMouse is set to YES, in which case the view will ensure it captures | ||||
| 	the mouse and returns only relative motion | ||||
| 	(Cf. CGAssociateMouseAndMouseCursorPosition). It will also elide mouseDragged: | ||||
| 	(and rightMouseDragged:, etc) and mouseMoved: events. | ||||
| */ | ||||
| - (void)mouseMoved:(nonnull NSEvent *)event; | ||||
|  | ||||
| /*! | ||||
| 	Supplies a mouse button down event. This elides mouseDown, rightMouseDown and otherMouseDown. | ||||
| 	@c shouldCaptureMouse must be set to @c YES to receive these events. | ||||
| */ | ||||
| - (void)mouseDown:(nonnull NSEvent *)event; | ||||
|  | ||||
| /*! | ||||
| 	Supplies a mouse button up event. This elides mouseUp, rightMouseUp and otherMouseUp. | ||||
| 	@c shouldCaptureMouse must be set to @c YES to receive these events. | ||||
| */ | ||||
| - (void)mouseUp:(nonnull NSEvent *)event; | ||||
|  | ||||
| @end | ||||
|  | ||||
| /*! | ||||
| @@ -74,6 +97,8 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { | ||||
| @property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate; | ||||
| @property (nonatomic, weak, nullable) id <CSOpenGLViewResponderDelegate> responderDelegate; | ||||
|  | ||||
| @property (nonatomic, assign) BOOL shouldCaptureMouse; | ||||
|  | ||||
| /*! | ||||
| 	Ends the timer tracking time; should be called prior to giving up the last owning reference | ||||
| 	to ensure that any retain cycles implied by the timer are resolved. | ||||
| @@ -89,4 +114,9 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { | ||||
| */ | ||||
| - (void)performWithGLContext:(nonnull dispatch_block_t)action; | ||||
|  | ||||
| /*! | ||||
| 	Instructs that the mouse cursor, if currently captured, should be released. | ||||
| */ | ||||
| - (void)releaseMouse; | ||||
|  | ||||
| @end | ||||
|   | ||||
| @@ -19,9 +19,12 @@ | ||||
|  | ||||
| 	NSTrackingArea *_mouseTrackingArea; | ||||
| 	NSTimer *_mouseHideTimer; | ||||
| 	BOOL _mouseIsCaptured; | ||||
| } | ||||
|  | ||||
| - (void)prepareOpenGL { | ||||
| 	[super prepareOpenGL]; | ||||
|  | ||||
| 	// Synchronize buffer swaps with vertical refresh rate | ||||
| 	GLint swapInt = 1; | ||||
| 	[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval]; | ||||
| @@ -146,6 +149,13 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt | ||||
|  | ||||
| - (void)flagsChanged:(NSEvent *)theEvent { | ||||
| 	[self.responderDelegate flagsChanged:theEvent]; | ||||
|  | ||||
| 	// Release the mouse upon a control + command. | ||||
| 	if(_mouseIsCaptured && | ||||
| 		theEvent.modifierFlags & NSEventModifierFlagControl && | ||||
| 		theEvent.modifierFlags & NSEventModifierFlagCommand) { | ||||
| 		[self releaseMouse]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)paste:(id)sender { | ||||
| @@ -168,6 +178,10 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt | ||||
|  | ||||
| #pragma mark - Mouse hiding | ||||
|  | ||||
| - (void)setShouldCaptureMouse:(BOOL)shouldCaptureMouse { | ||||
| 	_shouldCaptureMouse = shouldCaptureMouse; | ||||
| } | ||||
|  | ||||
| - (void)updateTrackingAreas { | ||||
| 	[super updateTrackingAreas]; | ||||
|  | ||||
| @@ -183,9 +197,14 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt | ||||
| 	[self addTrackingArea:_mouseTrackingArea]; | ||||
| } | ||||
|  | ||||
| - (void)mouseMoved:(NSEvent *)event { | ||||
| 	[super mouseMoved:event]; | ||||
| 	[self scheduleMouseHide]; | ||||
| - (void)scheduleMouseHide { | ||||
| 	if(!self.shouldCaptureMouse) { | ||||
| 		[_mouseHideTimer invalidate]; | ||||
|  | ||||
| 		_mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) { | ||||
| 			[NSCursor setHiddenUntilMouseMoves:YES]; | ||||
| 		}]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)mouseEntered:(NSEvent *)event { | ||||
| @@ -193,18 +212,119 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt | ||||
| 	[self scheduleMouseHide]; | ||||
| } | ||||
|  | ||||
| - (void)scheduleMouseHide { | ||||
| 	[_mouseHideTimer invalidate]; | ||||
| 	_mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) { | ||||
|         [NSCursor setHiddenUntilMouseMoves:YES]; | ||||
| 	}]; | ||||
| } | ||||
|  | ||||
| - (void)mouseExited:(NSEvent *)event { | ||||
| 	[super mouseExited:event]; | ||||
|  | ||||
| 	[_mouseHideTimer invalidate]; | ||||
| 	_mouseHideTimer = nil; | ||||
| } | ||||
|  | ||||
| - (void)releaseMouse { | ||||
| 	if(_mouseIsCaptured) { | ||||
| 		_mouseIsCaptured = NO; | ||||
| 		CGAssociateMouseAndMouseCursorPosition(true); | ||||
| 		[NSCursor unhide]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| #pragma mark - Mouse motion | ||||
|  | ||||
| - (void)applyMouseMotion:(NSEvent *)event { | ||||
| 	if(!self.shouldCaptureMouse) { | ||||
| 		// Mouse capture is off, so don't play games with the cursor, just schedule it to | ||||
| 		// hide in the near future. | ||||
| 		[self scheduleMouseHide]; | ||||
| 	} else { | ||||
| 		if(_mouseIsCaptured) { | ||||
| 			// Mouse capture is on, so move the cursor back to the middle of the window, and | ||||
| 			// forward the deltas to the listener. | ||||
| 			// | ||||
| 			// TODO: should I really need to invert the y coordinate myself? It suggests I | ||||
| 			// might have an error in mapping here. | ||||
| 			const NSPoint windowCentre = [self convertPoint:CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5) toView:nil]; | ||||
| 			const NSPoint screenCentre = [self.window convertPointToScreen:windowCentre]; | ||||
| 			const CGRect screenFrame = self.window.screen.frame; | ||||
| 			CGWarpMouseCursorPosition(NSMakePoint( | ||||
| 				screenFrame.origin.x + screenCentre.x, | ||||
| 				screenFrame.origin.y + screenFrame.size.height - screenCentre.y | ||||
| 			)); | ||||
|  | ||||
| 			[self.responderDelegate mouseMoved:event]; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)mouseDragged:(NSEvent *)event { | ||||
| 	[self applyMouseMotion:event]; | ||||
| 	[super mouseDragged:event]; | ||||
| } | ||||
|  | ||||
| - (void)rightMouseDragged:(NSEvent *)event { | ||||
| 	[self applyMouseMotion:event]; | ||||
| 	[super rightMouseDragged:event]; | ||||
| } | ||||
|  | ||||
| - (void)otherMouseDragged:(NSEvent *)event { | ||||
| 	[self applyMouseMotion:event]; | ||||
| 	[super otherMouseDragged:event]; | ||||
| } | ||||
|  | ||||
| - (void)mouseMoved:(NSEvent *)event { | ||||
| 	[self applyMouseMotion:event]; | ||||
| 	[super mouseMoved:event]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Mouse buttons | ||||
|  | ||||
| - (void)applyButtonDown:(NSEvent *)event { | ||||
| 	if(self.shouldCaptureMouse) { | ||||
| 		if(!_mouseIsCaptured) { | ||||
| 			_mouseIsCaptured = YES; | ||||
| 			[NSCursor hide]; | ||||
| 			CGAssociateMouseAndMouseCursorPosition(false); | ||||
|  | ||||
| 			// Don't report the first click to the delegate; treat that as merely | ||||
| 			// an invitation to capture the cursor. | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		[self.responderDelegate mouseDown:event]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)applyButtonUp:(NSEvent *)event { | ||||
| 	if(self.shouldCaptureMouse) { | ||||
| 		[self.responderDelegate mouseUp:event]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)mouseDown:(NSEvent *)event { | ||||
| 	[self applyButtonDown:event]; | ||||
| 	[super mouseDown:event]; | ||||
| } | ||||
|  | ||||
| - (void)rightMouseDown:(NSEvent *)event { | ||||
| 	[self applyButtonDown:event]; | ||||
| 	[super rightMouseDown:event]; | ||||
| } | ||||
|  | ||||
| - (void)otherMouseDown:(NSEvent *)event { | ||||
| 	[self applyButtonDown:event]; | ||||
| 	[super otherMouseDown:event]; | ||||
| } | ||||
|  | ||||
| - (void)mouseUp:(NSEvent *)event { | ||||
| 	[self applyButtonUp:event]; | ||||
| 	[super mouseUp:event]; | ||||
| } | ||||
|  | ||||
| - (void)rightMouseUp:(NSEvent *)event  { | ||||
| 	[self applyButtonUp:event]; | ||||
| 	[super rightMouseUp:event]; | ||||
| } | ||||
|  | ||||
| - (void)otherMouseUp:(NSEvent *)event { | ||||
| 	[self applyButtonUp:event]; | ||||
| 	[super otherMouseUp:event]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|   | ||||
| @@ -11,118 +11,244 @@ import Foundation | ||||
|  | ||||
| class MOS6522Tests: XCTestCase { | ||||
|  | ||||
| 	fileprivate func with6522(_ action: (MOS6522Bridge) -> ()) { | ||||
| 		let bridge = MOS6522Bridge() | ||||
| 		action(bridge) | ||||
| 	private var m6522: MOS6522Bridge! | ||||
|  | ||||
| 	override func setUp() { | ||||
| 		m6522 = MOS6522Bridge() | ||||
| 	} | ||||
|  | ||||
| 	// MARK: Timer tests | ||||
|  | ||||
| 	func testTimerCount() { | ||||
| 		with6522 { | ||||
| 			// set timer 1 to a value of $000a | ||||
| 			$0.setValue(10, forRegister: 4) | ||||
| 			$0.setValue(0, forRegister: 5) | ||||
| 		// set timer 1 to a value of m652200a | ||||
| 		m6522.setValue(10, forRegister: 4) | ||||
| 		m6522.setValue(0, forRegister: 5) | ||||
|  | ||||
| 		// complete the setting cycle | ||||
| 			$0.run(forHalfCycles: 2) | ||||
| 		m6522.run(forHalfCycles: 2) | ||||
|  | ||||
| 		// run for 5 cycles | ||||
| 			$0.run(forHalfCycles: 10) | ||||
| 		m6522.run(forHalfCycles: 10) | ||||
|  | ||||
| 		// check that the timer has gone down by 5 | ||||
| 			XCTAssert($0.value(forRegister: 4) == 5, "Low order byte should be 5; was \($0.value(forRegister: 4))") | ||||
| 			XCTAssert($0.value(forRegister: 5) == 0, "High order byte should be 0; was \($0.value(forRegister: 5))") | ||||
| 		} | ||||
| 		XCTAssert(m6522.value(forRegister: 4) == 5, "Low order byte should be 5; was \(m6522.value(forRegister: 4))") | ||||
| 		XCTAssert(m6522.value(forRegister: 5) == 0, "High order byte should be 0; was \(m6522.value(forRegister: 5))") | ||||
| 	} | ||||
|  | ||||
| 	func testTimerLatches() { | ||||
| 		with6522 { | ||||
| 		// set timer 2 to $1020 | ||||
| 			$0.setValue(0x10, forRegister: 8) | ||||
| 			$0.setValue(0x20, forRegister: 9) | ||||
| 		m6522.setValue(0x10, forRegister: 8) | ||||
| 		m6522.setValue(0x20, forRegister: 9) | ||||
|  | ||||
| 		// change the low-byte latch | ||||
| 			$0.setValue(0x40, forRegister: 8) | ||||
| 		m6522.setValue(0x40, forRegister: 8) | ||||
|  | ||||
| 		// complete the cycle | ||||
| 			$0.run(forHalfCycles: 2) | ||||
| 		m6522.run(forHalfCycles: 2) | ||||
|  | ||||
| 		// chek that the new latched value hasn't been copied | ||||
| 			XCTAssert($0.value(forRegister: 8) == 0x10, "Low order byte should be 0x10; was \($0.value(forRegister: 8))") | ||||
| 			XCTAssert($0.value(forRegister: 9) == 0x20, "High order byte should be 0x20; was \($0.value(forRegister: 9))") | ||||
| 		XCTAssert(m6522.value(forRegister: 8) == 0x10, "Low order byte should be 0x10; was \(m6522.value(forRegister: 8))") | ||||
| 		XCTAssert(m6522.value(forRegister: 9) == 0x20, "High order byte should be 0x20; was \(m6522.value(forRegister: 9))") | ||||
|  | ||||
| 		// write the low-byte latch | ||||
| 			$0.setValue(0x50, forRegister: 9) | ||||
| 		m6522.setValue(0x50, forRegister: 9) | ||||
|  | ||||
| 		// complete the cycle | ||||
| 			$0.run(forHalfCycles: 2) | ||||
| 		m6522.run(forHalfCycles: 2) | ||||
|  | ||||
| 		// chek that the latched value has been copied | ||||
| 			XCTAssert($0.value(forRegister: 8) == 0x40, "Low order byte should be 0x50; was \($0.value(forRegister: 8))") | ||||
| 			XCTAssert($0.value(forRegister: 9) == 0x50, "High order byte should be 0x40; was \($0.value(forRegister: 9))") | ||||
| 		} | ||||
| 		XCTAssert(m6522.value(forRegister: 8) == 0x40, "Low order byte should be 0x50; was \(m6522.value(forRegister: 8))") | ||||
| 		XCTAssert(m6522.value(forRegister: 9) == 0x50, "High order byte should be 0x40; was \(m6522.value(forRegister: 9))") | ||||
| 	} | ||||
|  | ||||
| 	func testTimerReload() { | ||||
| 		with6522 { | ||||
| 			// set timer 1 to a value of $0010, enable repeating mode | ||||
| 			$0.setValue(16, forRegister: 4) | ||||
| 			$0.setValue(0, forRegister: 5) | ||||
| 			$0.setValue(0x40, forRegister: 11) | ||||
| 			$0.setValue(0x40 | 0x80, forRegister: 14) | ||||
| 		// set timer 1 to a value of m6522010, enable repeating mode | ||||
| 		m6522.setValue(16, forRegister: 4) | ||||
| 		m6522.setValue(0, forRegister: 5) | ||||
| 		m6522.setValue(0x40, forRegister: 11) | ||||
| 		m6522.setValue(0x40 | 0x80, forRegister: 14) | ||||
|  | ||||
| 		// complete the cycle to set initial values | ||||
| 			$0.run(forHalfCycles: 2) | ||||
| 		m6522.run(forHalfCycles: 2) | ||||
|  | ||||
| 		// run for 16 cycles | ||||
| 			$0.run(forHalfCycles: 32) | ||||
| 		m6522.run(forHalfCycles: 32) | ||||
|  | ||||
| 		// check that the timer has gone down to 0 but not yet triggered an interrupt | ||||
| 			XCTAssert($0.value(forRegister: 4) == 0, "Low order byte should be 0; was \($0.value(forRegister: 4))") | ||||
| 			XCTAssert($0.value(forRegister: 5) == 0, "High order byte should be 0; was \($0.value(forRegister: 5))") | ||||
| 			XCTAssert(!$0.irqLine, "IRQ should not yet be active") | ||||
| 		XCTAssert(m6522.value(forRegister: 4) == 0, "Low order byte should be 0; was \(m6522.value(forRegister: 4))") | ||||
| 		XCTAssert(m6522.value(forRegister: 5) == 0, "High order byte should be 0; was \(m6522.value(forRegister: 5))") | ||||
| 		XCTAssert(!m6522.irqLine, "IRQ should not yet be active") | ||||
|  | ||||
| 		// check that two half-cycles later the timer is $ffff but IRQ still hasn't triggered | ||||
| 			$0.run(forHalfCycles: 2) | ||||
| 			XCTAssert($0.value(forRegister: 4) == 0xff, "Low order byte should be 0xff; was \($0.value(forRegister: 4))") | ||||
| 			XCTAssert($0.value(forRegister: 5) == 0xff, "High order byte should be 0xff; was \($0.value(forRegister: 5))") | ||||
| 			XCTAssert(!$0.irqLine, "IRQ should not yet be active") | ||||
| 		m6522.run(forHalfCycles: 2) | ||||
| 		XCTAssert(m6522.value(forRegister: 4) == 0xff, "Low order byte should be 0xff; was \(m6522.value(forRegister: 4))") | ||||
| 		XCTAssert(m6522.value(forRegister: 5) == 0xff, "High order byte should be 0xff; was \(m6522.value(forRegister: 5))") | ||||
| 		XCTAssert(!m6522.irqLine, "IRQ should not yet be active") | ||||
|  | ||||
| 		// check that one half-cycle later the timer is still $ffff and IRQ has triggered... | ||||
| 			$0.run(forHalfCycles: 1) | ||||
| 			XCTAssert($0.irqLine, "IRQ should be active") | ||||
| 			XCTAssert($0.value(forRegister: 4) == 0xff, "Low order byte should be 0xff; was \($0.value(forRegister: 4))") | ||||
| 			XCTAssert($0.value(forRegister: 5) == 0xff, "High order byte should be 0xff; was \($0.value(forRegister: 5))") | ||||
| 		m6522.run(forHalfCycles: 1) | ||||
| 		XCTAssert(m6522.irqLine, "IRQ should be active") | ||||
| 		XCTAssert(m6522.value(forRegister: 4) == 0xff, "Low order byte should be 0xff; was \(m6522.value(forRegister: 4))") | ||||
| 		XCTAssert(m6522.value(forRegister: 5) == 0xff, "High order byte should be 0xff; was \(m6522.value(forRegister: 5))") | ||||
|  | ||||
| 		// ... but that reading the timer cleared the interrupt | ||||
| 			XCTAssert(!$0.irqLine, "IRQ should be active") | ||||
| 		XCTAssert(!m6522.irqLine, "IRQ should be active") | ||||
|  | ||||
| 		// check that one half-cycles later the timer has reloaded | ||||
| 			$0.run(forHalfCycles: 1) | ||||
| 			XCTAssert($0.value(forRegister: 4) == 0x10, "Low order byte should be 0x10; was \($0.value(forRegister: 4))") | ||||
| 			XCTAssert($0.value(forRegister: 5) == 0x00, "High order byte should be 0x00; was \($0.value(forRegister: 5))") | ||||
| 		} | ||||
| 		m6522.run(forHalfCycles: 1) | ||||
| 		XCTAssert(m6522.value(forRegister: 4) == 0x10, "Low order byte should be 0x10; was \(m6522.value(forRegister: 4))") | ||||
| 		XCTAssert(m6522.value(forRegister: 5) == 0x00, "High order byte should be 0x00; was \(m6522.value(forRegister: 5))") | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	// MARK: Data direction tests | ||||
| 	func testDataDirection() { | ||||
| 		with6522 { | ||||
| 		// set low four bits of register B as output, the top four as input | ||||
| 			$0.setValue(0xf0, forRegister: 2) | ||||
| 		m6522.setValue(0xf0, forRegister: 2) | ||||
|  | ||||
| 		// ask to output 0x8c | ||||
| 			$0.setValue(0x8c, forRegister: 0) | ||||
| 		m6522.setValue(0x8c, forRegister: 0) | ||||
|  | ||||
| 		// complete the cycle | ||||
| 			$0.run(forHalfCycles: 2) | ||||
| 		m6522.run(forHalfCycles: 2) | ||||
|  | ||||
| 		// set current input as 0xda | ||||
| 			$0.portBInput = 0xda | ||||
| 		m6522.portBInput = 0xda | ||||
|  | ||||
| 		// test that the result of reading register B is therefore 0x8a | ||||
| 			XCTAssert($0.value(forRegister: 0) == 0x8a, "Data direction register should mix input and output; got \($0.value(forRegister: 0))") | ||||
| 		} | ||||
| 		XCTAssert(m6522.value(forRegister: 0) == 0x8a, "Data direction register should mix input and output; got \(m6522.value(forRegister: 0))") | ||||
| 	} | ||||
|  | ||||
| 	func testShiftDisabled() { | ||||
| 		/* | ||||
| 			Mode 0 disables the Shift Register. In this mode the microprocessor can | ||||
| 			write or read the SR and the SR will shift on each CB1 positive edge | ||||
| 			shifting in the value on CB2. In this mode the SR Interrupt Flag is | ||||
| 			disabled (held to a logic 0). | ||||
| 		*/ | ||||
| 	} | ||||
|  | ||||
| 	func testShiftInUnderT2() { | ||||
| 		/* | ||||
| 			In mode 1, the shifting rate is controlled by the low order 8 bits of T2 | ||||
| 			(Figure 22). Shift pulses are generated on the CB1 pin to control shifting | ||||
| 			in external devices. The time between transitions of this output clock is a | ||||
| 			function of the system clock period and the contents of the low order T2 | ||||
| 			latch (N). | ||||
|  | ||||
| 			The shifting operation is triggered by the read or write of the SR if the | ||||
| 			SR flag is set in the IFR. Otherwise the first shift will occur at the next | ||||
| 			time-out of T2 after a read or write of the SR. Data is shifted first into | ||||
| 			the low order bit of SR and is then shifted into the next higher order bit | ||||
| 			of the shift register on the negative-going edge of each clock pulse. The | ||||
| 			input data should change before the positive-going edge of the CB1 clock | ||||
| 			pulse. This data is shifted into shift register during the 02 clock cycle | ||||
| 			following the positive-going edge of the CB1 clock pulse. After 8 CB1 clock | ||||
| 			pulses, the shift register interrupt flag will set and IRQ will go low. | ||||
| 		*/ | ||||
| 	} | ||||
|  | ||||
| 	func testShiftInUnderPhase2() { | ||||
| 		/* | ||||
| 			In mode 2, the shift rate is a direct function of the system clock | ||||
| 			frequency (Figure 23). CB1 becomes an output which generates shift pulses | ||||
| 			for controlling external devices. Timer 2 operates as an independent | ||||
| 			interval timer and has no effect on SR. The shifting operation is triggered | ||||
| 			by reading or writing the Shift Register. Data is shifted, first into bit 0 | ||||
| 			and is then shifted into the next higher order bit of the shift register on | ||||
| 			the trailing edge of each 02 clock pulse. After 8 clock pulses, the shift | ||||
| 			register interrupt flag will be set, and the output clock pulses on CB1 | ||||
| 			will stop. | ||||
| 		*/ | ||||
| 	} | ||||
|  | ||||
| 	func testShiftInUnderCB1() { | ||||
| 		/* | ||||
| 			In mode 3, external pin CB1 becomes an input (Figure 24). This allows an | ||||
| 			external device to load the shift register at its own pace. The shift | ||||
| 			register counter will interrupt the processor each time 8 bits have been | ||||
| 			shifted in. However the shift register counter does not stop the shifting | ||||
| 			operation; it acts simply as a pulse counter. Reading or writing the Shift | ||||
| 			Register resets the Interrupt Flag and initializes the SR counter to count | ||||
| 			another 8 pulses. | ||||
|  | ||||
| 			Note that the data is shifted during the first system clock cycle | ||||
| 			following the positive-going edge of the CB1 shift pulse. For this reason, | ||||
| 			data must be held stable during the first full cycle following CB1 going | ||||
| 			high. | ||||
| 		*/ | ||||
| 	} | ||||
|  | ||||
| 	func testShiftOutUnderT2FreeRunning() { | ||||
| 		/* | ||||
| 			Mode 4 is very similar to mode 5 in which the shifting rate is set by T2. | ||||
| 			However, in mode 4 the SR Counter does not stop the shifting operation | ||||
| 			(Figure 25). Since the Shift Register bit 7 (SR7) is recirculated back into | ||||
| 			bit 0, the 8 bits loaded into the Shift Register will be clocked onto CB2 | ||||
| 			repetitively. In this mode the Shift Register Counter is disabled. | ||||
| 		*/ | ||||
| 	} | ||||
|  | ||||
| 	func testShiftOutUnderT2() { | ||||
| 		/* | ||||
| 			In mode 5, the shift rate is controlled by T2 (as in mode 4). The shifting | ||||
| 			operation is triggered by the read or write of the SR if the SR flag is set | ||||
| 			in the IFR (Figure 26). Otherwise the first shift will occur at the next | ||||
| 			time-out of T2 after a read or write of the SR. However, with each read or | ||||
| 			write of the shift register the SR Counter is reset and 8 bits are shifted | ||||
| 			onto CB2. At the same time, 8 shift pulses are generated on CB1 to control | ||||
| 			shifting in external devices. After the 8 shift pulses, the shifting is | ||||
| 			disabled, the SR Interrupt Flag is set and CB2 remains at the last data | ||||
| 			level. | ||||
| 		*/ | ||||
| 	} | ||||
|  | ||||
| 	func testShiftOutUnderPhase2() { | ||||
| 		/* | ||||
| 			In mode 6, the shift rate is controlled by the 02 system clock (Figure 27). | ||||
|  | ||||
| 			(... and I'm assuming the same behaviour as shift out under control of T2 | ||||
| 			otherwise, based on original context) | ||||
| 		*/ | ||||
| 		// Set the shift register to a non-zero something. | ||||
| 		m6522.setValue(0xaa, forRegister: 10) | ||||
|  | ||||
| 		// Set shift register mode 6. | ||||
| 		m6522.setValue(6 << 2, forRegister: 11) | ||||
|  | ||||
| 		// Make sure the shift register's interrupt bit is set. | ||||
| 		m6522.run(forHalfCycles: 16) | ||||
| 		XCTAssertEqual(m6522.value(forRegister: 13) & 0x04, 0x04) | ||||
|  | ||||
| 		// Test that output is now inhibited: CB2 should remain unchanged. | ||||
| 		let initialOutput = m6522.value(forControlLine: .two, port: .B) | ||||
| 		for _ in 1...8 { | ||||
| 			m6522.run(forHalfCycles: 2) | ||||
| 			XCTAssertEqual(m6522.value(forControlLine: .two, port: .B), initialOutput) | ||||
| 		} | ||||
|  | ||||
| 		// Set a new value to the shift register. | ||||
| 		m6522.setValue(0x16, forRegister: 10) | ||||
|  | ||||
| 		// Test that the new value is shifted out. | ||||
| 		var output = 0 | ||||
| 		for _ in 1..<8 { | ||||
| 			m6522.run(forHalfCycles: 2) | ||||
| 			output = (output << 1) | (m6522.value(forControlLine: .two, port: .B) ? 1 : 0) | ||||
| 		} | ||||
| 		XCTAssertEqual(output, 0x16) | ||||
| 	} | ||||
|  | ||||
| 	func testShiftOutUnderCB1() { | ||||
| 		/* | ||||
| 			In mode 7, shifting is controlled by pulses applied to the CB1 pin by an | ||||
| 			external device (Figure 28). The SR counter sets the SR Interrupt Flag each | ||||
| 			time it counts 8 pulses but it does not disable the shifting function. Each | ||||
| 			time the microprocessor, writes or reads the shift register, the SR | ||||
| 			Interrupt Flag is reset and the SR counter is initialized to begin counting | ||||
| 			the next 8 shift pulses on pin CB1. After 8 shift pulses, the Interrupt | ||||
| 			Flag is set. The microprocessor can then load the shift register with the | ||||
| 			next byte of data. | ||||
| 		*/ | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								OSBindings/Mac/Clock SignalTests/68000 Coverage/OPCLOGR2.BIN
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								OSBindings/Mac/Clock SignalTests/68000 Coverage/OPCLOGR2.BIN
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -0,0 +1,5 @@ | ||||
| This file provides a record of which opcodes are valid and which are invalid on a | ||||
| real 68000. It was generated by AtariZoll and made available via | ||||
| http://www.atari-forum.com/viewtopic.php?f=68&t=26820 . | ||||
|  | ||||
| No licence was specified. | ||||
							
								
								
									
										1580
									
								
								OSBindings/Mac/Clock SignalTests/68000ArithmeticTests.mm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1580
									
								
								OSBindings/Mac/Clock SignalTests/68000ArithmeticTests.mm
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										291
									
								
								OSBindings/Mac/Clock SignalTests/68000BCDTests.mm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								OSBindings/Mac/Clock SignalTests/68000BCDTests.mm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,291 @@ | ||||
| // | ||||
| //  68000BCDTests.m | ||||
| //  Clock SignalTests | ||||
| // | ||||
| //  Created by Thomas Harte on 29/06/2019. | ||||
| // | ||||
| //  Largely ported from the tests of the Portable 68k Emulator. | ||||
| // | ||||
|  | ||||
| #import <XCTest/XCTest.h> | ||||
|  | ||||
| #include "TestRunner68000.hpp" | ||||
|  | ||||
| @interface M68000BCDTests : XCTestCase | ||||
| @end | ||||
|  | ||||
| @implementation M68000BCDTests { | ||||
| 	std::unique_ptr<RAM68000> _machine; | ||||
| } | ||||
|  | ||||
| - (void)setUp { | ||||
|     _machine.reset(new RAM68000()); | ||||
| } | ||||
|  | ||||
| - (void)tearDown { | ||||
| 	_machine.reset(); | ||||
| } | ||||
|  | ||||
| // MARK: ABCD | ||||
|  | ||||
| - (void)testABCD { | ||||
| 	_machine->set_program({ | ||||
| 		0xc302,		// ABCD D2, D1 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.data[1] = 0x1234567a; | ||||
| 	state.data[2] = 0xf745ff78; | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.status & Flag::Carry); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345658); | ||||
| 	XCTAssertEqual(state.data[2], 0xf745ff78); | ||||
| } | ||||
|  | ||||
| - (void)testABCDZero { | ||||
| 	_machine->set_program({ | ||||
| 		0xc302,		// ABCD D2, D1 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.data[1] = 0x12345600; | ||||
| 	state.data[2] = 0x12345600; | ||||
| 	state.status = Flag::Zero; | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.status & Flag::Zero); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345600); | ||||
| 	XCTAssertEqual(state.data[2], 0x12345600); | ||||
| } | ||||
|  | ||||
| - (void)testABCDNegative { | ||||
| 	_machine->set_program({ | ||||
| 		0xc302,		// ABCD D2, D1 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.data[1] = 0x12345645; | ||||
| 	state.data[2] = 0x12345654; | ||||
| 	state.status = Flag::Zero; | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.status & Flag::Negative); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345699); | ||||
| 	XCTAssertEqual(state.data[2], 0x12345654); | ||||
| } | ||||
|  | ||||
| - (void)testABCDWithX { | ||||
| 	_machine->set_program({ | ||||
| 		0xc302,		// ABCD D2, D1 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.data[1] = 0x12345645; | ||||
| 	state.data[2] = 0x12345654; | ||||
| 	state.status = Flag::Extend; | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.status & Flag::Carry); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345600); | ||||
| 	XCTAssertEqual(state.data[2], 0x12345654); | ||||
| } | ||||
|  | ||||
| - (void)testABCDOverflow { | ||||
| 	_machine->set_program({ | ||||
| 		0xc302,		// ABCD D2, D1 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.data[1] = 0x1234563e; | ||||
| 	state.data[2] = 0x1234563e; | ||||
| 	state.status = Flag::Extend; | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.status & Flag::Overflow); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345683); | ||||
| 	XCTAssertEqual(state.data[2], 0x1234563e); | ||||
| } | ||||
|  | ||||
| - (void)testABCDPredecDifferent { | ||||
| 	_machine->set_program({ | ||||
| 		0xc30a,		// ABCD -(A2), -(A1) | ||||
| 	}); | ||||
| 	*_machine->ram_at(0x3000) = 0xa200; | ||||
| 	*_machine->ram_at(0x4000) = 0x1900; | ||||
|  | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.address[1] = 0x3001; | ||||
| 	state.address[2] = 0x4001; | ||||
| 	state.status = Flag::Extend; | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.status & Flag::Carry); | ||||
| 	XCTAssert(state.status & Flag::Extend); | ||||
| 	XCTAssertEqual(state.address[1], 0x3000); | ||||
| 	XCTAssertEqual(state.address[2], 0x4000); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x3000), 0x2200); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x4000), 0x1900); | ||||
| } | ||||
|  | ||||
| - (void)testABCDPredecSame { | ||||
| 	_machine->set_program({ | ||||
| 		0xc309,		// ABCD -(A1), -(A1) | ||||
| 	}); | ||||
| 	*_machine->ram_at(0x3000) = 0x19a2; | ||||
|  | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.address[1] = 0x3002; | ||||
| 	state.status = Flag::Extend; | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.status & Flag::Carry); | ||||
| 	XCTAssert(state.status & Flag::Extend); | ||||
| 	XCTAssertEqual(state.address[1], 0x3000); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x3000), 0x22a2); | ||||
| } | ||||
|  | ||||
| // MARK: NBCD | ||||
|  | ||||
| - (void)performNBCDd1:(uint32_t)d1 ccr:(uint8_t)ccr { | ||||
| 	_machine->set_program({ | ||||
| 		0x4801		// NBCD D1 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.status |= ccr; | ||||
| 	state.data[1] = d1; | ||||
|  | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	XCTAssertEqual(6, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| - (void)testNBCD_Dn { | ||||
| 	[self performNBCDd1:0x7a ccr:0]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[1], 0x20); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Overflow); | ||||
| } | ||||
|  | ||||
| - (void)testNBCD_Dn_extend { | ||||
| 	[self performNBCDd1:0x1234567a ccr:Flag::Extend | Flag::Zero]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[1], 0x1234561f); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Overflow); | ||||
| } | ||||
|  | ||||
| - (void)testNBCD_Dn_zero { | ||||
| 	[self performNBCDd1:0x12345600 ccr:Flag::Zero]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345600); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Zero); | ||||
| } | ||||
|  | ||||
| - (void)testNBCD_Dn_negative { | ||||
| 	[self performNBCDd1:0x123456ff ccr:Flag::Extend | Flag::Zero]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[1], 0x1234569a); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Negative); | ||||
| } | ||||
|  | ||||
| - (void)testNBCD_Dn_XXXw { | ||||
| 	_machine->set_program({ | ||||
| 		0x4838, 0x3000		// NBCD ($3000).w | ||||
| 	}); | ||||
| 	*_machine->ram_at(0x3000) = 0x0100; | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(16, _machine->get_cycle_count()); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x3000), 0x9900); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Negative); | ||||
| } | ||||
|  | ||||
| // MARK: SBCD | ||||
|  | ||||
| - (void)performSBCDd1:(uint32_t)d1 d2:(uint32_t)d2 ccr:(uint8_t)ccr { | ||||
| 	_machine->set_program({ | ||||
| 		0x8302		// SBCD D2, D1 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.status |= ccr; | ||||
| 	state.data[1] = d1; | ||||
| 	state.data[2] = d2; | ||||
|  | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(6, _machine->get_cycle_count()); | ||||
| 	XCTAssertEqual(state.data[2], d2); | ||||
| } | ||||
|  | ||||
| - (void)testSBCD_Dn { | ||||
| 	[self performSBCDd1:0x12345689 d2:0xf745ff78 ccr:Flag::Zero]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345611); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, 0); | ||||
| } | ||||
|  | ||||
| - (void)testSBCD_Dn_zero { | ||||
| 	[self performSBCDd1:0x123456ff d2:0xf745ffff ccr:Flag::Zero]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345600); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Zero); | ||||
| } | ||||
|  | ||||
| - (void)testSBCD_Dn_negative { | ||||
| 	[self performSBCDd1:0x12345634 d2:0xf745ff45 ccr:Flag::Extend]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345688); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Negative); | ||||
| } | ||||
|  | ||||
| - (void)testSBCD_Dn_overflow { | ||||
| 	[self performSBCDd1:0x123456a9 d2:0xf745ffff ccr:Flag::Extend]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[1], 0x12345643); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Overflow); | ||||
| } | ||||
|  | ||||
| - (void)testSBCD_Dn_PreDec { | ||||
| 	_machine->set_program({ | ||||
| 		0x830a		// SBCD -(A2), -(A1) | ||||
| 	}); | ||||
| 	*_machine->ram_at(0x3000) = 0xa200; | ||||
| 	*_machine->ram_at(0x4000) = 0x1900; | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.address[1] = 0x3001; | ||||
| 	state.address[2] = 0x4001; | ||||
| 	state.status |= Flag::Extend; | ||||
|  | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(18, _machine->get_cycle_count()); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x3000), 0x8200); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x4000), 0x1900); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Negative); | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										1502
									
								
								OSBindings/Mac/Clock SignalTests/68000BitwiseTests.mm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1502
									
								
								OSBindings/Mac/Clock SignalTests/68000BitwiseTests.mm
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										557
									
								
								OSBindings/Mac/Clock SignalTests/68000ControlFlowTests.mm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										557
									
								
								OSBindings/Mac/Clock SignalTests/68000ControlFlowTests.mm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,557 @@ | ||||
| // | ||||
| //  68000ControlFlowTests.m | ||||
| //  Clock SignalTests | ||||
| // | ||||
| //  Created by Thomas Harte on 28/06/2019. | ||||
| // | ||||
| //  Largely ported from the tests of the Portable 68k Emulator. | ||||
| // | ||||
|  | ||||
| #import <XCTest/XCTest.h> | ||||
|  | ||||
| #include "TestRunner68000.hpp" | ||||
|  | ||||
|  | ||||
| @interface M68000ControlFlowTests : XCTestCase | ||||
| @end | ||||
|  | ||||
| @implementation M68000ControlFlowTests { | ||||
| 	std::unique_ptr<RAM68000> _machine; | ||||
| } | ||||
|  | ||||
| - (void)setUp { | ||||
|     _machine.reset(new RAM68000()); | ||||
| } | ||||
|  | ||||
| - (void)tearDown { | ||||
| 	_machine.reset(); | ||||
| } | ||||
|  | ||||
| // MARK: Bcc | ||||
|  | ||||
| - (void)performBccb:(uint16_t)opcode { | ||||
| 	_machine->set_program({ | ||||
| 		uint16_t(opcode | 6)	// Bcc.b +6 | ||||
| 	}); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
| } | ||||
|  | ||||
| - (void)performBccw:(uint16_t)opcode { | ||||
| 	_machine->set_program({ | ||||
| 		opcode, 0x0006			// Bcc.w +6 | ||||
| 	}); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
| } | ||||
|  | ||||
| - (void)testBHIb { | ||||
| 	[self performBccb:0x6200]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1008 + 4); | ||||
| 	XCTAssertEqual(_machine->get_cycle_count(), 10); | ||||
| } | ||||
|  | ||||
| - (void)testBLOb { | ||||
| 	[self performBccb:0x6500]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1002 + 4); | ||||
| 	XCTAssertEqual(_machine->get_cycle_count(), 8); | ||||
| } | ||||
|  | ||||
| - (void)testBHIw { | ||||
| 	[self performBccw:0x6200]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1008 + 4); | ||||
| 	XCTAssertEqual(_machine->get_cycle_count(), 10); | ||||
| } | ||||
|  | ||||
| - (void)testBLOw { | ||||
| 	[self performBccw:0x6500]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1004 + 4); | ||||
| 	XCTAssertEqual(_machine->get_cycle_count(), 12); | ||||
| } | ||||
|  | ||||
| // MARK: BRA | ||||
|  | ||||
| - (void)testBRAb { | ||||
| 	_machine->set_program({ | ||||
| 		0x6004		// BRA.b +4 | ||||
| 	}); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1006 + 4); | ||||
| 	XCTAssertEqual(_machine->get_cycle_count(), 10); | ||||
| } | ||||
|  | ||||
| - (void)testBRAw { | ||||
| 	_machine->set_program({ | ||||
| 		0x6000, 0x0004		// BRA.b +4 | ||||
| 	}); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1006 + 4); | ||||
| 	XCTAssertEqual(_machine->get_cycle_count(), 10); | ||||
| } | ||||
|  | ||||
| // MARK: BSR | ||||
|  | ||||
| - (void)testBSRw { | ||||
| 	_machine->set_program({ | ||||
| 		0x6100, 0x0006		// BSR.w $1008 | ||||
| 	}); | ||||
| 	_machine->set_initial_stack_pointer(0x3000); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1008 + 4); | ||||
| 	XCTAssertEqual(state.stack_pointer(), 0x2ffc); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, 0); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x2ffc), 0); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x2ffe), 0x1004); | ||||
|  | ||||
| 	XCTAssertEqual(_machine->get_cycle_count(), 18); | ||||
| } | ||||
|  | ||||
| - (void)testBSRb { | ||||
| 	_machine->set_program({ | ||||
| 		0x6106		// BSR.b $1008 | ||||
| 	}); | ||||
| 	_machine->set_initial_stack_pointer(0x3000); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1008 + 4); | ||||
| 	XCTAssertEqual(state.stack_pointer(), 0x2ffc); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, 0); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x2ffc), 0); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x2ffe), 0x1002); | ||||
|  | ||||
| 	XCTAssertEqual(_machine->get_cycle_count(), 18); | ||||
| } | ||||
|  | ||||
| // MARK: CHK | ||||
|  | ||||
| - (void)performCHKd1:(uint32_t)d1 d2:(uint32_t)d2 { | ||||
| 	_machine->set_program({ | ||||
| 		0x4581		// CHK D1, D2 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.data[1] = d1; | ||||
| 	state.data[2] = d2; | ||||
| 	state.status |= Flag::ConditionCodes; | ||||
|  | ||||
| 	_machine->set_initial_stack_pointer(0); | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[1], d1); | ||||
| 	XCTAssertEqual(state.data[2], d2); | ||||
| } | ||||
|  | ||||
| - (void)testCHK_1111v1111 { | ||||
| 	[self performCHKd1:0x1111 d2:0x1111]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1002 + 4); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend); | ||||
| 	XCTAssertEqual(10, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| - (void)testCHK_1111v0000 { | ||||
| 	[self performCHKd1:0x1111 d2:0x0000]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1002 + 4); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Zero); | ||||
| 	XCTAssertEqual(10, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| - (void)testCHK_8000v8001 { | ||||
| 	[self performCHKd1:0x8000 d2:0x8001]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertNotEqual(state.program_counter, 0x1002 + 4); | ||||
| 	XCTAssertEqual(state.stack_pointer(), 0xfffffffa); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend); | ||||
| 	XCTAssertEqual(42, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| - (void)testCHK_8000v8000 { | ||||
| 	[self performCHKd1:0x8000 d2:0x8000]; | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertNotEqual(state.program_counter, 0x1002 + 4); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Negative); | ||||
| 	XCTAssertEqual(44, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| // MARK: DBcc | ||||
|  | ||||
| - (void)performDBccTestOpcode:(uint16_t)opcode status:(uint16_t)status d2Outcome:(uint32_t)d2Output { | ||||
| 	_machine->set_program({ | ||||
| 		opcode, 0x0008		// DBcc D2, +8 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.status = status; | ||||
| 	state.data[2] = 1; | ||||
|  | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.data[2], d2Output); | ||||
| 	XCTAssertEqual(state.status, status); | ||||
| } | ||||
|  | ||||
| - (void)testDBT { | ||||
| 	[self performDBccTestOpcode:0x50ca status:0 d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBF { | ||||
| 	[self performDBccTestOpcode:0x51ca status:0 d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBHI { | ||||
| 	[self performDBccTestOpcode:0x52ca status:0 d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBHI_Carry { | ||||
| 	[self performDBccTestOpcode:0x52ca status:Flag::Carry d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBHI_Zero { | ||||
| 	[self performDBccTestOpcode:0x52ca status:Flag::Zero d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLS_CarryOverflow { | ||||
| 	[self performDBccTestOpcode:0x53ca status:Flag::Carry | Flag::Overflow d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLS_Carry { | ||||
| 	[self performDBccTestOpcode:0x53ca status:Flag::Carry d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLS_Overflow { | ||||
| 	[self performDBccTestOpcode:0x53ca status:Flag::Overflow d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBCC_Carry { | ||||
| 	[self performDBccTestOpcode:0x54ca status:Flag::Carry d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBCC { | ||||
| 	[self performDBccTestOpcode:0x54ca status:0 d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBCS { | ||||
| 	[self performDBccTestOpcode:0x55ca status:0 d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBCS_Carry { | ||||
| 	[self performDBccTestOpcode:0x55ca status:Flag::Carry d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBNE { | ||||
| 	[self performDBccTestOpcode:0x56ca status:0 d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBNE_Zero { | ||||
| 	[self performDBccTestOpcode:0x56ca status:Flag::Zero d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBEQ { | ||||
| 	[self performDBccTestOpcode:0x57ca status:0 d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBEQ_Zero { | ||||
| 	[self performDBccTestOpcode:0x57ca status:Flag::Zero d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBVC { | ||||
| 	[self performDBccTestOpcode:0x58ca status:0 d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBVC_Overflow { | ||||
| 	[self performDBccTestOpcode:0x58ca status:Flag::Overflow d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBVS { | ||||
| 	[self performDBccTestOpcode:0x59ca status:0 d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBVS_Overflow { | ||||
| 	[self performDBccTestOpcode:0x59ca status:Flag::Overflow d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBPL { | ||||
| 	[self performDBccTestOpcode:0x5aca status:0 d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBPL_Negative { | ||||
| 	[self performDBccTestOpcode:0x5aca status:Flag::Negative d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBMI { | ||||
| 	[self performDBccTestOpcode:0x5bca status:0 d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBMI_Negative { | ||||
| 	[self performDBccTestOpcode:0x5bca status:Flag::Negative d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBGE_NegativeOverflow { | ||||
| 	[self performDBccTestOpcode:0x5cca status:Flag::Negative | Flag::Overflow d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBGE { | ||||
| 	[self performDBccTestOpcode:0x5cca status:0 d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBGE_Negative { | ||||
| 	[self performDBccTestOpcode:0x5cca status:Flag::Negative d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBGE_Overflow { | ||||
| 	[self performDBccTestOpcode:0x5cca status:Flag::Overflow d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLT_NegativeOverflow { | ||||
| 	[self performDBccTestOpcode:0x5dca status:Flag::Negative | Flag::Overflow d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLT { | ||||
| 	[self performDBccTestOpcode:0x5dca status:0 d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLT_Negative { | ||||
| 	[self performDBccTestOpcode:0x5dca status:Flag::Negative d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLT_Overflow { | ||||
| 	[self performDBccTestOpcode:0x5dca status:Flag::Overflow d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBGT { | ||||
| 	[self performDBccTestOpcode:0x5eca status:0 d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBGT_ZeroNegativeOverflow { | ||||
| 	[self performDBccTestOpcode:0x5eca status:Flag::Zero | Flag::Negative | Flag::Overflow d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBGT_NegativeOverflow { | ||||
| 	[self performDBccTestOpcode:0x5eca status:Flag::Negative | Flag::Overflow d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBGT_Zero { | ||||
| 	[self performDBccTestOpcode:0x5eca status:Flag::Zero d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLE { | ||||
| 	[self performDBccTestOpcode:0x5fca status:0 d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLE_Zero { | ||||
| 	[self performDBccTestOpcode:0x5fca status:Flag::Zero d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLE_Negative { | ||||
| 	[self performDBccTestOpcode:0x5fca status:Flag::Negative d2Outcome:1]; | ||||
| } | ||||
|  | ||||
| - (void)testDBLE_NegativeOverflow { | ||||
| 	[self performDBccTestOpcode:0x5fca status:Flag::Negative | Flag::Overflow d2Outcome:0]; | ||||
| } | ||||
|  | ||||
| /* Further DBF tests omitted; they seemed to be duplicative, assuming I'm not suffering a failure of comprehension. */ | ||||
|  | ||||
| // MARK: JMP | ||||
|  | ||||
| - (void)testJMP_A1 { | ||||
| 	_machine->set_program({ | ||||
| 		0x4ed1		// JMP (A1) | ||||
| 	}); | ||||
|  | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.address[1] = 0x3000; | ||||
|  | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.address[1], 0x3000); | ||||
| 	XCTAssertEqual(state.program_counter, 0x3000 + 4); | ||||
| 	XCTAssertEqual(8, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| - (void)testJMP_PC { | ||||
| 	_machine->set_program({ | ||||
| 		0x4efa, 0x000a		// JMP PC+a (i.e. to 0x100c) | ||||
| 	}); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x100c + 4); | ||||
| 	XCTAssertEqual(10, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| // MARK: JSR | ||||
|  | ||||
| - (void)testJSR_PC { | ||||
| 	_machine->set_program({ | ||||
| 		0x4eba, 0x000a		// JSR (+a)PC		; JSR to $100c | ||||
| 	}); | ||||
| 	_machine->set_initial_stack_pointer(0x2000); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.stack_pointer(), 0x1ffc); | ||||
| 	XCTAssertEqual(state.program_counter, 0x100c + 4); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x1ffc), 0x0000); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x1ffe), 0x1004); | ||||
| 	XCTAssertEqual(18, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| - (void)testJSR_XXXl { | ||||
| 	_machine->set_program({ | ||||
| 		0x4eb9, 0x0000, 0x1008		// JSR ($1008).l | ||||
| 	}); | ||||
| 	_machine->set_initial_stack_pointer(0x2000); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.stack_pointer(), 0x1ffc); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1008 + 4); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x1ffc), 0x0000); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x1ffe), 0x1006); | ||||
| 	XCTAssertEqual(20, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| // MARK: NOP | ||||
|  | ||||
| - (void)testNOP { | ||||
| 	_machine->set_program({ | ||||
| 		0x4e71		// NOP | ||||
| 	}); | ||||
| 	_machine->run_for_instructions(1); | ||||
| 	XCTAssertEqual(4, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| // MARK: RTR | ||||
|  | ||||
| - (void)testRTR { | ||||
| 	_machine->set_program({ | ||||
| 		0x4e77		// RTR | ||||
| 	}); | ||||
| 	_machine->set_initial_stack_pointer(0x2000); | ||||
| 	*_machine->ram_at(0x2000) = 0x7fff; | ||||
| 	*_machine->ram_at(0x2002) = 0; | ||||
| 	*_machine->ram_at(0x2004) = 0xc; | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.stack_pointer(), 0x2006); | ||||
| 	XCTAssertEqual(state.program_counter, 0x10); | ||||
| 	XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::ConditionCodes); | ||||
| 	XCTAssertEqual(20, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| // MARK: RTS | ||||
|  | ||||
| - (void)testRTS { | ||||
| 	_machine->set_program({ | ||||
| 		0x4e75		// RTS | ||||
| 	}); | ||||
| 	_machine->set_initial_stack_pointer(0x2000); | ||||
| 	*_machine->ram_at(0x2000) = 0x0000; | ||||
| 	*_machine->ram_at(0x2002) = 0x000c; | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.stack_pointer(), 0x2004); | ||||
| 	XCTAssertEqual(state.program_counter, 0x000c + 4); | ||||
| 	XCTAssertEqual(16, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| // MARK: TRAP | ||||
|  | ||||
| - (void)testTRAP { | ||||
| 	_machine->set_program({ | ||||
| 		0x4e41		// TRAP #1 | ||||
| 	}); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.status = 0x700; | ||||
| 	state.user_stack_pointer = 0x200; | ||||
| 	state.supervisor_stack_pointer = 0x206; | ||||
| 	*_machine->ram_at(0x84) = 0xfffe; | ||||
| 	*_machine->ram_at(0xfffe) = 0x4e71; | ||||
|  | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.status, 0x2700); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x200), 0x700); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x202), 0x0000); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x204), 0x1002); | ||||
| 	XCTAssertEqual(state.supervisor_stack_pointer, 0x200); | ||||
| 	XCTAssertEqual(34, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| // MARK: TRAPV | ||||
|  | ||||
| - (void)testTRAPV_taken { | ||||
| 	_machine->set_program({ | ||||
| 		0x4e76		// TRAPV | ||||
| 	}); | ||||
| 	_machine->set_initial_stack_pointer(0x206); | ||||
|  | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	state.status = 0x702; | ||||
| 	state.supervisor_stack_pointer = 0x206; | ||||
| 	*_machine->ram_at(0x1e) = 0xfffe; | ||||
| 	*_machine->ram_at(0xfffe) = 0x4e71; | ||||
|  | ||||
| 	_machine->set_processor_state(state); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.status, 0x2702); | ||||
| 	XCTAssertEqual(state.stack_pointer(), 0x200); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x202), 0x0000); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x204), 0x1002); | ||||
| 	XCTAssertEqual(*_machine->ram_at(0x200), 0x702); | ||||
| 	XCTAssertEqual(34, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| - (void)testTRAPV_untaken { | ||||
| 	_machine->set_program({ | ||||
| 		0x4e76		// TRAPV | ||||
| 	}); | ||||
|  | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssertEqual(state.program_counter, 0x1002 + 4); | ||||
| 	XCTAssertEqual(4, _machine->get_cycle_count()); | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										1250
									
								
								OSBindings/Mac/Clock SignalTests/68000MoveTests.mm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1250
									
								
								OSBindings/Mac/Clock SignalTests/68000MoveTests.mm
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1122
									
								
								OSBindings/Mac/Clock SignalTests/68000RollShiftTests.mm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1122
									
								
								OSBindings/Mac/Clock SignalTests/68000RollShiftTests.mm
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										425
									
								
								OSBindings/Mac/Clock SignalTests/68000Tests.mm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										425
									
								
								OSBindings/Mac/Clock SignalTests/68000Tests.mm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,425 @@ | ||||
| // | ||||
| //  68000Tests.m | ||||
| //  Clock SignalTests | ||||
| // | ||||
| //  Created by Thomas Harte on 13/03/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <XCTest/XCTest.h> | ||||
|  | ||||
| #include <cassert> | ||||
|  | ||||
| #define LOG_TRACE | ||||
| #include "TestRunner68000.hpp" | ||||
|  | ||||
| class CPU::MC68000::ProcessorStorageTests { | ||||
| 	public: | ||||
| 		ProcessorStorageTests(const CPU::MC68000::ProcessorStorage &storage, const char *coverage_file_name) { | ||||
| 			false_valids_ = [NSMutableSet set]; | ||||
| 			false_invalids_ = [NSMutableSet set]; | ||||
|  | ||||
| 			FILE *source = fopen(coverage_file_name, "rb"); | ||||
|  | ||||
| 			// The file format here is [2 bytes opcode][2 ASCII characters:VA for valid, IN for invalid]... | ||||
| 			// The file terminates with four additional bytes that begin with two zero bytes. | ||||
| 			// | ||||
| 			// The version of the file I grabbed seems to cover all opcodes, making their enumeration | ||||
| 			// arguably redundant; the code below nevertheless uses the codes from the file. | ||||
| 			// | ||||
| 			// Similarly, I'm testing for exactly the strings VA or IN to ensure no further | ||||
| 			// types creep into any updated version of the table that I then deal with incorrectly. | ||||
| 			uint16_t last_observed = 0; | ||||
| 			while(true) { | ||||
| 				// Fetch opcode number. | ||||
| 				uint16_t next_opcode = fgetc(source) << 8; | ||||
| 				next_opcode |= fgetc(source); | ||||
| 				if(next_opcode < last_observed) break; | ||||
| 				last_observed = next_opcode; | ||||
|  | ||||
| 				// Determine whether it's meant to be valid. | ||||
| 				char type[3]; | ||||
| 				type[0] = fgetc(source); | ||||
| 				type[1] = fgetc(source); | ||||
| 				type[2] = '\0'; | ||||
|  | ||||
| 				// TEMPORARY: factor out A- and F-line exceptions. | ||||
| 				if((next_opcode&0xf000) == 0xa000) continue; | ||||
| 				if((next_opcode&0xf000) == 0xf000) continue; | ||||
|  | ||||
| 				if(!strcmp(type, "VA")) { | ||||
| 					// Test for validity. | ||||
| 					if(storage.instructions[next_opcode].micro_operations == std::numeric_limits<uint32_t>::max()) { | ||||
| 						[false_invalids_ addObject:@(next_opcode)]; | ||||
| 					} | ||||
| 					continue; | ||||
| 				} | ||||
|  | ||||
| 				if(!strcmp(type, "IN")) { | ||||
| 					// Test for invalidity. | ||||
| 					if(storage.instructions[next_opcode].micro_operations != std::numeric_limits<uint32_t>::max()) { | ||||
| 						[false_valids_ addObject:@(next_opcode)]; | ||||
| 					} | ||||
| 					continue; | ||||
| 				} | ||||
|  | ||||
| 				assert(false); | ||||
| 			} | ||||
|  | ||||
| 			fclose(source); | ||||
| 		} | ||||
|  | ||||
| 		NSSet<NSNumber *> *false_valids() const { | ||||
| 			return false_valids_; | ||||
| 		} | ||||
|  | ||||
| 		NSSet<NSNumber *> *false_invalids() const { | ||||
| 			return false_invalids_; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		NSMutableSet<NSNumber *> *false_invalids_; | ||||
| 		NSMutableSet<NSNumber *> *false_valids_; | ||||
| }; | ||||
|  | ||||
| @interface NSSet (CSHexDump) | ||||
|  | ||||
| - (NSString *)hexDump; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation NSSet (CSHexDump) | ||||
|  | ||||
| - (NSString *)hexDump { | ||||
| 	NSMutableArray<NSString *> *components = [NSMutableArray array]; | ||||
|  | ||||
| 	for(NSNumber *number in [[self allObjects] sortedArrayUsingSelector:@selector(compare:)]) { | ||||
| 		[components addObject:[NSString stringWithFormat:@"%04x", number.intValue]]; | ||||
| 	} | ||||
|  | ||||
| 	return [components componentsJoinedByString:@" "]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| @interface M68000Tests : XCTestCase | ||||
| @end | ||||
|  | ||||
| @implementation M68000Tests { | ||||
| 	std::unique_ptr<RAM68000> _machine; | ||||
| } | ||||
|  | ||||
| - (void)setUp { | ||||
|     _machine.reset(new RAM68000()); | ||||
| } | ||||
|  | ||||
| - (void)tearDown { | ||||
| 	_machine.reset(); | ||||
| } | ||||
|  | ||||
| - (void)testABCDLong { | ||||
| 	for(int d = 0; d < 100; ++d) { | ||||
| 		_machine.reset(new RAM68000()); | ||||
| 		_machine->set_program({ | ||||
| 			0xc100		// ABCD D0, D0 | ||||
| 		}); | ||||
|  | ||||
| 		auto state = _machine->get_processor_state(); | ||||
| 		const uint8_t bcd_d = ((d / 10) * 16) + (d % 10); | ||||
| 		state.data[0] = bcd_d; | ||||
| 		_machine->set_processor_state(state); | ||||
|  | ||||
| 		_machine->run_for_instructions(1); | ||||
|  | ||||
| 		state = _machine->get_processor_state(); | ||||
| 		const uint8_t double_d = (d * 2) % 100; | ||||
| 		const uint8_t bcd_double_d = ((double_d / 10) * 16) + (double_d % 10); | ||||
| 		XCTAssert(state.data[0] == bcd_double_d, "%02x + %02x = %02x; should equal %02x", bcd_d, bcd_d, state.data[0], bcd_double_d); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (void)testDivideByZero { | ||||
| 	_machine->set_program({ | ||||
| 		0x7000,		// MOVE #0, D0;		location 0x400 | ||||
| 		0x3200,		// MOVE D0, D1;		location 0x402 | ||||
|  | ||||
| 		0x82C0,		// DIVU;			location 0x404 | ||||
|  | ||||
| 		/* Next instruction would be at 0x406 */ | ||||
| 	}); | ||||
| 	_machine->set_initial_stack_pointer(0x1000); | ||||
|  | ||||
| 	_machine->run_for_instructions(4); | ||||
|  | ||||
| 	const auto state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.supervisor_stack_pointer == 0x1000 - 6, @"Exception information should have been pushed to stack."); | ||||
|  | ||||
| 	const uint16_t *const stack_top = _machine->ram_at(state.supervisor_stack_pointer); | ||||
| 	XCTAssert(stack_top[1] == 0x0000 && stack_top[2] == 0x1006, @"Return address should point to instruction after DIVU."); | ||||
| } | ||||
|  | ||||
| - (void)testMOVE { | ||||
| 	_machine->set_program({ | ||||
| 		0x303c, 0xfb2e,		// MOVE #fb2e, D0 | ||||
| 		0x3200,				// MOVE D0, D1 | ||||
|  | ||||
| 		0x3040,				// MOVEA D0, A0 | ||||
| 		0x3278, 0x1000,		// MOVEA.w (0x1000), A1 | ||||
|  | ||||
| 		0x387c, 0x1000,		// MOVE #$1000, A4 | ||||
| 		0x2414,				// MOVE.l (A4), D2 | ||||
| 	}); | ||||
|  | ||||
| 	// run_for_instructions technically runs up to the next instruction | ||||
| 	// fetch; therefore run for '1' to get past the implied RESET. | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	// Perform MOVE #fb2e, D0 | ||||
| 	_machine->run_for_instructions(1); | ||||
| 	auto state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.data[0] == 0xfb2e); | ||||
|  | ||||
| 	// Perform MOVE D0, D1 | ||||
| 	_machine->run_for_instructions(1); | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.data[1] == 0xfb2e); | ||||
|  | ||||
| 	// Perform MOVEA D0, A0 | ||||
| 	_machine->run_for_instructions(1); | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.address[0] == 0xfffffb2e, "A0 was %08x instead of 0xfffffb2e", state.address[0]); | ||||
|  | ||||
| 	// Perform MOVEA.w (0x1000), A1 | ||||
| 	_machine->run_for_instructions(1); | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.address[1] == 0x0000303c, "A1 was %08x instead of 0x0000303c", state.address[1]); | ||||
|  | ||||
| 	// Perform MOVE #$400, A4; MOVE.l (A4), D2 | ||||
| 	_machine->run_for_instructions(2); | ||||
| 	state = _machine->get_processor_state(); | ||||
| 	XCTAssert(state.address[4] == 0x1000, "A4 was %08x instead of 0x00001000", state.address[4]); | ||||
| 	XCTAssert(state.data[2] == 0x303cfb2e, "D2 was %08x instead of 0x303cfb2e", state.data[2]); | ||||
| } | ||||
|  | ||||
| - (void)testVectoredInterrupt { | ||||
| 	_machine->set_program({ | ||||
| 		0x46fc,	0x2000,		// MOVE.w #$2000, SR | ||||
| 		0x4e71,				// NOP | ||||
| 		0x4e71,				// NOP | ||||
| 		0x4e71,				// NOP | ||||
| 		0x4e71,				// NOP | ||||
| 		0x4e71,				// NOP | ||||
| 	}); | ||||
|  | ||||
| 	// Set the vector that will be supplied back to the start of the | ||||
| 	// program; this will ensure no further exceptions following | ||||
| 	// the interrupt. | ||||
| 	const auto vector = _machine->ram_at(40); | ||||
| 	vector[0] = 0x0000; | ||||
| 	vector[1] = 0x1004; | ||||
|  | ||||
| 	_machine->run_for_instructions(3); | ||||
| 	_machine->processor().set_interrupt_level(1); | ||||
| 	_machine->run_for_instructions(1); | ||||
|  | ||||
| 	const auto state = _machine->processor().get_state(); | ||||
| 	XCTAssert(state.program_counter == 0x1008);	// i.e. the interrupt happened, the instruction performed was the one at 1004, and therefore | ||||
| 												// by the wonders of prefetch the program counter is now at 1008. | ||||
| } | ||||
|  | ||||
| - (void)testOpcodeCoverage { | ||||
| 	// Perform an audit of implemented instructions. | ||||
| 	CPU::MC68000::ProcessorStorageTests storage_tests( | ||||
| 		_machine->processor(), | ||||
| 		[[NSBundle bundleForClass:[self class]] pathForResource:@"OPCLOGR2" ofType:@"BIN"].UTF8String | ||||
| 	); | ||||
|  | ||||
| 	// This is a list of instructions nominated as valid with OPCLOGR2.BIN but with no obvious decoding — | ||||
| 	// the disassemblers I tried couldn't figure them out, and I didn't spot them anywhere in the PRM. | ||||
| 	NSSet<NSNumber *> *const undecodables = [NSSet setWithArray:@[ | ||||
| 		// These look like malformed MOVEs. | ||||
| 		@(0x2e7d),	@(0x2e7e),	@(0x2e7f),	@(0x2efd),	@(0x2efe),	@(0x2eff),	@(0x2f7d),	@(0x2f7e), | ||||
| 		@(0x2f7f),	@(0x2fc0),	@(0x2fc1),	@(0x2fc2),	@(0x2fc3),	@(0x2fc4),	@(0x2fc5),	@(0x2fc6), | ||||
| 		@(0x2fc7),	@(0x2fc8),	@(0x2fc9),	@(0x2fca),	@(0x2fcb),	@(0x2fcc),	@(0x2fcd),	@(0x2fce), | ||||
| 		@(0x2fcf),	@(0x2fd0),	@(0x2fd1),	@(0x2fd2),	@(0x2fd3),	@(0x2fd4),	@(0x2fd5),	@(0x2fd6), | ||||
| 		@(0x2fd7),	@(0x2fd8),	@(0x2fd9),	@(0x2fda),	@(0x2fdb),	@(0x2fdc),	@(0x2fdd),	@(0x2fde), | ||||
| 		@(0x2fdf),	@(0x2fe0),	@(0x2fe1),	@(0x2fe2),	@(0x2fe3),	@(0x2fe4),	@(0x2fe5),	@(0x2fe6), | ||||
| 		@(0x2fe7),	@(0x2fe8),	@(0x2fe9),	@(0x2fea),	@(0x2feb),	@(0x2fec),	@(0x2fed),	@(0x2fee), | ||||
| 		@(0x2fef),	@(0x2ff0),	@(0x2ff1),	@(0x2ff2),	@(0x2ff3),	@(0x2ff4),	@(0x2ff5),	@(0x2ff6), | ||||
| 		@(0x2ff7),	@(0x2ff8),	@(0x2ff9),	@(0x2ffa),	@(0x2ffb),	@(0x2ffc),	@(0x2ffd),	@(0x2ffe), | ||||
| 		@(0x2fff), | ||||
|  | ||||
| 		@(0x3e7d),	@(0x3e7e),	@(0x3e7f),	@(0x3efd),	@(0x3efe),	@(0x3eff),	@(0x3f7d),	@(0x3f7e), | ||||
| 		@(0x3f7f),	@(0x3fc0),	@(0x3fc1),	@(0x3fc2),	@(0x3fc3),	@(0x3fc4),	@(0x3fc5),	@(0x3fc6), | ||||
| 		@(0x3fc7),	@(0x3fc8),	@(0x3fc9),	@(0x3fca),	@(0x3fcb),	@(0x3fcc),	@(0x3fcd),	@(0x3fce), | ||||
| 		@(0x3fcf),	@(0x3fd0),	@(0x3fd1),	@(0x3fd2),	@(0x3fd3),	@(0x3fd4),	@(0x3fd5),	@(0x3fd6), | ||||
| 		@(0x3fd7),	@(0x3fd8),	@(0x3fd9),	@(0x3fda),	@(0x3fdb),	@(0x3fdc),	@(0x3fdd),	@(0x3fde), | ||||
| 		@(0x3fdf),	@(0x3fe0),	@(0x3fe1),	@(0x3fe2),	@(0x3fe3),	@(0x3fe4),	@(0x3fe5),	@(0x3fe6), | ||||
| 		@(0x3fe7),	@(0x3fe8),	@(0x3fe9),	@(0x3fea),	@(0x3feb),	@(0x3fec),	@(0x3fed),	@(0x3fee), | ||||
| 		@(0x3fef),	@(0x3ff0),	@(0x3ff1),	@(0x3ff2),	@(0x3ff3),	@(0x3ff4),	@(0x3ff5),	@(0x3ff6), | ||||
| 		@(0x3ff7),	@(0x3ff8),	@(0x3ff9),	@(0x3ffa),	@(0x3ffb),	@(0x3ffc),	@(0x3ffd),	@(0x3ffe), | ||||
| 		@(0x3fff), | ||||
|  | ||||
| 		@(0x46c8),	@(0x46c9),	@(0x46ca),	@(0x46cb),	@(0x46cc),	@(0x46cd),	@(0x46ce),	@(0x46cf), | ||||
| 		@(0x46fd),	@(0x46fe),	@(0x46ff),	@(0x47c0),	@(0x47c1),	@(0x47c2),	@(0x47c3),	@(0x47c4), | ||||
| 		@(0x47c5),	@(0x47c6),	@(0x47c7),	@(0x47c8),	@(0x47c9),	@(0x47ca),	@(0x47cb),	@(0x47cc), | ||||
| 		@(0x47cd),	@(0x47ce),	@(0x47cf),	@(0x47d8),	@(0x47d9),	@(0x47da),	@(0x47db),	@(0x47dc), | ||||
| 		@(0x47dd),	@(0x47de),	@(0x47df),	@(0x47e0),	@(0x47e1),	@(0x47e2),	@(0x47e3),	@(0x47e4), | ||||
| 		@(0x47e5),	@(0x47e6),	@(0x47e7),	@(0x47fc),	@(0x47fd),	@(0x47fe),	@(0x47ff),	@(0x4e80), | ||||
| 		@(0x4e81),	@(0x4e82),	@(0x4e83),	@(0x4e84),	@(0x4e85),	@(0x4e86),	@(0x4e87),	@(0x4e88), | ||||
| 		@(0x4e89),	@(0x4e8a),	@(0x4e8b),	@(0x4e8c),	@(0x4e8d),	@(0x4e8e),	@(0x4e8f),	@(0x4e98), | ||||
| 		@(0x4e99),	@(0x4e9a),	@(0x4e9b),	@(0x4e9c),	@(0x4e9d),	@(0x4e9e),	@(0x4e9f),	@(0x4ea0), | ||||
| 		@(0x4ea1),	@(0x4ea2),	@(0x4ea3),	@(0x4ea4),	@(0x4ea5),	@(0x4ea6),	@(0x4ea7),	@(0x4ebc), | ||||
| 		@(0x4ebd),	@(0x4ebe),	@(0x4ebf),	@(0x4ec0),	@(0x4ec1),	@(0x4ec2),	@(0x4ec3),	@(0x4ec4), | ||||
| 		@(0x4ec5),	@(0x4ec6),	@(0x4ec7),	@(0x4ec8),	@(0x4ec9),	@(0x4eca),	@(0x4ecb),	@(0x4ecc), | ||||
| 		@(0x4ecd),	@(0x4ece),	@(0x4ecf),	@(0x4ed8),	@(0x4ed9),	@(0x4eda),	@(0x4edb),	@(0x4edc), | ||||
| 		@(0x4edd),	@(0x4ede),	@(0x4edf),	@(0x4ee0),	@(0x4ee1),	@(0x4ee2),	@(0x4ee3),	@(0x4ee4), | ||||
| 		@(0x4ee5),	@(0x4ee6),	@(0x4ee7),	@(0x4efc),	@(0x4efd),	@(0x4efe),	@(0x4eff),	@(0x4f88), | ||||
| 		@(0x4f89),	@(0x4f8a),	@(0x4f8b),	@(0x4f8c),	@(0x4f8d),	@(0x4f8e),	@(0x4f8f),	@(0x4fbd), | ||||
| 		@(0x4fbe),	@(0x4fbf),	@(0x4fc0),	@(0x4fc1),	@(0x4fc2),	@(0x4fc3),	@(0x4fc4),	@(0x4fc5), | ||||
| 		@(0x4fc6),	@(0x4fc7),	@(0x4fc8),	@(0x4fc9),	@(0x4fca),	@(0x4fcb),	@(0x4fcc),	@(0x4fcd), | ||||
| 		@(0x4fce),	@(0x4fcf),	@(0x4fd8),	@(0x4fd9),	@(0x4fda),	@(0x4fdb),	@(0x4fdc),	@(0x4fdd), | ||||
| 		@(0x4fde),	@(0x4fdf),	@(0x4fe0),	@(0x4fe1),	@(0x4fe2),	@(0x4fe3),	@(0x4fe4),	@(0x4fe5), | ||||
| 		@(0x4fe6),	@(0x4fe7),	@(0x4ffc),	@(0x4ffd),	@(0x4ffe),	@(0x4fff), | ||||
|  | ||||
| 		@(0x50fa),	@(0x50fb),	@(0x50fc),	@(0x50fd),	@(0x50fe),	@(0x50ff),	@(0x51fa),	@(0x51fb), | ||||
| 		@(0x51fc),	@(0x51fd),	@(0x51fe),	@(0x51ff),	@(0x52fa),	@(0x52fb),	@(0x52fc),	@(0x52fd), | ||||
| 		@(0x52fe),	@(0x52ff),	@(0x53fa),	@(0x53fb),	@(0x53fc),	@(0x53fd),	@(0x53fe),	@(0x53ff), | ||||
| 		@(0x54fa),	@(0x54fb),	@(0x54fc),	@(0x54fd),	@(0x54fe),	@(0x54ff),	@(0x55fa),	@(0x55fb), | ||||
| 		@(0x55fc),	@(0x55fd),	@(0x55fe),	@(0x55ff),	@(0x56fa),	@(0x56fb),	@(0x56fc),	@(0x56fd), | ||||
| 		@(0x56fe),	@(0x56ff),	@(0x57fa),	@(0x57fb),	@(0x57fc),	@(0x57fd),	@(0x57fe),	@(0x57ff), | ||||
| 		@(0x58fa),	@(0x58fb),	@(0x58fc),	@(0x58fd),	@(0x58fe),	@(0x58ff),	@(0x59fa),	@(0x59fb), | ||||
| 		@(0x59fc),	@(0x59fd),	@(0x59fe),	@(0x59ff),	@(0x5afa),	@(0x5afb),	@(0x5afc),	@(0x5afd), | ||||
| 		@(0x5afe),	@(0x5aff),	@(0x5bfa),	@(0x5bfb),	@(0x5bfc),	@(0x5bfd),	@(0x5bfe),	@(0x5bff), | ||||
| 		@(0x5cfa),	@(0x5cfb),	@(0x5cfc),	@(0x5cfd),	@(0x5cfe),	@(0x5cff),	@(0x5dfa),	@(0x5dfb), | ||||
| 		@(0x5dfc),	@(0x5dfd),	@(0x5dfe),	@(0x5dff),	@(0x5eba),	@(0x5ebb),	@(0x5ebc),	@(0x5ebd), | ||||
| 		@(0x5ebe),	@(0x5ebf),	@(0x5efa),	@(0x5efb),	@(0x5efc),	@(0x5efd),	@(0x5efe),	@(0x5eff), | ||||
| 		@(0x5fba),	@(0x5fbb),	@(0x5fbc),	@(0x5fbd),	@(0x5fbe),	@(0x5fbf),	@(0x5ffa),	@(0x5ffb), | ||||
| 		@(0x5ffc),	@(0x5ffd),	@(0x5ffe),	@(0x5fff), | ||||
|  | ||||
| 		// These are almost MOVEQs if only bit 8 weren't set. | ||||
| 		@(0x71c8),	@(0x71c9),	@(0x71ca),	@(0x71cb),	@(0x71cc),	@(0x71cd),	@(0x71ce),	@(0x71cf), | ||||
| 		@(0x71d8),	@(0x71d9),	@(0x71da),	@(0x71db),	@(0x71dc),	@(0x71dd),	@(0x71de),	@(0x71df), | ||||
| 		@(0x71e8),	@(0x71e9),	@(0x71ea),	@(0x71eb),	@(0x71ec),	@(0x71ed),	@(0x71ee),	@(0x71ef), | ||||
| 		@(0x71f8),	@(0x71f9),	@(0x71fa),	@(0x71fb),	@(0x71fc),	@(0x71fd),	@(0x71fe),	@(0x71ff), | ||||
| 		@(0x73c8),	@(0x73c9),	@(0x73ca),	@(0x73cb),	@(0x73cc),	@(0x73cd),	@(0x73ce),	@(0x73cf), | ||||
| 		@(0x73d8),	@(0x73d9),	@(0x73da),	@(0x73db),	@(0x73dc),	@(0x73dd),	@(0x73de),	@(0x73df), | ||||
| 		@(0x73e8),	@(0x73e9),	@(0x73ea),	@(0x73eb),	@(0x73ec),	@(0x73ed),	@(0x73ee),	@(0x73ef), | ||||
| 		@(0x73f8),	@(0x73f9),	@(0x73fa),	@(0x73fb),	@(0x73fc),	@(0x73fd),	@(0x73fe),	@(0x73ff), | ||||
| 		@(0x75c8),	@(0x75c9),	@(0x75ca),	@(0x75cb),	@(0x75cc),	@(0x75cd),	@(0x75ce),	@(0x75cf), | ||||
| 		@(0x75d8),	@(0x75d9),	@(0x75da),	@(0x75db),	@(0x75dc),	@(0x75dd),	@(0x75de),	@(0x75df), | ||||
| 		@(0x75e8),	@(0x75e9),	@(0x75ea),	@(0x75eb),	@(0x75ec),	@(0x75ed),	@(0x75ee),	@(0x75ef), | ||||
| 		@(0x75f8),	@(0x75f9),	@(0x75fa),	@(0x75fb),	@(0x75fc),	@(0x75fd),	@(0x75fe),	@(0x75ff), | ||||
| 		@(0x77c0),	@(0x77c1),	@(0x77c2),	@(0x77c3),	@(0x77c4),	@(0x77c5),	@(0x77c6),	@(0x77c7), | ||||
| 		@(0x77c8),	@(0x77c9),	@(0x77ca),	@(0x77cb),	@(0x77cc),	@(0x77cd),	@(0x77ce),	@(0x77cf), | ||||
| 		@(0x77d0),	@(0x77d1),	@(0x77d2),	@(0x77d3),	@(0x77d4),	@(0x77d5),	@(0x77d6),	@(0x77d7), | ||||
| 		@(0x77d8),	@(0x77d9),	@(0x77da),	@(0x77db),	@(0x77dc),	@(0x77dd),	@(0x77de),	@(0x77df), | ||||
| 		@(0x77e0),	@(0x77e1),	@(0x77e2),	@(0x77e3),	@(0x77e4),	@(0x77e5),	@(0x77e6),	@(0x77e7), | ||||
| 		@(0x77e8),	@(0x77e9),	@(0x77ea),	@(0x77eb),	@(0x77ec),	@(0x77ed),	@(0x77ee),	@(0x77ef), | ||||
| 		@(0x77f0),	@(0x77f1),	@(0x77f2),	@(0x77f3),	@(0x77f4),	@(0x77f5),	@(0x77f6),	@(0x77f7), | ||||
| 		@(0x77f8),	@(0x77f9),	@(0x77fa),	@(0x77fb),	@(0x77fc),	@(0x77fd),	@(0x77fe),	@(0x77ff), | ||||
| 		@(0x79c8),	@(0x79c9),	@(0x79ca),	@(0x79cb),	@(0x79cc),	@(0x79cd),	@(0x79ce),	@(0x79cf), | ||||
| 		@(0x79d8),	@(0x79d9),	@(0x79da),	@(0x79db),	@(0x79dc),	@(0x79dd),	@(0x79de),	@(0x79df), | ||||
| 		@(0x79e8),	@(0x79e9),	@(0x79ea),	@(0x79eb),	@(0x79ec),	@(0x79ed),	@(0x79ee),	@(0x79ef), | ||||
| 		@(0x79f8),	@(0x79f9),	@(0x79fa),	@(0x79fb),	@(0x79fc),	@(0x79fd),	@(0x79fe),	@(0x79ff), | ||||
| 		@(0x7bc8),	@(0x7bc9),	@(0x7bca),	@(0x7bcb),	@(0x7bcc),	@(0x7bcd),	@(0x7bce),	@(0x7bcf), | ||||
| 		@(0x7bd8),	@(0x7bd9),	@(0x7bda),	@(0x7bdb),	@(0x7bdc),	@(0x7bdd),	@(0x7bde),	@(0x7bdf), | ||||
| 		@(0x7be8),	@(0x7be9),	@(0x7bea),	@(0x7beb),	@(0x7bec),	@(0x7bed),	@(0x7bee),	@(0x7bef), | ||||
| 		@(0x7bf8),	@(0x7bf9),	@(0x7bfa),	@(0x7bfb),	@(0x7bfc),	@(0x7bfd),	@(0x7bfe),	@(0x7bff), | ||||
| 		@(0x7dc8),	@(0x7dc9),	@(0x7dca),	@(0x7dcb),	@(0x7dcc),	@(0x7dcd),	@(0x7dce),	@(0x7dcf), | ||||
| 		@(0x7dd8),	@(0x7dd9),	@(0x7dda),	@(0x7ddb),	@(0x7ddc),	@(0x7ddd),	@(0x7dde),	@(0x7ddf), | ||||
| 		@(0x7de8),	@(0x7de9),	@(0x7dea),	@(0x7deb),	@(0x7dec),	@(0x7ded),	@(0x7dee),	@(0x7def), | ||||
| 		@(0x7df8),	@(0x7df9),	@(0x7dfa),	@(0x7dfb),	@(0x7dfc),	@(0x7dfd),	@(0x7dfe),	@(0x7dff), | ||||
| 		@(0x7f40),	@(0x7f41),	@(0x7f42),	@(0x7f43),	@(0x7f44),	@(0x7f45),	@(0x7f46),	@(0x7f47), | ||||
| 		@(0x7f48),	@(0x7f49),	@(0x7f4a),	@(0x7f4b),	@(0x7f4c),	@(0x7f4d),	@(0x7f4e),	@(0x7f4f), | ||||
| 		@(0x7f50),	@(0x7f51),	@(0x7f52),	@(0x7f53),	@(0x7f54),	@(0x7f55),	@(0x7f56),	@(0x7f57), | ||||
| 		@(0x7f58),	@(0x7f59),	@(0x7f5a),	@(0x7f5b),	@(0x7f5c),	@(0x7f5d),	@(0x7f5e),	@(0x7f5f), | ||||
| 		@(0x7f60),	@(0x7f61),	@(0x7f62),	@(0x7f63),	@(0x7f64),	@(0x7f65),	@(0x7f66),	@(0x7f67), | ||||
| 		@(0x7f68),	@(0x7f69),	@(0x7f6a),	@(0x7f6b),	@(0x7f6c),	@(0x7f6d),	@(0x7f6e),	@(0x7f6f), | ||||
| 		@(0x7f70),	@(0x7f71),	@(0x7f72),	@(0x7f73),	@(0x7f74),	@(0x7f75),	@(0x7f76),	@(0x7f77), | ||||
| 		@(0x7f78),	@(0x7f79),	@(0x7f7a),	@(0x7f7b),	@(0x7f7c),	@(0x7f7d),	@(0x7f7e),	@(0x7f7f), | ||||
| 		@(0x7f80),	@(0x7f81),	@(0x7f82),	@(0x7f83),	@(0x7f84),	@(0x7f85),	@(0x7f86),	@(0x7f87), | ||||
| 		@(0x7f88),	@(0x7f89),	@(0x7f8a),	@(0x7f8b),	@(0x7f8c),	@(0x7f8d),	@(0x7f8e),	@(0x7f8f), | ||||
| 		@(0x7f90),	@(0x7f91),	@(0x7f92),	@(0x7f93),	@(0x7f94),	@(0x7f95),	@(0x7f96),	@(0x7f97), | ||||
| 		@(0x7f98),	@(0x7f99),	@(0x7f9a),	@(0x7f9b),	@(0x7f9c),	@(0x7f9d),	@(0x7f9e),	@(0x7f9f), | ||||
| 		@(0x7fa0),	@(0x7fa1),	@(0x7fa2),	@(0x7fa3),	@(0x7fa4),	@(0x7fa5),	@(0x7fa6),	@(0x7fa7), | ||||
| 		@(0x7fa8),	@(0x7fa9),	@(0x7faa),	@(0x7fab),	@(0x7fac),	@(0x7fad),	@(0x7fae),	@(0x7faf), | ||||
| 		@(0x7fb0),	@(0x7fb1),	@(0x7fb2),	@(0x7fb3),	@(0x7fb4),	@(0x7fb5),	@(0x7fb6),	@(0x7fb7), | ||||
| 		@(0x7fb8),	@(0x7fb9),	@(0x7fba),	@(0x7fbb),	@(0x7fbc),	@(0x7fbd),	@(0x7fbe),	@(0x7fbf), | ||||
| 		@(0x7fc0),	@(0x7fc1),	@(0x7fc2),	@(0x7fc3),	@(0x7fc4),	@(0x7fc5),	@(0x7fc6),	@(0x7fc7), | ||||
| 		@(0x7fc8),	@(0x7fc9),	@(0x7fca),	@(0x7fcb),	@(0x7fcc),	@(0x7fcd),	@(0x7fce),	@(0x7fcf), | ||||
| 		@(0x7fd0),	@(0x7fd1),	@(0x7fd2),	@(0x7fd3),	@(0x7fd4),	@(0x7fd5),	@(0x7fd6),	@(0x7fd7), | ||||
| 		@(0x7fd8),	@(0x7fd9),	@(0x7fda),	@(0x7fdb),	@(0x7fdc),	@(0x7fdd),	@(0x7fde),	@(0x7fdf), | ||||
| 		@(0x7fe0),	@(0x7fe1),	@(0x7fe2),	@(0x7fe3),	@(0x7fe4),	@(0x7fe5),	@(0x7fe6),	@(0x7fe7), | ||||
| 		@(0x7fe8),	@(0x7fe9),	@(0x7fea),	@(0x7feb),	@(0x7fec),	@(0x7fed),	@(0x7fee),	@(0x7fef), | ||||
| 		@(0x7ff0),	@(0x7ff1),	@(0x7ff2),	@(0x7ff3),	@(0x7ff4),	@(0x7ff5),	@(0x7ff6),	@(0x7ff7), | ||||
| 		@(0x7ff8),	@(0x7ff9),	@(0x7ffa),	@(0x7ffb),	@(0x7ffc),	@(0x7ffd),	@(0x7ffe),	@(0x7fff), | ||||
|  | ||||
| 		@(0xbe7d),	@(0xbe7e),	@(0xbe7f),	@(0xbefd),	@(0xbefe),	@(0xbeff),	@(0xbf7a),	@(0xbf7b), | ||||
| 		@(0xbf7c),	@(0xbf7d),	@(0xbf7e),	@(0xbf7f),	@(0xbffd),	@(0xbffe),	@(0xbfff), | ||||
|  | ||||
| 		// | ||||
| 		@(0xc6c8),	@(0xc6c9),	@(0xc6ca),	@(0xc6cb),	@(0xc6cc),	@(0xc6cd),	@(0xc6ce),	@(0xc6cf), | ||||
| 		@(0xc6fd),	@(0xc6fe),	@(0xc6ff),	@(0xc7c8),	@(0xc7c9),	@(0xc7ca),	@(0xc7cb),	@(0xc7cc), | ||||
| 		@(0xc7cd),	@(0xc7ce),	@(0xc7cf),	@(0xc7fd),	@(0xc7fe),	@(0xc7ff),	@(0xce88),	@(0xce89), | ||||
| 		@(0xce8a),	@(0xce8b),	@(0xce8c),	@(0xce8d),	@(0xce8e),	@(0xce8f),	@(0xcebd),	@(0xcebe), | ||||
| 		@(0xcebf),	@(0xcec8),	@(0xcec9),	@(0xceca),	@(0xcecb),	@(0xcecc),	@(0xcecd),	@(0xcece), | ||||
| 		@(0xcecf),	@(0xcefd),	@(0xcefe),	@(0xceff),	@(0xcf80),	@(0xcf81),	@(0xcf82),	@(0xcf83), | ||||
| 		@(0xcf84),	@(0xcf85),	@(0xcf86),	@(0xcf87),	@(0xcfba),	@(0xcfbb),	@(0xcfbc),	@(0xcfbd), | ||||
| 		@(0xcfbe),	@(0xcfbf),	@(0xcfc8),	@(0xcfc9),	@(0xcfca),	@(0xcfcb),	@(0xcfcc),	@(0xcfcd), | ||||
| 		@(0xcfce),	@(0xcfcf),	@(0xcffd),	@(0xcffe),	@(0xcfff), | ||||
|  | ||||
| 		// These are from the Bcc/BRA/BSR page. | ||||
| 		@(0xd0fd),	@(0xd0fe),	@(0xd0ff),	@(0xd1fd),	@(0xd1fe),	@(0xd1ff),	@(0xd2fd),	@(0xd2fe), | ||||
| 		@(0xd2ff),	@(0xd3fd),	@(0xd3fe),	@(0xd3ff),	@(0xd4fd),	@(0xd4fe),	@(0xd4ff),	@(0xd5fd), | ||||
| 		@(0xd5fe),	@(0xd5ff),	@(0xd6fd),	@(0xd6fe),	@(0xd6ff),	@(0xd7fd),	@(0xd7fe),	@(0xd7ff), | ||||
| 		@(0xd8fd),	@(0xd8fe),	@(0xd8ff),	@(0xd9fd),	@(0xd9fe),	@(0xd9ff),	@(0xdafd),	@(0xdafe), | ||||
| 		@(0xdaff),	@(0xdbfd),	@(0xdbfe),	@(0xdbff),	@(0xdcfd),	@(0xdcfe),	@(0xdcff),	@(0xddfd), | ||||
| 		@(0xddfe),	@(0xddff),	@(0xdebd),	@(0xdebe),	@(0xdebf),	@(0xdefd),	@(0xdefe),	@(0xdeff), | ||||
| 		@(0xdfba),	@(0xdfbb),	@(0xdfbc),	@(0xdfbd),	@(0xdfbe),	@(0xdfbf),	@(0xdffd),	@(0xdffe), | ||||
| 		@(0xdfff), | ||||
|  | ||||
| 		// The E line is for shifts and rolls; none of the those listed below appear to nominate valid | ||||
| 		// addressing modes. | ||||
| 		@(0xe6c0),	@(0xe6c1),	@(0xe6c2),	@(0xe6c3),	@(0xe6c4),	@(0xe6c5),	@(0xe6c6),	@(0xe6c7), | ||||
| 		@(0xe6c8),	@(0xe6c9),	@(0xe6ca),	@(0xe6cb),	@(0xe6cc),	@(0xe6cd),	@(0xe6ce),	@(0xe6cf), | ||||
| 		@(0xe6fa),	@(0xe6fb),	@(0xe6fc),	@(0xe6fd),	@(0xe6fe),	@(0xe6ff),	@(0xe7c0),	@(0xe7c1), | ||||
| 		@(0xe7c2),	@(0xe7c3),	@(0xe7c4),	@(0xe7c5),	@(0xe7c6),	@(0xe7c7),	@(0xe7c8),	@(0xe7c9), | ||||
| 		@(0xe7ca),	@(0xe7cb),	@(0xe7cc),	@(0xe7cd),	@(0xe7ce),	@(0xe7cf),	@(0xe7fa),	@(0xe7fb), | ||||
| 		@(0xe7fc),	@(0xe7fd),	@(0xe7fe),	@(0xe7ff),	@(0xeec0),	@(0xeec1),	@(0xeec2),	@(0xeec3), | ||||
| 		@(0xeec4),	@(0xeec5),	@(0xeec6),	@(0xeec7),	@(0xeec8),	@(0xeec9),	@(0xeeca),	@(0xeecb), | ||||
| 		@(0xeecc),	@(0xeecd),	@(0xeece),	@(0xeecf),	@(0xeed0),	@(0xeed1),	@(0xeed2),	@(0xeed3), | ||||
| 		@(0xeed4),	@(0xeed5),	@(0xeed6),	@(0xeed7),	@(0xeed8),	@(0xeed9),	@(0xeeda),	@(0xeedb), | ||||
| 		@(0xeedc),	@(0xeedd),	@(0xeede),	@(0xeedf),	@(0xeee0),	@(0xeee1),	@(0xeee2),	@(0xeee3), | ||||
| 		@(0xeee4),	@(0xeee5),	@(0xeee6),	@(0xeee7),	@(0xeee8),	@(0xeee9),	@(0xeeea),	@(0xeeeb), | ||||
| 		@(0xeeec),	@(0xeeed),	@(0xeeee),	@(0xeeef),	@(0xeef0),	@(0xeef1),	@(0xeef2),	@(0xeef3), | ||||
| 		@(0xeef4),	@(0xeef5),	@(0xeef6),	@(0xeef7),	@(0xeef8),	@(0xeef9),	@(0xeefa),	@(0xeefb), | ||||
| 		@(0xeefc),	@(0xeefd),	@(0xeefe),	@(0xeeff),	@(0xefc0),	@(0xefc1),	@(0xefc2),	@(0xefc3), | ||||
| 		@(0xefc4),	@(0xefc5),	@(0xefc6),	@(0xefc7),	@(0xefc8),	@(0xefc9),	@(0xefca),	@(0xefcb), | ||||
| 		@(0xefcc),	@(0xefcd),	@(0xefce),	@(0xefcf),	@(0xefd0),	@(0xefd1),	@(0xefd2),	@(0xefd3), | ||||
| 		@(0xefd4),	@(0xefd5),	@(0xefd6),	@(0xefd7),	@(0xefd8),	@(0xefd9),	@(0xefda),	@(0xefdb), | ||||
| 		@(0xefdc),	@(0xefdd),	@(0xefde),	@(0xefdf),	@(0xefe0),	@(0xefe1),	@(0xefe2),	@(0xefe3), | ||||
| 		@(0xefe4),	@(0xefe5),	@(0xefe6),	@(0xefe7),	@(0xefe8),	@(0xefe9),	@(0xefea),	@(0xefeb), | ||||
| 		@(0xefec),	@(0xefed),	@(0xefee),	@(0xefef),	@(0xeff0),	@(0xeff1),	@(0xeff2),	@(0xeff3), | ||||
| 		@(0xeff4),	@(0xeff5),	@(0xeff6),	@(0xeff7),	@(0xeff8),	@(0xeff9),	@(0xeffa),	@(0xeffb), | ||||
| 		@(0xeffc),	@(0xeffd),	@(0xeffe),	@(0xefff) | ||||
| 	]]; | ||||
|  | ||||
| 	NSSet<NSNumber *> *const falseValids = storage_tests.false_valids(); | ||||
| 	NSSet<NSNumber *> *const falseInvalids = storage_tests.false_invalids(); | ||||
|  | ||||
| 	XCTAssert(!falseValids.count, "%@ opcodes should be invalid but aren't: %@", @(falseValids.count), falseValids.hexDump); | ||||
|  | ||||
| 	NSMutableSet<NSNumber *> *const decodedUndecodables = [undecodables mutableCopy]; | ||||
| 	[decodedUndecodables minusSet:falseInvalids]; | ||||
| 	XCTAssert(!decodedUndecodables.count, "This test considers these undecodable but they were decoded: %@", decodedUndecodables.hexDump); | ||||
|  | ||||
| 	NSMutableSet<NSNumber *> *const trimmedInvalids = [falseInvalids mutableCopy]; | ||||
| 	[trimmedInvalids minusSet:undecodables]; | ||||
| 	XCTAssert(!trimmedInvalids.count, "%@ opcodes should be valid but aren't: %@", @(trimmedInvalids.count), trimmedInvalids.hexDump); | ||||
|  | ||||
| //	XCTAssert(!falseInvalids.count, "%@ opcodes should be valid but aren't: %@", @(falseInvalids.count), falseInvalids.hexDump); | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -1,140 +0,0 @@ | ||||
| // | ||||
| //  ArrayBuilderTests.m | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 19/11/2016. | ||||
| //  Copyright 2016 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <XCTest/XCTest.h> | ||||
|  | ||||
| #include "ArrayBuilder.hpp" | ||||
|  | ||||
| static NSData *inputData, *outputData; | ||||
|  | ||||
| static void setData(bool is_input, uint8_t *data, size_t size) | ||||
| { | ||||
| 	NSData *dataObject = [NSData dataWithBytes:data length:size]; | ||||
| 	if(is_input) inputData = dataObject; else outputData = dataObject; | ||||
| } | ||||
|  | ||||
| @interface ArrayBuilderTests : XCTestCase | ||||
| @end | ||||
|  | ||||
| @implementation ArrayBuilderTests | ||||
|  | ||||
| + (void)setUp | ||||
| { | ||||
| 	inputData = nil; | ||||
| 	outputData = nil; | ||||
| } | ||||
|  | ||||
| - (void)assertMonotonicForInputSize:(size_t)inputSize outputSize:(size_t)outputSize | ||||
| { | ||||
| 	XCTAssert(inputData != nil, @"Should have received some input data"); | ||||
| 	XCTAssert(outputData != nil, @"Should have received some output data"); | ||||
|  | ||||
| 	XCTAssert(inputData.length == inputSize, @"Input data should be %lu bytes long, was %lu", inputSize, (unsigned long)inputData.length); | ||||
| 	XCTAssert(outputData.length == outputSize, @"Output data should be %lu bytes long, was %lu", outputSize, (unsigned long)outputData.length); | ||||
|  | ||||
| 	if(inputData.length == inputSize && outputData.length == outputSize) | ||||
| 	{ | ||||
| 		uint8_t *input = (uint8_t *)inputData.bytes; | ||||
| 		uint8_t *output = (uint8_t *)outputData.bytes; | ||||
|  | ||||
| 		for(int c = 0; c < inputSize; c++) XCTAssert(input[c] == c, @"Input item %d should be %d, was %d", c, c, input[c]); | ||||
| 		for(int c = 0; c < outputSize; c++) XCTAssert(output[c] == c + 0x80, @"Output item %d should be %d, was %d", c, c+0x80, output[c]); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| - (std::function<void(uint8_t *input, size_t input_size, uint8_t *output, size_t output_size)>)emptyFlushFunction | ||||
| { | ||||
| 	return [=] (uint8_t *input, size_t input_size, uint8_t *output, size_t output_size) {}; | ||||
| } | ||||
|  | ||||
| - (void)testSingleWriteSingleFlush | ||||
| { | ||||
| 	Outputs::CRT::ArrayBuilder arrayBuilder(200, 100, setData); | ||||
|  | ||||
| 	uint8_t *input = arrayBuilder.get_input_storage(5); | ||||
| 	uint8_t *output = arrayBuilder.get_output_storage(3); | ||||
|  | ||||
| 	for(int c = 0; c < 5; c++) input[c] = c; | ||||
| 	for(int c = 0; c < 3; c++) output[c] = c + 0x80; | ||||
|  | ||||
| 	arrayBuilder.flush(self.emptyFlushFunction); | ||||
| 	arrayBuilder.submit(); | ||||
|  | ||||
| 	[self assertMonotonicForInputSize:5 outputSize:3]; | ||||
| } | ||||
|  | ||||
| - (void)testDoubleWriteSingleFlush | ||||
| { | ||||
| 	Outputs::CRT::ArrayBuilder arrayBuilder(200, 100, setData); | ||||
| 	uint8_t *input; | ||||
| 	uint8_t *output; | ||||
|  | ||||
| 	input = arrayBuilder.get_input_storage(2); | ||||
| 	output = arrayBuilder.get_output_storage(2); | ||||
|  | ||||
| 	for(int c = 0; c < 2; c++) input[c] = c; | ||||
| 	for(int c = 0; c < 2; c++) output[c] = c + 0x80; | ||||
|  | ||||
| 	input = arrayBuilder.get_input_storage(2); | ||||
| 	output = arrayBuilder.get_output_storage(2); | ||||
|  | ||||
| 	for(int c = 0; c < 2; c++) input[c] = c+2; | ||||
| 	for(int c = 0; c < 2; c++) output[c] = c+2 + 0x80; | ||||
|  | ||||
| 	arrayBuilder.flush(self.emptyFlushFunction); | ||||
| 	arrayBuilder.submit(); | ||||
|  | ||||
| 	[self assertMonotonicForInputSize:4 outputSize:4]; | ||||
| } | ||||
|  | ||||
| - (void)testSubmitWithoutFlush | ||||
| { | ||||
| 	Outputs::CRT::ArrayBuilder arrayBuilder(200, 100, setData); | ||||
|  | ||||
| 	arrayBuilder.get_input_storage(5); | ||||
| 	arrayBuilder.get_input_storage(8); | ||||
| 	arrayBuilder.get_output_storage(6); | ||||
| 	arrayBuilder.get_input_storage(12); | ||||
| 	arrayBuilder.get_output_storage(3); | ||||
|  | ||||
| 	arrayBuilder.submit(); | ||||
|  | ||||
| 	XCTAssert(inputData.length == 0, @"No input data should have been received; %lu bytes were received", (unsigned long)inputData.length); | ||||
| 	XCTAssert(outputData.length == 0, @"No output data should have been received; %lu bytes were received", (unsigned long)outputData.length); | ||||
|  | ||||
| 	arrayBuilder.flush(self.emptyFlushFunction); | ||||
| 	arrayBuilder.submit(); | ||||
|  | ||||
| 	XCTAssert(inputData.length == 25, @"All input data should have been received; %lu bytes were received", (unsigned long)inputData.length); | ||||
| 	XCTAssert(outputData.length == 9, @"All output data should have been received; %lu bytes were received", (unsigned long)outputData.length); | ||||
| } | ||||
|  | ||||
| - (void)testSubmitContinuity | ||||
| { | ||||
| 	Outputs::CRT::ArrayBuilder arrayBuilder(200, 100, setData); | ||||
|  | ||||
| 	arrayBuilder.get_input_storage(5); | ||||
| 	arrayBuilder.get_output_storage(5); | ||||
|  | ||||
| 	arrayBuilder.flush(self.emptyFlushFunction); | ||||
|  | ||||
| 	uint8_t *input = arrayBuilder.get_input_storage(5); | ||||
| 	uint8_t *output = arrayBuilder.get_output_storage(5); | ||||
|  | ||||
| 	arrayBuilder.submit(); | ||||
|  | ||||
| 	for(int c = 0; c < 5; c++) input[c] = c; | ||||
| 	for(int c = 0; c < 5; c++) output[c] = c + 0x80; | ||||
|  | ||||
| 	arrayBuilder.flush(self.emptyFlushFunction); | ||||
| 	arrayBuilder.submit(); | ||||
|  | ||||
| 	[self assertMonotonicForInputSize:5 outputSize:5]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -8,6 +8,14 @@ | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| typedef NS_ENUM(NSInteger, MOS6522BridgePort) { | ||||
| 	MOS6522BridgePortA = 0, MOS6522BridgePortB = 1 | ||||
| }; | ||||
|  | ||||
| typedef NS_ENUM(NSInteger, MOS6522BridgeLine) { | ||||
| 	MOS6522BridgeLineOne = 0, MOS6522BridgeLineTwo = 1 | ||||
| }; | ||||
|  | ||||
| @interface MOS6522Bridge : NSObject | ||||
|  | ||||
| @property (nonatomic, readonly) BOOL irqLine; | ||||
| @@ -16,6 +24,7 @@ | ||||
|  | ||||
| - (void)setValue:(uint8_t)value forRegister:(NSUInteger)registerNumber; | ||||
| - (uint8_t)valueForRegister:(NSUInteger)registerNumber; | ||||
| - (BOOL)valueForControlLine:(MOS6522BridgeLine)line port:(MOS6522BridgePort)port; | ||||
|  | ||||
| - (void)runForHalfCycles:(NSUInteger)numberOfHalfCycles; | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,12 @@ class VanillaVIAPortHandler: public MOS::MOS6522::PortHandler { | ||||
| 		bool irq_line; | ||||
| 		uint8_t port_a_value; | ||||
| 		uint8_t port_b_value; | ||||
| 		bool control_line_values[2][2]; | ||||
|  | ||||
| 		/* | ||||
| 			All methods below here are to replace those defined by | ||||
| 			MOS::MOS6522::PortHandler. | ||||
| 		*/ | ||||
| 		void set_interrupt_status(bool new_status) { | ||||
| 			irq_line = new_status; | ||||
| 		} | ||||
| @@ -27,6 +32,10 @@ class VanillaVIAPortHandler: public MOS::MOS6522::PortHandler { | ||||
| 		uint8_t get_port_input(MOS::MOS6522::Port port) { | ||||
| 			return port ? port_b_value : port_a_value; | ||||
| 		} | ||||
|  | ||||
| 		void set_control_line_output(MOS::MOS6522::Port port, MOS::MOS6522::Line line, bool value) { | ||||
| 			control_line_values[int(port)][int(line)] = value; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| @implementation MOS6522Bridge { | ||||
| @@ -75,4 +84,8 @@ class VanillaVIAPortHandler: public MOS::MOS6522::PortHandler { | ||||
| 	return _viaPortHandler.port_b_value; | ||||
| } | ||||
|  | ||||
| - (BOOL)valueForControlLine:(MOS6522BridgeLine)line port:(MOS6522BridgePort)port { | ||||
| 	return _viaPortHandler.control_line_values[port][line]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|   | ||||
							
								
								
									
										59
									
								
								OSBindings/Mac/Clock SignalTests/Comparative68000.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								OSBindings/Mac/Clock SignalTests/Comparative68000.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| // | ||||
| //  Comparative68000.hpp | ||||
| //  Clock SignalTests | ||||
| // | ||||
| //  Created by Thomas Harte on 29/04/2019. | ||||
| //  Copyright © 2019 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Comparative68000_hpp | ||||
| #define Comparative68000_hpp | ||||
|  | ||||
| #include <zlib.h> | ||||
|  | ||||
| #include "68000.hpp" | ||||
|  | ||||
| class ComparativeBusHandler: public CPU::MC68000::BusHandler { | ||||
| 	public: | ||||
| 		ComparativeBusHandler(const char *trace_name) { | ||||
| 			trace = gzopen(trace_name, "rt"); | ||||
| 		} | ||||
|  | ||||
| 		~ComparativeBusHandler() { | ||||
| 			gzclose(trace); | ||||
| 		} | ||||
|  | ||||
| 		void will_perform(uint32_t address, uint16_t opcode) { | ||||
| 			// Obtain the next line from the trace file. | ||||
| 			char correct_state[300] = "\n"; | ||||
| 			gzgets(trace, correct_state, sizeof(correct_state)); | ||||
| 			++line_count; | ||||
|  | ||||
| 			// Generate state locally. | ||||
| 			const auto state = get_state(); | ||||
| 			char local_state[300]; | ||||
| 			sprintf(local_state, "%04x: %02x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n", | ||||
| 				address, | ||||
| 				state.status, | ||||
| 				state.data[0], state.data[1], state.data[2], state.data[3], state.data[4], state.data[5], state.data[6], state.data[7], | ||||
| 				state.address[0], state.address[1], state.address[2], state.address[3], state.address[4], state.address[5], state.address[6], | ||||
| 				(state.status & 0x2000) ? state.supervisor_stack_pointer : state.user_stack_pointer | ||||
| 				); | ||||
|  | ||||
| 			// Check that the two coincide. | ||||
| 			if(strcmp(correct_state, local_state)) { | ||||
| 				fprintf(stderr, "Diverges at line %d\n", line_count); | ||||
| 				fprintf(stderr, "Good: %s", correct_state); | ||||
| 				fprintf(stderr, "Bad:  %s", local_state); | ||||
| 				assert(false); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		virtual CPU::MC68000::ProcessorState get_state() = 0; | ||||
|  | ||||
| 	private: | ||||
| 		int line_count = 0; | ||||
| 		gzFile trace; | ||||
| }; | ||||
|  | ||||
| #endif /* Comparative68000_hpp */ | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user