mirror of
				https://github.com/TomHarte/CLK.git
				synced 2025-10-27 08:16:22 +00:00 
			
		
		
		
	Compare commits
	
		
			1173 Commits
		
	
	
		
			2017-07-15
			...
			2018-03-04
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | dfcc502a88 | ||
|  | 1c6faaae88 | ||
|  | 35c8a0dd8c | ||
|  | 38feedaf6a | ||
|  | 0a2f908af4 | ||
|  | 705d53cc21 | ||
|  | 35b18d58af | ||
|  | 3c5a8d9ff3 | ||
|  | 7ca02be578 | ||
|  | ea13c7dd32 | ||
|  | fdfd72a42c | ||
|  | da97bf95c0 | ||
|  | bdfc36427c | ||
|  | 74dfe56d2b | ||
|  | 6cce9aa54e | ||
|  | ba68b7247b | ||
|  | b02e4fbbf6 | ||
|  | 59b4c7314d | ||
|  | d328589bd0 | ||
|  | b05d2b26bf | ||
|  | 86239469e7 | ||
|  | 7890506b16 | ||
|  | 83f73c3f02 | ||
|  | 87760297fc | ||
|  | 5b854d51e7 | ||
|  | d4df101ab6 | ||
|  | 0ad2676640 | ||
|  | a074ee2071 | ||
|  | 204d5cc964 | ||
|  | 23d15a4d6c | ||
|  | 23c47e21de | ||
|  | 5530b96446 | ||
|  | 99d28a172b | ||
|  | d83178f29d | ||
|  | d9d5ffdaa2 | ||
|  | cabad6fc05 | ||
|  | a4dc9c0403 | ||
|  | 270723ae72 | ||
|  | b215cf83d5 | ||
|  | f237dcf904 | ||
|  | fc81bfa59b | ||
|  | 832ac173ae | ||
|  | 3673cfe9be | ||
|  | 6aaef97158 | ||
|  | b0ab617393 | ||
|  | 6780b0bf11 | ||
|  | 9c0a440c38 | ||
|  | 2439f5aee5 | ||
|  | 8265f289bd | ||
|  | 9728bea0a7 | ||
|  | fc9e84c72e | ||
|  | 7d75e864b1 | ||
|  | a005dabbe3 | ||
|  | c8a4432c63 | ||
|  | 7b420d56e3 | ||
|  | ddf1bf3cbf | ||
|  | 7ea4ca00dc | ||
|  | 6b8c223804 | ||
|  | 23105956d6 | ||
|  | d751b7e2cb | ||
|  | f02989649c | ||
|  | dcf313a833 | ||
|  | 9960121b08 | ||
|  | 8eea55b51c | ||
|  | e1cab52c84 | ||
|  | eb39617ad0 | ||
|  | 43b682a5af | ||
|  | 043fd5d404 | ||
|  | d63a95983d | ||
|  | 4cf258f952 | ||
|  | 4e720d57b2 | ||
|  | c12aaea747 | ||
|  | ca48497e87 | ||
|  | d493ea4bca | ||
|  | e025674eb2 | ||
|  | f2519f4fd7 | ||
|  | db914d8c56 | ||
|  | 66faed4008 | ||
|  | 11abc99ef8 | ||
|  | 21efb32b6f | ||
|  | 622a04aec8 | ||
|  | d360b2c62d | ||
|  | 6a112edc18 | ||
|  | 8fb4409ebb | ||
|  | d213341d9c | ||
|  | c2f1306d85 | ||
|  | 2143ea6f12 | ||
|  | edb30b3c6c | ||
|  | 234e4f6f66 | ||
|  | ce2d3c6e82 | ||
|  | 46c76b9c07 | ||
|  | 583c3cfe7d | ||
|  | e13312dcc5 | ||
|  | d9e49c0d5f | ||
|  | 8a370cc1ac | ||
|  | cdae0fa593 | ||
|  | 765c0d4ff8 | ||
|  | 4cf2e16b5c | ||
|  | 9cbd61e709 | ||
|  | 0202c7afb2 | ||
|  | c187c5a637 | ||
|  | 23c34a8c14 | ||
|  | 93ece2aec7 | ||
|  | e12ab8fe2e | ||
|  | 2fe0ceb52a | ||
|  | f354c12c81 | ||
|  | def82cba49 | ||
|  | e7bc7b94c9 | ||
|  | aafdff49be | ||
|  | 4ef583813a | ||
|  | 9f97fb738e | ||
|  | 4e124047c6 | ||
|  | 6eb56a1564 | ||
|  | 35fc0a5c16 | ||
|  | b36c917810 | ||
|  | a5ac8c824e | ||
|  | 0ccc104027 | ||
|  | 8be6cb827b | ||
|  | 2f59226300 | ||
|  | 793ef68206 | ||
|  | 513c067f94 | ||
|  | 999a0c22d4 | ||
|  | 5d0832613f | ||
|  | 2ffde4c3c2 | ||
|  | 57ddfcd645 | ||
|  | fc16e8eb8c | ||
|  | 655b971976 | ||
|  | 3e1d8ea082 | ||
|  | 772c320d5a | ||
|  | bcc7ad0c30 | ||
|  | 73b4e1722b | ||
|  | 185cd3c123 | ||
|  | ed564cb810 | ||
|  | b78ece1f1e | ||
|  | c8367a017f | ||
|  | 344a12566b | ||
|  | c07113ea95 | ||
|  | bc2879c412 | ||
|  | 1d47b55729 | ||
|  | db25b4554b | ||
|  | 05b95ea2e0 | ||
|  | 250f7bf6b0 | ||
|  | 34db35b500 | ||
|  | f75590253d | ||
|  | 4f6abc9059 | ||
|  | c70dbc6a49 | ||
|  | 1c255b9e7d | ||
|  | 188bfa9c18 | ||
|  | c7f8f37822 | ||
|  | 4a19dbb8cb | ||
|  | bf0601123b | ||
|  | 9339f3413f | ||
|  | c18517be4b | ||
|  | eef34adcbd | ||
|  | 769d9dfbb9 | ||
|  | 6a0bb83716 | ||
|  | 6da8a3e24b | ||
|  | e349161a53 | ||
|  | d5b1a9d918 | ||
|  | 76af0228dd | ||
|  | 2cc1a2684a | ||
|  | 98a9d57c0b | ||
|  | c481293aca | ||
|  | 5fd0a2b9ea | ||
|  | 11b73a9c0b | ||
|  | c4950574ea | ||
|  | 0b297f2972 | ||
|  | f9f870ad2d | ||
|  | cbba6a5595 | ||
|  | 0a079b0f94 | ||
|  | 9a7e974579 | ||
|  | f4d414d6e4 | ||
|  | b4bfcd4279 | ||
|  | e8ddff0ee0 | ||
|  | b61fab9df7 | ||
|  | 28fb1ce2ae | ||
|  | b9b107ee85 | ||
|  | f17758e7f9 | ||
|  | 0bb24075b6 | ||
|  | db6d9b59d0 | ||
|  | 51e82c10c5 | ||
|  | 2d892da225 | ||
|  | b99ba2bc02 | ||
|  | d36e9d0b0d | ||
|  | 2dc1d4443e | ||
|  | f8a2459c91 | ||
|  | ac80d10cd8 | ||
|  | eb6b612052 | ||
|  | d66a33f249 | ||
|  | ec4c259695 | ||
|  | ad50b6b1fb | ||
|  | 3da323c657 | ||
|  | aca7842ca4 | ||
|  | 38c912b968 | ||
|  | 7a52e7d6d2 | ||
|  | c36de4f640 | ||
|  | 504772bcda | ||
|  | 5d0c33d545 | ||
|  | 7bc1bcd493 | ||
|  | b0616ee10c | ||
|  | da57df55e8 | ||
|  | 4daea1121b | ||
|  | afcdd64d5e | ||
|  | 798cdba979 | ||
|  | f957344ac4 | ||
|  | b3fbd0f352 | ||
|  | 042edc72f7 | ||
|  | 943418c434 | ||
|  | 7d7e2538bd | ||
|  | 7a544731e2 | ||
|  | e1914b4f16 | ||
|  | 202958303e | ||
|  | 57b060ac3c | ||
|  | 8653eb8b55 | ||
|  | a4f0a260fd | ||
|  | d4a53e82bb | ||
|  | 6eedc99286 | ||
|  | ec266d6c8e | ||
|  | e3a5218e78 | ||
|  | a473338abe | ||
|  | ae21782adc | ||
|  | ee44d671e7 | ||
|  | 3766bef962 | ||
|  | ad3df36c20 | ||
|  | 38b11893e8 | ||
|  | e4534775b0 | ||
|  | fe7fc6b22e | ||
|  | fe0cdc8d69 | ||
|  | 7f8a13a409 | ||
|  | ca26ce8400 | ||
|  | d3dd8f3f2a | ||
|  | 3c8d2d579d | ||
|  | edcbb3dfed | ||
|  | 9c8158753e | ||
|  | 5da9cb2957 | ||
|  | 54c845b6e2 | ||
|  | ee84f33ab5 | ||
|  | f0f149c018 | ||
|  | 7dfbe4bb93 | ||
|  | aa4eef41d8 | ||
|  | 69ec8a362e | ||
|  | ecd7d4731b | ||
|  | 563aa051e4 | ||
|  | 642bb8333f | ||
|  | c558e86e03 | ||
|  | dbb14ea2e2 | ||
|  | 173e16b107 | ||
|  | 7d2adad67e | ||
|  | d33612def5 | ||
|  | 9cb6ca3440 | ||
|  | e957e40b14 | ||
|  | 7a8a43a96a | ||
|  | 0eb5dd9688 | ||
|  | a14b53a9ab | ||
|  | 576d554a2c | ||
|  | 68a2895753 | ||
|  | f90b3f06aa | ||
|  | f067fa9923 | ||
|  | ee9f89ccb5 | ||
|  | 573a9c6fb2 | ||
|  | a46a37fba9 | ||
|  | 324b57c054 | ||
|  | ae50ca9ab2 | ||
|  | 6e4bde00d3 | ||
|  | d4d0dd87c9 | ||
|  | 221c05ca76 | ||
|  | ff21ff90eb | ||
|  | fcf295fd68 | ||
|  | 2008dec1ed | ||
|  | b4f3c41aae | ||
|  | 90c4e3726f | ||
|  | c83b3cefbc | ||
|  | a8ac51da73 | ||
|  | bc65ba3f9b | ||
|  | 79674fdbd3 | ||
|  | adea4711f1 | ||
|  | bded406caa | ||
|  | 85085a6375 | ||
|  | d122d598d3 | ||
|  | d6192b8c58 | ||
|  | f02d4dbb59 | ||
|  | f3818991f6 | ||
|  | c7dd6247f0 | ||
|  | 99e17600d7 | ||
|  | 1d821ad459 | ||
|  | c60a9ee3c3 | ||
|  | ffcbd1e94d | ||
|  | 6c8b503402 | ||
|  | 55e1d25966 | ||
|  | 0bdd776114 | ||
|  | c1b7bceec8 | ||
|  | dc4f58e40c | ||
|  | 3b8cdd620c | ||
|  | 3365ff0200 | ||
|  | 89c3e2ba5a | ||
|  | c6306db47c | ||
|  | 8ddc64c82a | ||
|  | b887cb7255 | ||
|  | d54ee2af82 | ||
|  | 723c113186 | ||
|  | c368c4443e | ||
|  | 7b25b03cd5 | ||
|  | 9961d13e2d | ||
|  | 29b5ccc767 | ||
|  | 90af395df2 | ||
|  | 6f8d4d6c5c | ||
|  | 63381ff505 | ||
|  | 2ea050556b | ||
|  | 8dcac6561e | ||
|  | 90d33949f9 | ||
|  | d3e68914dd | ||
|  | 82ad0354c4 | ||
|  | 073e439518 | ||
|  | 27b123549b | ||
|  | de9db724a7 | ||
|  | 532ea35ee9 | ||
|  | e9ddec35d6 | ||
|  | 7647f8089b | ||
|  | f00f0353a6 | ||
|  | e19ae5d43d | ||
|  | 0a9622435c | ||
|  | f704932475 | ||
|  | d0f096a20b | ||
|  | 949d0f3928 | ||
|  | a2d48223c3 | ||
|  | fc080c773f | ||
|  | adb3811847 | ||
|  | dbbea78b76 | ||
|  | fd96e3e657 | ||
|  | 06d81b3a97 | ||
|  | 88551607a6 | ||
|  | 2a9dccff26 | ||
|  | 1027f85683 | ||
|  | 9bb9cb4a65 | ||
|  | 2de80646ec | ||
|  | bf4ed57f68 | ||
|  | 9578f3dc44 | ||
|  | a97c478a34 | ||
|  | e0113d5dce | ||
|  | 980cf541d2 | ||
|  | 69c983f9ee | ||
|  | 70039d22f1 | ||
|  | ebdb80c908 | ||
|  | 0eaac99d74 | ||
|  | 792061a82b | ||
|  | d2ba7d7430 | ||
|  | 8713cfa613 | ||
|  | aa77be1c10 | ||
|  | e6aa2321cd | ||
|  | c827d14d97 | ||
|  | 2979d19621 | ||
|  | 282e5c9d3e | ||
|  | ede47d4ba7 | ||
|  | 5408efe9b5 | ||
|  | d6141cb020 | ||
|  | 198d0fd1de | ||
|  | 6d80856f02 | ||
|  | 4778616fd7 | ||
|  | 2e025d85eb | ||
|  | 61f2191c86 | ||
|  | c1eab8d5f3 | ||
|  | 91d2d59ae5 | ||
|  | 5aef81cf24 | ||
|  | 3550196bed | ||
|  | bce58683fa | ||
|  | c91a5875b2 | ||
|  | 2e15fab651 | ||
|  | 6a176082a0 | ||
|  | fd346bac3e | ||
|  | 25e9dcc800 | ||
|  | 792cbb1536 | ||
|  | 2e12370251 | ||
|  | 7adc25694a | ||
|  | ca80da7fbe | ||
|  | f853d87884 | ||
|  | 524087805f | ||
|  | 916eb96b47 | ||
|  | 4add2c1051 | ||
|  | cb0f58ab7a | ||
|  | d9e56711ce | ||
|  | d60692b6fd | ||
|  | 5b6ea35d96 | ||
|  | 4cbc87a17d | ||
|  | 46e7c199b2 | ||
|  | ff7ba526fb | ||
|  | a825da3715 | ||
|  | fabaf4e607 | ||
|  | 153067c018 | ||
|  | f7f2736d4d | ||
|  | a16ca65825 | ||
|  | cb015c83e1 | ||
|  | 2203499215 | ||
|  | c0055a5a5f | ||
|  | 62218e81bf | ||
|  | c45d4831ec | ||
|  | 9fd33bdfde | ||
|  | 6e1d69581c | ||
|  | f95515ae81 | ||
|  | 09c855a659 | ||
|  | 16c96b605a | ||
|  | e10d369e53 | ||
|  | 0d1b63a8c5 | ||
|  | ddcdd07dd0 | ||
|  | 35da3edf60 | ||
|  | d605022ea3 | ||
|  | 0da78065ce | ||
|  | 4b68c372c6 | ||
|  | 13406fedd8 | ||
|  | a209ae76ca | ||
|  | 0116d7f071 | ||
|  | 512e877d06 | ||
|  | 1e1efcdcb8 | ||
|  | bc2f58e9de | ||
|  | fd10c42433 | ||
|  | 794437f20f | ||
|  | 23d5849cda | ||
|  | 5070a8414f | ||
|  | 5a3ca0e447 | ||
|  | e384c50580 | ||
|  | b9734278f6 | ||
|  | f807a6b608 | ||
|  | 833f8c02a4 | ||
|  | 0248c6a282 | ||
|  | 218b976dbc | ||
|  | 513903890e | ||
|  | 1157bde453 | ||
|  | 46345c6a3e | ||
|  | c13f8e5390 | ||
|  | ad9df4bb90 | ||
|  | e983854e71 | ||
|  | ec999446e8 | ||
|  | 5e3e91373a | ||
|  | c52348d8d7 | ||
|  | 9e0907ee76 | ||
|  | 9ad4025138 | ||
|  | 405f58d6a3 | ||
|  | afbd1c425c | ||
|  | b2c1b83fcd | ||
|  | 8d2b9a581a | ||
|  | 1825af0dd3 | ||
|  | c2f6799f0c | ||
|  | b5b6219cb7 | ||
|  | 185a699279 | ||
|  | 96b8f9ae9f | ||
|  | 88e2350b8f | ||
|  | 5c141af734 | ||
|  | da580e4186 | ||
|  | 57ee09dffb | ||
|  | 7c8e830b90 | ||
|  | ba5f668338 | ||
|  | 2c1e99858b | ||
|  | 7f2febeec9 | ||
|  | 2d7a4fe5f0 | ||
|  | 91b867a7b3 | ||
|  | 3944e734d3 | ||
|  | ce78d9d12c | ||
|  | edbc60a3fb | ||
|  | 6ea3ff62df | ||
|  | 88959571f1 | ||
|  | b4583e976e | ||
|  | 92d9805f09 | ||
|  | 0c2dd62328 | ||
|  | 3f4d90d775 | ||
|  | 542ec4312f | ||
|  | 18798c9886 | ||
|  | 7aaf27389c | ||
|  | ee179aa7bd | ||
|  | 3a05ce36de | ||
|  | 4f289ab10b | ||
|  | 78ee46270b | ||
|  | edb632af52 | ||
|  | 19c03a08a6 | ||
|  | 44cdc124af | ||
|  | b37787a414 | ||
|  | 53b99ea248 | ||
|  | 97a2be71e3 | ||
|  | f623bff5c3 | ||
|  | 2511fc8401 | ||
|  | d37ec9e5b0 | ||
|  | 95c82f5b36 | ||
|  | ec202ed8be | ||
|  | 7190225603 | ||
|  | 52e7cabd4e | ||
|  | 064f1dfdbc | ||
|  | f40e1fd840 | ||
|  | e194a2a015 | ||
|  | c39759333a | ||
|  | edb9fd301c | ||
|  | ea5023ac26 | ||
|  | 0fb363ea0e | ||
|  | 1cc85615d5 | ||
|  | 7b01c1bee6 | ||
|  | 35705c5345 | ||
|  | f41da83d97 | ||
|  | cd1e5dea4d | ||
|  | ef605eda51 | ||
|  | 2f48ee59fa | ||
|  | f86729c4ac | ||
|  | 5f99f4442c | ||
|  | 326857a84d | ||
|  | 5dd3945695 | ||
|  | 19eb975c73 | ||
|  | 698ffca51b | ||
|  | fe3cc5c57c | ||
|  | f488854720 | ||
|  | 51c0c45e04 | ||
|  | c3e1489a8e | ||
|  | e3420f62c6 | ||
|  | 970c80f2e3 | ||
|  | 9f4a407f94 | ||
|  | 5dda897334 | ||
|  | 3982e375e3 | ||
|  | a8524daecb | ||
|  | d1ce764201 | ||
|  | 8875982e1f | ||
|  | 3319a4f589 | ||
|  | c7f27b2db4 | ||
|  | 631f630549 | ||
|  | 2a08bd9ecc | ||
|  | f789ee4ff0 | ||
|  | a295b42497 | ||
|  | d8337492cc | ||
|  | 15c8debc16 | ||
|  | 67af153c16 | ||
|  | d72dad2d1a | ||
|  | 698e4fe550 | ||
|  | b5406b90cd | ||
|  | 05a93ba237 | ||
|  | 77548d14db | ||
|  | b85dd608e7 | ||
|  | 231f13d810 | ||
|  | 704bfa114c | ||
|  | 44a56724cb | ||
|  | 5fbea625ae | ||
|  | ac57b37e96 | ||
|  | e3e9baeaa4 | ||
|  | e071123f90 | ||
|  | 98adb01721 | ||
|  | d6a5f9a29e | ||
|  | 0d84b4b9dd | ||
|  | a85909198f | ||
|  | 98751e6ac8 | ||
|  | da082673d7 | ||
|  | 35fe4d50d4 | ||
|  | b835cb73e2 | ||
|  | 662d031e3c | ||
|  | bf20c717fb | ||
|  | 4d4a0cf1d2 | ||
|  | b62f3e726a | ||
|  | 82b13e98f2 | ||
|  | 9ac831b09c | ||
|  | 42616da7ff | ||
|  | 2f13517f38 | ||
|  | fb9fd26af7 | ||
|  | d3c385b471 | ||
|  | 96bf133924 | ||
|  | 6d6cac429d | ||
|  | dc0b65f9c9 | ||
|  | 8882aa496f | ||
|  | 0622187ddf | ||
|  | 523e1288fa | ||
|  | 1a96cce26f | ||
|  | a4e275e1fc | ||
|  | 6075064400 | ||
|  | ff6e65cca9 | ||
|  | 90d2347c90 | ||
|  | 90c7056d12 | ||
|  | fed2bc9fc9 | ||
|  | ff510f3b84 | ||
|  | 3b12fca417 | ||
|  | 8eeb7e73cd | ||
|  | 7fd6699e0b | ||
|  | ed70b15fc9 | ||
|  | ff24e1de31 | ||
|  | 6547102511 | ||
|  | d538ff5039 | ||
|  | a49594c6a3 | ||
|  | 3544c0f014 | ||
|  | f26fe3756c | ||
|  | a42ca290cb | ||
|  | da09098e49 | ||
|  | 450712f39c | ||
|  | 24b3faa427 | ||
|  | 40d11ea0e3 | ||
|  | ab2bcb939f | ||
|  | 45499050b6 | ||
|  | 0c9197df30 | ||
|  | a1e200cc65 | ||
|  | 8a612bb6ab | ||
|  | e6ac939ae0 | ||
|  | b034d4e6f8 | ||
|  | de218611e4 | ||
|  | 615f7ce176 | ||
|  | b306776ba9 | ||
|  | 0f85cffc78 | ||
|  | 96648df5fe | ||
|  | 2c99a2d6ec | ||
|  | 4af333d5ec | ||
|  | a5f9869769 | ||
|  | f10be2a18a | ||
|  | c88d627b4e | ||
|  | b30bb2a234 | ||
|  | d498080eb4 | ||
|  | 334afbc710 | ||
|  | 17c13624e5 | ||
|  | 113349d272 | ||
|  | 0ced7866fc | ||
|  | d06031dfcb | ||
|  | 3f22a71276 | ||
|  | 53a88a7e12 | ||
|  | 4a66dd9e82 | ||
|  | 522839143f | ||
|  | b4c532c0d5 | ||
|  | a3e2d142e3 | ||
|  | 63ee8c9d58 | ||
|  | 437023bff6 | ||
|  | 4465098157 | ||
|  | 56dd677e9c | ||
|  | 9aa150c338 | ||
|  | fab6908129 | ||
|  | e34d4ce903 | ||
|  | d411827733 | ||
|  | f1ba7755dd | ||
|  | 57bfec285f | ||
|  | bdda701207 | ||
|  | 487fe83dca | ||
|  | 6c5a03187b | ||
|  | 97f57a3948 | ||
|  | 7d7aa2f5d5 | ||
|  | e7ad79c79a | ||
|  | 28550c0227 | ||
|  | 6e99169348 | ||
|  | 1017bb9f6b | ||
|  | 3caa4705ca | ||
|  | 039aed1bd1 | ||
|  | d77d7fdd78 | ||
|  | c6e6c3fcfb | ||
|  | ecd3350a6f | ||
|  | fa19e2d9c2 | ||
|  | 95d360251d | ||
|  | 7af3de010e | ||
|  | cefd421992 | ||
|  | a914eadc85 | ||
|  | 131b340d75 | ||
|  | e956740c56 | ||
|  | 8afd83b91f | ||
|  | 40d7a603db | ||
|  | ee71be0e7e | ||
|  | cde29c4bf4 | ||
|  | e1aded0d95 | ||
|  | 1237f174fe | ||
|  | 0cbc1753b9 | ||
|  | 5cf0395936 | ||
|  | 6315c22b80 | ||
|  | 4614a56843 | ||
|  | 8f5ae4a326 | ||
|  | 8fdc5012e4 | ||
|  | e88a51e75e | ||
|  | 49285e9caa | ||
|  | e3f2118757 | ||
|  | daeaa4752f | ||
|  | 5344e3098b | ||
|  | cedb809c21 | ||
|  | 2d9efccc98 | ||
|  | 8ce46b6e49 | ||
|  | f2699a3f2b | ||
|  | 85253a5876 | ||
|  | 911ee5a0d3 | ||
|  | 57c5b38a6d | ||
|  | 669e0caff5 | ||
|  | b24d04fc09 | ||
|  | ef07c33741 | ||
|  | e559a65ede | ||
|  | 5bdd24d93f | ||
|  | af61a7fa28 | ||
|  | c8c1792c3f | ||
|  | e6683e7f2d | ||
|  | 0c1714b695 | ||
|  | dc0ca83003 | ||
|  | 2c2dd8073c | ||
|  | 4f8b89772e | ||
|  | 733ee5a5c3 | ||
|  | fedf5a44a6 | ||
|  | 6a09022896 | ||
|  | 5b3c707959 | ||
|  | 9b21ef1507 | ||
|  | da3e8655e9 | ||
|  | 41e4386164 | ||
|  | b0a98bd239 | ||
|  | 42ad670ec8 | ||
|  | 58063b69a6 | ||
|  | 378f231499 | ||
|  | f68565a33f | ||
|  | 175faebdc9 | ||
|  | 76c6b715a2 | ||
|  | b476f06524 | ||
|  | 48290a8bbe | ||
|  | 9d9a1c341d | ||
|  | 952da1e581 | ||
|  | a988255558 | ||
|  | cca66ab450 | ||
|  | bcd7a312a4 | ||
|  | 925e774015 | ||
|  | 4c15e46fd1 | ||
|  | 3c50903a2b | ||
|  | 75208b0762 | ||
|  | f1e64169cd | ||
|  | 903a17ae11 | ||
|  | de1c526789 | ||
|  | b7e0f64892 | ||
|  | 148591b7f2 | ||
|  | 2105597910 | ||
|  | 3c148f5721 | ||
|  | 360c8a99a3 | ||
|  | 06e31f5102 | ||
|  | 42b5b66305 | ||
|  | 27018ba64e | ||
|  | e208f03636 | ||
|  | cc9d23f23b | ||
|  | 1a831bcf9b | ||
|  | 82367a2246 | ||
|  | f18206767f | ||
|  | 3947347d88 | ||
|  | 8a37a0ff2e | ||
|  | 468770b382 | ||
|  | 6cfc3daacb | ||
|  | aefbafa18d | ||
|  | d12c834f9c | ||
|  | 56de5cb2b3 | ||
|  | 709257a0c5 | ||
|  | 75a9d2bb33 | ||
|  | 7b92b235e1 | ||
|  | c196f0018f | ||
|  | 73080d6c36 | ||
|  | 9541a2a5f0 | ||
|  | 944222eba4 | ||
|  | 9d77f33611 | ||
|  | 7d132f81f7 | ||
|  | 0972f19fc5 | ||
|  | 6553bf05b4 | ||
|  | 0816d3f5a9 | ||
|  | 55055c7847 | ||
|  | 113da93796 | ||
|  | cddcd0fb79 | ||
|  | a366298022 | ||
|  | 4df9307d25 | ||
|  | d7bed958b3 | ||
|  | 9038ba622e | ||
|  | 7b8bb0297a | ||
|  | 0da02d3902 | ||
|  | 334872d374 | ||
|  | 2e5ad19fe1 | ||
|  | a10389a22c | ||
|  | cefec7a19f | ||
|  | 7264fbb3d2 | ||
|  | 0e083e9dc6 | ||
|  | 8a7b23dc9e | ||
|  | b7065575f3 | ||
|  | 7ea703f150 | ||
|  | ea64125124 | ||
|  | 1011143dbe | ||
|  | 9ace6e1f71 | ||
|  | 750f2cb883 | ||
|  | 5221837be8 | ||
|  | 1576b4500b | ||
|  | e1e9a06712 | ||
|  | 6e36f8ffa4 | ||
|  | b0a7208cc7 | ||
|  | eec42aa7ae | ||
|  | 6d2e969e7d | ||
|  | 5f42022c1d | ||
|  | 11d0c37506 | ||
|  | 58bad1e2a3 | ||
|  | 27d1dc5c37 | ||
|  | e7345c7a20 | ||
|  | 186048a88e | ||
|  | 7135259cc1 | ||
|  | 4909325e79 | ||
|  | a4ee697ed1 | ||
|  | 0f15a2f97f | ||
|  | 89ace671a4 | ||
|  | e7db2a2f6d | ||
|  | 8c33ac71ee | ||
|  | 69914faf02 | ||
|  | daafebe7ac | ||
|  | 2d81acb82e | ||
|  | 82ca49c840 | ||
|  | bfe297052d | ||
|  | ffb1a14ace | ||
|  | 7e35e44934 | ||
|  | 0c8769e335 | ||
|  | 83c7d34df2 | ||
|  | ad3c9842d7 | ||
|  | 44dace2eef | ||
|  | a12671010a | ||
|  | 23c149368b | ||
|  | 09716d4716 | ||
|  | 4b7c504d22 | ||
|  | 1e4f9d4eda | ||
|  | e4f04d0977 | ||
|  | 0f75525640 | ||
|  | edb088526f | ||
|  | 80ebc63101 | ||
|  | cf1403bc79 | ||
|  | fcf63a7547 | ||
|  | 9c0b75faba | ||
|  | 1f2bfc9581 | ||
|  | 14ab03d1e0 | ||
|  | 3831fbaca2 | ||
|  | 1d8edf58dd | ||
|  | 4785e316ff | ||
|  | 44da9de5b0 | ||
|  | 4ecd093891 | ||
|  | dd4bc87d52 | ||
|  | 570d25214e | ||
|  | f0b7e58968 | ||
|  | 0411b51582 | ||
|  | dea782cff9 | ||
|  | 388dd99762 | ||
|  | 026101a268 | ||
|  | 734099a956 | ||
|  | 6be5851484 | ||
|  | 994179f188 | ||
|  | 6a65c7a52a | ||
|  | 0d2d3ea17c | ||
|  | 5d374ebb18 | ||
|  | 62eadbb51a | ||
|  | ad8c8166bc | ||
|  | a5593bec79 | ||
|  | a1e2646301 | ||
|  | cf810d8357 | ||
|  | f258d6fbb2 | ||
|  | 4961fda2a9 | ||
|  | 6a6e5ae79c | ||
|  | 02d792c003 | ||
|  | be8e7a4144 | ||
|  | b1dbd7833a | ||
|  | 84ab05c5ef | ||
|  | 78138261c2 | ||
|  | a4c910f1de | ||
|  | 2eed24e859 | ||
|  | 7d1023ea98 | ||
|  | b11d142cff | ||
|  | 021ff8674e | ||
|  | 1b86bc21ab | ||
|  | e3d1f4fe1e | ||
|  | a7452aebff | ||
|  | 484524d781 | ||
|  | dbf9927caf | ||
|  | 3bdedfd749 | ||
|  | 005084af3d | ||
|  | 46278ff297 | ||
|  | d73dcbb268 | ||
|  | 390ecec3d9 | ||
|  | 41a30c147d | ||
|  | 4709ae80cb | ||
|  | 7fbb455836 | ||
|  | 745afd217f | ||
|  | 4427e9a254 | ||
|  | 2b0dcf8573 | ||
|  | 47732ffb98 | ||
|  | d07f3216ab | ||
|  | 56d65ba6f3 | ||
|  | 895a3cbf24 | ||
|  | d951c8c1c2 | ||
|  | a294963e98 | ||
|  | 68c73184b1 | ||
|  | 7f824d6494 | ||
|  | 3219212f03 | ||
|  | d90e35e5bd | ||
|  | 73f8488150 | ||
|  | 3853966a1e | ||
|  | d63893a437 | ||
|  | 90c74043f5 | ||
|  | 600445d90a | ||
|  | e4b405fd3d | ||
|  | 3b7ecbdf0d | ||
|  | 01efb645cb | ||
|  | b5ec1f42d5 | ||
|  | c839556a27 | ||
|  | e9972aa0dd | ||
|  | 1c9a744b01 | ||
|  | e6d4bb29d8 | ||
|  | 6c5b562d97 | ||
|  | a7103f9333 | ||
|  | c12425e141 | ||
|  | 89f6de1383 | ||
|  | 77da582e88 | ||
|  | 34eaf75352 | ||
|  | ffadb04761 | ||
|  | 29288b690e | ||
|  | 25fd3f7e50 | ||
|  | 4d60b8801c | ||
|  | 3e984e75b6 | ||
|  | 9e8645ca7a | ||
|  | caf3ac0645 | ||
|  | 0f4343cd84 | ||
|  | 192f232d3f | ||
|  | 6e4d3b8a77 | ||
|  | 8eda24261c | ||
|  | 75c59fefab | ||
|  | 4b19cf60df | ||
|  | 2b476554e7 | ||
|  | b3788fed41 | ||
|  | b999c1d8aa | ||
|  | a63aa80dc9 | ||
|  | 63f57c8c4f | ||
|  | 7e6a6365c9 | ||
|  | 3dbf26ac49 | ||
|  | f075fea78c | ||
|  | cc8380c286 | ||
|  | c0f0c68f4f | ||
|  | c2bb019381 | ||
|  | 26ce6cdab2 | ||
|  | d9097facf1 | ||
|  | b927500487 | ||
|  | e71eabedf9 | ||
|  | 33ed27c3ad | ||
|  | 45cbab6751 | ||
|  | a7b74d6164 | ||
|  | 575b1dba75 | ||
|  | a3b16b6dfa | ||
|  | c8f4de6f11 | ||
|  | bbb17acf3a | ||
|  | ad3a98387f | ||
|  | 985fbf59c2 | ||
|  | 2f2071be8a | ||
|  | 6d510e4e70 | ||
|  | 8e0736fbe6 | ||
|  | 681d1e2f8d | ||
|  | 42e70ef993 | ||
|  | 039811ce6a | ||
|  | a54ccd1969 | ||
|  | 707821ca55 | ||
|  | d3bf8fa53b | ||
|  | f5e2dd410e | ||
|  | 3ca9c38777 | ||
|  | 2d2cefb0b0 | ||
|  | 2fd071e45d | ||
|  | d7a5c3f49a | ||
|  | 819761f9fb | ||
|  | e50adf1cc8 | ||
|  | dcab10f53e | ||
|  | 633d8965e2 | ||
|  | f602f9b6ec | ||
|  | f7e66dea61 | ||
|  | bda9441620 | ||
|  | 4d5d5041df | ||
|  | 587eb3a67c | ||
|  | 6ca07f1e28 | ||
|  | 8d39a20088 | ||
|  | 4b6370eb86 | ||
|  | c6e340a8a2 | ||
|  | 31c7153301 | ||
|  | 7e04d00cc1 | ||
|  | 9d43784c65 | ||
|  | eca9586a0f | ||
|  | 0267bc237f | ||
|  | 2e4577f741 | ||
|  | f5b278d683 | ||
|  | e6854ff8db | ||
|  | 3b292273c7 | ||
|  | cb732e5d5f | ||
|  | 2d4e202be3 | ||
|  | 64da8e17d1 | ||
|  | 08ad35efd9 | ||
|  | 58b98267fc | ||
|  | ace71280a0 | ||
|  | a27946102a | ||
|  | 1d99c116e7 | ||
|  | ee27e16fb1 | ||
|  | 6ac7132799 | ||
|  | ca42abab70 | ||
|  | 933d69a256 | ||
|  | 3b1db14817 | ||
|  | 10a5581aea | ||
|  | 3ae699964f | ||
|  | 9d953421d8 | ||
|  | 763e3b65d1 | ||
|  | 42dd27c9b1 | ||
|  | 2b168f7383 | ||
|  | 0536f089e1 | ||
|  | 3df13cddd4 | ||
|  | e3f677fa37 | ||
|  | c2253c1e0f | ||
|  | 5c68b6cc21 | ||
|  | ffaa627820 | ||
|  | f742fd5d4a | ||
|  | 69b99fe127 | ||
|  | 5a396f6787 | ||
|  | cb0dc7b434 | ||
|  | e28829bd1b | ||
|  | 68ceeab610 | ||
|  | 68dca9d047 | ||
|  | d88ca151f4 | ||
|  | 3c90218c3d | ||
|  | afd409c883 | ||
|  | 26b6c03a2a | ||
|  | 9c04d851e4 | ||
|  | 1d6fe11906 | ||
|  | c0f1313830 | ||
|  | fb51fadf00 | ||
|  | 55fd9122d0 | ||
|  | 5b5720fac0 | ||
|  | d25d7d7d40 | ||
|  | ba4f2d8917 | ||
|  | a2aec39633 | ||
|  | 0bf4fdc9af | ||
|  | ed8c73eb14 | ||
|  | 3528a7f78b | ||
|  | 54bcc40192 | ||
|  | 4b5e9ffb83 | ||
|  | a7f5f035a6 | ||
|  | 4abd62e62b | ||
|  | 1fb158b297 | ||
|  | 968d2bb8ba | ||
|  | 92a3dfe44a | ||
|  | 9ef232157b | ||
|  | b9f4f7a530 | ||
|  | 761afad118 | ||
|  | 8848ebbd4f | ||
|  | 37950143fc | ||
|  | 25fd95044c | ||
|  | 1da24d10fd | ||
|  | 60e374dca3 | ||
|  | 7a65f91575 | ||
|  | 6f8b558724 | ||
|  | 1a88b62bf7 | ||
|  | 8361756dc4 | ||
|  | 273299028e | ||
|  | 847e49ccdf | ||
|  | 81a3899381 | ||
|  | 9257a3f6d7 | ||
|  | 728143247d | ||
|  | 6ec4e4e3d7 | ||
|  | 7922a12f02 | ||
|  | 37ccb9d3b6 | ||
|  | 3c254360ba | ||
|  | d2a0beaa67 | ||
|  | cda223ffc0 | ||
|  | 3ca51bedc6 | ||
|  | 36076b7ea5 | ||
|  | e90d128a26 | ||
|  | 966b5e6372 | ||
|  | 279c369a1f | ||
|  | d9c6b3bcf7 | ||
|  | 1c2f68f129 | ||
|  | 296c7cec05 | ||
|  | 75d67ee770 | ||
|  | a1e9a54765 | ||
|  | 8d1dacd951 | ||
|  | 545683df6f | ||
|  | cfbd62a5dc | ||
|  | 40339a12e1 | ||
|  | 90bf6565d0 | ||
|  | 9be9bd9106 | ||
|  | c1527cc9e2 | ||
|  | a1a3aab115 | ||
|  | c77a83d86f | ||
|  | a6e377aa57 | ||
|  | df4732be2e | ||
|  | 9435c1e12a | ||
|  | efdac2ce8c | ||
|  | 2912d7055b | ||
|  | d056f2e246 | ||
|  | 55ecb0c022 | ||
|  | 13f7aa4063 | ||
|  | 915f587ef1 | ||
|  | b7f88e8f61 | ||
|  | 677ed463f0 | ||
|  | 8a2bdb8d22 | ||
|  | 9bff787ee1 | ||
|  | b3ae920746 | ||
|  | b82bef95f3 | ||
|  | e6578defcd | ||
|  | ace8e30818 | ||
|  | ec3aa06caf | ||
|  | ba088e5545 | ||
|  | 8a0b0cb3d7 | ||
|  | 6369138bd1 | ||
|  | c2a7dffa7d | ||
|  | b0c2325adc | ||
|  | 2ff157cf7a | ||
|  | 83628b285b | ||
|  | 1ba3f262a2 | ||
|  | 97334e10af | ||
|  | 31b4f8fa31 | ||
|  | 1bbb4cb478 | ||
|  | d46da6ac9d | ||
|  | 825b38850e | ||
|  | 8755824c64 | ||
|  | 4ea835e50b | ||
|  | 5fddbec132 | ||
|  | b3d3fd70aa | ||
|  | 6633537fb8 | ||
|  | 4dec9716c4 | ||
|  | 313b36944f | ||
|  | 456fdda6c2 | ||
|  | 6437c43147 | ||
|  | 5928a24803 | ||
|  | 20a6bcc676 | ||
|  | eaf313b0f6 | ||
|  | d51b66c204 | ||
|  | 660f0e4c40 | ||
|  | 540a03f75c | ||
|  | a6b239698c | ||
|  | 92d1dd9a4a | ||
|  | 45ec5f8eab | ||
|  | 1d01acce06 | ||
|  | be750ee427 | ||
|  | dddb30477b | ||
|  | 37459a3ebc | ||
|  | 449c33ee8b | ||
|  | 2b5d0877a8 | ||
|  | 6d5807ec4b | ||
|  | 64865b3f41 | ||
|  | 53f0e1896b | ||
|  | aaa60dab12 | ||
|  | 9b72c445a7 | ||
|  | 3f609e17b3 | ||
|  | ef03c84b21 | ||
|  | 163c0f1b44 | ||
|  | 807e1d36d5 | ||
|  | 5545cc8bab | ||
|  | d6e60c8e3a | ||
|  | 2d8e7e9f8b | ||
|  | 5b4c5b0cbf | ||
|  | 2471ef805b | ||
|  | 2a7fc86b15 | ||
|  | 3c8bf52fb8 | ||
|  | a026998682 | ||
|  | 06ea81fdb2 | ||
|  | d69b1e2d11 | ||
|  | a3e0024980 | ||
|  | d9a2c32aca | ||
|  | e152ed2e61 | ||
|  | 9e975a46a2 | ||
|  | 70af075012 | ||
|  | 44e5a03cf2 | ||
|  | c8cee88e33 | ||
|  | 35296017b5 | ||
|  | 130d598ec9 | ||
|  | 0350925c1e | ||
|  | 94e3dd0d4f | ||
|  | 5d44422230 | ||
|  | eafdd7dbd7 | ||
|  | 127b584e79 | ||
|  | 2179edc7fe | ||
|  | 9108495586 | ||
|  | fa617eac6b | ||
|  | b63971caf7 | ||
|  | 7327da6350 | ||
|  | 8f72fc4a44 | ||
|  | 238348c885 | ||
|  | 7b5f93510b | ||
|  | b3861ff755 | ||
|  | 5a6b7219b9 | ||
|  | c2bc34fd87 | ||
|  | 1d3ae31755 | ||
|  | 8ddd686049 | ||
|  | e5188a60dc | ||
|  | e71d13c090 | ||
|  | ba83dfd454 | ||
|  | 2fb0aea990 | ||
|  | 51177e4e1f | ||
|  | 004d6da9fb | ||
|  | 561373e793 | ||
|  | 279f4760d7 | ||
|  | f931cd582d | ||
|  | ab51bc443b | ||
|  | c8575fe6e0 | ||
|  | 4489f120f9 | ||
|  | 253f9603ed | 
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -18,9 +18,14 @@ DerivedData | ||||
| *.xcuserstate | ||||
| .DS_Store | ||||
|  | ||||
| # Exclude system ROMs | ||||
| # Exclude system ROMs and unit test ROMs | ||||
| ROMImages/* | ||||
| OSBindings/Mac/Clock SignalTests/Atari\ ROMs | ||||
| OSBindings/Mac/Clock SignalTests/Atari ROMs | ||||
| OSBindings/Mac/Clock SignalTests/MSX ROMs | ||||
|  | ||||
| # Exclude intermediate build products | ||||
| *.o | ||||
| .sconsign.dblite | ||||
|  | ||||
| # CocoaPods | ||||
| # | ||||
|   | ||||
							
								
								
									
										30
									
								
								Analyser/Dynamic/ConfidenceCounter.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								Analyser/Dynamic/ConfidenceCounter.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| // | ||||
| //  ConfidenceCounter.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 21/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "ConfidenceCounter.hpp" | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| float ConfidenceCounter::get_confidence() { | ||||
| 	return static_cast<float>(hits_) / static_cast<float>(hits_ + misses_); | ||||
| } | ||||
|  | ||||
| void ConfidenceCounter::add_hit() { | ||||
| 	hits_++; | ||||
| } | ||||
|  | ||||
| void ConfidenceCounter::add_miss() { | ||||
| 	misses_++; | ||||
| } | ||||
|  | ||||
| void ConfidenceCounter::add_equivocal() { | ||||
| 	if(hits_ > misses_) { | ||||
| 		hits_++; | ||||
| 		misses_++; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										47
									
								
								Analyser/Dynamic/ConfidenceCounter.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								Analyser/Dynamic/ConfidenceCounter.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| // | ||||
| //  ConfidenceCounter.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 21/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef ConfidenceCounter_hpp | ||||
| #define ConfidenceCounter_hpp | ||||
|  | ||||
| #include "ConfidenceSource.hpp" | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Provides a confidence source that calculates its probability by virtual of a history of events. | ||||
|  | ||||
| 	The initial value of the confidence counter is 0.5. | ||||
| */ | ||||
| class ConfidenceCounter: public ConfidenceSource { | ||||
| 	public: | ||||
| 		/*! @returns The computed probability, based on the history of events. */ | ||||
| 		float get_confidence() override; | ||||
|  | ||||
| 		/*! Records an event that implies this is the appropriate class — pushes probability up towards 1.0. */ | ||||
| 		void add_hit(); | ||||
|  | ||||
| 		/*! Records an event that implies this is not the appropriate class — pushes probability down towards 0.0. */ | ||||
| 		void add_miss(); | ||||
|  | ||||
| 		/*! | ||||
| 			Records an event that could be correct but isn't necessarily so; which can push probability | ||||
| 			down towards 0.5, but will never push it upwards. | ||||
| 		*/ | ||||
| 		void add_equivocal(); | ||||
|  | ||||
| 	private: | ||||
| 		int hits_ = 1; | ||||
| 		int misses_ = 1; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* ConfidenceCounter_hpp */ | ||||
							
								
								
									
										28
									
								
								Analyser/Dynamic/ConfidenceSource.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Analyser/Dynamic/ConfidenceSource.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| // | ||||
| //  ConfidenceSource.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 21/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef ConfidenceSource_hpp | ||||
| #define ConfidenceSource_hpp | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Provides an abstract interface through which objects can declare the probability | ||||
| 	that they are the proper target for their input; e.g. if an Acorn Electron is asked | ||||
| 	to run an Atari 2600 program then its confidence should shrink towards 0.0; if the | ||||
| 	program is handed to an Atari 2600 then its confidence should grow towards 1.0. | ||||
| */ | ||||
| struct ConfidenceSource { | ||||
| 	virtual float get_confidence() = 0; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* ConfidenceSource_hpp */ | ||||
							
								
								
									
										28
									
								
								Analyser/Dynamic/ConfidenceSummary.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Analyser/Dynamic/ConfidenceSummary.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| // | ||||
| //  ConfidenceSummary.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 21/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "ConfidenceSummary.hpp" | ||||
|  | ||||
| #include <cassert> | ||||
| #include <numeric> | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| ConfidenceSummary::ConfidenceSummary(const std::vector<ConfidenceSource *> &sources, const std::vector<float> &weights) : | ||||
| 	sources_(sources), weights_(weights) { | ||||
| 	assert(weights.size() == sources.size()); | ||||
| 	weight_sum_ = std::accumulate(weights.begin(), weights.end(), 0.0f); | ||||
| } | ||||
|  | ||||
| float ConfidenceSummary::get_confidence() { | ||||
| 	float result = 0.0f; | ||||
| 	for(std::size_t index = 0; index < sources_.size(); ++index) { | ||||
| 		result += sources_[index]->get_confidence() * weights_[index]; | ||||
| 	} | ||||
| 	return result / weight_sum_; | ||||
| } | ||||
							
								
								
									
										46
									
								
								Analyser/Dynamic/ConfidenceSummary.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Analyser/Dynamic/ConfidenceSummary.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| // | ||||
| //  ConfidenceSummary.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 21/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef ConfidenceSummary_hpp | ||||
| #define ConfidenceSummary_hpp | ||||
|  | ||||
| #include "ConfidenceSource.hpp" | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Summaries a collection of confidence sources by calculating their weighted sum. | ||||
| */ | ||||
| class ConfidenceSummary: public ConfidenceSource { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			Instantiates a summary that will produce the weighted sum of | ||||
| 			@c sources, each using the corresponding entry of @c weights. | ||||
|  | ||||
| 			Requires that @c sources and @c weights are of the same length. | ||||
| 		*/ | ||||
| 		ConfidenceSummary( | ||||
| 			const std::vector<ConfidenceSource *> &sources, | ||||
| 			const std::vector<float> &weights); | ||||
|  | ||||
| 		/*! @returns The weighted sum of all sources. */ | ||||
| 		float get_confidence() override; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<ConfidenceSource *> sources_; | ||||
| 		std::vector<float> weights_; | ||||
| 		float weight_sum_; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* ConfidenceSummary_hpp */ | ||||
							
								
								
									
										115
									
								
								Analyser/Dynamic/MultiMachine/Implementation/MultiCRTMachine.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								Analyser/Dynamic/MultiMachine/Implementation/MultiCRTMachine.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| // | ||||
| //  MultiCRTMachine.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 29/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MultiCRTMachine.hpp" | ||||
|  | ||||
| #include <condition_variable> | ||||
| #include <mutex> | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| MultiCRTMachine::MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::mutex &machines_mutex) : | ||||
| 	machines_(machines), machines_mutex_(machines_mutex), queues_(machines.size()) { | ||||
| 	speaker_ = MultiSpeaker::create(machines); | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Machine *)> &function) { | ||||
| 	// Apply a blunt force parallelisation of the machines; each run_for is dispatched | ||||
| 	// to a separate queue and this queue will block until all are done. | ||||
| 	volatile std::size_t outstanding_machines; | ||||
| 	std::condition_variable condition; | ||||
| 	std::mutex mutex; | ||||
| 	{ | ||||
| 		std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| 		std::lock_guard<std::mutex> lock(mutex); | ||||
| 		outstanding_machines = machines_.size(); | ||||
|  | ||||
| 		for(std::size_t index = 0; index < machines_.size(); ++index) { | ||||
| 			CRTMachine::Machine *crt_machine = machines_[index]->crt_machine(); | ||||
| 			queues_[index].enqueue([&mutex, &condition, crt_machine, function, &outstanding_machines]() { | ||||
| 				if(crt_machine) function(crt_machine); | ||||
|  | ||||
| 				std::lock_guard<std::mutex> lock(mutex); | ||||
| 				outstanding_machines--; | ||||
| 				condition.notify_all(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	std::unique_lock<std::mutex> lock(mutex); | ||||
| 	condition.wait(lock, [&outstanding_machines] { return !outstanding_machines; }); | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::perform_serial(const std::function<void (::CRTMachine::Machine *)> &function) { | ||||
| 	std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		CRTMachine::Machine *crt_machine = machine->crt_machine(); | ||||
| 		if(crt_machine) function(crt_machine); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::setup_output(float aspect_ratio) { | ||||
| 	perform_serial([=](::CRTMachine::Machine *machine) { | ||||
| 		machine->setup_output(aspect_ratio); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::close_output() { | ||||
| 	perform_serial([=](::CRTMachine::Machine *machine) { | ||||
| 		machine->close_output(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| Outputs::CRT::CRT *MultiCRTMachine::get_crt() { | ||||
| 	std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| 	CRTMachine::Machine *crt_machine = machines_.front()->crt_machine(); | ||||
| 	return crt_machine ? crt_machine->get_crt() : nullptr; | ||||
| } | ||||
|  | ||||
| Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() { | ||||
| 	return speaker_; | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::run_for(const Cycles cycles) { | ||||
| 	perform_parallel([=](::CRTMachine::Machine *machine) { | ||||
| 		if(machine->get_confidence() >= 0.01f) machine->run_for(cycles); | ||||
| 	}); | ||||
|  | ||||
| 	if(delegate_) delegate_->multi_crt_did_run_machines(); | ||||
| } | ||||
|  | ||||
| double MultiCRTMachine::get_clock_rate() { | ||||
| 	// TODO: something smarter than this? Not all clock rates will necessarily be the same. | ||||
| 	std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| 	CRTMachine::Machine *crt_machine = machines_.front()->crt_machine(); | ||||
| 	return crt_machine ? crt_machine->get_clock_rate() : 0.0; | ||||
| } | ||||
|  | ||||
| bool MultiCRTMachine::get_clock_is_unlimited() { | ||||
| 	std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| 	CRTMachine::Machine *crt_machine = machines_.front()->crt_machine(); | ||||
| 	return crt_machine ? crt_machine->get_clock_is_unlimited() : false; | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::did_change_machine_order() { | ||||
| 	if(speaker_) { | ||||
| 		speaker_->set_new_front_machine(machines_.front().get()); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::set_delegate(::CRTMachine::Machine::Delegate *delegate) { | ||||
| 	// TODO:  | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::machine_did_change_clock_rate(Machine *machine) { | ||||
| 	// TODO: consider passing along. | ||||
| } | ||||
|  | ||||
| void MultiCRTMachine::machine_did_change_clock_is_unlimited(Machine *machine) { | ||||
| 	// TODO: consider passing along. | ||||
| } | ||||
| @@ -0,0 +1,95 @@ | ||||
| // | ||||
| //  MultiCRTMachine.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 29/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MultiCRTMachine_hpp | ||||
| #define MultiCRTMachine_hpp | ||||
|  | ||||
| #include "../../../../Concurrency/AsyncTaskQueue.hpp" | ||||
| #include "../../../../Machines/CRTMachine.hpp" | ||||
| #include "../../../../Machines/DynamicMachine.hpp" | ||||
|  | ||||
| #include "MultiSpeaker.hpp" | ||||
|  | ||||
| #include <memory> | ||||
| #include <mutex> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Provides a class that multiplexes the CRT machine interface to multiple machines. | ||||
|  | ||||
| 	Keeps a reference to the original vector of machines; will access it only after | ||||
| 	acquiring a supplied mutex. The owner should also call did_change_machine_order() | ||||
| 	if the order of machines changes. | ||||
| */ | ||||
| class MultiCRTMachine: public CRTMachine::Machine, public CRTMachine::Machine::Delegate { | ||||
| 	public: | ||||
| 		MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::mutex &machines_mutex); | ||||
|  | ||||
| 		/*! | ||||
| 			Informs the MultiCRTMachine that the order of machines has changed; the MultiCRTMachine | ||||
| 			uses this as an opportunity to synthesis any CRTMachine::Machine::Delegate messages that | ||||
| 			are necessary to bridge the gap between one machine and the next. | ||||
| 		*/ | ||||
| 		void did_change_machine_order(); | ||||
|  | ||||
| 		/*! | ||||
| 			Provides a mechanism by which a delegate can be informed each time a call to run_for has | ||||
| 			been received. | ||||
| 		*/ | ||||
| 		struct Delegate { | ||||
| 			virtual void multi_crt_did_run_machines() = 0; | ||||
| 		}; | ||||
| 		/// Sets @c delegate as the receiver of delegate messages. | ||||
| 		void set_delegate(Delegate *delegate) { | ||||
| 			delegate_ = delegate; | ||||
| 		} | ||||
|  | ||||
| 		// Below is the standard CRTMachine::Machine interface; see there for documentation. | ||||
| 		void setup_output(float aspect_ratio) override; | ||||
| 		void close_output() override; | ||||
| 		Outputs::CRT::CRT *get_crt() override; | ||||
| 		Outputs::Speaker::Speaker *get_speaker() override; | ||||
| 		void run_for(const Cycles cycles) override; | ||||
| 		double get_clock_rate() override; | ||||
| 		bool get_clock_is_unlimited() override; | ||||
| 		void set_delegate(::CRTMachine::Machine::Delegate *delegate) override; | ||||
|  | ||||
| 	private: | ||||
| 		// CRTMachine::Machine::Delegate | ||||
| 		void machine_did_change_clock_rate(Machine *machine) override; | ||||
| 		void machine_did_change_clock_is_unlimited(Machine *machine) override; | ||||
|  | ||||
| 		const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_; | ||||
| 		std::mutex &machines_mutex_; | ||||
| 		std::vector<Concurrency::AsyncTaskQueue> queues_; | ||||
| 		MultiSpeaker *speaker_ = nullptr; | ||||
| 		Delegate *delegate_ = nullptr; | ||||
|  | ||||
| 		/*! | ||||
| 			Performs a parallel for operation across all machines, performing the supplied | ||||
| 			function on each and returning only once all applications have completed. | ||||
|  | ||||
| 			No guarantees are extended as to which thread operations will occur on. | ||||
| 		*/ | ||||
| 		void perform_parallel(const std::function<void(::CRTMachine::Machine *)> &); | ||||
|  | ||||
| 		/*! | ||||
| 			Performs a serial for operation across all machines, performing the supplied | ||||
| 			function on each on the calling thread. | ||||
| 		*/ | ||||
| 		void perform_serial(const std::function<void(::CRTMachine::Machine *)> &); | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| #endif /* MultiCRTMachine_hpp */ | ||||
| @@ -0,0 +1,64 @@ | ||||
| // | ||||
| //  MultiConfigurable.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 09/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MultiConfigurable.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| MultiConfigurable::MultiConfigurable(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	for(const auto &machine: machines) { | ||||
| 		Configurable::Device *device = machine->configurable_device(); | ||||
| 		if(device) devices_.push_back(device); | ||||
|     } | ||||
| } | ||||
|  | ||||
| std::vector<std::unique_ptr<Configurable::Option>> MultiConfigurable::get_options() { | ||||
| 	std::vector<std::unique_ptr<Configurable::Option>> options; | ||||
|  | ||||
| 	// Produce the list of unique options. | ||||
| 	for(const auto &device : devices_) { | ||||
| 		std::vector<std::unique_ptr<Configurable::Option>> device_options = device->get_options(); | ||||
| 		for(auto &option : device_options) { | ||||
| 			if(std::find(options.begin(), options.end(), option) == options.end()) { | ||||
| 				options.push_back(std::move(option)); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return options; | ||||
| } | ||||
|  | ||||
| void MultiConfigurable::set_selections(const Configurable::SelectionSet &selection_by_option) { | ||||
| 	for(const auto &device : devices_) { | ||||
| 		device->set_selections(selection_by_option); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Configurable::SelectionSet MultiConfigurable::get_accurate_selections() { | ||||
| 	Configurable::SelectionSet set; | ||||
| 	for(const auto &device : devices_) { | ||||
| 		Configurable::SelectionSet device_set = device->get_accurate_selections(); | ||||
| 		for(auto &selection : device_set) { | ||||
| 			set.insert(std::move(selection)); | ||||
| 		} | ||||
| 	} | ||||
| 	return set; | ||||
| } | ||||
|  | ||||
| Configurable::SelectionSet MultiConfigurable::get_user_friendly_selections() { | ||||
| 	Configurable::SelectionSet set; | ||||
| 	for(const auto &device : devices_) { | ||||
| 		Configurable::SelectionSet device_set = device->get_user_friendly_selections(); | ||||
| 		for(auto &selection : device_set) { | ||||
| 			set.insert(std::move(selection)); | ||||
| 		} | ||||
| 	} | ||||
| 	return set; | ||||
| } | ||||
| @@ -0,0 +1,43 @@ | ||||
| // | ||||
| //  MultiConfigurable.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 09/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MultiConfigurable_hpp | ||||
| #define MultiConfigurable_hpp | ||||
|  | ||||
| #include "../../../../Machines/DynamicMachine.hpp" | ||||
|  | ||||
| #include <memory> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Provides a class that multiplexes the configurable interface to multiple machines. | ||||
|  | ||||
| 	Makes a static internal copy of the list of machines; makes no guarantees about the | ||||
| 	order of delivered messages. | ||||
| */ | ||||
| class MultiConfigurable: public Configurable::Device { | ||||
| 	public: | ||||
| 		MultiConfigurable(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		// Below is the standard Configurable::Device interface; see there for documentation. | ||||
| 		std::vector<std::unique_ptr<Configurable::Option>> get_options() override; | ||||
| 		void set_selections(const Configurable::SelectionSet &selection_by_option) override; | ||||
| 		Configurable::SelectionSet get_accurate_selections() override; | ||||
| 		Configurable::SelectionSet get_user_friendly_selections() override; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<Configurable::Device *> devices_; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* MultiConfigurable_hpp */ | ||||
| @@ -0,0 +1,29 @@ | ||||
| // | ||||
| //  MultiConfigurationTarget.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 29/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MultiConfigurationTarget.hpp" | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| MultiConfigurationTarget::MultiConfigurationTarget(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	for(const auto &machine: machines) { | ||||
| 		ConfigurationTarget::Machine *configuration_target = machine->configuration_target(); | ||||
| 		if(configuration_target) targets_.push_back(configuration_target); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void MultiConfigurationTarget::configure_as_target(const Analyser::Static::Target &target) { | ||||
| } | ||||
|  | ||||
| bool MultiConfigurationTarget::insert_media(const Analyser::Static::Media &media) { | ||||
| 	bool inserted = false; | ||||
| 	for(const auto &target : targets_) { | ||||
| 		inserted |= target->insert_media(media); | ||||
| 	} | ||||
| 	return inserted; | ||||
| } | ||||
| @@ -0,0 +1,42 @@ | ||||
| // | ||||
| //  MultiConfigurationTarget.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 29/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MultiConfigurationTarget_hpp | ||||
| #define MultiConfigurationTarget_hpp | ||||
|  | ||||
| #include "../../../../Machines/ConfigurationTarget.hpp" | ||||
| #include "../../../../Machines/DynamicMachine.hpp" | ||||
|  | ||||
| #include <memory> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Provides a class that multiplexes the configuration target interface to multiple machines. | ||||
|  | ||||
| 	Makes a static internal copy of the list of machines; makes no guarantees about the | ||||
| 	order of delivered messages. | ||||
| */ | ||||
| struct MultiConfigurationTarget: public ConfigurationTarget::Machine { | ||||
| 	public: | ||||
| 		MultiConfigurationTarget(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		// Below is the standard ConfigurationTarget::Machine interface; see there for documentation. | ||||
| 		void configure_as_target(const Analyser::Static::Target &target) override; | ||||
| 		bool insert_media(const Analyser::Static::Media &media) override; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<ConfigurationTarget::Machine *> targets_; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* MultiConfigurationTarget_hpp */ | ||||
| @@ -0,0 +1,22 @@ | ||||
| // | ||||
| //  MultiJoystickMachine.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 09/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MultiJoystickMachine.hpp" | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| MultiJoystickMachine::MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	for(const auto &machine: machines) { | ||||
| 		JoystickMachine::Machine *joystick_machine = machine->joystick_machine(); | ||||
| 		if(joystick_machine) machines_.push_back(joystick_machine); | ||||
|     } | ||||
| } | ||||
|  | ||||
| std::vector<std::unique_ptr<Inputs::Joystick>> &MultiJoystickMachine::get_joysticks() { | ||||
| 	return joysticks_; | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| // | ||||
| //  MultiJoystickMachine.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 09/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MultiJoystickMachine_hpp | ||||
| #define MultiJoystickMachine_hpp | ||||
|  | ||||
| #include "../../../../Machines/DynamicMachine.hpp" | ||||
|  | ||||
| #include <memory> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Provides a class that multiplexes the joystick machine interface to multiple machines. | ||||
|  | ||||
| 	Makes a static internal copy of the list of machines; makes no guarantees about the | ||||
| 	order of delivered messages. | ||||
| */ | ||||
| class MultiJoystickMachine: public JoystickMachine::Machine { | ||||
| 	public: | ||||
| 		MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		// Below is the standard JoystickMachine::Machine interface; see there for documentation. | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<JoystickMachine::Machine *> machines_; | ||||
| 		std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* MultiJoystickMachine_hpp */ | ||||
| @@ -0,0 +1,43 @@ | ||||
| // | ||||
| //  MultiKeyboardMachine.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 09/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MultiKeyboardMachine.hpp" | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	for(const auto &machine: machines) { | ||||
| 		KeyboardMachine::Machine *keyboard_machine = machine->keyboard_machine(); | ||||
| 		if(keyboard_machine) machines_.push_back(keyboard_machine); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void MultiKeyboardMachine::clear_all_keys() { | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		machine->clear_all_keys(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void MultiKeyboardMachine::set_key_state(uint16_t key, bool is_pressed) { | ||||
|     for(const auto &machine: machines_) { | ||||
|         machine->set_key_state(key, is_pressed); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void MultiKeyboardMachine::type_string(const std::string &string) { | ||||
|     for(const auto &machine: machines_) { | ||||
|         machine->type_string(string); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void MultiKeyboardMachine::keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) { | ||||
|     for(const auto &machine: machines_) { | ||||
| 		uint16_t mapped_key = machine->get_keyboard_mapper()->mapped_key_for_key(key); | ||||
| 		if(mapped_key != KeyNotMapped) machine->set_key_state(mapped_key, is_pressed); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,44 @@ | ||||
| // | ||||
| //  MultiKeyboardMachine.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 09/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MultiKeyboardMachine_hpp | ||||
| #define MultiKeyboardMachine_hpp | ||||
|  | ||||
| #include "../../../../Machines/DynamicMachine.hpp" | ||||
| #include "../../../../Machines/KeyboardMachine.hpp" | ||||
|  | ||||
| #include <memory> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Provides a class that multiplexes the keyboard machine interface to multiple machines. | ||||
|  | ||||
| 	Makes a static internal copy of the list of machines; makes no guarantees about the | ||||
| 	order of delivered messages. | ||||
| */ | ||||
| class MultiKeyboardMachine: public KeyboardMachine::Machine { | ||||
| 	public: | ||||
| 		MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		// Below is the standard KeyboardMachine::Machine interface; see there for documentation. | ||||
| 		void clear_all_keys() override; | ||||
| 		void set_key_state(uint16_t key, bool is_pressed) override; | ||||
| 		void type_string(const std::string &) override; | ||||
| 		void keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) override; | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<::KeyboardMachine::Machine *> machines_; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* MultiKeyboardMachine_hpp */ | ||||
| @@ -0,0 +1,60 @@ | ||||
| // | ||||
| //  MultiSpeaker.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 18/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MultiSpeaker.hpp" | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| MultiSpeaker *MultiSpeaker::create(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) { | ||||
| 	std::vector<Outputs::Speaker::Speaker *> speakers; | ||||
| 	for(const auto &machine: machines) { | ||||
| 		Outputs::Speaker::Speaker *speaker = machine->crt_machine()->get_speaker(); | ||||
| 		if(speaker) speakers.push_back(speaker); | ||||
| 	} | ||||
| 	if(speakers.empty()) return nullptr; | ||||
|  | ||||
| 	return new MultiSpeaker(speakers); | ||||
| } | ||||
|  | ||||
| MultiSpeaker::MultiSpeaker(const std::vector<Outputs::Speaker::Speaker *> &speakers) : | ||||
| 	speakers_(speakers), front_speaker_(speakers.front()) { | ||||
| 	for(const auto &speaker: speakers_) { | ||||
| 		speaker->set_delegate(this); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum) { | ||||
| 	float ideal = 0.0f; | ||||
| 	for(const auto &speaker: speakers_) { | ||||
| 		ideal += speaker->get_ideal_clock_rate_in_range(minimum, maximum); | ||||
| 	} | ||||
|  | ||||
| 	return ideal / static_cast<float>(speakers_.size()); | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::set_output_rate(float cycles_per_second, int buffer_size) { | ||||
| 	for(const auto &speaker: speakers_) { | ||||
| 		speaker->set_output_rate(cycles_per_second, buffer_size); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) { | ||||
| 	delegate_ = delegate; | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) { | ||||
| 	std::lock_guard<std::mutex> lock_guard(front_speaker_mutex_); | ||||
| 	if(delegate_ && speaker == front_speaker_) { | ||||
| 		delegate_->speaker_did_complete_samples(this, buffer); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiSpeaker::set_new_front_machine(::Machine::DynamicMachine *machine) { | ||||
| 	std::lock_guard<std::mutex> lock_guard(front_speaker_mutex_); | ||||
| 	front_speaker_ = machine->crt_machine()->get_speaker(); | ||||
| } | ||||
| @@ -0,0 +1,58 @@ | ||||
| // | ||||
| //  MultiSpeaker.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 18/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MultiSpeaker_hpp | ||||
| #define MultiSpeaker_hpp | ||||
|  | ||||
| #include "../../../../Machines/DynamicMachine.hpp" | ||||
| #include "../../../../Outputs/Speaker/Speaker.hpp" | ||||
|  | ||||
| #include <memory> | ||||
| #include <mutex> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Provides a class that multiplexes calls to and from Outputs::Speaker::Speaker in order | ||||
| 	transparently to connect a single caller to multiple destinations. | ||||
|  | ||||
| 	Makes a static internal copy of the list of machines; expects the owner to keep it | ||||
| 	abreast of the current frontmost machine. | ||||
| */ | ||||
| class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker::Delegate { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			Provides a construction mechanism that may return nullptr, in the case that all included | ||||
| 			machines return nullptr as their speaker. | ||||
| 		*/ | ||||
| 		static MultiSpeaker *create(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines); | ||||
|  | ||||
| 		/// This class requires the caller to nominate changes in the frontmost machine. | ||||
| 		void set_new_front_machine(::Machine::DynamicMachine *machine); | ||||
|  | ||||
| 		// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation. | ||||
| 		float get_ideal_clock_rate_in_range(float minimum, float maximum); | ||||
| 		void set_output_rate(float cycles_per_second, int buffer_size); | ||||
| 		void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate); | ||||
|  | ||||
| 	private: | ||||
| 		void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer); | ||||
| 		MultiSpeaker(const std::vector<Outputs::Speaker::Speaker *> &speakers); | ||||
|  | ||||
| 		std::vector<Outputs::Speaker::Speaker *> speakers_; | ||||
| 		Outputs::Speaker::Speaker *front_speaker_ = nullptr; | ||||
| 		Outputs::Speaker::Speaker::Delegate *delegate_ = nullptr; | ||||
| 		std::mutex front_speaker_mutex_; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* MultiSpeaker_hpp */ | ||||
							
								
								
									
										104
									
								
								Analyser/Dynamic/MultiMachine/MultiMachine.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								Analyser/Dynamic/MultiMachine/MultiMachine.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| // | ||||
| //  MultiMachine.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 28/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "MultiMachine.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| using namespace Analyser::Dynamic; | ||||
|  | ||||
| MultiMachine::MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines) : | ||||
| 	machines_(std::move(machines)), | ||||
| 	configurable_(machines_), | ||||
| 	configuration_target_(machines_), | ||||
| 	crt_machine_(machines_, machines_mutex_), | ||||
| 	joystick_machine_(machines), | ||||
| 	keyboard_machine_(machines_) { | ||||
| 	crt_machine_.set_delegate(this); | ||||
| } | ||||
|  | ||||
| ConfigurationTarget::Machine *MultiMachine::configuration_target() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->configuration_target(); | ||||
| 	} else { | ||||
| 		return &configuration_target_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| CRTMachine::Machine *MultiMachine::crt_machine() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->crt_machine(); | ||||
| 	} else { | ||||
| 		return &crt_machine_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| JoystickMachine::Machine *MultiMachine::joystick_machine() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->joystick_machine(); | ||||
| 	} else { | ||||
| 		return &joystick_machine_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| KeyboardMachine::Machine *MultiMachine::keyboard_machine() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->keyboard_machine(); | ||||
| 	} else { | ||||
| 		return &keyboard_machine_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Configurable::Device *MultiMachine::configurable_device() { | ||||
| 	if(has_picked_) { | ||||
| 		return machines_.front()->configurable_device(); | ||||
| 	} else { | ||||
| 		return &configurable_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiMachine::multi_crt_did_run_machines() { | ||||
| 	std::lock_guard<std::mutex> machines_lock(machines_mutex_); | ||||
| 	for(const auto &machine: machines_) { | ||||
| 		CRTMachine::Machine *crt = machine->crt_machine(); | ||||
| 		printf("%0.2f ", crt->get_confidence()); | ||||
| 		crt->print_type(); | ||||
| 		printf("; "); | ||||
| 	} | ||||
| 	printf("\n"); | ||||
|  | ||||
| 	DynamicMachine *front = machines_.front().get(); | ||||
| 	std::stable_sort(machines_.begin(), machines_.end(), | ||||
|         [] (const std::unique_ptr<DynamicMachine> &lhs, const std::unique_ptr<DynamicMachine> &rhs){ | ||||
| 		    CRTMachine::Machine *lhs_crt = lhs->crt_machine(); | ||||
| 		    CRTMachine::Machine *rhs_crt = rhs->crt_machine(); | ||||
| 		    return lhs_crt->get_confidence() > rhs_crt->get_confidence(); | ||||
| 	    }); | ||||
|  | ||||
| 	if(machines_.front().get() != front) { | ||||
| 		crt_machine_.did_change_machine_order(); | ||||
| 	} | ||||
|  | ||||
| 	if( | ||||
| 		(machines_.front()->crt_machine()->get_confidence() > 0.9f) || | ||||
| 		(machines_.front()->crt_machine()->get_confidence() >= 2.0f * machines_[1]->crt_machine()->get_confidence()) | ||||
| 	) { | ||||
| 		pick_first(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MultiMachine::pick_first() { | ||||
| 	has_picked_ = true; | ||||
| //	machines_.erase(machines_.begin() + 1, machines_.end()); | ||||
| 	// TODO: this isn't quite correct, because it may leak OpenGL/etc resources through failure to | ||||
| 	// request a close_output while the context is active. | ||||
| } | ||||
|  | ||||
| void *MultiMachine::raw_pointer() { | ||||
| 	return nullptr; | ||||
| } | ||||
							
								
								
									
										71
									
								
								Analyser/Dynamic/MultiMachine/MultiMachine.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								Analyser/Dynamic/MultiMachine/MultiMachine.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| // | ||||
| //  MultiMachine.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 28/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef MultiMachine_hpp | ||||
| #define MultiMachine_hpp | ||||
|  | ||||
| #include "../../../Machines/DynamicMachine.hpp" | ||||
|  | ||||
| #include "Implementation/MultiConfigurable.hpp" | ||||
| #include "Implementation/MultiConfigurationTarget.hpp" | ||||
| #include "Implementation/MultiCRTMachine.hpp" | ||||
| #include "Implementation/MultiJoystickMachine.hpp" | ||||
| #include "Implementation/MultiKeyboardMachine.hpp" | ||||
|  | ||||
| #include <memory> | ||||
| #include <mutex> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Dynamic { | ||||
|  | ||||
| /*! | ||||
| 	Provides the same interface as to a single machine, while multiplexing all | ||||
| 	underlying calls to an array of real dynamic machines. | ||||
|  | ||||
| 	Calls to crt_machine->get_crt will return that for the frontmost machine; | ||||
| 	anything installed as the speaker's delegate will similarly receive | ||||
| 	feedback only from that machine. | ||||
|  | ||||
| 	Following each crt_machine->run_for, reorders the supplied machines by | ||||
| 	confidence. | ||||
|  | ||||
| 	If confidence for any machine becomes disproportionately low compared to | ||||
| 	the others in the set, that machine stops running. | ||||
| */ | ||||
| class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::Delegate { | ||||
| 	public: | ||||
| 		MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines); | ||||
|  | ||||
| 		ConfigurationTarget::Machine *configuration_target() override; | ||||
| 		CRTMachine::Machine *crt_machine() override; | ||||
| 		JoystickMachine::Machine *joystick_machine() override; | ||||
| 		KeyboardMachine::Machine *keyboard_machine() override; | ||||
| 		Configurable::Device *configurable_device() override; | ||||
| 		void *raw_pointer() override; | ||||
|  | ||||
| 	private: | ||||
| 		void multi_crt_did_run_machines() override; | ||||
|  | ||||
| 		std::vector<std::unique_ptr<DynamicMachine>> machines_; | ||||
| 		std::mutex machines_mutex_; | ||||
|  | ||||
| 		MultiConfigurable configurable_; | ||||
| 		MultiConfigurationTarget configuration_target_; | ||||
| 		MultiCRTMachine crt_machine_; | ||||
| 		MultiJoystickMachine joystick_machine_; | ||||
| 		MultiKeyboardMachine keyboard_machine_; | ||||
|  | ||||
| 		void pick_first(); | ||||
| 		bool has_picked_ = false; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* MultiMachine_hpp */ | ||||
							
								
								
									
										27
									
								
								Analyser/Machines.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Analyser/Machines.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  Machines.h | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 24/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Machines_h | ||||
| #define Machines_h | ||||
|  | ||||
| namespace Analyser { | ||||
|  | ||||
| enum class Machine { | ||||
| 	AmstradCPC, | ||||
| 	Atari2600, | ||||
| 	ColecoVision, | ||||
| 	Electron, | ||||
| 	MSX, | ||||
| 	Oric, | ||||
| 	Vic20, | ||||
| 	ZX8081 | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* Machines_h */ | ||||
							
								
								
									
										105
									
								
								Analyser/Static/Acorn/Disk.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								Analyser/Static/Acorn/Disk.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| // | ||||
| //  Disk.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 18/09/2016. | ||||
| //  Copyright © 2016 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Disk.hpp" | ||||
|  | ||||
| #include "../../../Storage/Disk/Controller/DiskController.hpp" | ||||
| #include "../../../Storage/Disk/Encodings/MFM/Parser.hpp" | ||||
| #include "../../../NumberTheory/CRC.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| using namespace Analyser::Static::Acorn; | ||||
|  | ||||
| std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) { | ||||
| 	// c.f. http://beebwiki.mdfs.net/Acorn_DFS_disc_format | ||||
| 	std::unique_ptr<Catalogue> catalogue(new Catalogue); | ||||
| 	Storage::Encodings::MFM::Parser parser(false, disk); | ||||
|  | ||||
| 	Storage::Encodings::MFM::Sector *names = parser.get_sector(0, 0, 0); | ||||
| 	Storage::Encodings::MFM::Sector *details = parser.get_sector(0, 0, 1); | ||||
|  | ||||
| 	if(!names || !details) return nullptr; | ||||
| 	if(names->samples.empty() || details->samples.empty()) return nullptr; | ||||
| 	if(names->samples[0].size() != 256 || details->samples[0].size() != 256) return nullptr; | ||||
|  | ||||
| 	uint8_t final_file_offset = details->samples[0][5]; | ||||
| 	if(final_file_offset&7) return nullptr; | ||||
| 	if(final_file_offset < 8) return nullptr; | ||||
|  | ||||
| 	char disk_name[13]; | ||||
| 	snprintf(disk_name, 13, "%.8s%.4s", &names->samples[0][0], &details->samples[0][0]); | ||||
| 	catalogue->name = disk_name; | ||||
|  | ||||
| 	switch((details->samples[0][6] >> 4)&3) { | ||||
| 		case 0: catalogue->bootOption = Catalogue::BootOption::None;		break; | ||||
| 		case 1: catalogue->bootOption = Catalogue::BootOption::LoadBOOT;	break; | ||||
| 		case 2: catalogue->bootOption = Catalogue::BootOption::RunBOOT;		break; | ||||
| 		case 3: catalogue->bootOption = Catalogue::BootOption::ExecBOOT;	break; | ||||
| 	} | ||||
|  | ||||
| 	for(std::size_t file_offset = 8; file_offset < final_file_offset; file_offset += 8) { | ||||
| 		File new_file; | ||||
| 		char name[10]; | ||||
| 		snprintf(name, 10, "%c.%.7s", names->samples[0][file_offset + 7] & 0x7f, &names->samples[0][file_offset]); | ||||
| 		new_file.name = name; | ||||
| 		new_file.load_address = (uint32_t)(details->samples[0][file_offset] | (details->samples[0][file_offset+1] << 8) | ((details->samples[0][file_offset+6]&0x0c) << 14)); | ||||
| 		new_file.execution_address = (uint32_t)(details->samples[0][file_offset+2] | (details->samples[0][file_offset+3] << 8) | ((details->samples[0][file_offset+6]&0xc0) << 10)); | ||||
| 		new_file.is_protected = !!(names->samples[0][file_offset + 7] & 0x80); | ||||
|  | ||||
| 		long data_length = static_cast<long>(details->samples[0][file_offset+4] | (details->samples[0][file_offset+5] << 8) | ((details->samples[0][file_offset+6]&0x30) << 12)); | ||||
| 		int start_sector = details->samples[0][file_offset+7] | ((details->samples[0][file_offset+6]&0x03) << 8); | ||||
| 		new_file.data.reserve(static_cast<std::size_t>(data_length)); | ||||
|  | ||||
| 		if(start_sector < 2) continue; | ||||
| 		while(data_length > 0) { | ||||
| 			uint8_t sector = static_cast<uint8_t>(start_sector % 10); | ||||
| 			uint8_t track = static_cast<uint8_t>(start_sector / 10); | ||||
| 			start_sector++; | ||||
|  | ||||
| 			Storage::Encodings::MFM::Sector *next_sector = parser.get_sector(0, track, sector); | ||||
| 			if(!next_sector) break; | ||||
|  | ||||
| 			long length_from_sector = std::min(data_length, 256l); | ||||
| 			new_file.data.insert(new_file.data.end(), next_sector->samples[0].begin(), next_sector->samples[0].begin() + length_from_sector); | ||||
| 			data_length -= length_from_sector; | ||||
| 		} | ||||
| 		if(!data_length) catalogue->files.push_back(new_file); | ||||
| 	} | ||||
|  | ||||
| 	return catalogue; | ||||
| } | ||||
| std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) { | ||||
| 	std::unique_ptr<Catalogue> catalogue(new Catalogue); | ||||
| 	Storage::Encodings::MFM::Parser parser(true, disk); | ||||
|  | ||||
| 	Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.get_sector(0, 0, 1); | ||||
| 	if(!free_space_map_second_half) return nullptr; | ||||
|  | ||||
| 	std::vector<uint8_t> root_directory; | ||||
| 	root_directory.reserve(5 * 256); | ||||
| 	for(uint8_t c = 2; c < 7; c++) { | ||||
| 		Storage::Encodings::MFM::Sector *sector = parser.get_sector(0, 0, c); | ||||
| 		if(!sector) return nullptr; | ||||
| 		root_directory.insert(root_directory.end(), sector->samples[0].begin(), sector->samples[0].end()); | ||||
| 	} | ||||
|  | ||||
| 	// Quick sanity checks. | ||||
| 	if(root_directory[0x4cb]) return nullptr; | ||||
| 	if(root_directory[1] != 'H' || root_directory[2] != 'u' || root_directory[3] != 'g' || root_directory[4] != 'o') return nullptr; | ||||
| 	if(root_directory[0x4FB] != 'H' || root_directory[0x4FC] != 'u' || root_directory[0x4FD] != 'g' || root_directory[0x4FE] != 'o') return nullptr; | ||||
|  | ||||
| 	switch(free_space_map_second_half->samples[0][0xfd]) { | ||||
| 		default: catalogue->bootOption = Catalogue::BootOption::None;		break; | ||||
| 		case 1: catalogue->bootOption = Catalogue::BootOption::LoadBOOT;	break; | ||||
| 		case 2: catalogue->bootOption = Catalogue::BootOption::RunBOOT;		break; | ||||
| 		case 3: catalogue->bootOption = Catalogue::BootOption::ExecBOOT;	break; | ||||
| 	} | ||||
|  | ||||
| 	return catalogue; | ||||
| } | ||||
| @@ -10,14 +10,16 @@ | ||||
| #define StaticAnalyser_Acorn_Disk_hpp | ||||
| 
 | ||||
| #include "File.hpp" | ||||
| #include "../../Storage/Disk/Disk.hpp" | ||||
| #include "../../../Storage/Disk/Disk.hpp" | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Acorn { | ||||
| 
 | ||||
| /// Describes a DFS- or ADFS-format catalogue(/directory) — the list of files available and the catalogue's boot option.
 | ||||
| struct Catalogue { | ||||
| 	std::string name; | ||||
| 	std::list<File> files; | ||||
| 	std::vector<File> files; | ||||
| 	enum class BootOption { | ||||
| 		None, | ||||
| 		LoadBOOT, | ||||
| @@ -29,6 +31,7 @@ struct Catalogue { | ||||
| std::unique_ptr<Catalogue> GetDFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk); | ||||
| std::unique_ptr<Catalogue> GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| @@ -6,15 +6,15 @@ | ||||
| //  Copyright © 2016 Thomas Harte. All rights reserved.
 | ||||
| //
 | ||||
| 
 | ||||
| #ifndef File_hpp | ||||
| #define File_hpp | ||||
| #ifndef StaticAnalyser_Acorn_File_hpp | ||||
| #define StaticAnalyser_Acorn_File_hpp | ||||
| 
 | ||||
| #include <list> | ||||
| #include <memory> | ||||
| #include <string> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Acorn { | ||||
| 
 | ||||
| struct File { | ||||
| @@ -38,9 +38,10 @@ struct File { | ||||
| 		std::vector<uint8_t> data; | ||||
| 	}; | ||||
| 
 | ||||
| 	std::list<Chunk> chunks; | ||||
| 	std::vector<Chunk> chunks; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| @@ -11,21 +11,21 @@ | ||||
| #include "Disk.hpp" | ||||
| #include "Tape.hpp" | ||||
| 
 | ||||
| using namespace StaticAnalyser::Acorn; | ||||
| using namespace Analyser::Static::Acorn; | ||||
| 
 | ||||
| static std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| 		AcornCartridgesFrom(const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) { | ||||
| 	std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> acorn_cartridges; | ||||
| static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| 		AcornCartridgesFrom(const std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) { | ||||
| 	std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> acorn_cartridges; | ||||
| 
 | ||||
| 	for(std::shared_ptr<Storage::Cartridge::Cartridge> cartridge : cartridges) { | ||||
| 		const std::list<Storage::Cartridge::Cartridge::Segment> &segments = cartridge->get_segments(); | ||||
| 	for(const auto &cartridge : cartridges) { | ||||
| 		const auto &segments = cartridge->get_segments(); | ||||
| 
 | ||||
| 		// only one mapped item is allowed
 | ||||
| 		if(segments.size() != 1) continue; | ||||
| 
 | ||||
| 		// which must be 16 kb in size
 | ||||
| 		Storage::Cartridge::Cartridge::Segment segment = segments.front(); | ||||
| 		if(segment.data.size() != 0x4000) continue; | ||||
| 		// which must be 8 or 16 kb in size
 | ||||
| 		const Storage::Cartridge::Cartridge::Segment &segment = segments.front(); | ||||
| 		if(segment.data.size() != 0x4000 && segment.data.size() != 0x2000) continue; | ||||
| 
 | ||||
| 		// is a copyright string present?
 | ||||
| 		uint8_t copyright_offset = segment.data[7]; | ||||
| @@ -56,25 +56,21 @@ static std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| 	return acorn_cartridges; | ||||
| } | ||||
| 
 | ||||
| void StaticAnalyser::Acorn::AddTargets( | ||||
| 		const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks, | ||||
| 		const std::list<std::shared_ptr<Storage::Tape::Tape>> &tapes, | ||||
| 		const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges, | ||||
| 		std::list<StaticAnalyser::Target> &destination) { | ||||
| 	Target target; | ||||
| 	target.machine = Target::Electron; | ||||
| 	target.probability = 1.0; // TODO: a proper estimation
 | ||||
| 	target.acorn.has_dfs = false; | ||||
| 	target.acorn.has_adfs = false; | ||||
| 	target.acorn.should_shift_restart = false; | ||||
| void Analyser::Static::Acorn::AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination) { | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	target->machine = Machine::Electron; | ||||
| 	target->confidence = 1.0; // TODO: a proper estimation
 | ||||
| 	target->acorn.has_dfs = false; | ||||
| 	target->acorn.has_adfs = false; | ||||
| 	target->acorn.should_shift_restart = false; | ||||
| 
 | ||||
| 	// strip out inappropriate cartridges
 | ||||
| 	target.cartridges = AcornCartridgesFrom(cartridges); | ||||
| 	target->media.cartridges = AcornCartridgesFrom(media.cartridges); | ||||
| 
 | ||||
| 	// if there are any tapes, attempt to get data from the first
 | ||||
| 	if(tapes.size() > 0) { | ||||
| 		std::shared_ptr<Storage::Tape::Tape> tape = tapes.front(); | ||||
| 		std::list<File> files = GetFiles(tape); | ||||
| 	if(media.tapes.size() > 0) { | ||||
| 		std::shared_ptr<Storage::Tape::Tape> tape = media.tapes.front(); | ||||
| 		std::vector<File> files = GetFiles(tape); | ||||
| 		tape->reset(); | ||||
| 
 | ||||
| 		// continue if there are any files
 | ||||
| @@ -86,9 +82,9 @@ void StaticAnalyser::Acorn::AddTargets( | ||||
| 
 | ||||
| 			// check also for a continuous threading of BASIC lines; if none then this probably isn't BASIC code,
 | ||||
| 			// so that's also justification to *RUN
 | ||||
| 			size_t pointer = 0; | ||||
| 			std::size_t pointer = 0; | ||||
| 			uint8_t *data = &files.front().data[0]; | ||||
| 			size_t data_size = files.front().data.size(); | ||||
| 			std::size_t data_size = files.front().data.size(); | ||||
| 			while(1) { | ||||
| 				if(pointer >= data_size-1 || data[pointer] != 13) { | ||||
| 					is_basic = false; | ||||
| @@ -100,30 +96,30 @@ void StaticAnalyser::Acorn::AddTargets( | ||||
| 
 | ||||
| 			// Inspect first file. If it's protected or doesn't look like BASIC
 | ||||
| 			// then the loading command is *RUN. Otherwise it's CHAIN"".
 | ||||
| 			target.loadingCommand = is_basic ? "CHAIN\"\"\n" : "*RUN\n"; | ||||
| 			target->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n"; | ||||
| 
 | ||||
| 			target.tapes = tapes; | ||||
| 			target->media.tapes = media.tapes; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if(disks.size() > 0) { | ||||
| 		std::shared_ptr<Storage::Disk::Disk> disk = disks.front(); | ||||
| 	if(media.disks.size() > 0) { | ||||
| 		std::shared_ptr<Storage::Disk::Disk> disk = media.disks.front(); | ||||
| 		std::unique_ptr<Catalogue> dfs_catalogue, adfs_catalogue; | ||||
| 		dfs_catalogue = GetDFSCatalogue(disk); | ||||
| 		if(dfs_catalogue == nullptr) adfs_catalogue = GetADFSCatalogue(disk); | ||||
| 		if(dfs_catalogue || adfs_catalogue) { | ||||
| 			target.disks = disks; | ||||
| 			target.acorn.has_dfs = !!dfs_catalogue; | ||||
| 			target.acorn.has_adfs = !!adfs_catalogue; | ||||
| 			target->media.disks = media.disks; | ||||
| 			target->acorn.has_dfs = !!dfs_catalogue; | ||||
| 			target->acorn.has_adfs = !!adfs_catalogue; | ||||
| 
 | ||||
| 			Catalogue::BootOption bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption; | ||||
| 			if(bootOption != Catalogue::BootOption::None) | ||||
| 				target.acorn.should_shift_restart = true; | ||||
| 				target->acorn.should_shift_restart = true; | ||||
| 			else | ||||
| 				target.loadingCommand = "*CAT\n"; | ||||
| 				target->loading_command = "*CAT\n"; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if(target.tapes.size() || target.disks.size() || target.cartridges.size()) | ||||
| 		destination.push_back(target); | ||||
| 	if(target->media.tapes.size() || target->media.disks.size() || target->media.cartridges.size()) | ||||
| 		destination.push_back(std::move(target)); | ||||
| } | ||||
| @@ -11,16 +11,13 @@ | ||||
| 
 | ||||
| #include "../StaticAnalyser.hpp" | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Acorn { | ||||
| 
 | ||||
| void AddTargets( | ||||
| 	const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks, | ||||
| 	const std::list<std::shared_ptr<Storage::Tape::Tape>> &tapes, | ||||
| 	const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges, | ||||
| 	std::list<Target> &destination | ||||
| ); | ||||
| void AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| @@ -9,10 +9,11 @@ | ||||
| #include "Tape.hpp" | ||||
| 
 | ||||
| #include <deque> | ||||
| #include "../../NumberTheory/CRC.hpp" | ||||
| #include "../../Storage/Tape/Parsers/Acorn.hpp" | ||||
| 
 | ||||
| using namespace StaticAnalyser::Acorn; | ||||
| #include "../../../NumberTheory/CRC.hpp" | ||||
| #include "../../../Storage/Tape/Parsers/Acorn.hpp" | ||||
| 
 | ||||
| using namespace Analyser::Static::Acorn; | ||||
| 
 | ||||
| static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::Tape::Tape> &tape, Storage::Tape::Acorn::Parser &parser) { | ||||
| 	std::unique_ptr<File::Chunk> new_chunk(new File::Chunk); | ||||
| @@ -38,7 +39,7 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage:: | ||||
| 
 | ||||
| 	// read out name
 | ||||
| 	char name[11]; | ||||
| 	int name_ptr = 0; | ||||
| 	std::size_t name_ptr = 0; | ||||
| 	while(!tape->is_at_end() && name_ptr < sizeof(name)) { | ||||
| 		name[name_ptr] = (char)parser.get_next_byte(tape); | ||||
| 		if(!name[name_ptr]) break; | ||||
| @@ -50,14 +51,14 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage:: | ||||
| 	// addresses
 | ||||
| 	new_chunk->load_address = (uint32_t)parser.get_next_word(tape); | ||||
| 	new_chunk->execution_address = (uint32_t)parser.get_next_word(tape); | ||||
| 	new_chunk->block_number = (uint16_t)parser.get_next_short(tape); | ||||
| 	new_chunk->block_length = (uint16_t)parser.get_next_short(tape); | ||||
| 	new_chunk->block_flag = (uint8_t)parser.get_next_byte(tape); | ||||
| 	new_chunk->block_number = static_cast<uint16_t>(parser.get_next_short(tape)); | ||||
| 	new_chunk->block_length = static_cast<uint16_t>(parser.get_next_short(tape)); | ||||
| 	new_chunk->block_flag = static_cast<uint8_t>(parser.get_next_byte(tape)); | ||||
| 	new_chunk->next_address = (uint32_t)parser.get_next_word(tape); | ||||
| 
 | ||||
| 	uint16_t calculated_header_crc = parser.get_crc(); | ||||
| 	uint16_t stored_header_crc = (uint16_t)parser.get_next_short(tape); | ||||
| 	stored_header_crc = (uint16_t)((stored_header_crc >> 8) | (stored_header_crc << 8)); | ||||
| 	uint16_t stored_header_crc = static_cast<uint16_t>(parser.get_next_short(tape)); | ||||
| 	stored_header_crc = static_cast<uint16_t>((stored_header_crc >> 8) | (stored_header_crc << 8)); | ||||
| 	new_chunk->header_crc_matched = stored_header_crc == calculated_header_crc; | ||||
| 
 | ||||
| 	if(!new_chunk->header_crc_matched) return nullptr; | ||||
| @@ -65,13 +66,13 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage:: | ||||
| 	parser.reset_crc(); | ||||
| 	new_chunk->data.reserve(new_chunk->block_length); | ||||
| 	for(int c = 0; c < new_chunk->block_length; c++) { | ||||
| 		new_chunk->data.push_back((uint8_t)parser.get_next_byte(tape)); | ||||
| 		new_chunk->data.push_back(static_cast<uint8_t>(parser.get_next_byte(tape))); | ||||
| 	} | ||||
| 
 | ||||
| 	if(new_chunk->block_length && !(new_chunk->block_flag&0x40)) { | ||||
| 		uint16_t calculated_data_crc = parser.get_crc(); | ||||
| 		uint16_t stored_data_crc = (uint16_t)parser.get_next_short(tape); | ||||
| 		stored_data_crc = (uint16_t)((stored_data_crc >> 8) | (stored_data_crc << 8)); | ||||
| 		uint16_t stored_data_crc = static_cast<uint16_t>(parser.get_next_short(tape)); | ||||
| 		stored_data_crc = static_cast<uint16_t>((stored_data_crc >> 8) | (stored_data_crc << 8)); | ||||
| 		new_chunk->data_crc_matched = stored_data_crc == calculated_data_crc; | ||||
| 	} else { | ||||
| 		new_chunk->data_crc_matched = true; | ||||
| @@ -80,7 +81,7 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage:: | ||||
| 	return parser.get_error_flag() ? nullptr : std::move(new_chunk); | ||||
| } | ||||
| 
 | ||||
| std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &chunks) { | ||||
| static std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &chunks) { | ||||
| 	// find next chunk with a block number of 0
 | ||||
| 	while(chunks.size() && chunks.front().block_number) { | ||||
| 		chunks.pop_front(); | ||||
| @@ -118,7 +119,7 @@ std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &chunks) { | ||||
| 	return file; | ||||
| } | ||||
| 
 | ||||
| std::list<File> StaticAnalyser::Acorn::GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape) { | ||||
| std::vector<File> Analyser::Static::Acorn::GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape) { | ||||
| 	Storage::Tape::Acorn::Parser parser; | ||||
| 
 | ||||
| 	// populate chunk list
 | ||||
| @@ -131,7 +132,7 @@ std::list<File> StaticAnalyser::Acorn::GetFiles(const std::shared_ptr<Storage::T | ||||
| 	} | ||||
| 
 | ||||
| 	// decompose into file list
 | ||||
| 	std::list<File> file_list; | ||||
| 	std::vector<File> file_list; | ||||
| 
 | ||||
| 	while(chunk_list.size()) { | ||||
| 		std::unique_ptr<File> next_file = GetNextFile(chunk_list); | ||||
| @@ -12,13 +12,15 @@ | ||||
| #include <memory> | ||||
| 
 | ||||
| #include "File.hpp" | ||||
| #include "../../Storage/Tape/Tape.hpp" | ||||
| #include "../../../Storage/Tape/Tape.hpp" | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Acorn { | ||||
| 
 | ||||
| std::list<File> GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape); | ||||
| std::vector<File> GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										228
									
								
								Analyser/Static/AmstradCPC/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								Analyser/Static/AmstradCPC/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,228 @@ | ||||
| // | ||||
| //  AmstradCPC.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 30/07/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "StaticAnalyser.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cstring> | ||||
|  | ||||
| #include "../../../Storage/Disk/Parsers/CPM.hpp" | ||||
| #include "../../../Storage/Disk/Encodings/MFM/Parser.hpp" | ||||
|  | ||||
| static bool strcmp_insensitive(const char *a, const char *b) { | ||||
| 	if(std::strlen(a) != std::strlen(b)) return false; | ||||
| 	while(*a) { | ||||
| 		if(std::tolower(*a) != std::tolower(*b)) return false; | ||||
| 		a++; | ||||
| 		b++; | ||||
| 	} | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| static bool is_implied_extension(const std::string &extension) { | ||||
| 	return | ||||
| 		extension == "   " || | ||||
| 		strcmp_insensitive(extension.c_str(), "BAS") || | ||||
| 		strcmp_insensitive(extension.c_str(), "BIN"); | ||||
| } | ||||
|  | ||||
| static void right_trim(std::string &string) { | ||||
| 	string.erase(std::find_if(string.rbegin(), string.rend(), [](int ch) { | ||||
|         return !std::isspace(ch); | ||||
|     }).base(), string.end()); | ||||
| } | ||||
|  | ||||
| static std::string RunCommandFor(const Storage::Disk::CPM::File &file) { | ||||
| 	// Trim spaces from the name. | ||||
| 	std::string name = file.name; | ||||
| 	right_trim(name); | ||||
|  | ||||
| 	// Form the basic command. | ||||
| 	std::string command = "run\"" + name; | ||||
|  | ||||
| 	// Consider whether the extension is required. | ||||
| 	if(!is_implied_extension(file.type)) { | ||||
| 		std::string type = file.type; | ||||
| 		right_trim(type); | ||||
| 		command += "." + type; | ||||
| 	} | ||||
|  | ||||
| 	// Add a newline and return. | ||||
| 	return command + "\n"; | ||||
| } | ||||
|  | ||||
| static void InspectCatalogue( | ||||
| 	const Storage::Disk::CPM::Catalogue &catalogue, | ||||
| 	const std::unique_ptr<Analyser::Static::Target> &target) { | ||||
|  | ||||
| 	std::vector<const Storage::Disk::CPM::File *> candidate_files; | ||||
| 	candidate_files.reserve(catalogue.files.size()); | ||||
| 	for(auto &file : catalogue.files) { | ||||
| 		candidate_files.push_back(&file); | ||||
| 	} | ||||
|  | ||||
| 	// Remove all files with untypable characters. | ||||
| 	candidate_files.erase( | ||||
| 		std::remove_if(candidate_files.begin(), candidate_files.end(), [](const Storage::Disk::CPM::File *file) { | ||||
| 			for(auto c : file->name + file->type) { | ||||
| 				if(c < 32) return true; | ||||
| 			} | ||||
| 			return false; | ||||
| 		}), | ||||
| 		candidate_files.end()); | ||||
|  | ||||
| 	// If that leaves a mix of 'system' (i.e. hidden) and non-system files, remove the system files. | ||||
| 	bool are_all_system = true; | ||||
| 	for(auto file : candidate_files) { | ||||
| 		if(!file->system) { | ||||
| 			are_all_system = false; | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if(!are_all_system) { | ||||
| 		candidate_files.erase( | ||||
| 			std::remove_if(candidate_files.begin(), candidate_files.end(), [](const Storage::Disk::CPM::File *file) { | ||||
| 				return file->system; | ||||
| 			}), | ||||
| 			candidate_files.end()); | ||||
| 	} | ||||
|  | ||||
| 	// If there's just one file, run that. | ||||
| 	if(candidate_files.size() == 1) { | ||||
| 		target->loading_command = RunCommandFor(*candidate_files[0]); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	// If only one file is [potentially] BASIC, run that one; otherwise if only one has a suffix | ||||
| 	// that AMSDOS allows to be omitted, pick that one. | ||||
| 	int basic_files = 0; | ||||
| 	int implicit_suffixed_files = 0; | ||||
|  | ||||
| 	std::size_t last_basic_file = 0; | ||||
| 	std::size_t last_implicit_suffixed_file = 0; | ||||
|  | ||||
| 	for(std::size_t c = 0; c < candidate_files.size(); c++) { | ||||
| 		// Files with nothing but spaces in their name can't be loaded by the user, so disregard them. | ||||
| 		if(candidate_files[c]->type == "   " && candidate_files[c]->name == "        ") | ||||
| 			continue; | ||||
|  | ||||
| 		// Check for whether this is [potentially] BASIC. | ||||
| 		if(candidate_files[c]->data.size() >= 128 && !((candidate_files[c]->data[18] >> 1) & 7)) { | ||||
| 			basic_files++; | ||||
| 			last_basic_file = c; | ||||
| 		} | ||||
|  | ||||
| 		// Check suffix for emptiness. | ||||
| 		if(is_implied_extension(candidate_files[c]->type)) { | ||||
| 			implicit_suffixed_files++; | ||||
| 			last_implicit_suffixed_file = c; | ||||
| 		} | ||||
| 	} | ||||
| 	if(basic_files == 1 || implicit_suffixed_files == 1) { | ||||
| 		std::size_t selected_file = (basic_files == 1) ? last_basic_file : last_implicit_suffixed_file; | ||||
| 		target->loading_command = RunCommandFor(*candidate_files[selected_file]); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	// One more guess: if only one remaining candidate file has a different name than the others, | ||||
| 	// assume it is intended to stand out. | ||||
| 	std::map<std::string, int> name_counts; | ||||
| 	std::map<std::string, std::size_t> indices_by_name; | ||||
| 	std::size_t index = 0; | ||||
| 	for(auto file : candidate_files) { | ||||
| 		name_counts[file->name]++; | ||||
| 		indices_by_name[file->name] = index; | ||||
| 		index++; | ||||
| 	} | ||||
| 	if(name_counts.size() == 2) { | ||||
| 		for(auto &pair : name_counts) { | ||||
| 			if(pair.second == 1) { | ||||
| 				target->loading_command = RunCommandFor(*candidate_files[indices_by_name[pair.first]]); | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Desperation. | ||||
| 	target->loading_command = "cat\n"; | ||||
| } | ||||
|  | ||||
| static bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, const std::unique_ptr<Analyser::Static::Target> &target) { | ||||
| 	Storage::Encodings::MFM::Parser parser(true, disk); | ||||
| 	Storage::Encodings::MFM::Sector *boot_sector = parser.get_sector(0, 0, 0x41); | ||||
| 	if(boot_sector != nullptr && !boot_sector->samples.empty()) { | ||||
| 		// Check that the first 64 bytes of the sector aren't identical; if they are then probably | ||||
| 		// this disk was formatted and the filler byte never replaced. | ||||
| 		bool matched = true; | ||||
| 		for(std::size_t c = 1; c < 64; c++) { | ||||
| 			if(boot_sector->samples[0][c] != boot_sector->samples[0][0]) { | ||||
| 				matched = false; | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// This is a system disk, then launch it as though it were CP/M. | ||||
| 		if(!matched) { | ||||
| 			target->loading_command = "|cpm\n"; | ||||
| 			return true; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| void Analyser::Static::AmstradCPC::AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination) { | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	target->machine = Machine::AmstradCPC; | ||||
| 	target->confidence = 1.0; | ||||
| 	target->media.disks = media.disks; | ||||
| 	target->media.tapes = media.tapes; | ||||
| 	target->media.cartridges = media.cartridges; | ||||
|  | ||||
| 	target->amstradcpc.model = AmstradCPCModel::CPC6128; | ||||
|  | ||||
| 	if(!target->media.tapes.empty()) { | ||||
| 		// Ugliness flows here: assume the CPC isn't smart enough to pause between pressing | ||||
| 		// enter and responding to the follow-on prompt to press a key, so just type for | ||||
| 		// a while. Yuck! | ||||
| 		target->loading_command = "|tape\nrun\"\n1234567890"; | ||||
| 	} | ||||
|  | ||||
| 	if(!target->media.disks.empty()) { | ||||
| 		Storage::Disk::CPM::ParameterBlock data_format; | ||||
| 		data_format.sectors_per_track = 9; | ||||
| 		data_format.tracks = 40; | ||||
| 		data_format.block_size = 1024; | ||||
| 		data_format.first_sector = 0xc1; | ||||
| 		data_format.catalogue_allocation_bitmap = 0xc000; | ||||
| 		data_format.reserved_tracks = 0; | ||||
|  | ||||
| 		std::unique_ptr<Storage::Disk::CPM::Catalogue> data_catalogue = Storage::Disk::CPM::GetCatalogue(target->media.disks.front(), data_format); | ||||
| 		if(data_catalogue) { | ||||
| 			InspectCatalogue(*data_catalogue, target); | ||||
| 		} else { | ||||
| 			if(!CheckBootSector(target->media.disks.front(), target)) { | ||||
| 				Storage::Disk::CPM::ParameterBlock system_format; | ||||
| 				system_format.sectors_per_track = 9; | ||||
| 				system_format.tracks = 40; | ||||
| 				system_format.block_size = 1024; | ||||
| 				system_format.first_sector = 0x41; | ||||
| 				system_format.catalogue_allocation_bitmap = 0xc000; | ||||
| 				system_format.reserved_tracks = 2; | ||||
|  | ||||
| 				std::unique_ptr<Storage::Disk::CPM::Catalogue> system_catalogue = Storage::Disk::CPM::GetCatalogue(target->media.disks.front(), system_format); | ||||
| 				if(system_catalogue) { | ||||
| 					InspectCatalogue(*system_catalogue, target); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	destination.push_back(std::move(target)); | ||||
| } | ||||
							
								
								
									
										24
									
								
								Analyser/Static/AmstradCPC/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Analyser/Static/AmstradCPC/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // | ||||
| //  StaticAnalyser.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 30/07/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef StaticAnalyser_AmstradCPC_StaticAnalyser_hpp | ||||
| #define StaticAnalyser_AmstradCPC_StaticAnalyser_hpp | ||||
|  | ||||
| #include "../StaticAnalyser.hpp" | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace AmstradCPC { | ||||
|  | ||||
| void AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination); | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* StaticAnalyser_AmstradCPC_StaticAnalyser_hpp */ | ||||
| @@ -8,35 +8,35 @@ | ||||
| 
 | ||||
| #include "StaticAnalyser.hpp" | ||||
| 
 | ||||
| #include "../Disassembler/Disassembler6502.hpp" | ||||
| #include "../Disassembler/6502.hpp" | ||||
| 
 | ||||
| using namespace StaticAnalyser::Atari; | ||||
| using namespace Analyser::Static::Atari; | ||||
| 
 | ||||
| static void DeterminePagingFor2kCartridge(StaticAnalyser::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||
| static void DeterminePagingFor2kCartridge(Analyser::Static::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||
| 	// if this is a 2kb cartridge then it's definitely either unpaged or a CommaVid
 | ||||
| 	uint16_t entry_address, break_address; | ||||
| 
 | ||||
| 	entry_address = ((uint16_t)(segment.data[0x7fc] | (segment.data[0x7fd] << 8))) & 0x1fff; | ||||
| 	break_address = ((uint16_t)(segment.data[0x7fe] | (segment.data[0x7ff] << 8))) & 0x1fff; | ||||
| 	entry_address = (static_cast<uint16_t>(segment.data[0x7fc] | (segment.data[0x7fd] << 8))) & 0x1fff; | ||||
| 	break_address = (static_cast<uint16_t>(segment.data[0x7fe] | (segment.data[0x7ff] << 8))) & 0x1fff; | ||||
| 
 | ||||
| 	// a CommaVid start address needs to be outside of its RAM
 | ||||
| 	if(entry_address < 0x1800 || break_address < 0x1800) return; | ||||
| 
 | ||||
| 	std::function<size_t(uint16_t address)> high_location_mapper = [](uint16_t address) { | ||||
| 	std::function<std::size_t(uint16_t address)> high_location_mapper = [](uint16_t address) { | ||||
| 		address &= 0x1fff; | ||||
| 		return (size_t)(address - 0x1800); | ||||
| 		return static_cast<std::size_t>(address - 0x1800); | ||||
| 	}; | ||||
| 	StaticAnalyser::MOS6502::Disassembly high_location_disassembly = | ||||
| 		StaticAnalyser::MOS6502::Disassemble(segment.data, high_location_mapper, {entry_address, break_address}); | ||||
| 	Analyser::Static::MOS6502::Disassembly high_location_disassembly = | ||||
| 		Analyser::Static::MOS6502::Disassemble(segment.data, high_location_mapper, {entry_address, break_address}); | ||||
| 
 | ||||
| 	// assume that any kind of store that looks likely to be intended for large amounts of memory implies
 | ||||
| 	// large amounts of memory
 | ||||
| 	bool has_wide_area_store = false; | ||||
| 	for(std::map<uint16_t, StaticAnalyser::MOS6502::Instruction>::value_type &entry : high_location_disassembly.instructions_by_address) { | ||||
| 		if(entry.second.operation == StaticAnalyser::MOS6502::Instruction::STA) { | ||||
| 			has_wide_area_store |= entry.second.addressing_mode == StaticAnalyser::MOS6502::Instruction::Indirect; | ||||
| 			has_wide_area_store |= entry.second.addressing_mode == StaticAnalyser::MOS6502::Instruction::IndexedIndirectX; | ||||
| 			has_wide_area_store |= entry.second.addressing_mode == StaticAnalyser::MOS6502::Instruction::IndirectIndexedY; | ||||
| 	for(std::map<uint16_t, Analyser::Static::MOS6502::Instruction>::value_type &entry : high_location_disassembly.instructions_by_address) { | ||||
| 		if(entry.second.operation == Analyser::Static::MOS6502::Instruction::STA) { | ||||
| 			has_wide_area_store |= entry.second.addressing_mode == Analyser::Static::MOS6502::Instruction::Indirect; | ||||
| 			has_wide_area_store |= entry.second.addressing_mode == Analyser::Static::MOS6502::Instruction::IndexedIndirectX; | ||||
| 			has_wide_area_store |= entry.second.addressing_mode == Analyser::Static::MOS6502::Instruction::IndirectIndexedY; | ||||
| 
 | ||||
| 			if(has_wide_area_store) break; | ||||
| 		} | ||||
| @@ -46,10 +46,10 @@ static void DeterminePagingFor2kCartridge(StaticAnalyser::Target &target, const | ||||
| 	// caveat: false positives aren't likely to be problematic; a false positive is a 2KB ROM that always addresses
 | ||||
| 	// itself so as to land in ROM even if mapped as a CommaVid and this code is on the fence as to whether it
 | ||||
| 	// attempts to modify itself but it probably doesn't
 | ||||
| 	if(has_wide_area_store) target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::CommaVid; | ||||
| 	if(has_wide_area_store) target.atari.paging_model = Analyser::Static::Atari2600PagingModel::CommaVid; | ||||
| } | ||||
| 
 | ||||
| static void DeterminePagingFor8kCartridge(StaticAnalyser::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const StaticAnalyser::MOS6502::Disassembly &disassembly) { | ||||
| static void DeterminePagingFor8kCartridge(Analyser::Static::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	// Activision stack titles have their vectors at the top of the low 4k, not the top, and
 | ||||
| 	// always list 0xf000 as both vectors; they do not repeat them, and, inexplicably, they all
 | ||||
| 	// issue an SEI as their first instruction (maybe some sort of relic of the development environment?)
 | ||||
| @@ -58,12 +58,12 @@ static void DeterminePagingFor8kCartridge(StaticAnalyser::Target &target, const | ||||
| 		(segment.data[8191] != 0xf0 || segment.data[8189] != 0xf0 || segment.data[8190] != 0x00 || segment.data[8188] != 0x00) && | ||||
| 		segment.data[0] == 0x78 | ||||
| 	) { | ||||
| 		target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::ActivisionStack; | ||||
| 		target.atari.paging_model = Analyser::Static::Atari2600PagingModel::ActivisionStack; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	// make an assumption that this is the Atari paging model
 | ||||
| 	target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::Atari8k; | ||||
| 	target.atari.paging_model = Analyser::Static::Atari2600PagingModel::Atari8k; | ||||
| 
 | ||||
| 	std::set<uint16_t> internal_accesses; | ||||
| 	internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end()); | ||||
| @@ -83,13 +83,13 @@ static void DeterminePagingFor8kCartridge(StaticAnalyser::Target &target, const | ||||
| 		tigervision_access_count += masked_address == 0x3f; | ||||
| 	} | ||||
| 
 | ||||
| 	if(parker_access_count > atari_access_count) target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::ParkerBros; | ||||
| 	else if(tigervision_access_count > atari_access_count) target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::Tigervision; | ||||
| 	if(parker_access_count > atari_access_count) target.atari.paging_model = Analyser::Static::Atari2600PagingModel::ParkerBros; | ||||
| 	else if(tigervision_access_count > atari_access_count) target.atari.paging_model = Analyser::Static::Atari2600PagingModel::Tigervision; | ||||
| } | ||||
| 
 | ||||
| static void DeterminePagingFor16kCartridge(StaticAnalyser::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const StaticAnalyser::MOS6502::Disassembly &disassembly) { | ||||
| static void DeterminePagingFor16kCartridge(Analyser::Static::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	// make an assumption that this is the Atari paging model
 | ||||
| 	target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::Atari16k; | ||||
| 	target.atari.paging_model = Analyser::Static::Atari2600PagingModel::Atari16k; | ||||
| 
 | ||||
| 	std::set<uint16_t> internal_accesses; | ||||
| 	internal_accesses.insert(disassembly.internal_stores.begin(), disassembly.internal_stores.end()); | ||||
| @@ -104,17 +104,17 @@ static void DeterminePagingFor16kCartridge(StaticAnalyser::Target &target, const | ||||
| 		mnetwork_access_count += masked_address >= 0x1fe0 && masked_address < 0x1ffb; | ||||
| 	} | ||||
| 
 | ||||
| 	if(mnetwork_access_count > atari_access_count) target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::MNetwork; | ||||
| 	if(mnetwork_access_count > atari_access_count) target.atari.paging_model = Analyser::Static::Atari2600PagingModel::MNetwork; | ||||
| } | ||||
| 
 | ||||
| static void DeterminePagingFor64kCartridge(StaticAnalyser::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const StaticAnalyser::MOS6502::Disassembly &disassembly) { | ||||
| static void DeterminePagingFor64kCartridge(Analyser::Static::Target &target, const Storage::Cartridge::Cartridge::Segment &segment, const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	// make an assumption that this is a Tigervision if there is a write to 3F
 | ||||
| 	target.atari.paging_model = | ||||
| 		(disassembly.external_stores.find(0x3f) != disassembly.external_stores.end()) ? | ||||
| 			StaticAnalyser::Atari2600PagingModel::Tigervision : StaticAnalyser::Atari2600PagingModel::MegaBoy; | ||||
| 			Analyser::Static::Atari2600PagingModel::Tigervision : Analyser::Static::Atari2600PagingModel::MegaBoy; | ||||
| } | ||||
| 
 | ||||
| static void DeterminePagingForCartridge(StaticAnalyser::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||
| static void DeterminePagingForCartridge(Analyser::Static::Target &target, const Storage::Cartridge::Cartridge::Segment &segment) { | ||||
| 	if(segment.data.size() == 2048) { | ||||
| 		DeterminePagingFor2kCartridge(target, segment); | ||||
| 		return; | ||||
| @@ -122,32 +122,32 @@ static void DeterminePagingForCartridge(StaticAnalyser::Target &target, const St | ||||
| 
 | ||||
| 	uint16_t entry_address, break_address; | ||||
| 
 | ||||
| 	entry_address = (uint16_t)(segment.data[segment.data.size() - 4] | (segment.data[segment.data.size() - 3] << 8)); | ||||
| 	break_address = (uint16_t)(segment.data[segment.data.size() - 2] | (segment.data[segment.data.size() - 1] << 8)); | ||||
| 	entry_address = static_cast<uint16_t>(segment.data[segment.data.size() - 4] | (segment.data[segment.data.size() - 3] << 8)); | ||||
| 	break_address = static_cast<uint16_t>(segment.data[segment.data.size() - 2] | (segment.data[segment.data.size() - 1] << 8)); | ||||
| 
 | ||||
| 	std::function<size_t(uint16_t address)> address_mapper = [](uint16_t address) { | ||||
| 		if(!(address & 0x1000)) return (size_t)-1; | ||||
| 		return (size_t)(address & 0xfff); | ||||
| 	std::function<std::size_t(uint16_t address)> address_mapper = [](uint16_t address) { | ||||
| 		if(!(address & 0x1000)) return static_cast<std::size_t>(-1); | ||||
| 		return static_cast<std::size_t>(address & 0xfff); | ||||
| 	}; | ||||
| 
 | ||||
| 	std::vector<uint8_t> final_4k(segment.data.end() - 4096, segment.data.end()); | ||||
| 	StaticAnalyser::MOS6502::Disassembly disassembly = StaticAnalyser::MOS6502::Disassemble(final_4k, address_mapper, {entry_address, break_address}); | ||||
| 	Analyser::Static::MOS6502::Disassembly disassembly = Analyser::Static::MOS6502::Disassemble(final_4k, address_mapper, {entry_address, break_address}); | ||||
| 
 | ||||
| 	switch(segment.data.size()) { | ||||
| 		case 8192: | ||||
| 			DeterminePagingFor8kCartridge(target, segment, disassembly); | ||||
| 		break; | ||||
| 		case 10495: | ||||
| 			target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::Pitfall2; | ||||
| 			target.atari.paging_model = Analyser::Static::Atari2600PagingModel::Pitfall2; | ||||
| 		break; | ||||
| 		case 12288: | ||||
| 			target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::CBSRamPlus; | ||||
| 			target.atari.paging_model = Analyser::Static::Atari2600PagingModel::CBSRamPlus; | ||||
| 		break; | ||||
| 		case 16384: | ||||
| 			DeterminePagingFor16kCartridge(target, segment, disassembly); | ||||
| 		break; | ||||
| 		case 32768: | ||||
| 			target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::Atari32k; | ||||
| 			target.atari.paging_model = Analyser::Static::Atari2600PagingModel::Atari32k; | ||||
| 		break; | ||||
| 		case 65536: | ||||
| 			DeterminePagingFor64kCartridge(target, segment, disassembly); | ||||
| @@ -159,10 +159,10 @@ static void DeterminePagingForCartridge(StaticAnalyser::Target &target, const St | ||||
| 	// check for a Super Chip. Atari ROM images [almost] always have the same value stored over RAM
 | ||||
| 	// regions; when they don't they at least seem to have the first 128 bytes be the same as the
 | ||||
| 	// next 128 bytes. So check for that.
 | ||||
| 	if(	target.atari.paging_model != StaticAnalyser::Atari2600PagingModel::CBSRamPlus && | ||||
| 		target.atari.paging_model != StaticAnalyser::Atari2600PagingModel::MNetwork) { | ||||
| 	if(	target.atari.paging_model != Analyser::Static::Atari2600PagingModel::CBSRamPlus && | ||||
| 		target.atari.paging_model != Analyser::Static::Atari2600PagingModel::MNetwork) { | ||||
| 		bool has_superchip = true; | ||||
| 		for(size_t address = 0; address < 128; address++) { | ||||
| 		for(std::size_t address = 0; address < 128; address++) { | ||||
| 			if(segment.data[address] != segment.data[address+128]) { | ||||
| 				has_superchip = false; | ||||
| 				break; | ||||
| @@ -172,36 +172,30 @@ static void DeterminePagingForCartridge(StaticAnalyser::Target &target, const St | ||||
| 	} | ||||
| 
 | ||||
| 	// check for a Tigervision or Tigervision-esque scheme
 | ||||
| 	if(target.atari.paging_model == StaticAnalyser::Atari2600PagingModel::None && segment.data.size() > 4096) { | ||||
| 	if(target.atari.paging_model == Analyser::Static::Atari2600PagingModel::None && segment.data.size() > 4096) { | ||||
| 		bool looks_like_tigervision = disassembly.external_stores.find(0x3f) != disassembly.external_stores.end(); | ||||
| 		if(looks_like_tigervision) target.atari.paging_model = StaticAnalyser::Atari2600PagingModel::Tigervision; | ||||
| 		if(looks_like_tigervision) target.atari.paging_model = Analyser::Static::Atari2600PagingModel::Tigervision; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| void StaticAnalyser::Atari::AddTargets( | ||||
| 		const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks, | ||||
| 		const std::list<std::shared_ptr<Storage::Tape::Tape>> &tapes, | ||||
| 		const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges, | ||||
| 		std::list<StaticAnalyser::Target> &destination) { | ||||
| 	// TODO: sanity checking; is this image really for an Atari 2600?
 | ||||
| 	Target target; | ||||
| 	target.machine = Target::Atari2600; | ||||
| 	target.probability = 1.0; | ||||
| 	target.disks = disks; | ||||
| 	target.tapes = tapes; | ||||
| 	target.cartridges = cartridges; | ||||
| 	target.atari.paging_model = Atari2600PagingModel::None; | ||||
| 	target.atari.uses_superchip = false; | ||||
| void Analyser::Static::Atari::AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination) { | ||||
| 	// TODO: sanity checking; is this image really for an Atari 2600.
 | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	target->machine = Machine::Atari2600; | ||||
| 	target->confidence = 1.0; | ||||
| 	target->media.cartridges = media.cartridges; | ||||
| 	target->atari.paging_model = Atari2600PagingModel::None; | ||||
| 	target->atari.uses_superchip = false; | ||||
| 
 | ||||
| 	// try to figure out the paging scheme
 | ||||
| 	if(!cartridges.empty()) { | ||||
| 		const std::list<Storage::Cartridge::Cartridge::Segment> &segments = cartridges.front()->get_segments(); | ||||
| 	if(!media.cartridges.empty()) { | ||||
| 		const auto &segments = media.cartridges.front()->get_segments(); | ||||
| 
 | ||||
| 		if(segments.size() == 1) { | ||||
| 			const Storage::Cartridge::Cartridge::Segment &segment = segments.front(); | ||||
| 			DeterminePagingForCartridge(target, segment); | ||||
| 			DeterminePagingForCartridge(*target, segment); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	destination.push_back(target); | ||||
| 	destination.push_back(std::move(target)); | ||||
| } | ||||
| @@ -11,16 +11,13 @@ | ||||
| 
 | ||||
| #include "../StaticAnalyser.hpp" | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Atari { | ||||
| 
 | ||||
| void AddTargets( | ||||
| 	const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks, | ||||
| 	const std::list<std::shared_ptr<Storage::Tape::Tape>> &tapes, | ||||
| 	const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges, | ||||
| 	std::list<Target> &destination | ||||
| ); | ||||
| void AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										62
									
								
								Analyser/Static/Coleco/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								Analyser/Static/Coleco/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| // | ||||
| //  StaticAnalyser.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 23/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "StaticAnalyser.hpp" | ||||
|  | ||||
| static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| 		ColecoCartridgesFrom(const std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) { | ||||
| 	std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> coleco_cartridges; | ||||
|  | ||||
| 	for(const auto &cartridge : cartridges) { | ||||
| 		const auto &segments = cartridge->get_segments(); | ||||
|  | ||||
| 		// only one mapped item is allowed | ||||
| 		if(segments.size() != 1) continue; | ||||
|  | ||||
| 		// which must be 8, 12, 16, 24 or 32 kb in size | ||||
| 		const Storage::Cartridge::Cartridge::Segment &segment = segments.front(); | ||||
| 		const std::size_t data_size = segment.data.size(); | ||||
| 		const std::size_t overflow = data_size&8191; | ||||
| 		if(overflow > 8 && overflow != 512 && (data_size != 12*1024)) continue; | ||||
| 		if(data_size < 8192) continue; | ||||
|  | ||||
| 		// the two bytes that will be first must be 0xaa and 0x55, either way around | ||||
| 		auto *start = &segment.data[0]; | ||||
| 		if((data_size & static_cast<std::size_t>(~8191)) > 32768) { | ||||
| 			start = &segment.data[segment.data.size() - 16384]; | ||||
| 		} | ||||
| 		if(start[0] != 0xaa && start[0] != 0x55 && start[1] != 0xaa && start[1] != 0x55) continue; | ||||
| 		if(start[0] == start[1]) continue; | ||||
|  | ||||
| 		// probability of a random binary blob that isn't a Coleco ROM proceeding to here is 1 - 1/32768. | ||||
| 		if(!overflow) { | ||||
| 			coleco_cartridges.push_back(cartridge); | ||||
| 		} else { | ||||
| 			// Size down to a multiple of 8kb and apply the start address. | ||||
| 			std::vector<Storage::Cartridge::Cartridge::Segment> output_segments; | ||||
|  | ||||
| 			std::vector<uint8_t> truncated_data; | ||||
| 			std::vector<uint8_t>::difference_type truncated_size = static_cast<std::vector<uint8_t>::difference_type>(segment.data.size()) & ~8191; | ||||
| 			truncated_data.insert(truncated_data.begin(), segment.data.begin(), segment.data.begin() + truncated_size); | ||||
| 			output_segments.emplace_back(0x8000, truncated_data); | ||||
|  | ||||
| 			coleco_cartridges.emplace_back(new Storage::Cartridge::Cartridge(output_segments)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return coleco_cartridges; | ||||
| } | ||||
|  | ||||
| void Analyser::Static::Coleco::AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination) { | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	target->machine = Machine::ColecoVision; | ||||
| 	target->confidence = 0.5; | ||||
| 	target->media.cartridges = ColecoCartridgesFrom(media.cartridges); | ||||
| 	if(!target->media.empty()) | ||||
| 		destination.push_back(std::move(target)); | ||||
| } | ||||
							
								
								
									
										25
									
								
								Analyser/Static/Coleco/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Analyser/Static/Coleco/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // | ||||
| //  StaticAnalyser.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 23/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef StaticAnalyser_Coleco_StaticAnalyser_hpp | ||||
| #define StaticAnalyser_Coleco_StaticAnalyser_hpp | ||||
|  | ||||
| #include "../StaticAnalyser.hpp" | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Coleco { | ||||
|  | ||||
| void AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination); | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| #endif /* StaticAnalyser_hpp */ | ||||
| @@ -7,24 +7,24 @@ | ||||
| //
 | ||||
| 
 | ||||
| #include "Disk.hpp" | ||||
| #include "../../Storage/Disk/DiskController.hpp" | ||||
| #include "../../Storage/Disk/Encodings/CommodoreGCR.hpp" | ||||
| #include "../../Storage/Data/Commodore.hpp" | ||||
| #include "../../../Storage/Disk/Controller/DiskController.hpp" | ||||
| #include "../../../Storage/Disk/Encodings/CommodoreGCR.hpp" | ||||
| #include "../../../Storage/Data/Commodore.hpp" | ||||
| 
 | ||||
| #include <limits> | ||||
| #include <vector> | ||||
| #include <array> | ||||
| 
 | ||||
| using namespace StaticAnalyser::Commodore; | ||||
| using namespace Analyser::Static::Commodore; | ||||
| 
 | ||||
| class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 	public: | ||||
| 		std::shared_ptr<Storage::Disk::Drive> drive; | ||||
| 
 | ||||
| 		CommodoreGCRParser() : Storage::Disk::Controller(4000000, 1, 300), shift_register_(0), track_(1) { | ||||
| 			drive.reset(new Storage::Disk::Drive); | ||||
| 		CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) { | ||||
| 			drive.reset(new Storage::Disk::Drive(4000000, 300, 2)); | ||||
| 			set_drive(drive); | ||||
| 			set_motor_on(true); | ||||
| 			drive->set_motor_on(true); | ||||
| 		} | ||||
| 
 | ||||
| 		struct Sector { | ||||
| @@ -40,14 +40,14 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 			@returns a sector if one was found; @c nullptr otherwise. | ||||
| 		*/ | ||||
| 		std::shared_ptr<Sector> get_sector(uint8_t track, uint8_t sector) { | ||||
| 			int difference = (int)track - (int)track_; | ||||
| 			int difference = static_cast<int>(track) - static_cast<int>(track_); | ||||
| 			track_ = track; | ||||
| 
 | ||||
| 			if(difference) { | ||||
| 				int direction = difference < 0 ? -1 : 1; | ||||
| 				difference *= 2 * direction; | ||||
| 
 | ||||
| 				for(int c = 0; c < difference; c++) step(direction); | ||||
| 				for(int c = 0; c < difference; c++) get_drive().step(direction); | ||||
| 
 | ||||
| 				unsigned int zone = 3; | ||||
| 				if(track >= 18) zone = 2; | ||||
| @@ -66,8 +66,8 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 		uint8_t track_; | ||||
| 		std::shared_ptr<Sector> sector_cache_[65536]; | ||||
| 
 | ||||
| 		void process_input_bit(int value, unsigned int cycles_since_index_hole) { | ||||
| 			shift_register_ = ((shift_register_ << 1) | (unsigned int)value) & 0x3ff; | ||||
| 		void process_input_bit(int value) { | ||||
| 			shift_register_ = ((shift_register_ << 1) | static_cast<unsigned int>(value)) & 0x3ff; | ||||
| 			bit_count_++; | ||||
| 		} | ||||
| 
 | ||||
| @@ -78,13 +78,13 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 
 | ||||
| 			// find end of lead-in
 | ||||
| 			while(shift_register_ == 0x3ff && index_count_ < 2) { | ||||
| 				run_for_cycles(1); | ||||
| 				run_for(Cycles(1)); | ||||
| 			} | ||||
| 
 | ||||
| 			// continue for a further nine bits
 | ||||
| 			bit_count_ = 0; | ||||
| 			while(bit_count_ < 9 && index_count_ < 2) { | ||||
| 				run_for_cycles(1); | ||||
| 				run_for(Cycles(1)); | ||||
| 			} | ||||
| 
 | ||||
| 			return Storage::Encodings::CommodoreGCR::decoding_from_dectet(shift_register_); | ||||
| @@ -92,14 +92,14 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 
 | ||||
| 		unsigned int get_next_byte() { | ||||
| 			bit_count_ = 0; | ||||
| 			while(bit_count_ < 10) run_for_cycles(1); | ||||
| 			while(bit_count_ < 10) run_for(Cycles(1)); | ||||
| 			return Storage::Encodings::CommodoreGCR::decoding_from_dectet(shift_register_); | ||||
| 		} | ||||
| 
 | ||||
| 		void proceed_to_shift_value(unsigned int shift_value) { | ||||
| 			index_count_ = 0; | ||||
| 			while(shift_register_ != shift_value && index_count_ < 2) { | ||||
| 				run_for_cycles(1); | ||||
| 				run_for(Cycles(1)); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| @@ -108,7 +108,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 		} | ||||
| 
 | ||||
| 		std::shared_ptr<Sector> get_sector(uint8_t sector) { | ||||
| 			uint16_t sector_address = (uint16_t)((track_ << 8) | sector); | ||||
| 			uint16_t sector_address = static_cast<uint16_t>((track_ << 8) | sector); | ||||
| 			if(sector_cache_[sector_address]) return sector_cache_[sector_address]; | ||||
| 
 | ||||
| 			std::shared_ptr<Sector> first_sector = get_next_sector(); | ||||
| @@ -134,12 +134,12 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 				} | ||||
| 
 | ||||
| 				// get sector details, skip if this looks malformed
 | ||||
| 				uint8_t checksum = (uint8_t)get_next_byte(); | ||||
| 				sector->sector = (uint8_t)get_next_byte(); | ||||
| 				sector->track = (uint8_t)get_next_byte(); | ||||
| 				uint8_t checksum = static_cast<uint8_t>(get_next_byte()); | ||||
| 				sector->sector = static_cast<uint8_t>(get_next_byte()); | ||||
| 				sector->track = static_cast<uint8_t>(get_next_byte()); | ||||
| 				uint8_t disk_id[2]; | ||||
| 				disk_id[0] = (uint8_t)get_next_byte(); | ||||
| 				disk_id[1] = (uint8_t)get_next_byte(); | ||||
| 				disk_id[0] = static_cast<uint8_t>(get_next_byte()); | ||||
| 				disk_id[1] = static_cast<uint8_t>(get_next_byte()); | ||||
| 				if(checksum != (sector->sector ^ sector->track ^ disk_id[0] ^ disk_id[1])) continue; | ||||
| 
 | ||||
| 				// look for the following data
 | ||||
| @@ -149,13 +149,13 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 				} | ||||
| 
 | ||||
| 				checksum = 0; | ||||
| 				for(size_t c = 0; c < 256; c++) { | ||||
| 					sector->data[c] = (uint8_t)get_next_byte(); | ||||
| 				for(std::size_t c = 0; c < 256; c++) { | ||||
| 					sector->data[c] = static_cast<uint8_t>(get_next_byte()); | ||||
| 					checksum ^= sector->data[c]; | ||||
| 				} | ||||
| 
 | ||||
| 				if(checksum == get_next_byte()) { | ||||
| 					uint16_t sector_address = (uint16_t)((sector->track << 8) | sector->sector); | ||||
| 					uint16_t sector_address = static_cast<uint16_t>((sector->track << 8) | sector->sector); | ||||
| 					sector_cache_[sector_address] = sector; | ||||
| 					return sector; | ||||
| 				} | ||||
| @@ -165,8 +165,8 @@ class CommodoreGCRParser: public Storage::Disk::Controller { | ||||
| 		} | ||||
| }; | ||||
| 
 | ||||
| std::list<File> StaticAnalyser::Commodore::GetFiles(const std::shared_ptr<Storage::Disk::Disk> &disk) { | ||||
| 	std::list<File> files; | ||||
| std::vector<File> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<Storage::Disk::Disk> &disk) { | ||||
| 	std::vector<File> files; | ||||
| 	CommodoreGCRParser parser; | ||||
| 	parser.drive->set_disk(disk); | ||||
| 
 | ||||
| @@ -188,7 +188,7 @@ std::list<File> StaticAnalyser::Commodore::GetFiles(const std::shared_ptr<Storag | ||||
| 	} | ||||
| 
 | ||||
| 	// parse directory
 | ||||
| 	size_t header_pointer = (size_t)-32; | ||||
| 	std::size_t header_pointer = static_cast<std::size_t>(-32); | ||||
| 	while(header_pointer+32+31 < directory.size()) { | ||||
| 		header_pointer += 32; | ||||
| 
 | ||||
| @@ -207,12 +207,12 @@ std::list<File> StaticAnalyser::Commodore::GetFiles(const std::shared_ptr<Storag | ||||
| 		next_sector = directory[header_pointer + 4]; | ||||
| 
 | ||||
| 		new_file.raw_name.reserve(16); | ||||
| 		for(size_t c = 0; c < 16; c++) { | ||||
| 		for(std::size_t c = 0; c < 16; c++) { | ||||
| 			new_file.raw_name.push_back(directory[header_pointer + 5 + c]); | ||||
| 		} | ||||
| 		new_file.name = Storage::Data::Commodore::petscii_from_bytes(&new_file.raw_name[0], 16, false); | ||||
| 
 | ||||
| 		size_t number_of_sectors = (size_t)directory[header_pointer + 0x1e] + ((size_t)directory[header_pointer + 0x1f] << 8); | ||||
| 		std::size_t number_of_sectors = static_cast<std::size_t>(directory[header_pointer + 0x1e]) + (static_cast<std::size_t>(directory[header_pointer + 0x1f]) << 8); | ||||
| 		new_file.data.reserve((number_of_sectors - 1) * 254 + 252); | ||||
| 
 | ||||
| 		bool is_first_sector = true; | ||||
| @@ -223,7 +223,7 @@ std::list<File> StaticAnalyser::Commodore::GetFiles(const std::shared_ptr<Storag | ||||
| 			next_track = sector->data[0]; | ||||
| 			next_sector = sector->data[1]; | ||||
| 
 | ||||
| 			if(is_first_sector) new_file.starting_address = (uint16_t)sector->data[2] | (uint16_t)(sector->data[3] << 8); | ||||
| 			if(is_first_sector) new_file.starting_address = static_cast<uint16_t>(sector->data[2]) | static_cast<uint16_t>(sector->data[3] << 8); | ||||
| 			if(next_track) | ||||
| 				new_file.data.insert(new_file.data.end(), sector->data.begin() + (is_first_sector ? 4 : 2), sector->data.end()); | ||||
| 			else | ||||
| @@ -9,17 +9,19 @@ | ||||
| #ifndef StaticAnalyser_Commodore_Disk_hpp | ||||
| #define StaticAnalyser_Commodore_Disk_hpp | ||||
| 
 | ||||
| #include "../../Storage/Disk/Disk.hpp" | ||||
| #include "../../../Storage/Disk/Disk.hpp" | ||||
| #include "File.hpp" | ||||
| #include <list> | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Commodore { | ||||
| 
 | ||||
| std::list<File> GetFiles(const std::shared_ptr<Storage::Disk::Disk> &disk); | ||||
| std::vector<File> GetFiles(const std::shared_ptr<Storage::Disk::Disk> &disk); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| #endif /* Disk_hpp */ | ||||
| @@ -8,7 +8,7 @@ | ||||
| 
 | ||||
| #include "File.hpp" | ||||
| 
 | ||||
| bool StaticAnalyser::Commodore::File::is_basic() { | ||||
| bool Analyser::Static::Commodore::File::is_basic() { | ||||
| 	// BASIC files are always relocatable (?)
 | ||||
| 	if(type != File::RelocatableProgram) return false; | ||||
| 
 | ||||
| @@ -23,7 +23,7 @@ bool StaticAnalyser::Commodore::File::is_basic() { | ||||
| 	//		... null-terminated code ...
 | ||||
| 	//	(with a next line address of 0000 indicating end of program)ß
 | ||||
| 	while(1) { | ||||
| 		if(line_address - starting_address >= data.size() + 2) break; | ||||
| 		if(static_cast<size_t>(line_address - starting_address) >= data.size() + 2) break; | ||||
| 
 | ||||
| 		uint16_t next_line_address = data[line_address - starting_address]; | ||||
| 		next_line_address |= data[line_address - starting_address + 1] << 8; | ||||
| @@ -33,13 +33,13 @@ bool StaticAnalyser::Commodore::File::is_basic() { | ||||
| 		} | ||||
| 		if(next_line_address < line_address + 5) break; | ||||
| 
 | ||||
| 		if(line_address - starting_address >= data.size() + 5) break; | ||||
| 		if(static_cast<size_t>(line_address - starting_address) >= data.size() + 5) break; | ||||
| 		uint16_t next_line_number = data[line_address - starting_address + 2]; | ||||
| 		next_line_number |= data[line_address - starting_address + 3] << 8; | ||||
| 
 | ||||
| 		if(next_line_number <= line_number) break; | ||||
| 
 | ||||
| 		line_number = (uint16_t)next_line_number; | ||||
| 		line_number = static_cast<uint16_t>(next_line_number); | ||||
| 		line_address = next_line_address; | ||||
| 	} | ||||
| 
 | ||||
| @@ -12,18 +12,17 @@ | ||||
| #include <string> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Commodore { | ||||
| 
 | ||||
| struct File { | ||||
| 	File() : is_closed(false), is_locked(false) {} | ||||
| 
 | ||||
| 	std::wstring name; | ||||
| 	std::vector<uint8_t> raw_name; | ||||
| 	uint16_t starting_address; | ||||
| 	uint16_t ending_address; | ||||
| 	bool is_locked; | ||||
| 	bool is_closed; | ||||
| 	bool is_locked = false; | ||||
| 	bool is_closed = false; | ||||
| 	enum { | ||||
| 		RelocatableProgram, | ||||
| 		NonRelocatableProgram, | ||||
| @@ -36,6 +35,7 @@ struct File { | ||||
| 	bool is_basic(); | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| @@ -8,21 +8,21 @@ | ||||
| 
 | ||||
| #include "StaticAnalyser.hpp" | ||||
| 
 | ||||
| #include "Disk.hpp" | ||||
| #include "File.hpp" | ||||
| #include "Tape.hpp" | ||||
| #include "Disk.hpp" | ||||
| #include "../../Storage/Cartridge/Encodings/CommodoreROM.hpp" | ||||
| #include "../../../Storage/Cartridge/Encodings/CommodoreROM.hpp" | ||||
| 
 | ||||
| #include <sstream> | ||||
| 
 | ||||
| using namespace StaticAnalyser::Commodore; | ||||
| using namespace Analyser::Static::Commodore; | ||||
| 
 | ||||
| static std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| 		Vic20CartridgesFrom(const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) { | ||||
| 	std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> vic20_cartridges; | ||||
| static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| 		Vic20CartridgesFrom(const std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) { | ||||
| 	std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> vic20_cartridges; | ||||
| 
 | ||||
| 	for(std::shared_ptr<Storage::Cartridge::Cartridge> cartridge : cartridges) { | ||||
| 		const std::list<Storage::Cartridge::Cartridge::Segment> &segments = cartridge->get_segments(); | ||||
| 	for(const auto &cartridge : cartridges) { | ||||
| 		const auto &segments = cartridge->get_segments(); | ||||
| 
 | ||||
| 		// only one mapped item is allowed
 | ||||
| 		if(segments.size() != 1) continue; | ||||
| @@ -38,46 +38,42 @@ static std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| 	return vic20_cartridges; | ||||
| } | ||||
| 
 | ||||
| void StaticAnalyser::Commodore::AddTargets( | ||||
| 		const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks, | ||||
| 		const std::list<std::shared_ptr<Storage::Tape::Tape>> &tapes, | ||||
| 		const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges, | ||||
| 		std::list<StaticAnalyser::Target> &destination) { | ||||
| 	Target target; | ||||
| 	target.machine = Target::Vic20;	// TODO: machine estimation
 | ||||
| 	target.probability = 1.0; // TODO: a proper estimation
 | ||||
| void Analyser::Static::Commodore::AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination) { | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	target->machine = Machine::Vic20;	// TODO: machine estimation
 | ||||
| 	target->confidence = 1.0; // TODO: a proper estimation
 | ||||
| 
 | ||||
| 	int device = 0; | ||||
| 	std::list<File> files; | ||||
| 	std::vector<File> files; | ||||
| 	bool is_disk = false; | ||||
| 
 | ||||
| 	// strip out inappropriate cartridges
 | ||||
| 	target.cartridges = Vic20CartridgesFrom(cartridges); | ||||
| 	target->media.cartridges = Vic20CartridgesFrom(media.cartridges); | ||||
| 
 | ||||
| 	// check disks
 | ||||
| 	for(auto &disk : disks) { | ||||
| 		std::list<File> disk_files = GetFiles(disk); | ||||
| 		if(disk_files.size()) { | ||||
| 	for(auto &disk : media.disks) { | ||||
| 		std::vector<File> disk_files = GetFiles(disk); | ||||
| 		if(!disk_files.empty()) { | ||||
| 			is_disk = true; | ||||
| 			files.splice(files.end(), disk_files); | ||||
| 			target.disks = disks; | ||||
| 			files.insert(files.end(), disk_files.begin(), disk_files.end()); | ||||
| 			target->media.disks.push_back(disk); | ||||
| 			if(!device) device = 8; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// check tapes
 | ||||
| 	for(auto &tape : tapes) { | ||||
| 		std::list<File> tape_files = GetFiles(tape); | ||||
| 	for(auto &tape : media.tapes) { | ||||
| 		std::vector<File> tape_files = GetFiles(tape); | ||||
| 		tape->reset(); | ||||
| 		if(tape_files.size()) { | ||||
| 			files.splice(files.end(), tape_files); | ||||
| 			target.tapes = tapes; | ||||
| 		if(!tape_files.empty()) { | ||||
| 			files.insert(files.end(), tape_files.begin(), tape_files.end()); | ||||
| 			target->media.tapes.push_back(tape); | ||||
| 			if(!device) device = 1; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if(files.size()) { | ||||
| 		target.vic20.memory_model = Vic20MemoryModel::Unexpanded; | ||||
| 	if(!files.empty()) { | ||||
| 		target->vic20.memory_model = Vic20MemoryModel::Unexpanded; | ||||
| 		std::ostringstream string_stream; | ||||
| 		string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ","; | ||||
|   		if(files.front().is_basic()) { | ||||
| @@ -86,23 +82,23 @@ void StaticAnalyser::Commodore::AddTargets( | ||||
| 			string_stream << "1"; | ||||
| 		} | ||||
| 		string_stream << "\nRUN\n"; | ||||
| 		target.loadingCommand = string_stream.str(); | ||||
| 		target->loading_command = string_stream.str(); | ||||
| 
 | ||||
| 		// make a first guess based on loading address
 | ||||
| 		switch(files.front().starting_address) { | ||||
| 			case 0x1001: | ||||
| 			default: break; | ||||
| 			case 0x1201: | ||||
| 				target.vic20.memory_model = Vic20MemoryModel::ThirtyTwoKB; | ||||
| 				target->vic20.memory_model = Vic20MemoryModel::ThirtyTwoKB; | ||||
| 			break; | ||||
| 			case 0x0401: | ||||
| 				target.vic20.memory_model = Vic20MemoryModel::EightKB; | ||||
| 				target->vic20.memory_model = Vic20MemoryModel::EightKB; | ||||
| 			break; | ||||
| 		} | ||||
| 
 | ||||
| 		// General approach: increase memory size conservatively such that the largest file found will fit.
 | ||||
| 		for(File &file : files) { | ||||
| 			size_t file_size = file.data.size(); | ||||
| 			std::size_t file_size = file.data.size(); | ||||
| //			bool is_basic = file.is_basic();
 | ||||
| 
 | ||||
| 			/*if(is_basic)
 | ||||
| @@ -112,9 +108,9 @@ void StaticAnalyser::Commodore::AddTargets( | ||||
| 				// An unexpanded machine has 3583 bytes free for BASIC;
 | ||||
| 				// a 3kb expanded machine has 6655 bytes free.
 | ||||
| 				if(file_size > 6655) | ||||
| 					target.vic20.memory_model = Vic20MemoryModel::ThirtyTwoKB; | ||||
| 				else if(target.vic20.memory_model == Vic20MemoryModel::Unexpanded && file_size > 3583) | ||||
| 					target.vic20.memory_model = Vic20MemoryModel::EightKB; | ||||
| 					target->vic20.memory_model = Vic20MemoryModel::ThirtyTwoKB; | ||||
| 				else if(target->vic20.memory_model == Vic20MemoryModel::Unexpanded && file_size > 3583) | ||||
| 					target->vic20.memory_model = Vic20MemoryModel::EightKB; | ||||
| 			} | ||||
| 			else | ||||
| 			{*/ | ||||
| @@ -133,13 +129,13 @@ void StaticAnalyser::Commodore::AddTargets( | ||||
| 				// If anything above the 8kb mark is touched, mark as a 32kb machine; otherwise if the
 | ||||
| 				// region 0x0400 to 0x1000 is touched and this is an unexpanded machine, mark as 3kb.
 | ||||
| 				if(starting_address + file_size > 0x2000) | ||||
| 					target.vic20.memory_model = Vic20MemoryModel::ThirtyTwoKB; | ||||
| 				else if(target.vic20.memory_model == Vic20MemoryModel::Unexpanded && !(starting_address >= 0x1000 || starting_address+file_size < 0x0400)) | ||||
| 					target.vic20.memory_model = Vic20MemoryModel::ThirtyTwoKB; | ||||
| 					target->vic20.memory_model = Vic20MemoryModel::ThirtyTwoKB; | ||||
| 				else if(target->vic20.memory_model == Vic20MemoryModel::Unexpanded && !(starting_address >= 0x1000 || starting_address+file_size < 0x0400)) | ||||
| 					target->vic20.memory_model = Vic20MemoryModel::ThirtyTwoKB; | ||||
| //			}
 | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if(target.tapes.size() || target.cartridges.size() || target.disks.size()) | ||||
| 		destination.push_back(target); | ||||
| 	if(!target->media.empty()) | ||||
| 		destination.push_back(std::move(target)); | ||||
| } | ||||
| @@ -11,16 +11,13 @@ | ||||
| 
 | ||||
| #include "../StaticAnalyser.hpp" | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Commodore { | ||||
| 
 | ||||
| void AddTargets( | ||||
| 	const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks, | ||||
| 	const std::list<std::shared_ptr<Storage::Tape::Tape>> &tapes, | ||||
| 	const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges, | ||||
| 	std::list<Target> &destination | ||||
| ); | ||||
| void AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| @@ -8,13 +8,13 @@ | ||||
| 
 | ||||
| #include "Tape.hpp" | ||||
| 
 | ||||
| #include "../../Storage/Tape/Parsers/Commodore.hpp" | ||||
| #include "../../../Storage/Tape/Parsers/Commodore.hpp" | ||||
| 
 | ||||
| using namespace StaticAnalyser::Commodore; | ||||
| using namespace Analyser::Static::Commodore; | ||||
| 
 | ||||
| std::list<File> StaticAnalyser::Commodore::GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape) { | ||||
| std::vector<File> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape) { | ||||
| 	Storage::Tape::Commodore::Parser parser; | ||||
| 	std::list<File> file_list; | ||||
| 	std::vector<File> file_list; | ||||
| 
 | ||||
| 	std::unique_ptr<Storage::Tape::Commodore::Header> header = parser.get_next_header(tape); | ||||
| 
 | ||||
| @@ -9,15 +9,16 @@ | ||||
| #ifndef StaticAnalyser_Commodore_Tape_hpp | ||||
| #define StaticAnalyser_Commodore_Tape_hpp | ||||
| 
 | ||||
| #include "../../Storage/Tape/Tape.hpp" | ||||
| #include "../../../Storage/Tape/Tape.hpp" | ||||
| #include "File.hpp" | ||||
| #include <list> | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Commodore { | ||||
| 
 | ||||
| std::list<File> GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape); | ||||
| std::vector<File> GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| @@ -6,24 +6,25 @@ | ||||
| //  Copyright © 2016 Thomas Harte. All rights reserved.
 | ||||
| //
 | ||||
| 
 | ||||
| #include "Disassembler6502.hpp" | ||||
| #include <map> | ||||
| #include "6502.hpp" | ||||
| 
 | ||||
| using namespace StaticAnalyser::MOS6502; | ||||
| #include "Kernel.hpp" | ||||
| 
 | ||||
| struct PartialDisassembly { | ||||
| 	Disassembly disassembly; | ||||
| 	std::vector<uint16_t> remaining_entry_points; | ||||
| }; | ||||
| using namespace Analyser::Static::MOS6502; | ||||
| namespace  { | ||||
| 
 | ||||
| static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector<uint8_t> &memory, const std::function<size_t(uint16_t)> &address_mapper, uint16_t entry_point) { | ||||
| using PartialDisassembly = Analyser::Static::Disassembly::PartialDisassembly<Disassembly, uint16_t>; | ||||
| 
 | ||||
| struct MOS6502Disassembler { | ||||
| 
 | ||||
| static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector<uint8_t> &memory, const std::function<std::size_t(uint16_t)> &address_mapper, uint16_t entry_point) { | ||||
| 	disassembly.disassembly.internal_calls.insert(entry_point); | ||||
| 	uint16_t address = entry_point; | ||||
| 	while(1) { | ||||
| 		size_t local_address = address_mapper(address); | ||||
| 	while(true) { | ||||
| 		std::size_t local_address = address_mapper(address); | ||||
| 		if(local_address >= memory.size()) return; | ||||
| 
 | ||||
| 		struct Instruction instruction; | ||||
| 		Instruction instruction; | ||||
| 		instruction.address = address; | ||||
| 		address++; | ||||
| 
 | ||||
| @@ -92,6 +93,10 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| #define IM_INSTRUCTION(base, op)	\ | ||||
| 	case base:	instruction.operation = op; break; | ||||
| 		switch(operation) { | ||||
| 			default: | ||||
| 				instruction.operation = Instruction::KIL; | ||||
| 			break; | ||||
| 
 | ||||
| 			IM_INSTRUCTION(0x00, Instruction::BRK) | ||||
| 			IM_INSTRUCTION(0x20, Instruction::JSR) | ||||
| 			IM_INSTRUCTION(0x40, Instruction::RTI) | ||||
| @@ -229,7 +234,7 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| 			case Instruction::ZeroPage: case Instruction::ZeroPageX: case Instruction::ZeroPageY: | ||||
| 			case Instruction::IndexedIndirectX: case Instruction::IndirectIndexedY: | ||||
| 			case Instruction::Relative: { | ||||
| 				size_t operand_address = address_mapper(address); | ||||
| 				std::size_t operand_address = address_mapper(address); | ||||
| 				if(operand_address >= memory.size()) return; | ||||
| 				address++; | ||||
| 
 | ||||
| @@ -240,12 +245,12 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| 			// two-byte operands
 | ||||
| 			case Instruction::Absolute: case Instruction::AbsoluteX: case Instruction::AbsoluteY: | ||||
| 			case Instruction::Indirect: { | ||||
| 				size_t low_operand_address = address_mapper(address); | ||||
| 				size_t high_operand_address = address_mapper(address + 1); | ||||
| 				std::size_t low_operand_address = address_mapper(address); | ||||
| 				std::size_t high_operand_address = address_mapper(address + 1); | ||||
| 				if(low_operand_address >= memory.size() || high_operand_address >= memory.size()) return; | ||||
| 				address += 2; | ||||
| 
 | ||||
| 				instruction.operand = memory[low_operand_address] | (uint16_t)(memory[high_operand_address] << 8); | ||||
| 				instruction.operand = memory[low_operand_address] | static_cast<uint16_t>(memory[high_operand_address] << 8); | ||||
| 			} | ||||
| 			break; | ||||
| 		} | ||||
| @@ -255,7 +260,7 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| 
 | ||||
| 		// TODO: something wider-ranging than this
 | ||||
| 		if(instruction.addressing_mode == Instruction::Absolute || instruction.addressing_mode == Instruction::ZeroPage) { | ||||
| 			size_t mapped_address = address_mapper(instruction.operand); | ||||
| 			std::size_t mapped_address = address_mapper(instruction.operand); | ||||
| 			bool is_external = mapped_address >= memory.size(); | ||||
| 
 | ||||
| 			switch(instruction.operation) { | ||||
| @@ -297,37 +302,19 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< | ||||
| 			return; | ||||
| 		} | ||||
| 		if(instruction.addressing_mode == Instruction::Relative) { | ||||
| 			uint16_t destination = (uint16_t)(address + (int8_t)instruction.operand); | ||||
| 			uint16_t destination = static_cast<uint16_t>(address + (int8_t)instruction.operand); | ||||
| 			disassembly.remaining_entry_points.push_back(destination); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| Disassembly StaticAnalyser::MOS6502::Disassemble(const std::vector<uint8_t> &memory, const std::function<size_t(uint16_t)> &address_mapper, std::vector<uint16_t> entry_points) { | ||||
| 	PartialDisassembly partialDisassembly; | ||||
| 	partialDisassembly.remaining_entry_points = entry_points; | ||||
| }; | ||||
| 
 | ||||
| 	while(!partialDisassembly.remaining_entry_points.empty()) { | ||||
| 		// pull the next entry point from the back of the vector
 | ||||
| 		uint16_t next_entry_point = partialDisassembly.remaining_entry_points.back(); | ||||
| 		partialDisassembly.remaining_entry_points.pop_back(); | ||||
| }	// end of anonymous namespace
 | ||||
| 
 | ||||
| 		// if that address has already bene visited, forget about it
 | ||||
| 		if(partialDisassembly.disassembly.instructions_by_address.find(next_entry_point) != partialDisassembly.disassembly.instructions_by_address.end()) continue; | ||||
| 
 | ||||
| 		// if it's outgoing, log it as such and forget about it; otherwise disassemble
 | ||||
| 		size_t mapped_entry_point = address_mapper(next_entry_point); | ||||
| 		if(mapped_entry_point >= memory.size()) | ||||
| 			partialDisassembly.disassembly.outward_calls.insert(next_entry_point); | ||||
| 		else | ||||
| 			AddToDisassembly(partialDisassembly, memory, address_mapper, next_entry_point); | ||||
| 	} | ||||
| 
 | ||||
| 	return std::move(partialDisassembly.disassembly); | ||||
| } | ||||
| 
 | ||||
| std::function<size_t(uint16_t)> StaticAnalyser::MOS6502::OffsetMapper(uint16_t start_address) { | ||||
| 	return [start_address](uint16_t argument) { | ||||
| 		return (size_t)(argument - start_address); | ||||
| 	}; | ||||
| Disassembly Analyser::Static::MOS6502::Disassemble( | ||||
| 	const std::vector<uint8_t> &memory, | ||||
| 	const std::function<std::size_t(uint16_t)> &address_mapper, | ||||
| 	std::vector<uint16_t> entry_points) { | ||||
| 	return Analyser::Static::Disassembly::Disassemble<Disassembly, uint16_t, MOS6502Disassembler>(memory, address_mapper, entry_points); | ||||
| } | ||||
							
								
								
									
										101
									
								
								Analyser/Static/Disassembler/6502.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								Analyser/Static/Disassembler/6502.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| // | ||||
| //  6502.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/11/2016. | ||||
| //  Copyright © 2016 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef StaticAnalyser_Disassembler_6502_hpp | ||||
| #define StaticAnalyser_Disassembler_6502_hpp | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <functional> | ||||
| #include <map> | ||||
| #include <memory> | ||||
| #include <set> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace MOS6502 { | ||||
|  | ||||
| /*! | ||||
| 	Describes a 6502 instruciton — its address, the operation it performs, its addressing mode | ||||
| 	and its operand, if any. | ||||
| */ | ||||
| struct Instruction { | ||||
| 	/*! The address this instruction starts at. This is a mapped address. */ | ||||
| 	uint16_t address = 0; | ||||
| 	/*! The operation this instruction performs. */ | ||||
| 	enum { | ||||
| 		BRK, JSR, RTI, RTS, JMP, | ||||
| 		CLC, SEC, CLD, SED, CLI, SEI, CLV, | ||||
| 		NOP, | ||||
|  | ||||
| 		SLO, RLA, SRE, RRA, ALR, ARR, | ||||
| 		SAX, LAX, DCP, ISC, | ||||
| 		ANC, XAA, AXS, | ||||
| 		AND, EOR, ORA, BIT, | ||||
| 		ADC, SBC, | ||||
| 		AHX, SHY, SHX, TAS, LAS, | ||||
|  | ||||
| 		LDA, STA, LDX, STX, LDY, STY, | ||||
|  | ||||
| 		BPL, BMI, BVC, BVS, BCC, BCS, BNE, BEQ, | ||||
|  | ||||
| 		CMP, CPX, CPY, | ||||
| 		INC, DEC, DEX, DEY, INX, INY, | ||||
| 		ASL, ROL, LSR, ROR, | ||||
| 		TAX, TXA, TAY, TYA, TSX, TXS, | ||||
| 		PLA, PHA, PLP, PHP, | ||||
|  | ||||
| 		KIL | ||||
| 	} operation = NOP; | ||||
| 	/*! The addressing mode used by the instruction. */ | ||||
| 	enum { | ||||
| 		Absolute, | ||||
| 		AbsoluteX, | ||||
| 		AbsoluteY, | ||||
| 		Immediate, | ||||
| 		Implied, | ||||
| 		ZeroPage, | ||||
| 		ZeroPageX, | ||||
| 		ZeroPageY, | ||||
| 		Indirect, | ||||
| 		IndexedIndirectX, | ||||
| 		IndirectIndexedY, | ||||
| 		Relative, | ||||
| 	} addressing_mode = Implied; | ||||
| 	/*! The instruction's operand, if any. */ | ||||
| 	uint16_t operand = 0; | ||||
| }; | ||||
|  | ||||
| /*! Represents the disassembled form of a program. */ | ||||
| struct Disassembly { | ||||
| 	/*! All instructions found, mapped by address. */ | ||||
| 	std::map<uint16_t, Instruction> instructions_by_address; | ||||
| 	/*! The set of all calls or jumps that land outside of the area covered by the data provided for disassembly. */ | ||||
| 	std::set<uint16_t> outward_calls; | ||||
| 	/*! The set of all calls or jumps that land inside of the area covered by the data provided for disassembly. */ | ||||
| 	std::set<uint16_t> internal_calls; | ||||
| 	/*! The sets of all stores, loads and modifies that occur to data outside of the area covered by the data provided for disassembly. */ | ||||
| 	std::set<uint16_t> external_stores, external_loads, external_modifies; | ||||
| 	/*! The sets of all stores, loads and modifies that occur to data inside of the area covered by the data provided for disassembly. */ | ||||
| 	std::set<uint16_t> internal_stores, internal_loads, internal_modifies; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Disassembles the data provided as @c memory, mapping it into the 6502's full address range via the @c address_mapper, | ||||
| 	starting disassembly from each of the @c entry_points. | ||||
| */ | ||||
| Disassembly Disassemble( | ||||
| 	const std::vector<uint8_t> &memory, | ||||
| 	const std::function<std::size_t(uint16_t)> &address_mapper, | ||||
| 	std::vector<uint16_t> entry_points); | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Disassembler6502_hpp */ | ||||
							
								
								
									
										9
									
								
								Analyser/Static/Disassembler/AddressMapper.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Analyser/Static/Disassembler/AddressMapper.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| // | ||||
| //  AddressMapper.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 30/12/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "AddressMapper.hpp" | ||||
							
								
								
									
										32
									
								
								Analyser/Static/Disassembler/AddressMapper.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								Analyser/Static/Disassembler/AddressMapper.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // | ||||
| //  AddressMapper.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 30/12/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef AddressMapper_hpp | ||||
| #define AddressMapper_hpp | ||||
|  | ||||
| #include <functional> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Disassembler { | ||||
|  | ||||
| /*! | ||||
| 	Provides an address mapper that relocates a chunk of memory so that it starts at | ||||
| 	address @c start_address. | ||||
| */ | ||||
| template <typename T> std::function<std::size_t(T)> OffsetMapper(T start_address) { | ||||
| 	return [start_address](T argument) { | ||||
| 		return static_cast<std::size_t>(argument - start_address); | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* AddressMapper_hpp */ | ||||
							
								
								
									
										52
									
								
								Analyser/Static/Disassembler/Kernel.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								Analyser/Static/Disassembler/Kernel.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| // | ||||
| //  Kernel.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 31/12/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Kernel_hpp | ||||
| #define Kernel_hpp | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Disassembly { | ||||
|  | ||||
| template <typename D, typename S> struct PartialDisassembly { | ||||
| 	D disassembly; | ||||
| 	std::vector<S> remaining_entry_points; | ||||
| }; | ||||
|  | ||||
| template <typename D, typename S, typename Disassembler> D Disassemble( | ||||
| 	const std::vector<uint8_t> &memory, | ||||
| 	const std::function<std::size_t(S)> &address_mapper, | ||||
| 	std::vector<S> entry_points) { | ||||
| 	PartialDisassembly<D, S> partial_disassembly; | ||||
| 	partial_disassembly.remaining_entry_points = entry_points; | ||||
|  | ||||
| 	while(!partial_disassembly.remaining_entry_points.empty()) { | ||||
| 		// pull the next entry point from the back of the vector | ||||
| 		S next_entry_point = partial_disassembly.remaining_entry_points.back(); | ||||
| 		partial_disassembly.remaining_entry_points.pop_back(); | ||||
|  | ||||
| 		// if that address has already been visited, forget about it | ||||
| 		if(	partial_disassembly.disassembly.instructions_by_address.find(next_entry_point) | ||||
| 			!= partial_disassembly.disassembly.instructions_by_address.end()) continue; | ||||
|  | ||||
| 		// if it's outgoing, log it as such and forget about it; otherwise disassemble | ||||
| 		std::size_t mapped_entry_point = address_mapper(next_entry_point); | ||||
| 		if(mapped_entry_point >= memory.size()) | ||||
| 			partial_disassembly.disassembly.outward_calls.insert(next_entry_point); | ||||
| 		else | ||||
| 			Disassembler::AddToDisassembly(partial_disassembly, memory, address_mapper, next_entry_point); | ||||
| 	} | ||||
|  | ||||
| 	return partial_disassembly.disassembly; | ||||
| } | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Kernel_hpp */ | ||||
							
								
								
									
										619
									
								
								Analyser/Static/Disassembler/Z80.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										619
									
								
								Analyser/Static/Disassembler/Z80.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,619 @@ | ||||
| // | ||||
| //  Z80.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 30/12/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Z80.hpp" | ||||
|  | ||||
| #include "Kernel.hpp" | ||||
|  | ||||
| using namespace Analyser::Static::Z80; | ||||
| namespace  { | ||||
|  | ||||
| using PartialDisassembly = Analyser::Static::Disassembly::PartialDisassembly<Disassembly, uint16_t>; | ||||
|  | ||||
| class Accessor { | ||||
| 	public: | ||||
| 		Accessor(const std::vector<uint8_t> &memory, const std::function<std::size_t(uint16_t)> &address_mapper, uint16_t address) : | ||||
| 			memory_(memory), address_mapper_(address_mapper), address_(address) {} | ||||
|  | ||||
| 		uint8_t byte() { | ||||
| 			std::size_t mapped_address = address_mapper_(address_); | ||||
| 			address_++; | ||||
| 			if(mapped_address >= memory_.size()) { | ||||
| 				overrun_ = true; | ||||
| 				return 0xff; | ||||
| 			} | ||||
| 			return memory_[mapped_address]; | ||||
| 		} | ||||
|  | ||||
| 		uint16_t word() { | ||||
| 			uint8_t low = byte(); | ||||
| 			uint8_t high = byte(); | ||||
| 			return static_cast<uint16_t>(low | (high << 8)); | ||||
| 		} | ||||
|  | ||||
| 		bool overrun() { | ||||
| 			return overrun_; | ||||
| 		} | ||||
|  | ||||
| 		bool at_end() { | ||||
| 			std::size_t mapped_address = address_mapper_(address_); | ||||
| 			return mapped_address >= memory_.size(); | ||||
| 		} | ||||
|  | ||||
| 		uint16_t address() { | ||||
| 			return address_; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		const std::vector<uint8_t> &memory_; | ||||
| 		const std::function<std::size_t(uint16_t)> &address_mapper_; | ||||
| 		uint16_t address_; | ||||
| 		bool overrun_ = false; | ||||
| }; | ||||
|  | ||||
| #define x(v) (v >> 6) | ||||
| #define y(v) ((v >> 3) & 7) | ||||
| #define q(v) ((v >> 3) & 1) | ||||
| #define p(v) ((v >> 4) & 3) | ||||
| #define z(v) (v & 7) | ||||
|  | ||||
| Instruction::Condition condition_table[] = { | ||||
| 	Instruction::Condition::NZ, 	Instruction::Condition::Z, | ||||
| 	Instruction::Condition::NC, 	Instruction::Condition::C, | ||||
| 	Instruction::Condition::PO, 	Instruction::Condition::PE, | ||||
| 	Instruction::Condition::P,		Instruction::Condition::M | ||||
| }; | ||||
|  | ||||
| Instruction::Location register_pair_table[] = { | ||||
| 	Instruction::Location::BC, | ||||
| 	Instruction::Location::DE, | ||||
| 	Instruction::Location::HL, | ||||
| 	Instruction::Location::SP | ||||
| }; | ||||
|  | ||||
| Instruction::Location register_pair_table2[] = { | ||||
| 	Instruction::Location::BC, | ||||
| 	Instruction::Location::DE, | ||||
| 	Instruction::Location::HL, | ||||
| 	Instruction::Location::AF | ||||
| }; | ||||
|  | ||||
| Instruction::Location RegisterTableEntry(int offset, Accessor &accessor, Instruction &instruction, bool needs_indirect_offset) { | ||||
| 	Instruction::Location register_table[] = { | ||||
| 		Instruction::Location::B,	Instruction::Location::C, | ||||
| 		Instruction::Location::D,	Instruction::Location::E, | ||||
| 		Instruction::Location::H,	Instruction::Location::L, | ||||
| 		Instruction::Location::HL_Indirect, | ||||
| 		Instruction::Location::A | ||||
| 	}; | ||||
|  | ||||
| 	Instruction::Location location = register_table[offset]; | ||||
| 	if(location == Instruction::Location::HL_Indirect && needs_indirect_offset) { | ||||
| 		instruction.offset = accessor.byte() - 128; | ||||
| 	} | ||||
|  | ||||
| 	return location; | ||||
| } | ||||
|  | ||||
| Instruction::Operation alu_table[] = { | ||||
| 	Instruction::Operation::ADD, | ||||
| 	Instruction::Operation::ADC, | ||||
| 	Instruction::Operation::SUB, | ||||
| 	Instruction::Operation::SBC, | ||||
| 	Instruction::Operation::AND, | ||||
| 	Instruction::Operation::XOR, | ||||
| 	Instruction::Operation::OR, | ||||
| 	Instruction::Operation::CP | ||||
| }; | ||||
|  | ||||
| Instruction::Operation rotation_table[] = { | ||||
| 	Instruction::Operation::RLC, | ||||
| 	Instruction::Operation::RRC, | ||||
| 	Instruction::Operation::RL, | ||||
| 	Instruction::Operation::RR, | ||||
| 	Instruction::Operation::SLA, | ||||
| 	Instruction::Operation::SRA, | ||||
| 	Instruction::Operation::SLL, | ||||
| 	Instruction::Operation::SRL | ||||
| }; | ||||
|  | ||||
| Instruction::Operation block_table[][4] = { | ||||
| 	{Instruction::Operation::LDI, Instruction::Operation::CPI, Instruction::Operation::INI, Instruction::Operation::OUTI}, | ||||
| 	{Instruction::Operation::LDD, Instruction::Operation::CPD, Instruction::Operation::IND, Instruction::Operation::OUTD}, | ||||
| 	{Instruction::Operation::LDIR, Instruction::Operation::CPIR, Instruction::Operation::INIR, Instruction::Operation::OTIR}, | ||||
| 	{Instruction::Operation::LDDR, Instruction::Operation::CPDR, Instruction::Operation::INDR, Instruction::Operation::OTDR}, | ||||
| }; | ||||
|  | ||||
| void DisassembleCBPage(Accessor &accessor, Instruction &instruction, bool needs_indirect_offset) { | ||||
| 	const uint8_t operation = accessor.byte(); | ||||
|  | ||||
| 	if(!x(operation)) { | ||||
| 		instruction.operation = rotation_table[y(operation)]; | ||||
| 		instruction.source = instruction.destination = RegisterTableEntry(z(operation), accessor, instruction, needs_indirect_offset); | ||||
| 	} else { | ||||
| 		instruction.destination = RegisterTableEntry(z(operation), accessor, instruction, needs_indirect_offset); | ||||
| 		instruction.source = Instruction::Location::Operand; | ||||
| 		instruction.operand = y(operation); | ||||
|  | ||||
| 		switch(x(operation)) { | ||||
| 			case 1:	instruction.operation = Instruction::Operation::BIT;	break; | ||||
| 			case 2:	instruction.operation = Instruction::Operation::RES;	break; | ||||
| 			case 3:	instruction.operation = Instruction::Operation::SET;	break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void DisassembleEDPage(Accessor &accessor, Instruction &instruction, bool needs_indirect_offset) { | ||||
| 	const uint8_t operation = accessor.byte(); | ||||
|  | ||||
| 	switch(x(operation)) { | ||||
| 		default: | ||||
| 			instruction.operation = Instruction::Operation::Invalid; | ||||
| 		break; | ||||
| 		case 2: | ||||
| 			if(z(operation) < 4 && y(operation) >= 4) { | ||||
| 				instruction.operation = block_table[y(operation)-4][z(operation)]; | ||||
| 			} else { | ||||
| 				instruction.operation = Instruction::Operation::Invalid; | ||||
| 			} | ||||
| 		break; | ||||
| 		case 3: | ||||
| 			switch(z(operation)) { | ||||
| 				case 0: | ||||
| 					instruction.operation = Instruction::Operation::IN; | ||||
| 					instruction.source = Instruction::Location::BC_Indirect; | ||||
| 					if(y(operation) == 6) { | ||||
| 						instruction.destination = Instruction::Location::None; | ||||
| 					} else { | ||||
| 						instruction.destination = RegisterTableEntry(y(operation), accessor, instruction, needs_indirect_offset); | ||||
| 					} | ||||
| 				break; | ||||
| 				case 1: | ||||
| 					instruction.operation = Instruction::Operation::OUT; | ||||
| 					instruction.destination = Instruction::Location::BC_Indirect; | ||||
| 					if(y(operation) == 6) { | ||||
| 						instruction.source = Instruction::Location::None; | ||||
| 					} else { | ||||
| 						instruction.source = RegisterTableEntry(y(operation), accessor, instruction, needs_indirect_offset); | ||||
| 					} | ||||
| 				break; | ||||
| 				case 2: | ||||
| 					instruction.operation = (y(operation)&1) ? Instruction::Operation::ADC : Instruction::Operation::SBC; | ||||
| 					instruction.destination = Instruction::Location::HL; | ||||
| 					instruction.source = register_pair_table[y(operation) >> 1]; | ||||
| 				break; | ||||
| 				case 3: | ||||
| 					instruction.operation = Instruction::Operation::LD; | ||||
| 					if(q(operation)) { | ||||
| 						instruction.destination = RegisterTableEntry(p(operation), accessor, instruction, needs_indirect_offset); | ||||
| 						instruction.source = Instruction::Location::Operand_Indirect; | ||||
| 					} else { | ||||
| 						instruction.destination = Instruction::Location::Operand_Indirect; | ||||
| 						instruction.source = RegisterTableEntry(p(operation), accessor, instruction, needs_indirect_offset); | ||||
| 					} | ||||
| 					instruction.operand = accessor.word(); | ||||
| 				break; | ||||
| 				case 4: | ||||
| 					instruction.operation = Instruction::Operation::NEG; | ||||
| 				break; | ||||
| 				case 5: | ||||
| 					instruction.operation = (y(operation) == 1) ? Instruction::Operation::RETI : Instruction::Operation::RETN; | ||||
| 				break; | ||||
| 				case 6: | ||||
| 					instruction.operation = Instruction::Operation::IM; | ||||
| 					instruction.source = Instruction::Location::Operand; | ||||
| 					switch(y(operation)&3) { | ||||
| 						case 0:	instruction.operand = 0;	break; | ||||
| 						case 1:	instruction.operand = 0;	break; | ||||
| 						case 2:	instruction.operand = 1;	break; | ||||
| 						case 3:	instruction.operand = 2;	break; | ||||
| 					} | ||||
| 				break; | ||||
| 				case 7: | ||||
| 					switch(y(operation)) { | ||||
| 						case 0: | ||||
| 							instruction.operation = Instruction::Operation::LD; | ||||
| 							instruction.destination = Instruction::Location::I; | ||||
| 							instruction.source = Instruction::Location::A; | ||||
| 						break; | ||||
| 						case 1: | ||||
| 							instruction.operation = Instruction::Operation::LD; | ||||
| 							instruction.destination = Instruction::Location::R; | ||||
| 							instruction.source = Instruction::Location::A; | ||||
| 						break; | ||||
| 						case 2: | ||||
| 							instruction.operation = Instruction::Operation::LD; | ||||
| 							instruction.destination = Instruction::Location::A; | ||||
| 							instruction.source = Instruction::Location::I; | ||||
| 						break; | ||||
| 						case 3: | ||||
| 							instruction.operation = Instruction::Operation::LD; | ||||
| 							instruction.destination = Instruction::Location::A; | ||||
| 							instruction.source = Instruction::Location::R; | ||||
| 						break; | ||||
| 						case 4:		instruction.operation = Instruction::Operation::RRD;	break; | ||||
| 						case 5:		instruction.operation = Instruction::Operation::RLD;	break; | ||||
| 						default:	instruction.operation = Instruction::Operation::NOP;	break; | ||||
| 					} | ||||
| 				break; | ||||
| 			} | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void DisassembleMainPage(Accessor &accessor, Instruction &instruction) { | ||||
| 	bool needs_indirect_offset = false; | ||||
| 	enum HLSubstitution { | ||||
| 		None, IX, IY | ||||
| 	} hl_substitution = None; | ||||
|  | ||||
| 	while(true) { | ||||
| 		uint8_t operation = accessor.byte(); | ||||
|  | ||||
| 		switch(x(operation)) { | ||||
| 			case 0: | ||||
| 				switch(z(operation)) { | ||||
| 					case 0: | ||||
| 						switch(y(operation)) { | ||||
| 							case 0: instruction.operation = Instruction::Operation::NOP;		break; | ||||
| 							case 1: instruction.operation = Instruction::Operation::EXAFAFd;	break; | ||||
| 							case 2: | ||||
| 								instruction.operation = Instruction::Operation::DJNZ; | ||||
| 								instruction.operand = accessor.byte() - 128; | ||||
| 							break; | ||||
| 							default: | ||||
| 								instruction.operation = Instruction::Operation::JR; | ||||
| 								instruction.operand = accessor.byte() - 128; | ||||
| 								if(y(operation) >= 4) instruction.condition = condition_table[y(operation) - 4]; | ||||
| 							break; | ||||
| 						} | ||||
| 					break; | ||||
| 					case 1: | ||||
| 						if(y(operation)&1) { | ||||
| 							instruction.operation = Instruction::Operation::ADD; | ||||
| 							instruction.destination = Instruction::Location::HL; | ||||
| 							instruction.source = register_pair_table[y(operation) >> 1]; | ||||
| 						} else { | ||||
| 							instruction.operation = Instruction::Operation::LD; | ||||
| 							instruction.destination = register_pair_table[y(operation) >> 1]; | ||||
| 							instruction.source = Instruction::Location::Operand; | ||||
| 							instruction.operand = accessor.word(); | ||||
| 						} | ||||
| 					break; | ||||
| 					case 2: | ||||
| 						switch(y(operation)) { | ||||
| 							case 0: | ||||
| 								instruction.operation = Instruction::Operation::LD; | ||||
| 								instruction.destination = Instruction::Location::BC_Indirect; | ||||
| 								instruction.source = Instruction::Location::A; | ||||
| 							break; | ||||
| 							case 1: | ||||
| 								instruction.operation = Instruction::Operation::LD; | ||||
| 								instruction.destination = Instruction::Location::A; | ||||
| 								instruction.source = Instruction::Location::BC_Indirect; | ||||
| 							break; | ||||
| 							case 2: | ||||
| 								instruction.operation = Instruction::Operation::LD; | ||||
| 								instruction.destination = Instruction::Location::DE_Indirect; | ||||
| 								instruction.source = Instruction::Location::A; | ||||
| 							break; | ||||
| 							case 3: | ||||
| 								instruction.operation = Instruction::Operation::LD; | ||||
| 								instruction.destination = Instruction::Location::A; | ||||
| 								instruction.source = Instruction::Location::DE_Indirect; | ||||
| 							break; | ||||
| 							case 4: | ||||
| 								instruction.operation = Instruction::Operation::LD; | ||||
| 								instruction.destination = Instruction::Location::Operand_Indirect; | ||||
| 								instruction.source = Instruction::Location::HL; | ||||
| 							break; | ||||
| 							case 5: | ||||
| 								instruction.operation = Instruction::Operation::LD; | ||||
| 								instruction.destination = Instruction::Location::HL; | ||||
| 								instruction.source = Instruction::Location::Operand_Indirect; | ||||
| 							break; | ||||
| 							case 6: | ||||
| 								instruction.operation = Instruction::Operation::LD; | ||||
| 								instruction.destination = Instruction::Location::Operand_Indirect; | ||||
| 								instruction.source = Instruction::Location::A; | ||||
| 							break; | ||||
| 							case 7: | ||||
| 								instruction.operation = Instruction::Operation::LD; | ||||
| 								instruction.destination = Instruction::Location::A; | ||||
| 								instruction.source = Instruction::Location::Operand_Indirect; | ||||
| 							break; | ||||
| 						} | ||||
|  | ||||
| 						if(y(operation) > 3) { | ||||
| 							instruction.operand = accessor.word(); | ||||
| 						} | ||||
| 					break; | ||||
| 					case 3: | ||||
| 						if(y(operation)&1) { | ||||
| 							instruction.operation = Instruction::Operation::DEC; | ||||
| 						} else { | ||||
| 							instruction.operation = Instruction::Operation::INC; | ||||
| 						} | ||||
| 						instruction.source = instruction.destination = register_pair_table[y(operation) >> 1]; | ||||
| 					break; | ||||
| 					case 4: | ||||
| 						instruction.operation = Instruction::Operation::INC; | ||||
| 						instruction.source = instruction.destination = RegisterTableEntry(y(operation), accessor, instruction, needs_indirect_offset); | ||||
| 					break; | ||||
| 					case 5: | ||||
| 						instruction.operation = Instruction::Operation::DEC; | ||||
| 						instruction.source = instruction.destination = RegisterTableEntry(y(operation), accessor, instruction, needs_indirect_offset); | ||||
| 					break; | ||||
| 					case 6: | ||||
| 						instruction.operation = Instruction::Operation::LD; | ||||
| 						instruction.destination = RegisterTableEntry(y(operation), accessor, instruction, needs_indirect_offset); | ||||
| 						instruction.source = Instruction::Location::Operand; | ||||
| 						instruction.operand = accessor.byte(); | ||||
| 					break; | ||||
| 					case 7: | ||||
| 						switch(y(operation)) { | ||||
| 							case 0:	instruction.operation = Instruction::Operation::RLCA;	break; | ||||
| 							case 1:	instruction.operation = Instruction::Operation::RRCA;	break; | ||||
| 							case 2:	instruction.operation = Instruction::Operation::RLA;	break; | ||||
| 							case 3:	instruction.operation = Instruction::Operation::RRA;	break; | ||||
| 							case 4:	instruction.operation = Instruction::Operation::DAA;	break; | ||||
| 							case 5:	instruction.operation = Instruction::Operation::CPL;	break; | ||||
| 							case 6:	instruction.operation = Instruction::Operation::SCF;	break; | ||||
| 							case 7:	instruction.operation = Instruction::Operation::CCF;	break; | ||||
| 						} | ||||
| 					break; | ||||
| 				} | ||||
| 			break; | ||||
| 			case 1: | ||||
| 				if(y(operation) == 6 && z(operation) == 6) { | ||||
| 					instruction.operation = Instruction::Operation::HALT; | ||||
| 				} else { | ||||
| 					instruction.operation = Instruction::Operation::LD; | ||||
| 					instruction.source = RegisterTableEntry(z(operation), accessor, instruction, needs_indirect_offset); | ||||
| 					instruction.destination = RegisterTableEntry(y(operation), accessor, instruction, needs_indirect_offset); | ||||
| 				} | ||||
| 			break; | ||||
| 			case 2: | ||||
| 				instruction.operation = alu_table[y(operation)]; | ||||
| 				instruction.source = RegisterTableEntry(z(operation), accessor, instruction, needs_indirect_offset); | ||||
| 				instruction.destination = Instruction::Location::A; | ||||
| 			break; | ||||
| 			case 3: | ||||
| 				switch(z(operation)) { | ||||
| 					case 0: | ||||
| 						instruction.operation = Instruction::Operation::RET; | ||||
| 						instruction.condition = condition_table[y(operation)]; | ||||
| 					break; | ||||
| 					case 1: | ||||
| 						switch(y(operation)) { | ||||
| 							default: | ||||
| 								instruction.operation = Instruction::Operation::POP; | ||||
| 								instruction.source = register_pair_table2[y(operation) >> 1]; | ||||
| 							break; | ||||
| 							case 1: | ||||
| 								instruction.operation = Instruction::Operation::RET; | ||||
| 							break; | ||||
| 							case 3: | ||||
| 								instruction.operation = Instruction::Operation::EXX; | ||||
| 							break; | ||||
| 							case 5: | ||||
| 								instruction.operation = Instruction::Operation::JP; | ||||
| 								instruction.source = Instruction::Location::HL; | ||||
| 							break; | ||||
| 							case 7: | ||||
| 								instruction.operation = Instruction::Operation::LD; | ||||
| 								instruction.destination = Instruction::Location::SP; | ||||
| 								instruction.source = Instruction::Location::HL; | ||||
| 							break; | ||||
| 						} | ||||
| 					break; | ||||
| 					case 2: | ||||
| 						instruction.operation = Instruction::Operation::JP; | ||||
| 						instruction.condition = condition_table[y(operation)]; | ||||
| 						instruction.operand = accessor.word(); | ||||
| 					break; | ||||
| 					case 3: | ||||
| 						switch(y(operation)) { | ||||
| 							case 0: | ||||
| 								instruction.operation = Instruction::Operation::JP; | ||||
| 								instruction.source = Instruction::Location::Operand; | ||||
| 								instruction.operand = accessor.word(); | ||||
| 							break; | ||||
| 							case 1: | ||||
| 								DisassembleCBPage(accessor, instruction, needs_indirect_offset); | ||||
| 							break; | ||||
| 							case 2: | ||||
| 								instruction.operation = Instruction::Operation::OUT; | ||||
| 								instruction.source = Instruction::Location::A; | ||||
| 								instruction.destination = Instruction::Location::Operand_Indirect; | ||||
| 								instruction.operand = accessor.byte(); | ||||
| 							break; | ||||
| 							case 3: | ||||
| 								instruction.operation = Instruction::Operation::IN; | ||||
| 								instruction.destination = Instruction::Location::A; | ||||
| 								instruction.source = Instruction::Location::Operand_Indirect; | ||||
| 								instruction.operand = accessor.byte(); | ||||
| 							break; | ||||
| 							case 4: | ||||
| 								instruction.operation = Instruction::Operation::EX; | ||||
| 								instruction.destination = Instruction::Location::SP_Indirect; | ||||
| 								instruction.source = Instruction::Location::HL; | ||||
| 							break; | ||||
| 							case 5: | ||||
| 								instruction.operation = Instruction::Operation::EX; | ||||
| 								instruction.destination = Instruction::Location::DE; | ||||
| 								instruction.source = Instruction::Location::HL; | ||||
| 							break; | ||||
| 							case 6: | ||||
| 								instruction.operation = Instruction::Operation::DI; | ||||
| 							break; | ||||
| 							case 7: | ||||
| 								instruction.operation = Instruction::Operation::EI; | ||||
| 							break; | ||||
| 						} | ||||
| 					break; | ||||
| 					case 4: | ||||
| 						instruction.operation = Instruction::Operation::CALL; | ||||
| 						instruction.source = Instruction::Location::Operand_Indirect; | ||||
| 						instruction.operand = accessor.word(); | ||||
| 						instruction.condition = condition_table[y(operation)]; | ||||
| 					break; | ||||
| 					case 5: | ||||
| 						switch(y(operation)) { | ||||
| 							default: | ||||
| 								instruction.operation = Instruction::Operation::PUSH; | ||||
| 								instruction.source = register_pair_table2[y(operation) >> 1]; | ||||
| 							break; | ||||
| 							case 1: | ||||
| 								instruction.operation = Instruction::Operation::CALL; | ||||
| 								instruction.source = Instruction::Location::Operand; | ||||
| 								instruction.operand = accessor.word(); | ||||
| 							break; | ||||
| 							case 3: | ||||
| 								needs_indirect_offset = true; | ||||
| 								hl_substitution = IX; | ||||
| 							continue;	// i.e. repeat loop. | ||||
| 							case 5: | ||||
| 								DisassembleEDPage(accessor, instruction, needs_indirect_offset); | ||||
| 							break; | ||||
| 							case 7: | ||||
| 								needs_indirect_offset = true; | ||||
| 								hl_substitution = IY; | ||||
| 							continue;	// i.e. repeat loop. | ||||
| 						} | ||||
| 					break; | ||||
| 					case 6: | ||||
| 						instruction.operation = alu_table[y(operation)]; | ||||
| 						instruction.source = Instruction::Location::Operand; | ||||
| 						instruction.destination = Instruction::Location::A; | ||||
| 						instruction.operand = accessor.byte(); | ||||
| 					break; | ||||
| 					case 7: | ||||
| 						instruction.operation = Instruction::Operation::RST; | ||||
| 						instruction.source = Instruction::Location::Operand; | ||||
| 						instruction.operand = y(operation) << 3; | ||||
| 					break; | ||||
| 				} | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		// This while(true) isn't an infinite loop for everything except those paths that opt in | ||||
| 		// via continue. | ||||
| 		break; | ||||
| 	} | ||||
|  | ||||
| 	// Perform IX/IY substitution for HL, if applicable. | ||||
| 	if(hl_substitution != None) { | ||||
| 		// EX DE, HL is not affected. | ||||
| 		if(instruction.operation == Instruction::Operation::EX) return; | ||||
|  | ||||
| 		// If an (HL) is involved, switch it for IX+d or IY+d. | ||||
| 		if(	instruction.source == Instruction::Location::HL_Indirect || | ||||
| 			instruction.destination == Instruction::Location::HL_Indirect) { | ||||
|  | ||||
| 			if(instruction.source == Instruction::Location::HL_Indirect) { | ||||
| 				instruction.source = (hl_substitution == IX) ? Instruction::Location::IX_Indirect_Offset : Instruction::Location::IY_Indirect_Offset; | ||||
| 			} | ||||
| 			if(instruction.destination == Instruction::Location::HL_Indirect) { | ||||
| 				instruction.destination = (hl_substitution == IX) ? Instruction::Location::IX_Indirect_Offset : Instruction::Location::IY_Indirect_Offset; | ||||
| 			} | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Otherwise, switch either of H or L for I[X/Y]h and I[X/Y]l. | ||||
| 		if(instruction.source == Instruction::Location::H) { | ||||
| 			instruction.source = (hl_substitution == IX) ? Instruction::Location::IXh : Instruction::Location::IYh; | ||||
| 		} | ||||
| 		if(instruction.source == Instruction::Location::L) { | ||||
| 			instruction.source = (hl_substitution == IX) ? Instruction::Location::IXl : Instruction::Location::IYl; | ||||
| 		} | ||||
| 		if(instruction.destination == Instruction::Location::H) { | ||||
| 			instruction.destination = (hl_substitution == IX) ? Instruction::Location::IXh : Instruction::Location::IYh; | ||||
| 		} | ||||
| 		if(instruction.destination == Instruction::Location::L) { | ||||
| 			instruction.destination = (hl_substitution == IX) ? Instruction::Location::IXl : Instruction::Location::IYl; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| struct Z80Disassembler { | ||||
| 	static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector<uint8_t> &memory, const std::function<std::size_t(uint16_t)> &address_mapper, uint16_t entry_point) { | ||||
| 		disassembly.disassembly.internal_calls.insert(entry_point); | ||||
| 		Accessor accessor(memory, address_mapper, entry_point); | ||||
|  | ||||
| 		while(!accessor.at_end()) { | ||||
| 			Instruction instruction; | ||||
| 			instruction.address = accessor.address(); | ||||
|  | ||||
| 			DisassembleMainPage(accessor, instruction); | ||||
|  | ||||
| 			// If any memory access was invalid, end disassembly. | ||||
| 			if(accessor.overrun()) return; | ||||
|  | ||||
| 			// Store the instruction away. | ||||
| 			disassembly.disassembly.instructions_by_address[instruction.address] = instruction; | ||||
|  | ||||
| 			// Update access tables. | ||||
| 			int access_type = | ||||
| 				((instruction.source == Instruction::Location::Operand_Indirect) ? 1 : 0) | | ||||
| 				((instruction.destination == Instruction::Location::Operand_Indirect) ? 2 : 0); | ||||
| 			uint16_t address = static_cast<uint16_t>(instruction.operand); | ||||
| 			bool is_internal = address_mapper(address) < memory.size(); | ||||
| 			switch(access_type) { | ||||
| 				default: break; | ||||
| 				case 1: | ||||
| 					if(is_internal) { | ||||
| 						disassembly.disassembly.internal_loads.insert(address); | ||||
| 					} else { | ||||
| 						disassembly.disassembly.external_loads.insert(address); | ||||
| 					} | ||||
| 				break; | ||||
| 				case 2: | ||||
| 					if(is_internal) { | ||||
| 						disassembly.disassembly.internal_stores.insert(address); | ||||
| 					} else { | ||||
| 						disassembly.disassembly.external_stores.insert(address); | ||||
| 					} | ||||
| 				break; | ||||
| 				case 3: | ||||
| 					if(is_internal) { | ||||
| 						disassembly.disassembly.internal_modifies.insert(address); | ||||
| 					} else { | ||||
| 						disassembly.disassembly.internal_modifies.insert(address); | ||||
| 					} | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			// Add any (potentially) newly discovered entry point. | ||||
| 			if(	instruction.operation == Instruction::Operation::JP || | ||||
| 				instruction.operation == Instruction::Operation::JR || | ||||
| 				instruction.operation == Instruction::Operation::CALL || | ||||
| 				instruction.operation == Instruction::Operation::RST) { | ||||
| 				disassembly.remaining_entry_points.push_back(static_cast<uint16_t>(instruction.operand)); | ||||
| 			} | ||||
|  | ||||
| 			// This is it if: an unconditional RET, RETI, RETN, JP or JR is found. | ||||
| 			if(instruction.condition != Instruction::Condition::None)	continue; | ||||
|  | ||||
| 			if(instruction.operation == Instruction::Operation::RET)	return; | ||||
| 			if(instruction.operation == Instruction::Operation::RETI)	return; | ||||
| 			if(instruction.operation == Instruction::Operation::RETN)	return; | ||||
| 			if(instruction.operation == Instruction::Operation::JP)		return; | ||||
| 			if(instruction.operation == Instruction::Operation::JR)		return; | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| }	// end of anonymous namespace | ||||
|  | ||||
| Disassembly Analyser::Static::Z80::Disassemble( | ||||
| 	const std::vector<uint8_t> &memory, | ||||
| 	const std::function<std::size_t(uint16_t)> &address_mapper, | ||||
| 	std::vector<uint16_t> entry_points) { | ||||
| 	return Analyser::Static::Disassembly::Disassemble<Disassembly, uint16_t, Z80Disassembler>(memory, address_mapper, entry_points); | ||||
| } | ||||
							
								
								
									
										90
									
								
								Analyser/Static/Disassembler/Z80.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								Analyser/Static/Disassembler/Z80.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| // | ||||
| //  Z80.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 30/12/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef StaticAnalyser_Disassembler_Z80_hpp | ||||
| #define StaticAnalyser_Disassembler_Z80_hpp | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <functional> | ||||
| #include <map> | ||||
| #include <set> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Z80 { | ||||
|  | ||||
| struct Instruction { | ||||
| 	/*! The address this instruction starts at. This is a mapped address. */ | ||||
| 	uint16_t address = 0; | ||||
|  | ||||
| 	/*! The operation this instruction performs. */ | ||||
| 	enum class Operation { | ||||
| 		NOP, | ||||
| 		EXAFAFd, EXX, EX, | ||||
| 		LD, HALT, | ||||
| 		ADD, ADC, SUB, SBC, AND, XOR, OR, CP, | ||||
| 		INC, DEC, | ||||
| 		RLCA, RRCA, RLA, RRA, DAA, CPL, SCF, CCF, | ||||
| 		RLD, RRD, | ||||
| 		DJNZ, JR, JP, CALL, RST, RET, RETI, RETN, | ||||
| 		PUSH, POP, | ||||
| 		IN, OUT, | ||||
| 		EI, DI, | ||||
| 		RLC, RRC, RL, RR, SLA, SRA, SLL, SRL, | ||||
| 		BIT, RES, SET, | ||||
| 		LDI, CPI, INI, OUTI, | ||||
| 		LDD, CPD, IND, OUTD, | ||||
| 		LDIR, CPIR, INIR, OTIR, | ||||
| 		LDDR, CPDR, INDR, OTDR, | ||||
| 		NEG, | ||||
| 		IM, | ||||
| 		Invalid | ||||
| 	} operation = Operation::NOP; | ||||
|  | ||||
| 	/*! The condition required for this instruction to take effect. */ | ||||
| 	enum class Condition { | ||||
| 		None, NZ, Z, NC, C, PO, PE, P, M | ||||
| 	} condition = Condition::None; | ||||
|  | ||||
| 	enum class Location { | ||||
| 		B, C, D, E, H, L, HL_Indirect, A, I, R, | ||||
| 		BC, DE, HL, SP, AF, Operand, | ||||
| 		IX_Indirect_Offset, IY_Indirect_Offset, IXh, IXl, IYh, IYl, | ||||
| 		Operand_Indirect, | ||||
| 		BC_Indirect, DE_Indirect, SP_Indirect, | ||||
| 		None | ||||
| 	}; | ||||
| 	/*! The locations of source data for this instruction. */ | ||||
| 	Location source = Location::None; | ||||
| 	/*! The locations of destination data from this instruction. */ | ||||
| 	Location destination = Location::None; | ||||
| 	/*! The operand, if any; if this is used then it'll be referenced by either the source or destination location. */ | ||||
| 	int operand = 0; | ||||
| 	/*! The offset to apply, if any; applies to IX_Indirect_Offset and IY_Indirect_Offset locations. */ | ||||
| 	int offset = 0; | ||||
| }; | ||||
|  | ||||
| struct Disassembly { | ||||
| 	std::map<uint16_t, Instruction> instructions_by_address; | ||||
| 	std::set<uint16_t> outward_calls; | ||||
| 	std::set<uint16_t> internal_calls; | ||||
| 	std::set<uint16_t> external_stores, external_loads, external_modifies; | ||||
| 	std::set<uint16_t> internal_stores, internal_loads, internal_modifies; | ||||
| }; | ||||
|  | ||||
| Disassembly Disassemble( | ||||
| 	const std::vector<uint8_t> &memory, | ||||
| 	const std::function<std::size_t(uint16_t)> &address_mapper, | ||||
| 	std::vector<uint16_t> entry_points); | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* StaticAnalyser_Disassembler_Z80_hpp */ | ||||
							
								
								
									
										40
									
								
								Analyser/Static/MSX/Cartridge.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								Analyser/Static/MSX/Cartridge.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| // | ||||
| //  Cartridge.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 25/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Cartridge_hpp | ||||
| #define Cartridge_hpp | ||||
|  | ||||
| #include "../../../Storage/Cartridge/Cartridge.hpp" | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace MSX { | ||||
|  | ||||
| /*! | ||||
| 	Extends the base cartridge class by adding a (guess at) the banking scheme. | ||||
| */ | ||||
| struct Cartridge: public ::Storage::Cartridge::Cartridge { | ||||
| 	enum Type { | ||||
| 		None, | ||||
| 		Konami, | ||||
| 		KonamiWithSCC, | ||||
| 		ASCII8kb, | ||||
| 		ASCII16kb, | ||||
| 		FMPac | ||||
| 	}; | ||||
| 	const Type type; | ||||
|  | ||||
| 	Cartridge(const std::vector<Segment> &segments, Type type) : | ||||
| 		Storage::Cartridge::Cartridge(segments), type(type) {} | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* Cartridge_hpp */ | ||||
							
								
								
									
										292
									
								
								Analyser/Static/MSX/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								Analyser/Static/MSX/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,292 @@ | ||||
| // | ||||
| //  StaticAnalyser.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 25/11/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "StaticAnalyser.hpp" | ||||
|  | ||||
| #include "Cartridge.hpp" | ||||
| #include "Tape.hpp" | ||||
| #include "../Disassembler/Z80.hpp" | ||||
| #include "../Disassembler/AddressMapper.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| static std::unique_ptr<Analyser::Static::Target> CartridgeTarget( | ||||
| 	const Storage::Cartridge::Cartridge::Segment &segment, | ||||
| 	uint16_t start_address, | ||||
| 	Analyser::Static::MSX::Cartridge::Type type, | ||||
| 	float confidence) { | ||||
|  | ||||
| 	// Size down to a multiple of 8kb in size and apply the start address. | ||||
| 	std::vector<Storage::Cartridge::Cartridge::Segment> output_segments; | ||||
| 	if(segment.data.size() & 0x1fff) { | ||||
| 		std::vector<uint8_t> truncated_data; | ||||
| 		std::vector<uint8_t>::difference_type truncated_size = static_cast<std::vector<uint8_t>::difference_type>(segment.data.size()) & ~0x1fff; | ||||
| 		truncated_data.insert(truncated_data.begin(), segment.data.begin(), segment.data.begin() + truncated_size); | ||||
| 		output_segments.emplace_back(start_address, truncated_data); | ||||
| 	} else { | ||||
| 		output_segments.emplace_back(start_address, segment.data); | ||||
| 	} | ||||
|  | ||||
| 	std::unique_ptr<Analyser::Static::Target> target(new Analyser::Static::Target); | ||||
| 	target->machine = Analyser::Machine::MSX; | ||||
| 	target->confidence = confidence; | ||||
|  | ||||
| 	if(type == Analyser::Static::MSX::Cartridge::Type::None) { | ||||
| 		target->media.cartridges.emplace_back(new Storage::Cartridge::Cartridge(output_segments)); | ||||
| 	} else { | ||||
| 		target->media.cartridges.emplace_back(new Analyser::Static::MSX::Cartridge(output_segments, type)); | ||||
| 	} | ||||
|  | ||||
| 	return target; | ||||
| } | ||||
|  | ||||
| /* | ||||
| 	Expected standard cartridge format: | ||||
|  | ||||
| 		DEFB "AB" ; expansion ROM header | ||||
| 		DEFW initcode ; start of the init code, 0 if no initcode | ||||
| 		DEFW callstat; pointer to CALL statement handler, 0 if no such handler | ||||
| 		DEFW device; pointer to expansion device handler, 0 if no such handler | ||||
| 		DEFW basic ; pointer to the start of a tokenized basicprogram, 0 if no basicprogram | ||||
| 		DEFS 6,0 ; room reserved for future extensions | ||||
|  | ||||
| 	MSX cartridges often include banking hardware; those games were marketed as MegaROMs. The file | ||||
| 	format that the MSX community has decided upon doesn't retain the type of hardware included, so | ||||
| 	this analyser has to guess. | ||||
|  | ||||
| 	(additional audio hardware is also sometimes included, but it's implied by the banking hardware) | ||||
| */ | ||||
| static std::vector<std::unique_ptr<Analyser::Static::Target>> CartridgeTargetsFrom( | ||||
| 	const std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) { | ||||
| 	// No cartridges implies no targets. | ||||
| 	if(cartridges.empty()) { | ||||
| 		return {}; | ||||
| 	} | ||||
|  | ||||
| 	std::vector<std::unique_ptr<Analyser::Static::Target>> targets; | ||||
| 	for(const auto &cartridge : cartridges) { | ||||
| 		const auto &segments = cartridge->get_segments(); | ||||
|  | ||||
| 		// Only one mapped item is allowed. | ||||
| 		if(segments.size() != 1) continue; | ||||
|  | ||||
| 		// Which must be no more than 63 bytes larger than a multiple of 8 kb in size. | ||||
| 		Storage::Cartridge::Cartridge::Segment segment = segments.front(); | ||||
| 		const size_t data_size = segment.data.size(); | ||||
| 		if(data_size < 0x2000 || (data_size & 0x1fff) > 64) continue; | ||||
|  | ||||
| 		// Check for a ROM header at address 0; if it's not found then try 0x4000 | ||||
| 		// and adjust the start address; | ||||
| 		uint16_t start_address = 0; | ||||
| 		bool found_start = false; | ||||
| 		if(segment.data[0] == 0x41 && segment.data[1] == 0x42) { | ||||
| 			start_address = 0x4000; | ||||
| 			found_start = true; | ||||
| 		} else if(segment.data.size() >= 0x8000 && segment.data[0x4000] == 0x41 && segment.data[0x4001] == 0x42) { | ||||
| 			start_address = 0; | ||||
| 			found_start = true; | ||||
| 		} | ||||
|  | ||||
| 		// Reject cartridge if the ROM header wasn't found. | ||||
| 		if(!found_start) continue; | ||||
|  | ||||
| 		uint16_t init_address = static_cast<uint16_t>(segment.data[2] | (segment.data[3] << 8)); | ||||
| 		// TODO: check for a rational init address? | ||||
|  | ||||
| 		// If this ROM is less than 48kb in size then it's an ordinary ROM. Just emplace it and move on. | ||||
| 		if(data_size <= 0xc000) { | ||||
| 			targets.emplace_back(CartridgeTarget(segment, start_address, Analyser::Static::MSX::Cartridge::Type::None, 1.0)); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		// If this ROM is greater than 48kb in size then some sort of MegaROM scheme must | ||||
| 		// be at play; disassemble to try to figure it out. | ||||
| 		std::vector<uint8_t> first_8k; | ||||
| 		first_8k.insert(first_8k.begin(), segment.data.begin(), segment.data.begin() + 8192); | ||||
| 		Analyser::Static::Z80::Disassembly disassembly = | ||||
| 			Analyser::Static::Z80::Disassemble( | ||||
| 				first_8k, | ||||
| 				Analyser::Static::Disassembler::OffsetMapper(start_address), | ||||
| 				{ init_address } | ||||
| 			); | ||||
|  | ||||
| //		// Look for a indirect store followed by an unconditional JP or CALL into another | ||||
| //		// segment, that's a fairly explicit sign where found. | ||||
| 		using Instruction = Analyser::Static::Z80::Instruction; | ||||
| 		std::map<uint16_t, Instruction> &instructions = disassembly.instructions_by_address; | ||||
| 		bool is_ascii = false; | ||||
| //		auto iterator = instructions.begin(); | ||||
| //		while(iterator != instructions.end()) { | ||||
| //			auto next_iterator = iterator; | ||||
| //			next_iterator++; | ||||
| //			if(next_iterator == instructions.end()) break; | ||||
| // | ||||
| //			if(	iterator->second.operation == Instruction::Operation::LD && | ||||
| //				iterator->second.destination == Instruction::Location::Operand_Indirect && | ||||
| //				( | ||||
| //					iterator->second.operand == 0x5000 || | ||||
| //					iterator->second.operand == 0x6000 || | ||||
| //					iterator->second.operand == 0x6800 || | ||||
| //					iterator->second.operand == 0x7000 || | ||||
| //					iterator->second.operand == 0x77ff || | ||||
| //					iterator->second.operand == 0x7800 || | ||||
| //					iterator->second.operand == 0x8000 || | ||||
| //					iterator->second.operand == 0x9000 || | ||||
| //					iterator->second.operand == 0xa000 | ||||
| //				) && | ||||
| //				( | ||||
| //					next_iterator->second.operation == Instruction::Operation::CALL || | ||||
| //					next_iterator->second.operation == Instruction::Operation::JP | ||||
| //				) && | ||||
| //				((next_iterator->second.operand >> 13) != (0x4000 >> 13)) | ||||
| //			) { | ||||
| //				const uint16_t address = static_cast<uint16_t>(next_iterator->second.operand); | ||||
| //				switch(iterator->second.operand) { | ||||
| //					case 0x6000: | ||||
| //						if(address >= 0x6000 && address < 0x8000) { | ||||
| //							target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; | ||||
| //						} | ||||
| //					break; | ||||
| //					case 0x6800: | ||||
| //						if(address >= 0x6000 && address < 0x6800) { | ||||
| //							target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::ASCII8kb; | ||||
| //						} | ||||
| //					break; | ||||
| //					case 0x7000: | ||||
| //						if(address >= 0x6000 && address < 0x8000) { | ||||
| //							target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; | ||||
| //						} | ||||
| //						if(address >= 0x7000 && address < 0x7800) { | ||||
| //							is_ascii = true; | ||||
| //						} | ||||
| //					break; | ||||
| //					case 0x77ff: | ||||
| //						if(address >= 0x7000 && address < 0x7800) { | ||||
| //							target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::ASCII16kb; | ||||
| //						} | ||||
| //					break; | ||||
| //					case 0x7800: | ||||
| //						if(address >= 0xa000 && address < 0xc000) { | ||||
| //							target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::ASCII8kb; | ||||
| //						} | ||||
| //					break; | ||||
| //					case 0x8000: | ||||
| //						if(address >= 0x8000 && address < 0xa000) { | ||||
| //							target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; | ||||
| //						} | ||||
| //					break; | ||||
| //					case 0x9000: | ||||
| //						if(address >= 0x8000 && address < 0xa000) { | ||||
| //							target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; | ||||
| //						} | ||||
| //					break; | ||||
| //					case 0xa000: | ||||
| //						if(address >= 0xa000 && address < 0xc000) { | ||||
| //							target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::Konami; | ||||
| //						} | ||||
| //					break; | ||||
| //					case 0xb000: | ||||
| //						if(address >= 0xa000 && address < 0xc000) { | ||||
| //							target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; | ||||
| //						} | ||||
| //					break; | ||||
| //				} | ||||
| //			} | ||||
| // | ||||
| //			iterator = next_iterator; | ||||
|  | ||||
| 		// Look for LD (nnnn), A instructions, and collate those addresses. | ||||
| 		std::map<uint16_t, int> address_counts; | ||||
| 		for(const auto &instruction_pair : instructions) { | ||||
| 			if(	instruction_pair.second.operation == Instruction::Operation::LD && | ||||
| 				instruction_pair.second.destination == Instruction::Location::Operand_Indirect && | ||||
| 				instruction_pair.second.source == Instruction::Location::A) { | ||||
| 				address_counts[static_cast<uint16_t>(instruction_pair.second.operand)]++; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Weight confidences by number of observed hits. | ||||
| 		float total_hits = | ||||
| 			static_cast<float>( | ||||
| 				address_counts[0x6000] + address_counts[0x6800] + | ||||
| 				address_counts[0x7000] + address_counts[0x7800] + | ||||
| 				address_counts[0x77ff] + address_counts[0x8000] + | ||||
| 				address_counts[0xa000] + address_counts[0x5000] + | ||||
| 				address_counts[0x9000] + address_counts[0xb000] | ||||
| 			); | ||||
|  | ||||
| 		targets.push_back(CartridgeTarget( | ||||
| 			segment, | ||||
| 			start_address, | ||||
| 			Analyser::Static::MSX::Cartridge::ASCII8kb, | ||||
| 			static_cast<float>(	address_counts[0x6000] + | ||||
| 								address_counts[0x6800] + | ||||
| 								address_counts[0x7000] + | ||||
| 								address_counts[0x7800]) / total_hits)); | ||||
| 		targets.push_back(CartridgeTarget( | ||||
| 			segment, | ||||
| 			start_address, | ||||
| 			Analyser::Static::MSX::Cartridge::ASCII16kb, | ||||
| 			static_cast<float>(	address_counts[0x6000] + | ||||
| 								address_counts[0x7000] + | ||||
| 								address_counts[0x77ff]) / total_hits)); | ||||
| 		if(!is_ascii) { | ||||
| 			targets.push_back(CartridgeTarget( | ||||
| 				segment, | ||||
| 				start_address, | ||||
| 				Analyser::Static::MSX::Cartridge::Konami, | ||||
| 				static_cast<float>(	address_counts[0x6000] + | ||||
| 									address_counts[0x8000] + | ||||
| 									address_counts[0xa000]) / total_hits)); | ||||
| 		} | ||||
| 		if(!is_ascii) { | ||||
| 			targets.push_back(CartridgeTarget( | ||||
| 				segment, | ||||
| 				start_address, | ||||
| 				Analyser::Static::MSX::Cartridge::KonamiWithSCC, | ||||
| 				static_cast<float>(	address_counts[0x5000] + | ||||
| 									address_counts[0x7000] + | ||||
| 									address_counts[0x9000] + | ||||
| 									address_counts[0xb000]) / total_hits)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return targets; | ||||
| } | ||||
|  | ||||
| void Analyser::Static::MSX::AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination) { | ||||
| 	// Append targets for any cartridges that look correct. | ||||
| 	std::vector<std::unique_ptr<Target>> cartridge_targets = CartridgeTargetsFrom(media.cartridges); | ||||
| 	std::move(cartridge_targets.begin(), cartridge_targets.end(), std::back_inserter(destination)); | ||||
|  | ||||
| 	// Consider building a target for disks and/or tapes. | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
|  | ||||
| 	// Check tapes for loadable files. | ||||
| 	for(const auto &tape : media.tapes) { | ||||
| 		std::vector<File> files_on_tape = GetFiles(tape); | ||||
| 		if(!files_on_tape.empty()) { | ||||
| 			switch(files_on_tape.front().type) { | ||||
| 				case File::Type::ASCII:				target->loading_command = "RUN\"CAS:\r";		break; | ||||
| 				case File::Type::TokenisedBASIC:	target->loading_command = "CLOAD\rRUN\r";		break; | ||||
| 				case File::Type::Binary:			target->loading_command = "BLOAD\"CAS:\",R\r";	break; | ||||
| 				default: break; | ||||
| 			} | ||||
| 			target->media.tapes.push_back(tape); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Blindly accept disks for now. | ||||
| 	target->media.disks = media.disks; | ||||
|  | ||||
| 	if(!target->media.empty()) { | ||||
| 		target->machine = Machine::MSX; | ||||
| 		target->confidence = 1.0; | ||||
| 		destination.push_back(std::move(target)); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										24
									
								
								Analyser/Static/MSX/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Analyser/Static/MSX/StaticAnalyser.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // | ||||
| //  StaticAnalyser.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 25/11/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef StaticAnalyser_MSX_StaticAnalyser_hpp | ||||
| #define StaticAnalyser_MSX_StaticAnalyser_hpp | ||||
|  | ||||
| #include "../StaticAnalyser.hpp" | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace MSX { | ||||
|  | ||||
| void AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination); | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* StaticAnalyser_MSX_StaticAnalyser_hpp */ | ||||
							
								
								
									
										165
									
								
								Analyser/Static/MSX/Tape.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								Analyser/Static/MSX/Tape.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| // | ||||
| //  Tape.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 25/12/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Tape.hpp" | ||||
|  | ||||
| #include "../../../Storage/Tape/Parsers/MSX.hpp" | ||||
|  | ||||
| using namespace Analyser::Static::MSX; | ||||
|  | ||||
| File::File(File &&rhs) : | ||||
| 	name(std::move(rhs.name)), | ||||
| 	type(rhs.type), | ||||
| 	data(std::move(rhs.data)), | ||||
| 	starting_address(rhs.starting_address), | ||||
| 	entry_address(rhs.entry_address) {} | ||||
|  | ||||
| File::File() : | ||||
| 	type(Type::Binary), | ||||
| 	starting_address(0), | ||||
| 	entry_address(0) {}	// For the sake of initialising in a defined state. | ||||
|  | ||||
| std::vector<File> Analyser::Static::MSX::GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape) { | ||||
| 	std::vector<File> files; | ||||
|  | ||||
| 	Storage::Tape::BinaryTapePlayer tape_player(1000000); | ||||
| 	tape_player.set_motor_control(true); | ||||
| 	tape_player.set_tape(tape); | ||||
|  | ||||
| 	using Parser = Storage::Tape::MSX::Parser; | ||||
|  | ||||
| 	// Get all recognisable files from the tape. | ||||
| 	while(!tape->is_at_end()) { | ||||
| 		// Try to locate and measure a header. | ||||
| 		std::unique_ptr<Parser::FileSpeed> file_speed = Parser::find_header(tape_player); | ||||
| 		if(!file_speed) continue; | ||||
|  | ||||
| 		// Check whether what follows is a recognisable file type. | ||||
| 		uint8_t header[10] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; | ||||
| 		for(std::size_t c = 0; c < sizeof(header); ++c) { | ||||
| 			int next_byte = Parser::get_byte(*file_speed, tape_player); | ||||
| 			if(next_byte == -1) break; | ||||
| 			header[c] = static_cast<uint8_t>(next_byte); | ||||
| 		} | ||||
|  | ||||
| 		bool bytes_are_same = true; | ||||
| 		for(std::size_t c = 1; c < sizeof(header); ++c) | ||||
| 			bytes_are_same &= (header[c] == header[0]); | ||||
|  | ||||
| 		if(!bytes_are_same) continue; | ||||
| 		if(header[0] != 0xd0 && header[0] != 0xd3 && header[0] != 0xea) continue; | ||||
|  | ||||
| 		File file; | ||||
|  | ||||
| 		// Determine file type from information already collected. | ||||
| 		switch(header[0]) { | ||||
| 			case 0xd0:	file.type = File::Type::Binary;			break; | ||||
| 			case 0xd3:	file.type = File::Type::TokenisedBASIC;	break; | ||||
| 			case 0xea:	file.type = File::Type::ASCII;			break; | ||||
| 			default: break;	// Unreachable. | ||||
| 		} | ||||
|  | ||||
| 		// Read file name. | ||||
| 		char name[7]; | ||||
| 		for(std::size_t c = 1; c < 6; ++c) | ||||
| 			name[c] = static_cast<char>(Parser::get_byte(*file_speed, tape_player)); | ||||
| 		name[6] = '\0'; | ||||
| 		file.name = name; | ||||
|  | ||||
| 		// ASCII: Read 256-byte segments until one ends with an end-of-file character. | ||||
| 		if(file.type == File::Type::ASCII) { | ||||
| 			while(true) { | ||||
| 				file_speed = Parser::find_header(tape_player); | ||||
| 				if(!file_speed) break; | ||||
| 				int c = 256; | ||||
| 				bool contains_end_of_file = false; | ||||
| 				while(c--) { | ||||
| 					int byte = Parser::get_byte(*file_speed, tape_player); | ||||
| 					if(byte == -1) break; | ||||
| 					contains_end_of_file |= (byte == 0x1a); | ||||
| 					file.data.push_back(static_cast<uint8_t>(byte)); | ||||
| 				} | ||||
| 				if(c != -1) break; | ||||
| 				if(contains_end_of_file) { | ||||
| 					files.push_back(std::move(file)); | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		// Read a single additional segment, using the information at the begging to determine length. | ||||
| 		file_speed = Parser::find_header(tape_player); | ||||
| 		if(!file_speed) continue; | ||||
|  | ||||
| 		// Binary: read start address, end address, entry address, then that many bytes. | ||||
| 		if(file.type == File::Type::Binary) { | ||||
| 			uint8_t locations[6]; | ||||
| 			uint16_t end_address; | ||||
| 			std::size_t c; | ||||
| 			for(c = 0; c < sizeof(locations); ++c) { | ||||
| 				int byte = Parser::get_byte(*file_speed, tape_player); | ||||
| 				if(byte == -1) break; | ||||
| 				locations[c] = static_cast<uint8_t>(byte); | ||||
| 			} | ||||
| 			if(c != sizeof(locations)) continue; | ||||
|  | ||||
| 			file.starting_address = static_cast<uint16_t>(locations[0] | (locations[1] << 8)); | ||||
| 			end_address = static_cast<uint16_t>(locations[2] | (locations[3] << 8)); | ||||
| 			file.entry_address = static_cast<uint16_t>(locations[4] | (locations[5] << 8)); | ||||
|  | ||||
| 			if(end_address < file.starting_address) continue; | ||||
|  | ||||
| 			std::size_t length = end_address - file.starting_address; | ||||
| 			while(length--) { | ||||
| 				int byte = Parser::get_byte(*file_speed, tape_player); | ||||
| 				if(byte == -1) continue; | ||||
| 				file.data.push_back(static_cast<uint8_t>(byte)); | ||||
| 			} | ||||
|  | ||||
| 			files.push_back(std::move(file)); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		// Tokenised BASIC, then: keep following 'next line' links from a hypothetical start of | ||||
| 		// 0x8001, until finding the final line. | ||||
| 		uint16_t current_address = 0x8001; | ||||
| 		while(current_address) { | ||||
| 			int next_address_buffer[2]; | ||||
| 			next_address_buffer[0] = Parser::get_byte(*file_speed, tape_player); | ||||
| 			next_address_buffer[1] = Parser::get_byte(*file_speed, tape_player); | ||||
|  | ||||
| 			if(next_address_buffer[0] == -1 || next_address_buffer[1] == -1) break; | ||||
| 			file.data.push_back(static_cast<uint8_t>(next_address_buffer[0])); | ||||
| 			file.data.push_back(static_cast<uint8_t>(next_address_buffer[1])); | ||||
|  | ||||
| 			uint16_t next_address = static_cast<uint16_t>(next_address_buffer[0] | (next_address_buffer[1] << 8)); | ||||
| 			if(!next_address) { | ||||
| 				files.push_back(std::move(file)); | ||||
| 				break; | ||||
| 			} | ||||
| 			if(next_address < current_address+2) break; | ||||
|  | ||||
| 			// This line makes sense, so push it all in. | ||||
| 			std::size_t length = next_address - current_address - 2; | ||||
| 			current_address = next_address; | ||||
| 			bool found_error = false; | ||||
| 			while(length--) { | ||||
| 				int byte = Parser::get_byte(*file_speed, tape_player); | ||||
| 				if(byte == -1) { | ||||
| 					found_error = true; | ||||
| 					break; | ||||
| 				} | ||||
| 				file.data.push_back(static_cast<uint8_t>(byte)); | ||||
| 			} | ||||
| 			if(found_error) break; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return files; | ||||
| } | ||||
							
								
								
									
										44
									
								
								Analyser/Static/MSX/Tape.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								Analyser/Static/MSX/Tape.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| // | ||||
| //  Tape.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 25/12/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef StaticAnalyser_MSX_Tape_hpp | ||||
| #define StaticAnalyser_MSX_Tape_hpp | ||||
|  | ||||
| #include "../../../Storage/Tape/Tape.hpp" | ||||
|  | ||||
| #include <string> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace MSX { | ||||
|  | ||||
| struct File { | ||||
| 	std::string name; | ||||
| 	enum Type { | ||||
| 		Binary, | ||||
| 		TokenisedBASIC, | ||||
| 		ASCII | ||||
| 	} type; | ||||
|  | ||||
| 	std::vector<uint8_t> data; | ||||
|  | ||||
| 	uint16_t starting_address;	// Provided only for Type::Binary files. | ||||
| 	uint16_t entry_address;		// Provided only for Type::Binary files. | ||||
|  | ||||
| 	File(File &&rhs); | ||||
| 	File(); | ||||
| }; | ||||
|  | ||||
| std::vector<File> GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape); | ||||
|  | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* StaticAnalyser_MSX_Tape_hpp */ | ||||
| @@ -9,11 +9,12 @@ | ||||
| #include "StaticAnalyser.hpp" | ||||
| 
 | ||||
| #include "Tape.hpp" | ||||
| #include "../Disassembler/Disassembler6502.hpp" | ||||
| #include "../Disassembler/6502.hpp" | ||||
| #include "../Disassembler/AddressMapper.hpp" | ||||
| 
 | ||||
| using namespace StaticAnalyser::Oric; | ||||
| using namespace Analyser::Static::Oric; | ||||
| 
 | ||||
| static int Score(const StaticAnalyser::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) { | ||||
| static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) { | ||||
| 	int score = 0; | ||||
| 
 | ||||
| 	for(auto address : disassembly.outward_calls)	score += (rom_functions.find(address) != rom_functions.end()) ? 1 : -1; | ||||
| @@ -23,7 +24,7 @@ static int Score(const StaticAnalyser::MOS6502::Disassembly &disassembly, const | ||||
| 	return score; | ||||
| } | ||||
| 
 | ||||
| static int Basic10Score(const StaticAnalyser::MOS6502::Disassembly &disassembly) { | ||||
| static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	std::set<uint16_t> rom_functions = { | ||||
| 		0x0228,	0x022b, | ||||
| 		0xc3ca,	0xc3f8,	0xc448,	0xc47c,	0xc4b5,	0xc4e3,	0xc4e0,	0xc524,	0xc56f,	0xc5a2,	0xc5f8,	0xc60a,	0xc6a5,	0xc6de,	0xc719,	0xc738, | ||||
| @@ -47,7 +48,7 @@ static int Basic10Score(const StaticAnalyser::MOS6502::Disassembly &disassembly) | ||||
| 	return Score(disassembly, rom_functions, variable_locations); | ||||
| } | ||||
| 
 | ||||
| static int Basic11Score(const StaticAnalyser::MOS6502::Disassembly &disassembly) { | ||||
| static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembly) { | ||||
| 	std::set<uint16_t> rom_functions = { | ||||
| 		0x0238,	0x023b,	0x023e,	0x0241,	0x0244,	0x0247, | ||||
| 		0xc3c6,	0xc3f4,	0xc444,	0xc47c,	0xc4a8,	0xc4d3,	0xc4e0,	0xc524,	0xc55f,	0xc592,	0xc5e8,	0xc5fa,	0xc692,	0xc6b3,	0xc6ee,	0xc70d, | ||||
| @@ -72,27 +73,23 @@ static int Basic11Score(const StaticAnalyser::MOS6502::Disassembly &disassembly) | ||||
| 	return Score(disassembly, rom_functions, variable_locations); | ||||
| } | ||||
| 
 | ||||
| void StaticAnalyser::Oric::AddTargets( | ||||
| 		const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks, | ||||
| 		const std::list<std::shared_ptr<Storage::Tape::Tape>> &tapes, | ||||
| 		const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges, | ||||
| 		std::list<StaticAnalyser::Target> &destination) { | ||||
| 	Target target; | ||||
| 	target.machine = Target::Oric; | ||||
| 	target.probability = 1.0; | ||||
| void Analyser::Static::Oric::AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination) { | ||||
| 	std::unique_ptr<Target> target(new Target); | ||||
| 	target->machine = Machine::Oric; | ||||
| 	target->confidence = 1.0; | ||||
| 
 | ||||
| 	int basic10_votes = 0; | ||||
| 	int basic11_votes = 0; | ||||
| 
 | ||||
| 	for(auto &tape : tapes) { | ||||
| 		std::list<File> tape_files = GetFiles(tape); | ||||
| 	for(auto &tape : media.tapes) { | ||||
| 		std::vector<File> tape_files = GetFiles(tape); | ||||
| 		tape->reset(); | ||||
| 		if(tape_files.size()) { | ||||
| 			for(auto file : tape_files) { | ||||
| 				if(file.data_type == File::MachineCode) { | ||||
| 					std::vector<uint16_t> entry_points = {file.starting_address}; | ||||
| 					StaticAnalyser::MOS6502::Disassembly disassembly = | ||||
| 						StaticAnalyser::MOS6502::Disassemble(file.data, StaticAnalyser::MOS6502::OffsetMapper(file.starting_address), entry_points); | ||||
| 					Analyser::Static::MOS6502::Disassembly disassembly = | ||||
| 						Analyser::Static::MOS6502::Disassemble(file.data, Analyser::Static::Disassembler::OffsetMapper(file.starting_address), entry_points); | ||||
| 
 | ||||
| 					int basic10_score = Basic10Score(disassembly); | ||||
| 					int basic11_score = Basic11Score(disassembly); | ||||
| @@ -100,23 +97,23 @@ void StaticAnalyser::Oric::AddTargets( | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			target.tapes.push_back(tape); | ||||
| 			target.loadingCommand = "CLOAD\"\"\n"; | ||||
| 			target->media.tapes.push_back(tape); | ||||
| 			target->loading_command = "CLOAD\"\"\n"; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// trust that any disk supplied can be handled by the Microdisc. TODO: check.
 | ||||
| 	if(!disks.empty()) { | ||||
| 		target.oric.has_microdisc = true; | ||||
| 		target.disks = disks; | ||||
| 	if(!media.disks.empty()) { | ||||
| 		target->oric.has_microdisc = true; | ||||
| 		target->media.disks = media.disks; | ||||
| 	} else { | ||||
| 		target.oric.has_microdisc = false; | ||||
| 		target->oric.has_microdisc = false; | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: really this should add two targets if not all votes agree
 | ||||
| 	target.oric.use_atmos_rom = basic11_votes >= basic10_votes; | ||||
| 	if(target.oric.has_microdisc) target.oric.use_atmos_rom = true; | ||||
| 	target->oric.use_atmos_rom = basic11_votes >= basic10_votes; | ||||
| 	if(target->oric.has_microdisc) target->oric.use_atmos_rom = true; | ||||
| 
 | ||||
| 	if(target.tapes.size() || target.disks.size() || target.cartridges.size()) | ||||
| 		destination.push_back(target); | ||||
| 	if(target->media.tapes.size() || target->media.disks.size() || target->media.cartridges.size()) | ||||
| 		destination.push_back(std::move(target)); | ||||
| } | ||||
| @@ -11,18 +11,14 @@ | ||||
| 
 | ||||
| #include "../StaticAnalyser.hpp" | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Oric { | ||||
| 
 | ||||
| void AddTargets( | ||||
| 	const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks, | ||||
| 	const std::list<std::shared_ptr<Storage::Tape::Tape>> &tapes, | ||||
| 	const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges, | ||||
| 	std::list<Target> &destination | ||||
| ); | ||||
| void AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| #endif /* StaticAnalyser_hpp */ | ||||
| @@ -7,12 +7,12 @@ | ||||
| //
 | ||||
| 
 | ||||
| #include "Tape.hpp" | ||||
| #include "../../Storage/Tape/Parsers/Oric.hpp" | ||||
| #include "../../../Storage/Tape/Parsers/Oric.hpp" | ||||
| 
 | ||||
| using namespace StaticAnalyser::Oric; | ||||
| using namespace Analyser::Static::Oric; | ||||
| 
 | ||||
| std::list<File> StaticAnalyser::Oric::GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape) { | ||||
| 	std::list<File> files; | ||||
| std::vector<File> Analyser::Static::Oric::GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape) { | ||||
| 	std::vector<File> files; | ||||
| 	Storage::Tape::Oric::Parser parser; | ||||
| 
 | ||||
| 	while(!tape->is_at_end()) { | ||||
| @@ -49,10 +49,10 @@ std::list<File> StaticAnalyser::Oric::GetFiles(const std::shared_ptr<Storage::Ta | ||||
| 		} | ||||
| 
 | ||||
| 		// read end and start addresses
 | ||||
| 		new_file.ending_address = (uint16_t)(parser.get_next_byte(tape, is_fast) << 8); | ||||
| 		new_file.ending_address |= (uint16_t)parser.get_next_byte(tape, is_fast); | ||||
| 		new_file.starting_address = (uint16_t)(parser.get_next_byte(tape, is_fast) << 8); | ||||
| 		new_file.starting_address |= (uint16_t)parser.get_next_byte(tape, is_fast); | ||||
| 		new_file.ending_address = static_cast<uint16_t>(parser.get_next_byte(tape, is_fast) << 8); | ||||
| 		new_file.ending_address |= static_cast<uint16_t>(parser.get_next_byte(tape, is_fast)); | ||||
| 		new_file.starting_address = static_cast<uint16_t>(parser.get_next_byte(tape, is_fast) << 8); | ||||
| 		new_file.starting_address |= static_cast<uint16_t>(parser.get_next_byte(tape, is_fast)); | ||||
| 
 | ||||
| 		// skip an empty byte
 | ||||
| 		parser.get_next_byte(tape, is_fast); | ||||
| @@ -69,10 +69,10 @@ std::list<File> StaticAnalyser::Oric::GetFiles(const std::shared_ptr<Storage::Ta | ||||
| 		new_file.name = file_name; | ||||
| 
 | ||||
| 		// read body
 | ||||
| 		size_t body_length = new_file.ending_address - new_file.starting_address + 1; | ||||
| 		std::size_t body_length = new_file.ending_address - new_file.starting_address + 1; | ||||
| 		new_file.data.reserve(body_length); | ||||
| 		for(size_t c = 0; c < body_length; c++) { | ||||
| 			new_file.data.push_back((uint8_t)parser.get_next_byte(tape, is_fast)); | ||||
| 		for(std::size_t c = 0; c < body_length; c++) { | ||||
| 			new_file.data.push_back(static_cast<uint8_t>(parser.get_next_byte(tape, is_fast))); | ||||
| 		} | ||||
| 
 | ||||
| 		// only one validation check: was there enough tape?
 | ||||
| @@ -9,12 +9,13 @@ | ||||
| #ifndef StaticAnalyser_Oric_Tape_hpp | ||||
| #define StaticAnalyser_Oric_Tape_hpp | ||||
| 
 | ||||
| #include "../../Storage/Tape/Tape.hpp" | ||||
| #include <list> | ||||
| #include "../../../Storage/Tape/Tape.hpp" | ||||
| 
 | ||||
| #include <string> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace Oric { | ||||
| 
 | ||||
| struct File { | ||||
| @@ -30,8 +31,9 @@ struct File { | ||||
| 	std::vector<uint8_t> data; | ||||
| }; | ||||
| 
 | ||||
| std::list<File> GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape); | ||||
| std::vector<File> GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										182
									
								
								Analyser/Static/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								Analyser/Static/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| // | ||||
| //  StaticAnalyser.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 23/08/2016. | ||||
| //  Copyright © 2016 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "StaticAnalyser.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cstdlib> | ||||
| #include <cstring> | ||||
|  | ||||
| // Analysers | ||||
| #include "Acorn/StaticAnalyser.hpp" | ||||
| #include "AmstradCPC/StaticAnalyser.hpp" | ||||
| #include "Atari/StaticAnalyser.hpp" | ||||
| #include "Coleco/StaticAnalyser.hpp" | ||||
| #include "Commodore/StaticAnalyser.hpp" | ||||
| #include "MSX/StaticAnalyser.hpp" | ||||
| #include "Oric/StaticAnalyser.hpp" | ||||
| #include "ZX8081/StaticAnalyser.hpp" | ||||
|  | ||||
| // Cartridges | ||||
| #include "../../Storage/Cartridge/Formats/BinaryDump.hpp" | ||||
| #include "../../Storage/Cartridge/Formats/PRG.hpp" | ||||
|  | ||||
| // Disks | ||||
| #include "../../Storage/Disk/DiskImage/Formats/AcornADF.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/D64.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/G64.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/DMK.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/HFE.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/MSXDSK.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp" | ||||
| #include "../../Storage/Disk/DiskImage/Formats/SSD.hpp" | ||||
|  | ||||
| // Tapes | ||||
| #include "../../Storage/Tape/Formats/CAS.hpp" | ||||
| #include "../../Storage/Tape/Formats/CommodoreTAP.hpp" | ||||
| #include "../../Storage/Tape/Formats/CSW.hpp" | ||||
| #include "../../Storage/Tape/Formats/OricTAP.hpp" | ||||
| #include "../../Storage/Tape/Formats/TapePRG.hpp" | ||||
| #include "../../Storage/Tape/Formats/TapeUEF.hpp" | ||||
| #include "../../Storage/Tape/Formats/TZX.hpp" | ||||
| #include "../../Storage/Tape/Formats/ZX80O81P.hpp" | ||||
|  | ||||
| // Target Platform Types | ||||
| #include "../../Storage/TargetPlatforms.hpp" | ||||
|  | ||||
| using namespace Analyser::Static; | ||||
|  | ||||
| static Media GetMediaAndPlatforms(const char *file_name, TargetPlatform::IntType &potential_platforms) { | ||||
| 	// Get the extension, if any; it will be assumed that extensions are reliable, so an extension is a broad-phase | ||||
| 	// test as to file format. | ||||
| 	const char *mixed_case_extension = strrchr(file_name, '.'); | ||||
| 	char *lowercase_extension = nullptr; | ||||
| 	if(mixed_case_extension) { | ||||
| 		lowercase_extension = strdup(mixed_case_extension+1); | ||||
| 		char *parser = lowercase_extension; | ||||
| 		while(*parser) { | ||||
| 			*parser = (char)tolower(*parser); | ||||
| 			parser++; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	Media result; | ||||
| #define Insert(list, class, platforms) \ | ||||
| 	list.emplace_back(new Storage::class(file_name));\ | ||||
| 	potential_platforms |= platforms;\ | ||||
| 	TargetPlatform::TypeDistinguisher *distinguisher = dynamic_cast<TargetPlatform::TypeDistinguisher *>(list.back().get());\ | ||||
| 	if(distinguisher) potential_platforms &= distinguisher->target_platform_type(); | ||||
|  | ||||
| #define TryInsert(list, class, platforms) \ | ||||
| 	try {\ | ||||
| 		Insert(list, class, platforms) \ | ||||
| 	} catch(...) {} | ||||
|  | ||||
| #define Format(extension, list, class, platforms) \ | ||||
| 	if(!std::strcmp(lowercase_extension, extension))	{	\ | ||||
| 		TryInsert(list, class, platforms)	\ | ||||
| 	} | ||||
|  | ||||
| 	if(lowercase_extension) { | ||||
| 		Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// 80 | ||||
| 		Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// 81 | ||||
| 		Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600)						// A26 | ||||
| 		Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn)		// ADF | ||||
| 		Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600)						// BIN | ||||
| 		Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX)												// CAS | ||||
| 		Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC)										// CDT | ||||
| 		Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision)					// COL | ||||
| 		Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape)											// CSW | ||||
| 		Format("d64", result.disks, Disk::DiskImageHolder<Storage::Disk::D64>, TargetPlatform::Commodore)		// D64 | ||||
| 		Format("dmk", result.disks, Disk::DiskImageHolder<Storage::Disk::DMK>, TargetPlatform::MSX)				// DMK | ||||
| 		Format("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::MSXDSK>, TargetPlatform::MSX)			// DSK (MSX) | ||||
| 		Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric)		// DSK (Oric) | ||||
| 		Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore)		// G64 | ||||
| 		Format("hfe", result.disks, Disk::DiskImageHolder<Storage::Disk::HFE>, TargetPlatform::AmstradCPC)		// HFE (TODO: plus other target platforms) | ||||
| 		Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// O | ||||
| 		Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// P | ||||
| 		Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)										// P81 | ||||
|  | ||||
| 		// PRG | ||||
| 		if(!std::strcmp(lowercase_extension, "prg")) { | ||||
| 			// try instantiating as a ROM; failing that accept as a tape | ||||
| 			try { | ||||
| 				Insert(result.cartridges, Cartridge::PRG, TargetPlatform::Commodore) | ||||
| 			} catch(...) { | ||||
| 				try { | ||||
| 					Insert(result.tapes, Tape::PRG, TargetPlatform::Commodore) | ||||
| 				} catch(...) {} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		Format(	"rom", | ||||
| 				result.cartridges, | ||||
| 				Cartridge::BinaryDump, | ||||
| 				TargetPlatform::Acorn | TargetPlatform::MSX | TargetPlatform::ColecoVision)						// ROM | ||||
| 		Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn)			// SSD | ||||
| 		Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore)								// TAP (Commodore) | ||||
| 		Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric)										// TAP (Oric) | ||||
| 		Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX)												// TSX | ||||
| 		Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081)											// TZX | ||||
| 		Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn)											// UEF (tape) | ||||
|  | ||||
| #undef Format | ||||
| #undef Insert | ||||
| #undef TryInsert | ||||
|  | ||||
| 		// Filter potential platforms as per file preferences, if any. | ||||
|  | ||||
| 		free(lowercase_extension); | ||||
| 	} | ||||
|  | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| Media Analyser::Static::GetMedia(const char *file_name) { | ||||
| 	TargetPlatform::IntType throwaway; | ||||
| 	return GetMediaAndPlatforms(file_name, throwaway); | ||||
| } | ||||
|  | ||||
| std::vector<std::unique_ptr<Target>> Analyser::Static::GetTargets(const char *file_name) { | ||||
| 	std::vector<std::unique_ptr<Target>> targets; | ||||
|  | ||||
| 	// Collect all disks, tapes and ROMs as can be extrapolated from this file, forming the | ||||
| 	// union of all platforms this file might be a target for. | ||||
| 	TargetPlatform::IntType potential_platforms = 0; | ||||
| 	Media media = GetMediaAndPlatforms(file_name, potential_platforms); | ||||
|  | ||||
| 	// Hand off to platform-specific determination of whether these things are actually compatible and, | ||||
| 	// if so, how to load them. | ||||
| 	if(potential_platforms & TargetPlatform::Acorn)			Acorn::AddTargets(media, targets); | ||||
| 	if(potential_platforms & TargetPlatform::AmstradCPC)	AmstradCPC::AddTargets(media, targets); | ||||
| 	if(potential_platforms & TargetPlatform::Atari2600)		Atari::AddTargets(media, targets); | ||||
| 	if(potential_platforms & TargetPlatform::ColecoVision)	Coleco::AddTargets(media, targets); | ||||
| 	if(potential_platforms & TargetPlatform::Commodore)		Commodore::AddTargets(media, targets); | ||||
| 	if(potential_platforms & TargetPlatform::MSX)			MSX::AddTargets(media, targets); | ||||
| 	if(potential_platforms & TargetPlatform::Oric)			Oric::AddTargets(media, targets); | ||||
| 	if(potential_platforms & TargetPlatform::ZX8081)		ZX8081::AddTargets(media, targets, potential_platforms); | ||||
|  | ||||
| 	// Reset any tapes to their initial position | ||||
| 	for(auto &target : targets) { | ||||
| 		for(auto &tape : target->media.tapes) { | ||||
| 			tape->reset(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Sort by initial confidence. Use a stable sort in case any of the machine-specific analysers | ||||
| 	// picked their insertion order carefully. | ||||
| 	std::stable_sort(targets.begin(), targets.end(), | ||||
|         [] (const std::unique_ptr<Target> &a, const std::unique_ptr<Target> &b) { | ||||
| 		    return a->confidence > b->confidence; | ||||
| 	    }); | ||||
|  | ||||
| 	return targets; | ||||
| } | ||||
| @@ -9,15 +9,17 @@ | ||||
| #ifndef StaticAnalyser_hpp | ||||
| #define StaticAnalyser_hpp | ||||
| 
 | ||||
| #include "../Storage/Tape/Tape.hpp" | ||||
| #include "../Storage/Disk/Disk.hpp" | ||||
| #include "../Storage/Cartridge/Cartridge.hpp" | ||||
| #include "../Machines.hpp" | ||||
| 
 | ||||
| #include "../../Storage/Tape/Tape.hpp" | ||||
| #include "../../Storage/Disk/Disk.hpp" | ||||
| #include "../../Storage/Cartridge/Cartridge.hpp" | ||||
| 
 | ||||
| #include <string> | ||||
| #include <list> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| 
 | ||||
| enum class Vic20MemoryModel { | ||||
| 	Unexpanded, | ||||
| @@ -46,20 +48,38 @@ enum class ZX8081MemoryModel { | ||||
| 	SixtyFourKB | ||||
| }; | ||||
| 
 | ||||
| enum class AmstradCPCModel { | ||||
| 	CPC464, | ||||
| 	CPC664, | ||||
| 	CPC6128 | ||||
| }; | ||||
| 
 | ||||
| /*!
 | ||||
| 	A list of disks, tapes and cartridges. | ||||
| */ | ||||
| struct Media { | ||||
| 	std::vector<std::shared_ptr<Storage::Disk::Disk>> disks; | ||||
| 	std::vector<std::shared_ptr<Storage::Tape::Tape>> tapes; | ||||
| 	std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges; | ||||
| 
 | ||||
| 	bool empty() const { | ||||
| 		return disks.empty() && tapes.empty() && cartridges.empty(); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| /*!
 | ||||
| 	A list of disks, tapes and cartridges plus information about the machine to which to attach them and its configuration, | ||||
| 	and instructions on how to launch the software attached, plus a measure of confidence in this target's correctness. | ||||
| */ | ||||
| struct Target { | ||||
| 	enum  { | ||||
| 		Atari2600, | ||||
| 		Electron, | ||||
| 		Vic20, | ||||
| 		Oric, | ||||
| 		ZX8081 | ||||
| 	} machine; | ||||
| 	float probability; | ||||
| 	Machine machine; | ||||
| 	Media media; | ||||
| 
 | ||||
| 	float confidence; | ||||
| 	std::string loading_command; | ||||
| 
 | ||||
| 	// TODO: this is too C-like a solution; make Target a base class and
 | ||||
| 	// turn the following into information held by more specific subclasses.
 | ||||
| 	union { | ||||
| 		struct { | ||||
| 			bool has_adfs; | ||||
| @@ -86,13 +106,11 @@ struct Target { | ||||
| 			ZX8081MemoryModel memory_model; | ||||
| 			bool isZX81; | ||||
| 		} zx8081; | ||||
| 
 | ||||
| 		struct { | ||||
| 			AmstradCPCModel model; | ||||
| 		} amstradcpc; | ||||
| 	}; | ||||
| 
 | ||||
| 	std::string loadingCommand; | ||||
| 
 | ||||
| 	std::list<std::shared_ptr<Storage::Disk::Disk>> disks; | ||||
| 	std::list<std::shared_ptr<Storage::Tape::Tape>> tapes; | ||||
| 	std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges; | ||||
| }; | ||||
| 
 | ||||
| /*!
 | ||||
| @@ -100,8 +118,14 @@ struct Target { | ||||
| 
 | ||||
| 	@returns The list of potential targets, sorted from most to least probable. | ||||
| */ | ||||
| std::list<Target> GetTargets(const char *file_name); | ||||
| std::vector<std::unique_ptr<Target>> GetTargets(const char *file_name); | ||||
| 
 | ||||
| /*!
 | ||||
| 	Inspects the supplied file and determines the media included. | ||||
| */ | ||||
| Media GetMedia(const char *file_name); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| #endif /* StaticAnalyser_hpp */ | ||||
							
								
								
									
										67
									
								
								Analyser/Static/ZX8081/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								Analyser/Static/ZX8081/StaticAnalyser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| // | ||||
| //  StaticAnalyser.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 04/06/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "StaticAnalyser.hpp" | ||||
|  | ||||
| #include <string> | ||||
| #include <vector> | ||||
|  | ||||
| #include "../../../Storage/Tape/Parsers/ZX8081.hpp" | ||||
|  | ||||
| static std::vector<Storage::Data::ZX8081::File> GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape) { | ||||
| 	std::vector<Storage::Data::ZX8081::File> files; | ||||
| 	Storage::Tape::ZX8081::Parser parser; | ||||
|  | ||||
| 	while(!tape->is_at_end()) { | ||||
| 		std::shared_ptr<Storage::Data::ZX8081::File> next_file = parser.get_next_file(tape); | ||||
| 		if(next_file != nullptr) { | ||||
| 			files.push_back(*next_file); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return files; | ||||
| } | ||||
|  | ||||
| void Analyser::Static::ZX8081::AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination, TargetPlatform::IntType potential_platforms) { | ||||
| 	if(!media.tapes.empty()) { | ||||
| 		std::vector<Storage::Data::ZX8081::File> files = GetFiles(media.tapes.front()); | ||||
| 		media.tapes.front()->reset(); | ||||
| 		if(!files.empty()) { | ||||
| 			std::unique_ptr<Target> target(new Target); | ||||
| 			target->machine = Machine::ZX8081; | ||||
|  | ||||
| 			// Guess the machine type from the file only if it isn't already known. | ||||
| 			switch(potential_platforms & (TargetPlatform::ZX80 | TargetPlatform::ZX81)) { | ||||
| 				default: | ||||
| 					target->zx8081.isZX81 = files.front().isZX81; | ||||
| 				break; | ||||
|  | ||||
| 				case TargetPlatform::ZX80:	target->zx8081.isZX81 = false;	break; | ||||
| 				case TargetPlatform::ZX81:	target->zx8081.isZX81 = true;	break; | ||||
| 			} | ||||
|  | ||||
| 			/*if(files.front().data.size() > 16384) { | ||||
| 				target->zx8081.memory_model = ZX8081MemoryModel::SixtyFourKB; | ||||
| 			} else*/ if(files.front().data.size() > 1024) { | ||||
| 				target->zx8081.memory_model = ZX8081MemoryModel::SixteenKB; | ||||
| 			} else { | ||||
| 				target->zx8081.memory_model = ZX8081MemoryModel::Unexpanded; | ||||
| 			} | ||||
| 			target->media.tapes = media.tapes; | ||||
|  | ||||
| 			// TODO: how to run software once loaded? Might require a BASIC detokeniser. | ||||
| 			if(target->zx8081.isZX81) { | ||||
| 				target->loading_command = "J\"\"\n"; | ||||
| 			} else { | ||||
| 				target->loading_command = "W\n"; | ||||
| 			} | ||||
|  | ||||
| 			destination.push_back(std::move(target)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -10,17 +10,15 @@ | ||||
| #define StaticAnalyser_ZX8081_StaticAnalyser_hpp | ||||
| 
 | ||||
| #include "../StaticAnalyser.hpp" | ||||
| #include "../../../Storage/TargetPlatforms.hpp" | ||||
| 
 | ||||
| namespace StaticAnalyser { | ||||
| namespace Analyser { | ||||
| namespace Static { | ||||
| namespace ZX8081 { | ||||
| 
 | ||||
| void AddTargets( | ||||
| 	const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks, | ||||
| 	const std::list<std::shared_ptr<Storage::Tape::Tape>> &tapes, | ||||
| 	const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges, | ||||
| 	std::list<Target> &destination | ||||
| ); | ||||
| void AddTargets(const Media &media, std::vector<std::unique_ptr<Target>> &destination, TargetPlatform::IntType potential_platforms); | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										30
									
								
								BUILD.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								BUILD.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| Linux, BSD | ||||
| ========== | ||||
|  | ||||
| Prerequisites are SDL 2, ZLib and OpenGL (or Mesa), and SCons for the provided build script. OpenGL 3.2 or better is required at runtime. | ||||
|  | ||||
| Build: | ||||
|  | ||||
| 	cd OSBindings/SDL | ||||
| 	scons | ||||
|  | ||||
| Optionally: | ||||
|  | ||||
| 	cp clksignal /usr/bin | ||||
|  | ||||
| To launch: | ||||
|  | ||||
| 	clksignal file | ||||
|  | ||||
| Setting up clksignal as the associated program for supported file types in your favoured filesystem browser is recommended; it has no file navigation abilities of its own. | ||||
|  | ||||
| Some emulated systems require the provision of original machine ROMs. These are not included and may be located in either /usr/local/share/CLK/ or /usr/share/CLK/. You will be prompted for them if they are found to be missing. The structure should mirror that under OSBindings in the source archive; see the readme.txt in each folder to determine the proper files and names ahead of time. | ||||
|  | ||||
| macOS | ||||
| ===== | ||||
|  | ||||
| There are no prerequisites beyond the normal system libraries; the macOS build is a native Cocoa application. | ||||
|  | ||||
| Build: open the Xcode project in OSBindings/Mac and press command+b. | ||||
|  | ||||
| Machine ROMs are intended to be built into the application bundle; populate the dummy folders below ROMImages before building. | ||||
							
								
								
									
										216
									
								
								ClockReceiver/ClockReceiver.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								ClockReceiver/ClockReceiver.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,216 @@ | ||||
| // | ||||
| //  ClockReceiver.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 22/07/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef ClockReceiver_hpp | ||||
| #define ClockReceiver_hpp | ||||
|  | ||||
| /* | ||||
| 	Informal pattern for all classes that run from a clock cycle: | ||||
|  | ||||
| 		Each will implement either or both of run_for(Cycles) and run_for(HalfCycles), as | ||||
| 		is appropriate. | ||||
|  | ||||
| 		Callers that are accumulating HalfCycles but want to talk to receivers that implement | ||||
| 		only run_for(Cycles) can use HalfCycle.flush_cycles if they have appropriate storage, or | ||||
| 		can wrap the receiver in HalfClockReceiver in order automatically to bind half-cycle | ||||
| 		storage to it. | ||||
|  | ||||
| 	Alignment rule: | ||||
|  | ||||
| 		run_for(Cycles) may be called only after an even number of half cycles. E.g. the following | ||||
| 		sequence will have undefined results: | ||||
|  | ||||
| 			run_for(HalfCycles(1)) | ||||
| 			run_for(Cycles(1)) | ||||
|  | ||||
| 		An easy way to ensure this as a caller is to pick only one of run_for(Cycles) and | ||||
| 		run_for(HalfCycles) to use. | ||||
|  | ||||
| 	Reasoning: | ||||
|  | ||||
| 		Users of this template may with to implement run_for(Cycles) and run_for(HalfCycles) | ||||
| 		where there is a need to implement at half-cycle precision but a faster execution | ||||
| 		path can be offered for full-cycle precision. Those users are permitted to assume | ||||
| 		phase in run_for(Cycles) and should do so to be compatible with callers that use | ||||
| 		only run_for(Cycles). | ||||
|  | ||||
| 	Corollary: | ||||
|  | ||||
| 		Starting from nothing, the first run_for(HalfCycles(1)) will do the **first** half | ||||
| 		of a full cycle. The second will do the second half. Etc. | ||||
|  | ||||
| */ | ||||
|  | ||||
| /*! | ||||
| 	Provides a class that wraps a plain int, providing most of the basic arithmetic and | ||||
| 	Boolean operators, but forcing callers and receivers to be explicit as to usage. | ||||
| */ | ||||
| template <class T> class WrappedInt { | ||||
| 	public: | ||||
| 		inline WrappedInt(int l) : length_(l) {} | ||||
| 		inline WrappedInt() : length_(0) {} | ||||
|  | ||||
| 		inline T &operator =(const T &rhs) { | ||||
| 			length_ = rhs.length_; | ||||
| 			return *this; | ||||
| 		} | ||||
|  | ||||
| 		inline T &operator +=(const T &rhs) { | ||||
| 			length_ += rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		inline T &operator -=(const T &rhs) { | ||||
| 			length_ -= rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		inline T &operator ++() { | ||||
| 			++ length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		inline T &operator ++(int) { | ||||
| 			length_ ++; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		inline T &operator --() { | ||||
| 			-- length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		inline T &operator --(int) { | ||||
| 			length_ --; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		inline T &operator %=(const T &rhs) { | ||||
| 			length_ %= rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		inline T &operator &=(const T &rhs) { | ||||
| 			length_ &= rhs.length_; | ||||
| 			return *static_cast<T *>(this); | ||||
| 		} | ||||
|  | ||||
| 		inline T operator +(const T &rhs) const			{	return T(length_ + rhs.length_);	} | ||||
| 		inline T operator -(const T &rhs) const			{	return T(length_ - rhs.length_);	} | ||||
|  | ||||
| 		inline T operator %(const T &rhs) const			{	return T(length_ % rhs.length_);	} | ||||
| 		inline T operator &(const T &rhs) const			{	return T(length_ & rhs.length_);	} | ||||
|  | ||||
| 		inline T operator -() const						{	return T(- length_);				} | ||||
|  | ||||
| 		inline bool operator <(const T &rhs) const		{	return length_ < rhs.length_;		} | ||||
| 		inline bool operator >(const T &rhs) const		{	return length_ > rhs.length_;		} | ||||
| 		inline bool operator <=(const T &rhs) const		{	return length_ <= rhs.length_;		} | ||||
| 		inline bool operator >=(const T &rhs) const		{	return length_ >= rhs.length_;		} | ||||
| 		inline bool operator ==(const T &rhs) const		{	return length_ == rhs.length_;		} | ||||
| 		inline bool operator !=(const T &rhs) const		{	return length_ != rhs.length_;		} | ||||
|  | ||||
| 		inline 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 | ||||
|  | ||||
| 		inline 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. | ||||
| 		*/ | ||||
| 		inline T divide(const T &divisor) { | ||||
| 			T result(length_ / divisor.length_); | ||||
| 			length_ %= divisor.length_; | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Flushes the value in @c this. The current value is returned, and the internal value | ||||
| 			is reset to zero. | ||||
| 		*/ | ||||
| 		inline T flush() { | ||||
| 			T result(length_); | ||||
| 			length_ = 0; | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		// operator int() is deliberately not provided, to avoid accidental subtitution of | ||||
| 		// classes that use this template. | ||||
|  | ||||
| 	protected: | ||||
| 		int length_; | ||||
| }; | ||||
|  | ||||
| /// Describes an integer number of whole cycles — pairs of clock signal transitions. | ||||
| class Cycles: public WrappedInt<Cycles> { | ||||
| 	public: | ||||
| 		inline Cycles(int l) : WrappedInt<Cycles>(l) {} | ||||
| 		inline Cycles() : WrappedInt<Cycles>() {} | ||||
| 		inline Cycles(const Cycles &cycles) : WrappedInt<Cycles>(cycles.length_) {} | ||||
| }; | ||||
|  | ||||
| /// Describes an integer number of half cycles — single clock signal transitions. | ||||
| class HalfCycles: public WrappedInt<HalfCycles> { | ||||
| 	public: | ||||
| 		inline HalfCycles(int l) : WrappedInt<HalfCycles>(l) {} | ||||
| 		inline HalfCycles() : WrappedInt<HalfCycles>() {} | ||||
|  | ||||
| 		inline HalfCycles(const Cycles cycles) : WrappedInt<HalfCycles>(cycles.as_int() * 2) {} | ||||
| 		inline HalfCycles(const HalfCycles &half_cycles) : WrappedInt<HalfCycles>(half_cycles.length_) {} | ||||
|  | ||||
| 		/// @returns The number of whole cycles completely covered by this span of half cycles. | ||||
| 		inline Cycles cycles() { | ||||
| 			return Cycles(length_ >> 1); | ||||
| 		} | ||||
|  | ||||
| 		/// Flushes the whole cycles in @c this, subtracting that many from the total stored here. | ||||
| 		inline 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. | ||||
| 		inline 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. | ||||
| 		*/ | ||||
| 		inline Cycles divide_cycles(const Cycles &divisor) { | ||||
| 			HalfCycles half_divisor = HalfCycles(divisor); | ||||
| 			Cycles result(length_ / half_divisor.length_); | ||||
| 			length_ %= half_divisor.length_; | ||||
| 			return result; | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	If a component implements only run_for(Cycles), an owner can wrap it in HalfClockReceiver | ||||
| 	automatically to gain run_for(HalfCycles). | ||||
| */ | ||||
| template <class T> class HalfClockReceiver: public T { | ||||
| 	public: | ||||
| 		using T::T; | ||||
|  | ||||
| 		using T::run_for; | ||||
| 		inline void run_for(const HalfCycles half_cycles) { | ||||
| 			half_cycles_ += half_cycles; | ||||
| 			T::run_for(half_cycles_.flush_cycles()); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		HalfCycles half_cycles_; | ||||
| }; | ||||
|  | ||||
| #endif /* ClockReceiver_hpp */ | ||||
							
								
								
									
										26
									
								
								ClockReceiver/ForceInline.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								ClockReceiver/ForceInline.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| // | ||||
| //  ForceInline.h | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 01/08/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef ForceInline_hpp | ||||
| #define ForceInline_hpp | ||||
|  | ||||
| #ifdef DEBUG | ||||
|  | ||||
| #define forceinline | ||||
|  | ||||
| #else | ||||
|  | ||||
| #ifdef __GNUC__ | ||||
| #define forceinline __attribute__((always_inline)) inline | ||||
| #elif _MSC_VER | ||||
| #define forceinline __forceinline | ||||
| #endif | ||||
|  | ||||
| #endif | ||||
|  | ||||
| #endif /* ForceInline_h */ | ||||
							
								
								
									
										60
									
								
								ClockReceiver/Sleeper.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								ClockReceiver/Sleeper.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| // | ||||
| //  Sleeper.h | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 20/08/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Sleeper_hpp | ||||
| #define Sleeper_hpp | ||||
|  | ||||
| /*! | ||||
| 	A sleeper is any component that sometimes requires a clock but at other times is 'asleep' — i.e. is not doing | ||||
| 	any clock-derived work, so needn't receive a clock. A disk controller is an archetypal example. | ||||
|  | ||||
| 	A sleeper will signal sleeps and wakes to an observer. | ||||
|  | ||||
| 	This is intended to allow for performance improvements to machines with components that can sleep. The observer | ||||
| 	callout is virtual so the intended use case is that a machine holds a component that might sleep. Its transitions | ||||
| 	into and out of sleep are sufficiently infrequent that a virtual call to announce them costs sufficiently little that | ||||
| 	the saved ::run_fors add up to a substantial amount. | ||||
|  | ||||
| 	By convention, sleeper components must be willing to accept ::run_for even after announcing sleep. It's a hint, | ||||
| 	not a command. | ||||
| */ | ||||
| class Sleeper { | ||||
| 	public: | ||||
| 		Sleeper() : sleep_observer_(nullptr) {} | ||||
|  | ||||
| 		class SleepObserver { | ||||
| 			public: | ||||
| 				/// Called to inform an observer that the component @c component has either gone to sleep or become awake. | ||||
| 				virtual void set_component_is_sleeping(void *component, bool is_sleeping) = 0; | ||||
| 		}; | ||||
|  | ||||
| 		/// Registers @c observer as the new sleep observer; | ||||
| 		void set_sleep_observer(SleepObserver *observer) { | ||||
| 			sleep_observer_ = observer; | ||||
| 		} | ||||
|  | ||||
| 		/// @returns @c true if the component is currently sleeping; @c false otherwise. | ||||
| 		virtual bool is_sleeping() = 0; | ||||
|  | ||||
| 	protected: | ||||
| 		/// Provided for subclasses; send sleep announcements to the sleep_observer_. | ||||
| 		SleepObserver *sleep_observer_; | ||||
|  | ||||
| 		/*! | ||||
| 			Provided for subclasses; call this whenever is_sleeping might have changed, and the observer will be notified, | ||||
| 			if one exists. | ||||
|  | ||||
| 			@c is_sleeping will be called only if there is an observer. | ||||
| 		*/ | ||||
| 		void update_sleep_observer() { | ||||
| 			if(!sleep_observer_) return; | ||||
| 			sleep_observer_->set_component_is_sleeping(this, is_sleeping()); | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| #endif /* Sleeper_h */ | ||||
| @@ -7,60 +7,35 @@ | ||||
| // | ||||
|  | ||||
| #include "1770.hpp" | ||||
| #include "../../Storage/Disk/Encodings/MFM.hpp" | ||||
| #include "../../Storage/Disk/Encodings/MFM/Constants.hpp" | ||||
|  | ||||
| using namespace WD; | ||||
|  | ||||
| WD1770::Status::Status() : | ||||
| 		type(Status::One), | ||||
| 		write_protect(false), | ||||
| 		record_type(false), | ||||
| 		spin_up(false), | ||||
| 		record_not_found(false), | ||||
| 		crc_error(false), | ||||
| 		seek_error(false), | ||||
| 		lost_data(false), | ||||
| 		data_request(false), | ||||
| 		interrupt_request(false), | ||||
| 		busy(false) {} | ||||
|  | ||||
| WD1770::WD1770(Personality p) : | ||||
| 		Storage::Disk::Controller(8000000, 16, 300), | ||||
| 		crc_generator_(0x1021, 0xffff), | ||||
| 		interesting_event_mask_(Event::Command), | ||||
| 		resume_point_(0), | ||||
| 		delay_time_(0), | ||||
| 		index_hole_count_target_(-1), | ||||
| 		is_awaiting_marker_value_(false), | ||||
| 		data_mode_(DataMode::Scanning), | ||||
| 		delegate_(nullptr), | ||||
| 		Storage::Disk::MFMController(8000000), | ||||
| 		personality_(p), | ||||
| 		head_is_loaded_(false) { | ||||
| 		interesting_event_mask_(static_cast<int>(Event1770::Command)) { | ||||
| 	set_is_double_density(false); | ||||
| 	posit_event(Event::Command); | ||||
| } | ||||
|  | ||||
| void WD1770::set_is_double_density(bool is_double_density) { | ||||
| 	is_double_density_ = is_double_density; | ||||
| 	Storage::Time bit_length; | ||||
| 	bit_length.length = 1; | ||||
| 	bit_length.clock_rate = is_double_density ? 500000 : 250000; | ||||
| 	set_expected_bit_length(bit_length); | ||||
|  | ||||
| 	if(!is_double_density) is_awaiting_marker_value_ = false; | ||||
| 	posit_event(static_cast<int>(Event1770::Command)); | ||||
| } | ||||
|  | ||||
| void WD1770::set_register(int address, uint8_t value) { | ||||
| 	switch(address&3) { | ||||
| 		case 0: { | ||||
| 			if((value&0xf0) == 0xd0) { | ||||
| 				if(value == 0xd0) { | ||||
| 					// Force interrupt **immediately**. | ||||
| 					printf("Force interrupt immediately\n"); | ||||
| 					posit_event(static_cast<int>(Event1770::ForceInterrupt)); | ||||
| 				} else { | ||||
| 					printf("!!!TODO: force interrupt!!!\n"); | ||||
| 					update_status([] (Status &status) { | ||||
| 						status.type = Status::One; | ||||
| 					}); | ||||
| 				} | ||||
| 			} else { | ||||
| 				command_ = value; | ||||
| 				posit_event(Event::Command); | ||||
| 				posit_event(static_cast<int>(Event1770::Command)); | ||||
| 			} | ||||
| 		} | ||||
| 		break; | ||||
| @@ -88,7 +63,7 @@ uint8_t WD1770::get_register(int address) { | ||||
| 			switch(status_.type) { | ||||
| 				case Status::One: | ||||
| 					status |= | ||||
| 						(get_is_track_zero() ? Flag::TrackZero : 0) | | ||||
| 						(get_drive().get_is_track_zero() ? Flag::TrackZero : 0) | | ||||
| 						(status_.seek_error ? Flag::SeekError : 0); | ||||
| 						// TODO: index hole | ||||
| 				break; | ||||
| @@ -104,11 +79,11 @@ uint8_t WD1770::get_register(int address) { | ||||
| 			} | ||||
|  | ||||
| 			if(!has_motor_on_line()) { | ||||
| 				status |= get_drive_is_ready() ? 0 : Flag::NotReady; | ||||
| 				status |= get_drive().get_is_ready() ? 0 : Flag::NotReady; | ||||
| 				if(status_.type == Status::One) | ||||
| 					status |= (head_is_loaded_ ? Flag::HeadLoaded : 0); | ||||
| 			} else { | ||||
| 				status |= (get_motor_on() ? Flag::MotorOn : 0); | ||||
| 				status |= (get_drive().get_motor_on() ? Flag::MotorOn : 0); | ||||
| 				if(status_.type == Status::One) | ||||
| 					status |= (status_.spin_up ? Flag::SpinUp : 0); | ||||
| 			} | ||||
| @@ -124,154 +99,31 @@ uint8_t WD1770::get_register(int address) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void WD1770::run_for_cycles(unsigned int number_of_cycles) { | ||||
| 	Storage::Disk::Controller::run_for_cycles((int)number_of_cycles); | ||||
| void WD1770::run_for(const Cycles cycles) { | ||||
| 	Storage::Disk::Controller::run_for(cycles); | ||||
|  | ||||
| 	if(delay_time_) { | ||||
| 		unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_int()); | ||||
| 		if(delay_time_ <= number_of_cycles) { | ||||
| 			delay_time_ = 0; | ||||
| 			posit_event(Event::Timer); | ||||
| 			posit_event(static_cast<int>(Event1770::Timer)); | ||||
| 		} else { | ||||
| 			delay_time_ -= number_of_cycles; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void WD1770::process_input_bit(int value, unsigned int cycles_since_index_hole) { | ||||
| 	if(data_mode_ == DataMode::Writing) return; | ||||
|  | ||||
| 	shift_register_ = (shift_register_ << 1) | value; | ||||
| 	bits_since_token_++; | ||||
|  | ||||
| 	if(data_mode_ == DataMode::Scanning) { | ||||
| 		Token::Type token_type = Token::Byte; | ||||
| 		if(!is_double_density_) { | ||||
| 			switch(shift_register_ & 0xffff) { | ||||
| 				case Storage::Encodings::MFM::FMIndexAddressMark: | ||||
| 					token_type = Token::Index; | ||||
| 					crc_generator_.reset(); | ||||
| 					crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::IndexAddressByte); | ||||
| 				break; | ||||
| 				case Storage::Encodings::MFM::FMIDAddressMark: | ||||
| 					token_type = Token::ID; | ||||
| 					crc_generator_.reset(); | ||||
| 					crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::IDAddressByte); | ||||
| 				break; | ||||
| 				case Storage::Encodings::MFM::FMDataAddressMark: | ||||
| 					token_type = Token::Data; | ||||
| 					crc_generator_.reset(); | ||||
| 					crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::DataAddressByte); | ||||
| 				break; | ||||
| 				case Storage::Encodings::MFM::FMDeletedDataAddressMark: | ||||
| 					token_type = Token::DeletedData; | ||||
| 					crc_generator_.reset(); | ||||
| 					crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::DeletedDataAddressByte); | ||||
| 				break; | ||||
| 				default: | ||||
| 				break; | ||||
| 			} | ||||
| 		} else { | ||||
| 			switch(shift_register_ & 0xffff) { | ||||
| 				case Storage::Encodings::MFM::MFMIndexSync: | ||||
| 					bits_since_token_ = 0; | ||||
| 					is_awaiting_marker_value_ = true; | ||||
|  | ||||
| 					token_type = Token::Sync; | ||||
| 					latest_token_.byte_value = Storage::Encodings::MFM::MFMIndexSyncByteValue; | ||||
| 				break; | ||||
| 				case Storage::Encodings::MFM::MFMSync: | ||||
| 					bits_since_token_ = 0; | ||||
| 					is_awaiting_marker_value_ = true; | ||||
| 					crc_generator_.set_value(Storage::Encodings::MFM::MFMPostSyncCRCValue); | ||||
|  | ||||
| 					token_type = Token::Sync; | ||||
| 					latest_token_.byte_value = Storage::Encodings::MFM::MFMSyncByteValue; | ||||
| 				break; | ||||
| 				default: | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if(token_type != Token::Byte) { | ||||
| 			latest_token_.type = token_type; | ||||
| 			bits_since_token_ = 0; | ||||
| 			posit_event(Event::Token); | ||||
| 			return; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if(bits_since_token_ == 16) { | ||||
| 		latest_token_.type = Token::Byte; | ||||
| 		latest_token_.byte_value = (uint8_t)( | ||||
| 			((shift_register_ & 0x0001) >> 0) | | ||||
| 			((shift_register_ & 0x0004) >> 1) | | ||||
| 			((shift_register_ & 0x0010) >> 2) | | ||||
| 			((shift_register_ & 0x0040) >> 3) | | ||||
| 			((shift_register_ & 0x0100) >> 4) | | ||||
| 			((shift_register_ & 0x0400) >> 5) | | ||||
| 			((shift_register_ & 0x1000) >> 6) | | ||||
| 			((shift_register_ & 0x4000) >> 7)); | ||||
| 		bits_since_token_ = 0; | ||||
|  | ||||
| 		if(is_awaiting_marker_value_ && is_double_density_) { | ||||
| 			is_awaiting_marker_value_ = false; | ||||
| 			switch(latest_token_.byte_value) { | ||||
| 				case Storage::Encodings::MFM::IndexAddressByte: | ||||
| 					latest_token_.type = Token::Index; | ||||
| 				break; | ||||
| 				case Storage::Encodings::MFM::IDAddressByte: | ||||
| 					latest_token_.type = Token::ID; | ||||
| 				break; | ||||
| 				case Storage::Encodings::MFM::DataAddressByte: | ||||
| 					latest_token_.type = Token::Data; | ||||
| 				break; | ||||
| 				case Storage::Encodings::MFM::DeletedDataAddressByte: | ||||
| 					latest_token_.type = Token::DeletedData; | ||||
| 				break; | ||||
| 				default: break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		crc_generator_.add(latest_token_.byte_value); | ||||
| 		posit_event(Event::Token); | ||||
| 		return; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void WD1770::process_index_hole() { | ||||
| 	index_hole_count_++; | ||||
| 	posit_event(Event::IndexHole); | ||||
| 	if(index_hole_count_target_ == index_hole_count_) { | ||||
| 		posit_event(Event::IndexHoleTarget); | ||||
| 		index_hole_count_target_ = -1; | ||||
| 	} | ||||
|  | ||||
| 	// motor power-down | ||||
| 	if(index_hole_count_ == 9 && !status_.busy && has_motor_on_line()) { | ||||
| 		set_motor_on(false); | ||||
| 	} | ||||
|  | ||||
| 	// head unload | ||||
| 	if(index_hole_count_ == 15 && !status_.busy && has_head_load_line()) { | ||||
| 		set_head_load_request(false); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void WD1770::process_write_completed() { | ||||
| 	posit_event(Event::DataWritten); | ||||
| } | ||||
|  | ||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = mask; return; case __LINE__: | ||||
| #define WAIT_FOR_TIME(ms)		resume_point_ = __LINE__; interesting_event_mask_ = Event::Timer; delay_time_ = ms * 8000; if(delay_time_) return; case __LINE__: | ||||
| #define WAIT_FOR_BYTES(count)	resume_point_ = __LINE__; interesting_event_mask_ = Event::Token; distance_into_section_ = 0; return; case __LINE__: if(latest_token_.type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = Event::Token; return; } | ||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(mask); return; case __LINE__: | ||||
| #define WAIT_FOR_TIME(ms)		resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer); | ||||
| #define WAIT_FOR_BYTES(count)	resume_point_ = __LINE__; distance_into_section_ = 0; WAIT_FOR_EVENT(Event::Token); if(get_latest_token().type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = static_cast<int>(Event::Token); return; } | ||||
| #define BEGIN_SECTION()	switch(resume_point_) { default: | ||||
| #define END_SECTION()	0; } | ||||
| #define END_SECTION()	(void)0; } | ||||
|  | ||||
| #define READ_ID()	\ | ||||
| 		if(new_event_type == Event::Token) {	\ | ||||
| 			if(!distance_into_section_ && latest_token_.type == Token::ID) {data_mode_ = DataMode::Reading; distance_into_section_++; }	\ | ||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && latest_token_.type == Token::Byte) {	\ | ||||
| 				header_[distance_into_section_ - 1] = latest_token_.byte_value;	\ | ||||
| 		if(new_event_type == static_cast<int>(Event::Token)) {	\ | ||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; }	\ | ||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) {	\ | ||||
| 				header_[distance_into_section_ - 1] = get_latest_token().byte_value;	\ | ||||
| 				distance_into_section_++;	\ | ||||
| 			}	\ | ||||
| 		} | ||||
| @@ -284,7 +136,7 @@ void WD1770::process_write_completed() { | ||||
| 		set_motor_on(true);	\ | ||||
| 		index_hole_count_ = 0;	\ | ||||
| 		index_hole_count_target_ = 6;	\ | ||||
| 		WAIT_FOR_EVENT(Event::IndexHoleTarget);	\ | ||||
| 		WAIT_FOR_EVENT(Event1770::IndexHoleTarget);	\ | ||||
| 		status_.spin_up = true; | ||||
|  | ||||
| //     +--------+----------+-------------------------+ | ||||
| @@ -304,17 +156,45 @@ void WD1770::process_write_completed() { | ||||
| //     !	 4  ! Forc int !  1  1	0  1 i3 i2 i1 i0 ! | ||||
| //     +--------+----------+-------------------------+ | ||||
|  | ||||
| void WD1770::posit_event(Event new_event_type) { | ||||
| 	if(!(interesting_event_mask_ & (int)new_event_type)) return; | ||||
| void WD1770::posit_event(int new_event_type) { | ||||
| 	if(new_event_type == static_cast<int>(Event::IndexHole)) { | ||||
| 		index_hole_count_++; | ||||
| 		if(index_hole_count_target_ == index_hole_count_) { | ||||
| 			posit_event(static_cast<int>(Event1770::IndexHoleTarget)); | ||||
| 			index_hole_count_target_ = -1; | ||||
| 		} | ||||
|  | ||||
| 		// motor power-down | ||||
| 		if(index_hole_count_ == 9 && !status_.busy && has_motor_on_line()) { | ||||
| 			set_motor_on(false); | ||||
| 		} | ||||
|  | ||||
| 		// head unload | ||||
| 		if(index_hole_count_ == 15 && !status_.busy && has_head_load_line()) { | ||||
| 			set_head_load_request(false); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if(new_event_type == static_cast<int>(Event1770::ForceInterrupt)) { | ||||
| 		interesting_event_mask_ = 0; | ||||
| 		resume_point_ = 0; | ||||
| 		update_status([] (Status &status) { | ||||
| 			status.type = Status::One; | ||||
| 			status.data_request = false; | ||||
| 		}); | ||||
| 	} else { | ||||
| 		if(!(interesting_event_mask_ & static_cast<int>(new_event_type))) return; | ||||
| 		interesting_event_mask_ &= ~new_event_type; | ||||
| 	} | ||||
|  | ||||
| 	Status new_status; | ||||
| 	BEGIN_SECTION() | ||||
|  | ||||
| 	// Wait for a new command, branch to the appropriate handler. | ||||
| 	case 0: | ||||
| 	wait_for_command: | ||||
| 		printf("Idle...\n"); | ||||
| 		data_mode_ = DataMode::Scanning; | ||||
| 		set_data_mode(DataMode::Scanning); | ||||
| 		index_hole_count_ = 0; | ||||
|  | ||||
| 		update_status([] (Status &status) { | ||||
| @@ -322,7 +202,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 			status.interrupt_request = true; | ||||
| 		}); | ||||
|  | ||||
| 		WAIT_FOR_EVENT(Event::Command); | ||||
| 		WAIT_FOR_EVENT(Event1770::Command); | ||||
|  | ||||
| 		update_status([] (Status &status) { | ||||
| 			status.busy = true; | ||||
| @@ -371,11 +251,11 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 		} | ||||
| 		set_head_load_request(true); | ||||
| 		if(head_is_loaded_) goto test_type1_type; | ||||
| 		WAIT_FOR_EVENT(Event::HeadLoad); | ||||
| 		WAIT_FOR_EVENT(Event1770::HeadLoad); | ||||
| 		goto test_type1_type; | ||||
|  | ||||
| 	begin_type1_spin_up: | ||||
| 		if((command_&0x08) || get_motor_on()) goto test_type1_type; | ||||
| 		if((command_&0x08) || get_drive().get_motor_on()) goto test_type1_type; | ||||
| 		SPIN_UP(); | ||||
|  | ||||
| 	test_type1_type: | ||||
| @@ -398,12 +278,12 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 		if(step_direction_) track_++; else track_--; | ||||
|  | ||||
| 	perform_step: | ||||
| 		if(!step_direction_ && get_is_track_zero()) { | ||||
| 		if(!step_direction_ && get_drive().get_is_track_zero()) { | ||||
| 			track_ = 0; | ||||
| 			goto verify; | ||||
| 		} | ||||
| 		step(step_direction_ ? 1 : -1); | ||||
| 		int time_to_wait; | ||||
| 		get_drive().step(step_direction_ ? 1 : -1); | ||||
| 		unsigned int time_to_wait; | ||||
| 		switch(command_ & 3) { | ||||
| 			default: | ||||
| 			case 0: time_to_wait = 6;	break; | ||||
| @@ -428,7 +308,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 		distance_into_section_ = 0; | ||||
|  | ||||
| 	verify_read_data: | ||||
| 		WAIT_FOR_EVENT(Event::IndexHole | Event::Token); | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | ||||
| 		READ_ID(); | ||||
|  | ||||
| 		if(index_hole_count_ == 6) { | ||||
| @@ -438,8 +318,8 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		if(distance_into_section_ == 7) { | ||||
| 			data_mode_ = DataMode::Scanning; | ||||
| 			if(crc_generator_.get_value()) { | ||||
| 			set_data_mode(DataMode::Scanning); | ||||
| 			if(get_crc_generator().get_value()) { | ||||
| 				update_status([] (Status &status) { | ||||
| 					status.crc_error = true; | ||||
| 				}); | ||||
| @@ -490,11 +370,11 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 	begin_type2_load_head: | ||||
| 		set_head_load_request(true); | ||||
| 		if(head_is_loaded_) goto test_type2_delay; | ||||
| 		WAIT_FOR_EVENT(Event::HeadLoad); | ||||
| 		WAIT_FOR_EVENT(Event1770::HeadLoad); | ||||
| 		goto test_type2_delay; | ||||
|  | ||||
| 	begin_type2_spin_up: | ||||
| 		if(get_motor_on()) goto test_type2_delay; | ||||
| 		if(get_drive().get_motor_on()) goto test_type2_delay; | ||||
| 		// Perform spin up. | ||||
| 		SPIN_UP(); | ||||
|  | ||||
| @@ -504,7 +384,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 		WAIT_FOR_TIME(30); | ||||
|  | ||||
| 	test_type2_write_protection: | ||||
| 		if(command_&0x20 && get_drive_is_read_only()) { | ||||
| 		if(command_&0x20 && get_drive().get_is_read_only()) { | ||||
| 			update_status([] (Status &status) { | ||||
| 				status.write_protect = true; | ||||
| 			}); | ||||
| @@ -512,7 +392,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 		} | ||||
|  | ||||
| 	type2_get_header: | ||||
| 		WAIT_FOR_EVENT(Event::IndexHole | Event::Token); | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | ||||
| 		READ_ID(); | ||||
|  | ||||
| 		if(index_hole_count_ == 5) { | ||||
| @@ -524,11 +404,11 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 		} | ||||
| 		if(distance_into_section_ == 7) { | ||||
| 			printf("Considering %d/%d\n", header_[0], header_[2]); | ||||
| 			data_mode_ = DataMode::Scanning; | ||||
| 			set_data_mode(DataMode::Scanning); | ||||
| 			if(		header_[0] == track_ && header_[2] == sector_ && | ||||
| 					(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) { | ||||
| 				printf("Found %d/%d\n", header_[0], header_[2]); | ||||
| 				if(crc_generator_.get_value()) { | ||||
| 				if(get_crc_generator().get_value()) { | ||||
| 					printf("CRC error; back to searching\n"); | ||||
| 					update_status([] (Status &status) { | ||||
| 						status.crc_error = true; | ||||
| @@ -553,20 +433,20 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 	type2_read_data: | ||||
| 		WAIT_FOR_EVENT(Event::Token); | ||||
| 		// TODO: timeout | ||||
| 		if(latest_token_.type == Token::Data || latest_token_.type == Token::DeletedData) { | ||||
| 		if(get_latest_token().type == Token::Data || get_latest_token().type == Token::DeletedData) { | ||||
| 			update_status([this] (Status &status) { | ||||
| 				status.record_type = (latest_token_.type == Token::DeletedData); | ||||
| 				status.record_type = (get_latest_token().type == Token::DeletedData); | ||||
| 			}); | ||||
| 			distance_into_section_ = 0; | ||||
| 			data_mode_ = DataMode::Reading; | ||||
| 			set_data_mode(DataMode::Reading); | ||||
| 			goto type2_read_byte; | ||||
| 		} | ||||
| 		goto type2_read_data; | ||||
|  | ||||
| 	type2_read_byte: | ||||
| 		WAIT_FOR_EVENT(Event::Token); | ||||
| 		if(latest_token_.type != Token::Byte) goto type2_read_byte; | ||||
| 		data_ = latest_token_.byte_value; | ||||
| 		if(get_latest_token().type != Token::Byte) goto type2_read_byte; | ||||
| 		data_ = get_latest_token().byte_value; | ||||
| 		update_status([] (Status &status) { | ||||
| 			status.lost_data |= status.data_request; | ||||
| 			status.data_request = true; | ||||
| @@ -580,11 +460,11 @@ void WD1770::posit_event(Event new_event_type) { | ||||
|  | ||||
| 	type2_check_crc: | ||||
| 		WAIT_FOR_EVENT(Event::Token); | ||||
| 		if(latest_token_.type != Token::Byte) goto type2_read_byte; | ||||
| 		header_[distance_into_section_] = latest_token_.byte_value; | ||||
| 		if(get_latest_token().type != Token::Byte) goto type2_read_byte; | ||||
| 		header_[distance_into_section_] = get_latest_token().byte_value; | ||||
| 		distance_into_section_++; | ||||
| 		if(distance_into_section_ == 2) { | ||||
| 			if(crc_generator_.get_value()) { | ||||
| 			if(get_crc_generator().get_value()) { | ||||
| 				printf("CRC error; terminating\n"); | ||||
| 				update_status([this] (Status &status) { | ||||
| 					status.crc_error = true; | ||||
| @@ -596,7 +476,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 				sector_++; | ||||
| 				goto test_type2_write_protection; | ||||
| 			} | ||||
| 			printf("Read sector %d\n", sector_); | ||||
| 			printf("Finished reading sector %d\n", sector_); | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		goto type2_check_crc; | ||||
| @@ -615,24 +495,24 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		WAIT_FOR_BYTES(1); | ||||
| 		if(is_double_density_) { | ||||
| 		if(get_is_double_density()) { | ||||
| 			WAIT_FOR_BYTES(11); | ||||
| 		} | ||||
|  | ||||
| 		data_mode_ = DataMode::Writing; | ||||
| 		begin_writing(); | ||||
| 		for(int c = 0; c < (is_double_density_ ? 12 : 6); c++) { | ||||
| 		set_data_mode(DataMode::Writing); | ||||
| 		begin_writing(false); | ||||
| 		for(int c = 0; c < (get_is_double_density() ? 12 : 6); c++) { | ||||
| 			write_byte(0); | ||||
| 		} | ||||
| 		WAIT_FOR_EVENT(Event::DataWritten); | ||||
|  | ||||
| 		if(is_double_density_) { | ||||
| 			crc_generator_.set_value(Storage::Encodings::MFM::MFMPostSyncCRCValue); | ||||
| 		if(get_is_double_density()) { | ||||
| 			get_crc_generator().set_value(Storage::Encodings::MFM::MFMPostSyncCRCValue); | ||||
| 			for(int c = 0; c < 3; c++) write_raw_short(Storage::Encodings::MFM::MFMSync); | ||||
| 			write_byte((command_&0x01) ? Storage::Encodings::MFM::DeletedDataAddressByte : Storage::Encodings::MFM::DataAddressByte); | ||||
| 		} else { | ||||
| 			crc_generator_.reset(); | ||||
| 			crc_generator_.add((command_&0x01) ? Storage::Encodings::MFM::DeletedDataAddressByte : Storage::Encodings::MFM::DataAddressByte); | ||||
| 			get_crc_generator().reset(); | ||||
| 			get_crc_generator().add((command_&0x01) ? Storage::Encodings::MFM::DeletedDataAddressByte : Storage::Encodings::MFM::DataAddressByte); | ||||
| 			write_raw_short((command_&0x01) ? Storage::Encodings::MFM::FMDeletedDataAddressMark : Storage::Encodings::MFM::FMDataAddressMark); | ||||
| 		} | ||||
|  | ||||
| @@ -668,11 +548,8 @@ void WD1770::posit_event(Event new_event_type) { | ||||
|  | ||||
| 		goto type2_write_loop; | ||||
|  | ||||
| 	type2_write_crc: { | ||||
| 			uint16_t crc = crc_generator_.get_value(); | ||||
| 			write_byte(crc >> 8); | ||||
| 			write_byte(crc & 0xff); | ||||
| 		} | ||||
| 	type2_write_crc: | ||||
| 		write_crc(); | ||||
| 		write_byte(0xff); | ||||
| 		WAIT_FOR_EVENT(Event::DataWritten); | ||||
| 		end_writing(); | ||||
| @@ -711,11 +588,11 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 	begin_type3_load_head: | ||||
| 		set_head_load_request(true); | ||||
| 		if(head_is_loaded_) goto type3_test_delay; | ||||
| 		WAIT_FOR_EVENT(Event::HeadLoad); | ||||
| 		WAIT_FOR_EVENT(Event1770::HeadLoad); | ||||
| 		goto type3_test_delay; | ||||
|  | ||||
| 	begin_type3_spin_up: | ||||
| 		if((command_&0x08) || get_motor_on()) goto type3_test_delay; | ||||
| 		if((command_&0x08) || get_drive().get_motor_on()) goto type3_test_delay; | ||||
| 		SPIN_UP(); | ||||
|  | ||||
| 	type3_test_delay: | ||||
| @@ -732,17 +609,17 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 		distance_into_section_ = 0; | ||||
|  | ||||
| 	read_address_get_header: | ||||
| 		WAIT_FOR_EVENT(Event::IndexHole | Event::Token); | ||||
| 		if(new_event_type == Event::Token) { | ||||
| 			if(!distance_into_section_ && latest_token_.type == Token::ID) {data_mode_ = DataMode::Reading; distance_into_section_++; } | ||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && latest_token_.type == Token::Byte) { | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::IndexHole) | static_cast<int>(Event::Token)); | ||||
| 		if(new_event_type == static_cast<int>(Event::Token)) { | ||||
| 			if(!distance_into_section_ && get_latest_token().type == Token::ID) {set_data_mode(DataMode::Reading); distance_into_section_++; } | ||||
| 			else if(distance_into_section_ && distance_into_section_ < 7 && get_latest_token().type == Token::Byte) { | ||||
| 				if(status_.data_request) { | ||||
| 					update_status([] (Status &status) { | ||||
| 						status.lost_data = true; | ||||
| 					}); | ||||
| 					goto wait_for_command; | ||||
| 				} | ||||
| 				header_[distance_into_section_ - 1] = data_ = latest_token_.byte_value; | ||||
| 				header_[distance_into_section_ - 1] = data_ = get_latest_token().byte_value; | ||||
| 				track_ = header_[0]; | ||||
| 				update_status([] (Status &status) { | ||||
| 					status.data_request = true; | ||||
| @@ -750,7 +627,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 				distance_into_section_++; | ||||
|  | ||||
| 				if(distance_into_section_ == 7) { | ||||
| 					if(crc_generator_.get_value()) { | ||||
| 					if(get_crc_generator().get_value()) { | ||||
| 						update_status([] (Status &status) { | ||||
| 							status.crc_error = true; | ||||
| 						}); | ||||
| @@ -773,7 +650,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 		index_hole_count_ = 0; | ||||
|  | ||||
| 	read_track_read_byte: | ||||
| 		WAIT_FOR_EVENT(Event::Token | Event::IndexHole); | ||||
| 		WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); | ||||
| 		if(index_hole_count_) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| @@ -783,7 +660,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 			}); | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
| 		data_ = latest_token_.byte_value; | ||||
| 		data_ = get_latest_token().byte_value; | ||||
| 		update_status([] (Status &status) { | ||||
| 			status.data_request = true; | ||||
| 		}); | ||||
| @@ -795,8 +672,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 			status.lost_data = false; | ||||
| 		}); | ||||
|  | ||||
| 	write_track_test_write_protect: | ||||
| 		if(get_drive_is_read_only()) { | ||||
| 		if(get_drive().get_is_read_only()) { | ||||
| 			update_status([] (Status &status) { | ||||
| 				status.write_protect = true; | ||||
| 			}); | ||||
| @@ -814,25 +690,23 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 			goto wait_for_command; | ||||
| 		} | ||||
|  | ||||
| 		WAIT_FOR_EVENT(Event::IndexHoleTarget); | ||||
| 		begin_writing(); | ||||
| 		WAIT_FOR_EVENT(Event1770::IndexHoleTarget); | ||||
| 		begin_writing(true); | ||||
| 		index_hole_count_ = 0; | ||||
|  | ||||
| 	write_track_write_loop: | ||||
| 		if(is_double_density_) { | ||||
| 		if(get_is_double_density()) { | ||||
| 			switch(data_) { | ||||
| 				case 0xf5: | ||||
| 					write_raw_short(Storage::Encodings::MFM::MFMSync); | ||||
| 					crc_generator_.set_value(Storage::Encodings::MFM::MFMPostSyncCRCValue); | ||||
| 					get_crc_generator().set_value(Storage::Encodings::MFM::MFMPostSyncCRCValue); | ||||
| 				break; | ||||
| 				case 0xf6: | ||||
| 					write_raw_short(Storage::Encodings::MFM::MFMIndexSync); | ||||
| 				break; | ||||
| 				case 0xff: { | ||||
| 					uint16_t crc = crc_generator_.get_value(); | ||||
| 					write_byte(crc >> 8); | ||||
| 					write_byte(crc & 0xff); | ||||
| 				} break; | ||||
| 				case 0xff: | ||||
| 					write_crc(); | ||||
| 				break; | ||||
| 				default: | ||||
| 					write_byte(data_); | ||||
| 				break; | ||||
| @@ -843,7 +717,7 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 				case 0xfd: case 0xfe: | ||||
| 					// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022 | ||||
| 					write_raw_short( | ||||
| 						(uint16_t)( | ||||
| 						static_cast<uint16_t>( | ||||
| 							0xa022 | | ||||
| 							((data_ & 0x80) << 7) | | ||||
| 							((data_ & 0x40) << 6) | | ||||
| @@ -855,17 +729,15 @@ void WD1770::posit_event(Event new_event_type) { | ||||
| 							(data_ & 0x01) | ||||
| 						) | ||||
| 					); | ||||
| 					crc_generator_.reset(); | ||||
| 					crc_generator_.add(data_); | ||||
| 					get_crc_generator().reset(); | ||||
| 					get_crc_generator().add(data_); | ||||
| 				break; | ||||
| 				case 0xfc: | ||||
| 					write_raw_short(Storage::Encodings::MFM::FMIndexAddressMark); | ||||
| 				break; | ||||
| 				case 0xf7: { | ||||
| 					uint16_t crc = crc_generator_.get_value(); | ||||
| 					write_byte(crc >> 8); | ||||
| 					write_byte(crc & 0xff); | ||||
| 				} break; | ||||
| 				case 0xf7: | ||||
| 					write_crc(); | ||||
| 				break; | ||||
| 				default: | ||||
| 					write_byte(data_); | ||||
| 				break; | ||||
| @@ -906,30 +778,9 @@ void WD1770::update_status(std::function<void(Status &)> updater) { | ||||
| } | ||||
|  | ||||
| void WD1770::set_head_load_request(bool head_load) {} | ||||
| void WD1770::set_motor_on(bool motor_on) {} | ||||
|  | ||||
| void WD1770::set_head_loaded(bool head_loaded) { | ||||
| 	head_is_loaded_ = head_loaded; | ||||
| 	if(head_loaded) posit_event(Event::HeadLoad); | ||||
| } | ||||
|  | ||||
| void WD1770::write_bit(int bit) { | ||||
| 	if(is_double_density_) { | ||||
| 		Controller::write_bit(!bit && !last_bit_); | ||||
| 		Controller::write_bit(!!bit); | ||||
| 		last_bit_ = bit; | ||||
| 	} else { | ||||
| 		Controller::write_bit(true); | ||||
| 		Controller::write_bit(!!bit); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void WD1770::write_byte(uint8_t byte) { | ||||
| 	for(int c = 0; c < 8; c++) write_bit((byte << c)&0x80); | ||||
| 	crc_generator_.add(byte); | ||||
| } | ||||
|  | ||||
| void WD1770::write_raw_short(uint16_t value) { | ||||
| 	for(int c = 0; c < 16; c++) { | ||||
| 		Controller::write_bit(!!((value << c)&0x8000)); | ||||
| 	} | ||||
| 	if(head_loaded) posit_event(static_cast<int>(Event1770::HeadLoad)); | ||||
| } | ||||
|   | ||||
| @@ -9,26 +9,41 @@ | ||||
| #ifndef _770_hpp | ||||
| #define _770_hpp | ||||
|  | ||||
| #include "../../Storage/Disk/DiskController.hpp" | ||||
| #include "../../NumberTheory/CRC.hpp" | ||||
| #include "../../Storage/Disk/Controller/MFMDiskController.hpp" | ||||
|  | ||||
| namespace WD { | ||||
|  | ||||
| class WD1770: public Storage::Disk::Controller { | ||||
| /*! | ||||
| 	Provides an emulation of various Western Digital drive controllers, including the | ||||
| 	WD1770, WD1772, FDC1773 and FDC1793. | ||||
| */ | ||||
| class WD1770: public Storage::Disk::MFMController { | ||||
| 	public: | ||||
| 		enum Personality { | ||||
| 			P1770,	// implies automatic motor-on management with Type 2 commands offering a spin-up disable | ||||
| 			P1770,	// implies automatic motor-on management, with Type 2 commands offering a spin-up disable | ||||
| 			P1772,	// as per the 1770, with different stepping rates | ||||
| 			P1773,	// implements the side number-testing logic of the 1793; omits spin-up/loading logic | ||||
| 			P1793	// implies Type 2 commands use side number testing logic; spin-up/loading is by HLD and HLT | ||||
| 		}; | ||||
|  | ||||
| 		/*! | ||||
| 			Constructs an instance of the drive controller that behaves according to personality @c p. | ||||
| 			@param p The type of controller to emulate. | ||||
| 		*/ | ||||
| 		WD1770(Personality p); | ||||
|  | ||||
| 		void set_is_double_density(bool is_double_density); | ||||
| 		/// Sets the value of the double-density input; when @c is_double_density is @c true, reads and writes double-density format data. | ||||
| 		using Storage::Disk::MFMController::set_is_double_density; | ||||
|  | ||||
| 		/// Writes @c value to the register at @c address. Only the low two bits of the address are decoded. | ||||
| 		void set_register(int address, uint8_t value); | ||||
|  | ||||
| 		/// Fetches the value of the register @c address. Only the low two bits of the address are decoded. | ||||
| 		uint8_t get_register(int address); | ||||
|  | ||||
| 		void run_for_cycles(unsigned int number_of_cycles); | ||||
| 		/// Runs the controller for @c number_of_cycles cycles. | ||||
| 		void run_for(const Cycles cycles); | ||||
| 		using Storage::Disk::Controller::run_for; | ||||
|  | ||||
| 		enum Flag: uint8_t { | ||||
| 			NotReady		= 0x80, | ||||
| @@ -47,8 +62,12 @@ class WD1770: public Storage::Disk::Controller { | ||||
| 			Busy			= 0x01 | ||||
| 		}; | ||||
|  | ||||
| 		/// @returns The current value of the IRQ line output. | ||||
| 		inline bool get_interrupt_request_line()		{	return status_.interrupt_request;	} | ||||
|  | ||||
| 		/// @returns The current value of the DRQ line output. | ||||
| 		inline bool get_data_request_line()				{	return status_.data_request;		} | ||||
|  | ||||
| 		class Delegate { | ||||
| 			public: | ||||
| 				virtual void wd1770_did_change_output(WD1770 *wd1770) = 0; | ||||
| @@ -57,6 +76,7 @@ class WD1770: public Storage::Disk::Controller { | ||||
|  | ||||
| 	protected: | ||||
| 		virtual void set_head_load_request(bool head_load); | ||||
| 		virtual void set_motor_on(bool motor_on); | ||||
| 		void set_head_loaded(bool head_loaded); | ||||
|  | ||||
| 	private: | ||||
| @@ -65,20 +85,19 @@ class WD1770: public Storage::Disk::Controller { | ||||
| 		inline bool has_head_load_line() { return (personality_ == P1793 ); } | ||||
|  | ||||
| 		struct Status { | ||||
| 			Status(); | ||||
| 			bool write_protect; | ||||
| 			bool record_type; | ||||
| 			bool spin_up; | ||||
| 			bool record_not_found; | ||||
| 			bool crc_error; | ||||
| 			bool seek_error; | ||||
| 			bool lost_data; | ||||
| 			bool data_request; | ||||
| 			bool interrupt_request; | ||||
| 			bool busy; | ||||
| 			bool write_protect = false; | ||||
| 			bool record_type = false; | ||||
| 			bool spin_up = false; | ||||
| 			bool record_not_found = false; | ||||
| 			bool crc_error = false; | ||||
| 			bool seek_error = false; | ||||
| 			bool lost_data = false; | ||||
| 			bool data_request = false; | ||||
| 			bool interrupt_request = false; | ||||
| 			bool busy = false; | ||||
| 			enum { | ||||
| 				One, Two, Three | ||||
| 			} type; | ||||
| 			} type = One; | ||||
| 		} status_; | ||||
| 		uint8_t track_; | ||||
| 		uint8_t sector_; | ||||
| @@ -86,67 +105,33 @@ class WD1770: public Storage::Disk::Controller { | ||||
| 		uint8_t command_; | ||||
|  | ||||
| 		int index_hole_count_; | ||||
| 		int index_hole_count_target_; | ||||
| 		int bits_since_token_; | ||||
| 		int index_hole_count_target_ = -1; | ||||
| 		int distance_into_section_; | ||||
| 		bool is_awaiting_marker_value_; | ||||
|  | ||||
| 		int step_direction_; | ||||
| 		void update_status(std::function<void(Status &)> updater); | ||||
|  | ||||
| 		// Tokeniser | ||||
| 		enum DataMode { | ||||
| 			Scanning, | ||||
| 			Reading, | ||||
| 			Writing | ||||
| 		} data_mode_; | ||||
| 		bool is_double_density_; | ||||
| 		int shift_register_; | ||||
| 		struct Token { | ||||
| 			enum Type { | ||||
| 				Index, ID, Data, DeletedData, Sync, Byte | ||||
| 			} type; | ||||
| 			uint8_t byte_value; | ||||
| 		} latest_token_; | ||||
|  | ||||
| 		// Events | ||||
| 		enum Event: int { | ||||
| 			Command			= (1 << 0),	// Indicates receipt of a new command. | ||||
| 			Token			= (1 << 1),	// Indicates recognition of a new token in the flux stream. Interrogate latest_token_ for details. | ||||
| 			IndexHole		= (1 << 2),	// Indicates the passing of a physical index hole. | ||||
| 			HeadLoad		= (1 << 3),	// Indicates the head has been loaded (1973 only). | ||||
| 			DataWritten		= (1 << 4),	// Indicates that all queued bits have been written | ||||
|  | ||||
| 		enum Event1770: int { | ||||
| 			Command			= (1 << 3),	// Indicates receipt of a new command. | ||||
| 			HeadLoad		= (1 << 4),	// Indicates the head has been loaded (1973 only). | ||||
| 			Timer			= (1 << 5),	// Indicates that the delay_time_-powered timer has timed out. | ||||
| 			IndexHoleTarget	= (1 << 6)	// Indicates that index_hole_count_ has reached index_hole_count_target_. | ||||
| 			IndexHoleTarget	= (1 << 6),	// Indicates that index_hole_count_ has reached index_hole_count_target_. | ||||
| 			ForceInterrupt	= (1 << 7)	// Indicates a forced interrupt. | ||||
| 		}; | ||||
| 		void posit_event(Event type); | ||||
| 		void posit_event(int type); | ||||
| 		int interesting_event_mask_; | ||||
| 		int resume_point_; | ||||
| 		int delay_time_; | ||||
|  | ||||
| 		// Output | ||||
| 		int last_bit_; | ||||
| 		void write_bit(int bit); | ||||
| 		void write_byte(uint8_t byte); | ||||
| 		void write_raw_short(uint16_t value); | ||||
| 		int resume_point_ = 0; | ||||
| 		unsigned int delay_time_ = 0; | ||||
|  | ||||
| 		// ID buffer | ||||
| 		uint8_t header_[6]; | ||||
|  | ||||
| 		// CRC generator | ||||
| 		NumberTheory::CRC16 crc_generator_; | ||||
|  | ||||
| 		// 1793 head-loading logic | ||||
| 		bool head_is_loaded_; | ||||
| 		bool head_is_loaded_ = false; | ||||
|  | ||||
| 		// delegate | ||||
| 		Delegate *delegate_; | ||||
|  | ||||
| 		// Storage::Disk::Controller | ||||
| 		virtual void process_input_bit(int value, unsigned int cycles_since_index_hole); | ||||
| 		virtual void process_index_hole(); | ||||
| 		virtual void process_write_completed(); | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -13,7 +13,83 @@ | ||||
| #include <typeinfo> | ||||
| #include <cstdio> | ||||
|  | ||||
| #include "Implementation/6522Storage.hpp" | ||||
|  | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
|  | ||||
| namespace MOS { | ||||
| namespace MOS6522 { | ||||
|  | ||||
| enum Port { | ||||
| 	A = 0, | ||||
| 	B = 1 | ||||
| }; | ||||
|  | ||||
| enum Line { | ||||
| 	One = 0, | ||||
| 	Two = 1 | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Provides the mechanism for just-int-time communication from a 6522; the normal use case is to compose a | ||||
| 	6522 and a subclass of PortHandler in order to reproduce a 6522 and its original bus wiring. | ||||
| */ | ||||
| class PortHandler { | ||||
| 	public: | ||||
| 		/// Requests the current input value of @c port from the port handler. | ||||
| 		uint8_t get_port_input(Port port)										{	return 0xff;	} | ||||
|  | ||||
| 		/// Sets the current output value of @c port and provides @c direction_mask, indicating which pins are marked as output. | ||||
| 		void set_port_output(Port port, uint8_t value, uint8_t direction_mask)	{} | ||||
|  | ||||
| 		/// Sets the current logical output level for line @c line on port @c port. | ||||
| 		void set_control_line_output(Port port, Line line, bool value)			{} | ||||
|  | ||||
| 		/// Sets the current logical value of the interrupt line. | ||||
| 		void set_interrupt_status(bool status)									{} | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Provided as an optional alternative base to @c PortHandler for port handlers; via the delegate pattern adds | ||||
| 	a virtual level of indirection for receiving changes to the interrupt line. | ||||
| */ | ||||
| class IRQDelegatePortHandler: public PortHandler { | ||||
| 	public: | ||||
| 		class Delegate { | ||||
| 			public: | ||||
| 				/// Indicates that the interrupt status has changed for the IRQDelegatePortHandler provided. | ||||
| 				virtual void mos6522_did_change_interrupt_status(void *irq_delegate) = 0; | ||||
| 		}; | ||||
|  | ||||
| 		/// Sets the delegate that will receive notification of changes in the interrupt line. | ||||
| 		void set_interrupt_delegate(Delegate *delegate); | ||||
|  | ||||
| 		/// Overrides PortHandler::set_interrupt_status, notifying the delegate if one is set. | ||||
| 		void set_interrupt_status(bool new_status); | ||||
|  | ||||
| 	private: | ||||
| 		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'). | ||||
| @@ -26,367 +102,27 @@ namespace MOS { | ||||
| 	Consumers should derive their own curiously-recurring-template-pattern subclass, | ||||
| 	implementing bus communications as required. | ||||
| */ | ||||
| template <class T> class MOS6522 { | ||||
| 	private: | ||||
| 		enum InterruptFlag: uint8_t { | ||||
| 			CA2ActiveEdge	= 1 << 0, | ||||
| 			CA1ActiveEdge	= 1 << 1, | ||||
| 			ShiftRegister	= 1 << 2, | ||||
| 			CB2ActiveEdge	= 1 << 3, | ||||
| 			CB1ActiveEdge	= 1 << 4, | ||||
| 			Timer2			= 1 << 5, | ||||
| 			Timer1			= 1 << 6, | ||||
| 		}; | ||||
|  | ||||
| template <class T> class MOS6522: public MOS6522Base { | ||||
| 	public: | ||||
| 		enum Port { | ||||
| 			A = 0, | ||||
| 			B = 1 | ||||
| 		}; | ||||
|  | ||||
| 		enum Line { | ||||
| 			One = 0, | ||||
| 			Two = 1 | ||||
| 		}; | ||||
| 		MOS6522(T &bus_handler) noexcept : bus_handler_(bus_handler) {} | ||||
| 		MOS6522(const MOS6522 &) = delete; | ||||
|  | ||||
| 		/*! Sets a register value. */ | ||||
| 		inline void set_register(int address, uint8_t value) { | ||||
| 			address &= 0xf; | ||||
| //			printf("6522 [%s]: %0x <- %02x\n", typeid(*this).name(), address, value); | ||||
| 			switch(address) { | ||||
| 				case 0x0: | ||||
| 					registers_.output[1] = value; | ||||
| 					static_cast<T *>(this)->set_port_output(Port::B, value, registers_.data_direction[1]);	// TODO: handshake | ||||
|  | ||||
| 					registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | ((registers_.peripheral_control&0x20) ? 0 : InterruptFlag::CB2ActiveEdge)); | ||||
| 					reevaluate_interrupts(); | ||||
| 				break; | ||||
| 				case 0xf: | ||||
| 				case 0x1: | ||||
| 					registers_.output[0] = value; | ||||
| 					static_cast<T *>(this)->set_port_output(Port::A, value, registers_.data_direction[0]);	// TODO: handshake | ||||
|  | ||||
| 					registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | ((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge)); | ||||
| 					reevaluate_interrupts(); | ||||
| 				break; | ||||
| //					// No handshake, so write directly | ||||
| //					registers_.output[0] = value; | ||||
| //					static_cast<T *>(this)->set_port_output(0, value); | ||||
| //				break; | ||||
|  | ||||
| 				case 0x2: | ||||
| 					registers_.data_direction[1] = value; | ||||
| 				break; | ||||
| 				case 0x3: | ||||
| 					registers_.data_direction[0] = value; | ||||
| 				break; | ||||
|  | ||||
| 				// Timer 1 | ||||
| 				case 0x6:	case 0x4:	registers_.timer_latch[0] = (registers_.timer_latch[0]&0xff00) | value;	break; | ||||
| 				case 0x5:	case 0x7: | ||||
| 					registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | (uint16_t)(value << 8); | ||||
| 					registers_.interrupt_flags &= ~InterruptFlag::Timer1; | ||||
| 					if(address == 0x05) { | ||||
| 						registers_.next_timer[0] = registers_.timer_latch[0]; | ||||
| 						timer_is_running_[0] = true; | ||||
| 					} | ||||
| 					reevaluate_interrupts(); | ||||
| 				break; | ||||
|  | ||||
| 				// Timer 2 | ||||
| 				case 0x8:	registers_.timer_latch[1] = value;	break; | ||||
| 				case 0x9: | ||||
| 					registers_.interrupt_flags &= ~InterruptFlag::Timer2; | ||||
| 					registers_.next_timer[1] = registers_.timer_latch[1] | (uint16_t)(value << 8); | ||||
| 					timer_is_running_[1] = true; | ||||
| 					reevaluate_interrupts(); | ||||
| 				break; | ||||
|  | ||||
| 				// Shift | ||||
| 				case 0xa:	registers_.shift = value;				break; | ||||
|  | ||||
| 				// Control | ||||
| 				case 0xb: | ||||
| 					registers_.auxiliary_control = value; | ||||
| 				break; | ||||
| 				case 0xc: | ||||
| //					printf("Peripheral control %02x\n", value); | ||||
| 					registers_.peripheral_control = value; | ||||
|  | ||||
| 					// TODO: simplify below; trying to avoid improper logging of unimplemented warnings in input mode | ||||
| 					if(value & 0x08) { | ||||
| 						switch(value & 0x0e) { | ||||
| 							default: printf("Unimplemented control line mode %d\n", (value >> 1)&7); break; | ||||
| 							case 0x0c:	static_cast<T *>(this)->set_control_line_output(Port::A, Line::Two, false);		break; | ||||
| 							case 0x0e:	static_cast<T *>(this)->set_control_line_output(Port::A, Line::Two, true);		break; | ||||
| 						} | ||||
| 					} | ||||
| 					if(value & 0x80) { | ||||
| 						switch(value & 0xe0) { | ||||
| 							default: printf("Unimplemented control line mode %d\n", (value >> 5)&7); break; | ||||
| 							case 0xc0:	static_cast<T *>(this)->set_control_line_output(Port::B, Line::Two, false);		break; | ||||
| 							case 0xe0:	static_cast<T *>(this)->set_control_line_output(Port::B, Line::Two, true);		break; | ||||
| 						} | ||||
| 					} | ||||
| 				break; | ||||
|  | ||||
| 				// Interrupt control | ||||
| 				case 0xd: | ||||
| 					registers_.interrupt_flags &= ~value; | ||||
| 					reevaluate_interrupts(); | ||||
| 				break; | ||||
| 				case 0xe: | ||||
| 					if(value&0x80) | ||||
| 						registers_.interrupt_enable |= value; | ||||
| 					else | ||||
| 						registers_.interrupt_enable &= ~value; | ||||
| 					reevaluate_interrupts(); | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
| 		void set_register(int address, uint8_t value); | ||||
|  | ||||
| 		/*! Gets a register value. */ | ||||
| 		inline uint8_t get_register(int address) { | ||||
| 			address &= 0xf; | ||||
| //			printf("6522 %p: %d\n", this, 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 0x1: | ||||
| 					registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge); | ||||
| 					reevaluate_interrupts(); | ||||
| 				return get_port_input(Port::A, registers_.data_direction[0], registers_.output[0]); | ||||
|  | ||||
| 				case 0x2:	return registers_.data_direction[1]; | ||||
| 				case 0x3:	return registers_.data_direction[0]; | ||||
|  | ||||
| 				// Timer 1 | ||||
| 				case 0x4: | ||||
| 					registers_.interrupt_flags &= ~InterruptFlag::Timer1; | ||||
| 					reevaluate_interrupts(); | ||||
| 				return registers_.timer[0] & 0x00ff; | ||||
| 				case 0x5:	return registers_.timer[0] >> 8; | ||||
| 				case 0x6:	return registers_.timer_latch[0] & 0x00ff; | ||||
| 				case 0x7:	return registers_.timer_latch[0] >> 8; | ||||
|  | ||||
| 				// Timer 2 | ||||
| 				case 0x8: | ||||
| 					registers_.interrupt_flags &= ~InterruptFlag::Timer2; | ||||
| 					reevaluate_interrupts(); | ||||
| 				return registers_.timer[1] & 0x00ff; | ||||
| 				case 0x9:	return registers_.timer[1] >> 8; | ||||
|  | ||||
| 				case 0xa:	return registers_.shift; | ||||
|  | ||||
| 				case 0xb:	return registers_.auxiliary_control; | ||||
| 				case 0xc:	return registers_.peripheral_control; | ||||
|  | ||||
| 				case 0xd:	return registers_.interrupt_flags | (get_interrupt_line() ? 0x80 : 0x00); | ||||
| 				case 0xe:	return registers_.interrupt_enable | 0x80; | ||||
| 			} | ||||
|  | ||||
| 			return 0xff; | ||||
| 		} | ||||
|  | ||||
| 		inline void 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; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| #define 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] = (uint16_t)registers_.next_timer[0]; registers_.next_timer[0] = -1; }\ | ||||
| 	if(registers_.next_timer[1] >= 0) { registers_.timer[1] = (uint16_t)registers_.next_timer[1]; registers_.next_timer[1] = -1; }\ | ||||
|  | ||||
| 	// IRQ is raised on the half cycle after overflow | ||||
| #define phase1()	\ | ||||
| 	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. | ||||
|  | ||||
| 			Although the original chip accepts only a phase-2 input, timer reloads are specified as occuring | ||||
| 			1.5 cycles after the timer hits zero. It therefore may be necessary to emulate at half-cycle precision. | ||||
|  | ||||
| 			The first emulated half-cycle will be the period between the trailing edge of a phase-2 input and the | ||||
| 			next rising edge. So it should align with a full system's phase-1. The next emulated half-cycle will be | ||||
| 			that which occurs during phase-2. | ||||
|  | ||||
| 			Callers should decide whether they are going to use @c run_for_half_cycles or @c run_for_cycles, and not | ||||
| 			intermingle usage. | ||||
| 		*/ | ||||
| 		inline void run_for_half_cycles(unsigned int number_of_cycles) { | ||||
| 			if(is_phase2_) { | ||||
| 				phase2(); | ||||
| 				number_of_cycles--; | ||||
| 			} | ||||
|  | ||||
| 			while(number_of_cycles >= 2) { | ||||
| 				phase1(); | ||||
| 				phase2(); | ||||
| 				number_of_cycles -= 2; | ||||
| 			} | ||||
|  | ||||
| 			if(number_of_cycles) { | ||||
| 				phase1(); | ||||
| 				is_phase2_ = true; | ||||
| 			} else { | ||||
| 				is_phase2_ = false; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Runs for a specified number of cycles. | ||||
|  | ||||
| 			Callers should decide whether they are going to use @c run_for_half_cycles or @c run_for_cycles, and not | ||||
| 			intermingle usage. | ||||
| 		*/ | ||||
| 		inline void run_for_cycles(unsigned int number_of_cycles) { | ||||
| 			while(number_of_cycles--) { | ||||
| 				phase1(); | ||||
| 				phase2(); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| #undef phase1 | ||||
| #undef phase2 | ||||
|  | ||||
| 		/*! @returns @c true if the IRQ line is currently active; @c false otherwise. */ | ||||
| 		inline bool get_interrupt_line() { | ||||
| 			uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f; | ||||
| 			return !!interrupt_status; | ||||
| 		} | ||||
|  | ||||
| 		MOS6522() : | ||||
| 			timer_is_running_{false, false}, | ||||
| 			last_posted_interrupt_status_(false), | ||||
| 			is_phase2_(false) {} | ||||
| 		uint8_t get_register(int address); | ||||
|  | ||||
| 	private: | ||||
| 		// Expected to be overridden | ||||
| 		uint8_t get_port_input(Port port)										{	return 0xff;	} | ||||
| 		void set_port_output(Port port, uint8_t value, uint8_t direction_mask)	{} | ||||
| 		void set_control_line_output(Port port, Line line, bool value)			{} | ||||
| 		void set_interrupt_status(bool status)									{} | ||||
| 		T &bus_handler_; | ||||
|  | ||||
| 		// Input/output multiplexer | ||||
| 		uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output) { | ||||
| 			uint8_t input = static_cast<T *>(this)->get_port_input(port); | ||||
| 			return (input & ~output_mask) | (output & output_mask); | ||||
| 		} | ||||
|  | ||||
| 		// Phase toggle | ||||
| 		bool is_phase2_; | ||||
|  | ||||
| 		// Delegate and communications | ||||
| 		bool last_posted_interrupt_status_; | ||||
| 		inline void reevaluate_interrupts() { | ||||
| 			bool new_interrupt_status = get_interrupt_line(); | ||||
| 			if(new_interrupt_status != last_posted_interrupt_status_) { | ||||
| 				last_posted_interrupt_status_ = new_interrupt_status; | ||||
| 				static_cast<T *>(this)->set_interrupt_status(new_interrupt_status); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// The registers | ||||
| 		struct Registers { | ||||
| 			uint8_t output[2], input[2], data_direction[2]; | ||||
| 			uint16_t timer[2], timer_latch[2], last_timer[2]; | ||||
| 			int next_timer[2]; | ||||
| 			uint8_t shift; | ||||
| 			uint8_t auxiliary_control, peripheral_control; | ||||
| 			uint8_t interrupt_flags, interrupt_enable; | ||||
| 			bool timer_needs_reload; | ||||
|  | ||||
| 			// "A  low  reset  (RES)  input  clears  all  R6522  internal registers to logic 0" | ||||
| 			Registers() : | ||||
| 				output{0, 0}, input{0, 0}, data_direction{0, 0}, | ||||
| 				auxiliary_control(0), peripheral_control(0), | ||||
| 				interrupt_flags(0), interrupt_enable(0), | ||||
| 				last_timer{0, 0}, timer_needs_reload(false), | ||||
| 				next_timer{-1, -1} {} | ||||
| 		} registers_; | ||||
|  | ||||
| 		// control state | ||||
| 		struct { | ||||
| 			bool line_one, line_two; | ||||
| 		} control_inputs_[2]; | ||||
|  | ||||
| 		// Internal state other than the registers | ||||
| 		bool timer_is_running_[2]; | ||||
| 		uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output); | ||||
| 		inline void reevaluate_interrupts(); | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Provided for optional composition with @c MOS6522, @c MOS6522IRQDelegate provides for a delegate | ||||
| 	that will receive IRQ line change notifications. | ||||
| */ | ||||
| class MOS6522IRQDelegate { | ||||
| 	public: | ||||
| 		class Delegate { | ||||
| 			public: | ||||
| 				virtual void mos6522_did_change_interrupt_status(void *mos6522) = 0; | ||||
| 		}; | ||||
|  | ||||
| 		inline void set_interrupt_delegate(Delegate *delegate) { | ||||
| 			delegate_ = delegate; | ||||
| 		} | ||||
|  | ||||
| 		inline void set_interrupt_status(bool new_status) { | ||||
| 			if(delegate_) delegate_->mos6522_did_change_interrupt_status(this); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		Delegate *delegate_; | ||||
| }; | ||||
| #include "Implementation/6522Implementation.hpp" | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* _522_hpp */ | ||||
|   | ||||
							
								
								
									
										116
									
								
								Components/6522/Implementation/6522Base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								Components/6522/Implementation/6522Base.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| // | ||||
| //  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; | ||||
| } | ||||
							
								
								
									
										155
									
								
								Components/6522/Implementation/6522Implementation.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								Components/6522/Implementation/6522Implementation.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| // | ||||
| //  Implementation.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 04/09/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) { | ||||
| 	address &= 0xf; | ||||
| 	switch(address) { | ||||
| 		case 0x0: | ||||
| 			registers_.output[1] = value; | ||||
| 			bus_handler_.set_port_output(Port::B, value, registers_.data_direction[1]);	// TODO: handshake | ||||
|  | ||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | ((registers_.peripheral_control&0x20) ? 0 : InterruptFlag::CB2ActiveEdge)); | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
| 		case 0xf: | ||||
| 		case 0x1: | ||||
| 			registers_.output[0] = value; | ||||
| 			bus_handler_.set_port_output(Port::A, value, registers_.data_direction[0]);	// TODO: handshake | ||||
|  | ||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | ((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge)); | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
|  | ||||
| 		case 0x2: | ||||
| 			registers_.data_direction[1] = value; | ||||
| 		break; | ||||
| 		case 0x3: | ||||
| 			registers_.data_direction[0] = value; | ||||
| 		break; | ||||
|  | ||||
| 		// Timer 1 | ||||
| 		case 0x6:	case 0x4:	registers_.timer_latch[0] = (registers_.timer_latch[0]&0xff00) | value;	break; | ||||
| 		case 0x5:	case 0x7: | ||||
| 			registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | static_cast<uint16_t>(value << 8); | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer1; | ||||
| 			if(address == 0x05) { | ||||
| 				registers_.next_timer[0] = registers_.timer_latch[0]; | ||||
| 				timer_is_running_[0] = true; | ||||
| 			} | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
|  | ||||
| 		// Timer 2 | ||||
| 		case 0x8:	registers_.timer_latch[1] = value;	break; | ||||
| 		case 0x9: | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer2; | ||||
| 			registers_.next_timer[1] = registers_.timer_latch[1] | static_cast<uint16_t>(value << 8); | ||||
| 			timer_is_running_[1] = true; | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
|  | ||||
| 		// Shift | ||||
| 		case 0xa:	registers_.shift = value;				break; | ||||
|  | ||||
| 		// Control | ||||
| 		case 0xb: | ||||
| 			registers_.auxiliary_control = value; | ||||
| 		break; | ||||
| 		case 0xc: | ||||
| //			printf("Peripheral control %02x\n", value); | ||||
| 			registers_.peripheral_control = value; | ||||
|  | ||||
| 			// TODO: simplify below; trying to avoid improper logging of unimplemented warnings in input mode | ||||
| 			if(value & 0x08) { | ||||
| 				switch(value & 0x0e) { | ||||
| 					default: printf("Unimplemented control line mode %d\n", (value >> 1)&7);		break; | ||||
| 					case 0x0c:	bus_handler_.set_control_line_output(Port::A, Line::Two, false);	break; | ||||
| 					case 0x0e:	bus_handler_.set_control_line_output(Port::A, Line::Two, true);		break; | ||||
| 				} | ||||
| 			} | ||||
| 			if(value & 0x80) { | ||||
| 				switch(value & 0xe0) { | ||||
| 					default: printf("Unimplemented control line mode %d\n", (value >> 5)&7);		break; | ||||
| 					case 0xc0:	bus_handler_.set_control_line_output(Port::B, Line::Two, false);	break; | ||||
| 					case 0xe0:	bus_handler_.set_control_line_output(Port::B, Line::Two, true);		break; | ||||
| 				} | ||||
| 			} | ||||
| 		break; | ||||
|  | ||||
| 		// Interrupt control | ||||
| 		case 0xd: | ||||
| 			registers_.interrupt_flags &= ~value; | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
| 		case 0xe: | ||||
| 			if(value&0x80) | ||||
| 				registers_.interrupt_enable |= value; | ||||
| 			else | ||||
| 				registers_.interrupt_enable &= ~value; | ||||
| 			reevaluate_interrupts(); | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <typename T> uint8_t MOS6522<T>::get_register(int address) { | ||||
| 	address &= 0xf; | ||||
| 	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 0x1: | ||||
| 			registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge); | ||||
| 			reevaluate_interrupts(); | ||||
| 		return get_port_input(Port::A, registers_.data_direction[0], registers_.output[0]); | ||||
|  | ||||
| 		case 0x2:	return registers_.data_direction[1]; | ||||
| 		case 0x3:	return registers_.data_direction[0]; | ||||
|  | ||||
| 		// Timer 1 | ||||
| 		case 0x4: | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer1; | ||||
| 			reevaluate_interrupts(); | ||||
| 		return registers_.timer[0] & 0x00ff; | ||||
| 		case 0x5:	return registers_.timer[0] >> 8; | ||||
| 		case 0x6:	return registers_.timer_latch[0] & 0x00ff; | ||||
| 		case 0x7:	return registers_.timer_latch[0] >> 8; | ||||
|  | ||||
| 		// Timer 2 | ||||
| 		case 0x8: | ||||
| 			registers_.interrupt_flags &= ~InterruptFlag::Timer2; | ||||
| 			reevaluate_interrupts(); | ||||
| 		return registers_.timer[1] & 0x00ff; | ||||
| 		case 0x9:	return registers_.timer[1] >> 8; | ||||
|  | ||||
| 		case 0xa:	return registers_.shift; | ||||
|  | ||||
| 		case 0xb:	return registers_.auxiliary_control; | ||||
| 		case 0xc:	return registers_.peripheral_control; | ||||
|  | ||||
| 		case 0xd:	return registers_.interrupt_flags | (get_interrupt_line() ? 0x80 : 0x00); | ||||
| 		case 0xe:	return registers_.interrupt_enable | 0x80; | ||||
| 	} | ||||
|  | ||||
| 	return 0xff; | ||||
| } | ||||
|  | ||||
| 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); | ||||
| 	return (input & ~output_mask) | (output & output_mask); | ||||
| } | ||||
|  | ||||
| // Delegate and communications | ||||
| 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_.set_interrupt_status(new_interrupt_status); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										63
									
								
								Components/6522/Implementation/6522Storage.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Components/6522/Implementation/6522Storage.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| // | ||||
| //  6522Storage.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 04/09/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef _522Storage_hpp | ||||
| #define _522Storage_hpp | ||||
|  | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace MOS { | ||||
| namespace MOS6522 { | ||||
|  | ||||
| class MOS6522Storage { | ||||
| 	protected: | ||||
| 		// Phase toggle | ||||
| 		bool is_phase2_ = false; | ||||
|  | ||||
| 		// The registers | ||||
| 		struct Registers { | ||||
| 			// "A  low  reset  (RES)  input  clears  all  R6522  internal registers to logic 0" | ||||
| 			uint8_t output[2] = {0, 0}; | ||||
| 			uint8_t input[2] = {0, 0}; | ||||
| 			uint8_t data_direction[2] = {0, 0}; | ||||
| 			uint16_t timer[2] = {0, 0}; | ||||
| 			uint16_t timer_latch[2] = {0, 0}; | ||||
| 			uint16_t last_timer[2] = {0, 0}; | ||||
| 			int next_timer[2] = {-1, -1}; | ||||
| 			uint8_t shift = 0; | ||||
| 			uint8_t auxiliary_control = 0; | ||||
| 			uint8_t peripheral_control = 0; | ||||
| 			uint8_t interrupt_flags = 0; | ||||
| 			uint8_t interrupt_enable = 0; | ||||
| 			bool timer_needs_reload = false; | ||||
| 		} registers_; | ||||
|  | ||||
| 		// control state | ||||
| 		struct { | ||||
| 			bool line_one = false; | ||||
| 			bool line_two = false; | ||||
| 		} control_inputs_[2]; | ||||
|  | ||||
| 		bool timer_is_running_[2] = {false, false}; | ||||
| 		bool last_posted_interrupt_status_ = false; | ||||
|  | ||||
| 		enum InterruptFlag: uint8_t { | ||||
| 			CA2ActiveEdge	= 1 << 0, | ||||
| 			CA1ActiveEdge	= 1 << 1, | ||||
| 			ShiftRegister	= 1 << 2, | ||||
| 			CB2ActiveEdge	= 1 << 3, | ||||
| 			CB1ActiveEdge	= 1 << 4, | ||||
| 			Timer2			= 1 << 5, | ||||
| 			Timer1			= 1 << 6, | ||||
| 		}; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* _522Storage_hpp */ | ||||
							
								
								
									
										19
									
								
								Components/6522/Implementation/IRQDelegatePortHandler.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Components/6522/Implementation/IRQDelegatePortHandler.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  IRQDelegatePortHandler.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 IRQDelegatePortHandler::set_interrupt_delegate(Delegate *delegate) { | ||||
| 	delegate_ = delegate; | ||||
| } | ||||
|  | ||||
| void IRQDelegatePortHandler::set_interrupt_status(bool new_status) { | ||||
| 	if(delegate_) delegate_->mos6522_did_change_interrupt_status(this); | ||||
| } | ||||
| @@ -12,6 +12,8 @@ | ||||
| #include <cstdint> | ||||
| #include <cstdio> | ||||
|  | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
|  | ||||
| namespace MOS { | ||||
|  | ||||
| /*! | ||||
| @@ -49,7 +51,7 @@ template <class T> class MOS6532 { | ||||
| 				case 0x04: case 0x05: case 0x06: case 0x07: | ||||
| 					if(address & 0x10) { | ||||
| 						timer_.writtenShift = timer_.activeShift = (decodedAddress - 0x04) * 3 + (decodedAddress / 0x07);	// i.e. 0, 3, 6, 10 | ||||
| 						timer_.value = ((unsigned int)value << timer_.activeShift) ; | ||||
| 						timer_.value = (static_cast<unsigned int>(value) << timer_.activeShift) ; | ||||
| 						timer_.interrupt_enabled = !!(address&0x08); | ||||
| 						interrupt_status_ &= ~InterruptFlag::Timer; | ||||
| 						evaluate_interrupts(); | ||||
| @@ -77,7 +79,7 @@ template <class T> class MOS6532 { | ||||
|  | ||||
| 				// Timer and interrupt control | ||||
| 				case 0x04: case 0x06: { | ||||
| 					uint8_t value = (uint8_t)(timer_.value >> timer_.activeShift); | ||||
| 					uint8_t value = static_cast<uint8_t>(timer_.value >> timer_.activeShift); | ||||
| 					timer_.interrupt_enabled = !!(address&0x08); | ||||
| 					interrupt_status_ &= ~InterruptFlag::Timer; | ||||
| 					evaluate_interrupts(); | ||||
| @@ -104,7 +106,9 @@ template <class T> class MOS6532 { | ||||
| 			return 0xff; | ||||
| 		} | ||||
|  | ||||
| 		inline void run_for_cycles(unsigned int number_of_cycles) { | ||||
| 		inline void run_for(const Cycles cycles) { | ||||
| 			unsigned int number_of_cycles = static_cast<unsigned int>(cycles.as_int()); | ||||
|  | ||||
| 			// permit counting _to_ zero; counting _through_ zero initiates the other behaviour | ||||
| 			if(timer_.value >= number_of_cycles) { | ||||
| 				timer_.value -= number_of_cycles; | ||||
| @@ -117,12 +121,9 @@ template <class T> class MOS6532 { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		MOS6532() : | ||||
| 			interrupt_status_(0), | ||||
| 			port_{{.output_mask = 0, .output = 0}, {.output_mask = 0, .output = 0}}, | ||||
| 			a7_interrupt_({.last_port_value = 0, .enabled = false}), | ||||
| 			interrupt_line_(false), | ||||
| 			timer_{.value = (unsigned int)((rand() & 0xff) << 10), .activeShift = 10, .writtenShift = 10, .interrupt_enabled = false} {} | ||||
| 		MOS6532() { | ||||
| 			timer_.value = static_cast<unsigned int>((rand() & 0xff) << 10); | ||||
| 		} | ||||
|  | ||||
| 		inline void set_port_did_change(int port) { | ||||
| 			if(!port) { | ||||
| @@ -150,26 +151,26 @@ template <class T> class MOS6532 { | ||||
|  | ||||
| 		struct { | ||||
| 			unsigned int value; | ||||
| 			unsigned int activeShift, writtenShift; | ||||
| 			bool interrupt_enabled; | ||||
| 			unsigned int activeShift = 10, writtenShift = 10; | ||||
| 			bool interrupt_enabled = false; | ||||
| 		} timer_; | ||||
|  | ||||
| 		struct { | ||||
| 			bool enabled; | ||||
| 			bool active_on_positive; | ||||
| 			uint8_t last_port_value; | ||||
| 			bool enabled = false; | ||||
| 			bool active_on_positive = false; | ||||
| 			uint8_t last_port_value = 0; | ||||
| 		} a7_interrupt_; | ||||
|  | ||||
| 		struct { | ||||
| 			uint8_t output_mask, output; | ||||
| 			uint8_t output_mask = 0, output = 0; | ||||
| 		} port_[2]; | ||||
|  | ||||
| 		uint8_t interrupt_status_; | ||||
| 		uint8_t interrupt_status_ = 0; | ||||
| 		enum InterruptFlag: uint8_t { | ||||
| 			Timer = 0x80, | ||||
| 			PA7 = 0x40 | ||||
| 		}; | ||||
| 		bool interrupt_line_; | ||||
| 		bool interrupt_line_ = false; | ||||
|  | ||||
| 		// expected to be overridden | ||||
| 		uint8_t get_port_input(int port)										{	return 0xff;	} | ||||
|   | ||||
| @@ -8,22 +8,22 @@ | ||||
|  | ||||
| #include "6560.hpp" | ||||
|  | ||||
| #include <cstring> | ||||
|  | ||||
| using namespace MOS; | ||||
|  | ||||
| Speaker::Speaker() : | ||||
| 	volume_(0), | ||||
| 	control_registers_{0, 0, 0, 0}, | ||||
| 	shift_registers_{0, 0, 0, 0}, | ||||
| 	counters_{2, 1, 0, 0} {}	// create a slight phase offset for the three channels | ||||
| AudioGenerator::AudioGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue) : | ||||
| 	audio_queue_(audio_queue) {} | ||||
|  | ||||
| void Speaker::set_volume(uint8_t volume) { | ||||
| 	enqueue([=]() { | ||||
|  | ||||
| void AudioGenerator::set_volume(uint8_t volume) { | ||||
| 	audio_queue_.defer([=]() { | ||||
| 		volume_ = volume; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void Speaker::set_control(int channel, uint8_t value) { | ||||
| 	enqueue([=]() { | ||||
| void AudioGenerator::set_control(int channel, uint8_t value) { | ||||
| 	audio_queue_.defer([=]() { | ||||
| 		control_registers_[channel] = value; | ||||
| 	}); | ||||
| } | ||||
| @@ -98,14 +98,14 @@ static uint8_t noise_pattern[] = { | ||||
|  | ||||
| #define shift(r) shift_registers_[r] = (shift_registers_[r] << 1) | (((shift_registers_[r]^0x80)&control_registers_[r]) >> 7) | ||||
| #define increment(r) shift_registers_[r] = (shift_registers_[r]+1)%8191 | ||||
| #define update(r, m, up) counters_[r]++; if((counters_[r] >> m) == 0x80) { up(r); counters_[r] = (unsigned int)(control_registers_[r]&0x7f) << m; } | ||||
| #define update(r, m, up) counters_[r]++; if((counters_[r] >> m) == 0x80) { up(r); counters_[r] = static_cast<unsigned int>(control_registers_[r]&0x7f) << m; } | ||||
| // Note on slightly askew test: as far as I can make out, if the value in the register is 0x7f then what's supposed to happen | ||||
| // is that the 0x7f is loaded, on the next clocked cycle the Vic spots a 0x7f, pumps the output, reloads, etc. No increment | ||||
| // ever occurs. It's conditional. I don't really want two conditionals if I can avoid it so I'm incrementing regardless and | ||||
| // testing against 0x80. The effect should be the same: loading with 0x7f means an output update every cycle, loading with 0x7e | ||||
| // means every second cycle, etc. | ||||
|  | ||||
| void Speaker::get_samples(unsigned int number_of_samples, int16_t *target) { | ||||
| void AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| 	for(unsigned int c = 0; c < number_of_samples; c++) { | ||||
| 		update(0, 2, shift); | ||||
| 		update(1, 1, shift); | ||||
| @@ -123,7 +123,7 @@ void Speaker::get_samples(unsigned int number_of_samples, int16_t *target) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void Speaker::skip_samples(unsigned int number_of_samples) { | ||||
| void AudioGenerator::skip_samples(std::size_t number_of_samples) { | ||||
| 	for(unsigned int c = 0; c < number_of_samples; c++) { | ||||
| 		update(0, 2, shift); | ||||
| 		update(1, 1, shift); | ||||
|   | ||||
| @@ -9,27 +9,32 @@ | ||||
| #ifndef _560_hpp | ||||
| #define _560_hpp | ||||
|  | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
| #include "../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../Outputs/Speaker.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
|  | ||||
| namespace MOS { | ||||
|  | ||||
| // audio state | ||||
| class Speaker: public ::Outputs::Filter<Speaker> { | ||||
| class AudioGenerator: public ::Outputs::Speaker::SampleSource { | ||||
| 	public: | ||||
| 		Speaker(); | ||||
| 		AudioGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue); | ||||
|  | ||||
| 		void set_volume(uint8_t volume); | ||||
| 		void set_control(int channel, uint8_t value); | ||||
|  | ||||
| 		void get_samples(unsigned int number_of_samples, int16_t *target); | ||||
| 		void skip_samples(unsigned int number_of_samples); | ||||
| 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||
| 		void skip_samples(std::size_t number_of_samples); | ||||
|  | ||||
| 	private: | ||||
| 		unsigned int counters_[4]; | ||||
| 		unsigned int shift_registers_[4]; | ||||
| 		uint8_t control_registers_[4]; | ||||
| 		uint8_t volume_; | ||||
| 		Concurrency::DeferringAsyncTaskQueue &audio_queue_; | ||||
|  | ||||
| 		unsigned int counters_[4] = {2, 1, 0, 0}; 	// create a slight phase offset for the three channels | ||||
| 		unsigned int shift_registers_[4] = {0, 0, 0, 0}; | ||||
| 		uint8_t control_registers_[4] = {0, 0, 0, 0}; | ||||
| 		uint8_t volume_ = 0; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| @@ -43,13 +48,10 @@ class Speaker: public ::Outputs::Filter<Speaker> { | ||||
| template <class T> class MOS6560 { | ||||
| 	public: | ||||
| 		MOS6560() : | ||||
| 				crt_(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::NTSC60, 2)), | ||||
| 				speaker_(new Speaker), | ||||
| 				horizontal_counter_(0), | ||||
| 				vertical_counter_(0), | ||||
| 				cycles_since_speaker_update_(0), | ||||
| 				is_odd_frame_(false), | ||||
| 				is_odd_line_(false) { | ||||
| 				crt_(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::DisplayType::NTSC60, 2)), | ||||
| 				audio_generator_(audio_queue_), | ||||
| 				speaker_(audio_generator_) | ||||
| 		{ | ||||
| 			crt_->set_composite_sampling_function( | ||||
| 				"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)" | ||||
| 				"{" | ||||
| @@ -65,11 +67,15 @@ template <class T> class MOS6560 { | ||||
| 		} | ||||
|  | ||||
| 		void set_clock_rate(double clock_rate) { | ||||
| 			speaker_->set_input_rate((float)(clock_rate / 4.0)); | ||||
| 			speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0)); | ||||
| 		} | ||||
|  | ||||
| 		std::shared_ptr<Outputs::CRT::CRT> get_crt() { return crt_; } | ||||
| 		std::shared_ptr<Outputs::Speaker> get_speaker() { return speaker_; } | ||||
| 		Outputs::CRT::CRT *get_crt() { return crt_.get(); } | ||||
| 		Outputs::Speaker::Speaker *get_speaker() { return &speaker_; } | ||||
|  | ||||
| 		void set_high_frequency_cutoff(float cutoff) { | ||||
| 			speaker_.set_high_frequency_cutoff(cutoff); | ||||
| 		} | ||||
|  | ||||
| 		enum OutputMode { | ||||
| 			PAL, NTSC | ||||
| @@ -99,18 +105,18 @@ template <class T> class MOS6560 { | ||||
| 				8,		88,		120,	56, | ||||
| 			}; | ||||
| 			const uint8_t ntsc_chrominances[16] = { | ||||
| 				255,	255,	40,		104, | ||||
| 				64,		120,	80,		16, | ||||
| 				32,		32,		40,		104, | ||||
| 				64,		120,	80,		16, | ||||
| 				255,	255,	8,		72, | ||||
| 				32,		88,		48,		112, | ||||
| 				0,		0,		8,		72, | ||||
| 				32,		88,		48,		112, | ||||
| 			}; | ||||
| 			const uint8_t *chrominances; | ||||
| 			Outputs::CRT::DisplayType display_type; | ||||
|  | ||||
| 			switch(output_mode) { | ||||
| 				case OutputMode::PAL: | ||||
| 				default: | ||||
| 					chrominances = pal_chrominances; | ||||
| 					display_type = Outputs::CRT::PAL50; | ||||
| 					display_type = Outputs::CRT::DisplayType::PAL50; | ||||
| 					timing_.cycles_per_line = 71; | ||||
| 					timing_.line_counter_increment_offset = 0; | ||||
| 					timing_.lines_per_progressive_field = 312; | ||||
| @@ -119,7 +125,7 @@ template <class T> class MOS6560 { | ||||
|  | ||||
| 				case OutputMode::NTSC: | ||||
| 					chrominances = ntsc_chrominances; | ||||
| 					display_type = Outputs::CRT::NTSC60; | ||||
| 					display_type = Outputs::CRT::DisplayType::NTSC60; | ||||
| 					timing_.cycles_per_line = 65; | ||||
| 					timing_.line_counter_increment_offset = 65 - 33;	// TODO: this is a bit of a hack; separate vertical and horizontal counting | ||||
| 					timing_.lines_per_progressive_field = 261; | ||||
| @@ -127,7 +133,7 @@ template <class T> class MOS6560 { | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			crt_->set_new_display_type((unsigned int)(timing_.cycles_per_line*4), display_type); | ||||
| 			crt_->set_new_display_type(static_cast<unsigned int>(timing_.cycles_per_line*4), display_type); | ||||
| 			crt_->set_visible_area(Outputs::CRT::Rect(0.05f, 0.05f, 0.9f, 0.9f)); | ||||
|  | ||||
| //			switch(output_mode) { | ||||
| @@ -140,7 +146,7 @@ template <class T> class MOS6560 { | ||||
| //			} | ||||
|  | ||||
| 			for(int c = 0; c < 16; c++) { | ||||
| 				uint8_t *colour = (uint8_t *)&colours_[c]; | ||||
| 				uint8_t *colour = reinterpret_cast<uint8_t *>(&colours_[c]); | ||||
| 				colour[0] = luminances[c]; | ||||
| 				colour[1] = chrominances[c]; | ||||
| 			} | ||||
| @@ -149,10 +155,11 @@ template <class T> class MOS6560 { | ||||
| 		/*! | ||||
| 			Runs for cycles. Derr. | ||||
| 		*/ | ||||
| 		inline void run_for_cycles(unsigned int number_of_cycles) { | ||||
| 		inline void run_for(const Cycles cycles) { | ||||
| 			// keep track of the amount of time since the speaker was updated; lazy updates are applied | ||||
| 			cycles_since_speaker_update_ += number_of_cycles; | ||||
| 			cycles_since_speaker_update_ += cycles; | ||||
|  | ||||
| 			int number_of_cycles = cycles.as_int(); | ||||
| 			while(number_of_cycles--) { | ||||
| 				// keep an old copy of the vertical count because that test is a cycle later than the actual changes | ||||
| 				int previous_vertical_counter = vertical_counter_; | ||||
| @@ -216,7 +223,7 @@ template <class T> class MOS6560 { | ||||
| 					if(column_counter_&1) { | ||||
| 						fetch_address = registers_.character_cell_start_address + (character_code_*(registers_.tall_characters ? 16 : 8)) + current_character_row_; | ||||
| 					} else { | ||||
| 						fetch_address = (uint16_t)(registers_.video_matrix_start_address + video_matrix_address_counter_); | ||||
| 						fetch_address = static_cast<uint16_t>(registers_.video_matrix_start_address + video_matrix_address_counter_); | ||||
| 						video_matrix_address_counter_++; | ||||
| 						if( | ||||
| 							(current_character_row_ == 15) || | ||||
| @@ -268,7 +275,7 @@ template <class T> class MOS6560 { | ||||
|  | ||||
| 					pixel_pointer = nullptr; | ||||
| 					if(output_state_ == State::Pixels) { | ||||
| 						pixel_pointer = (uint16_t *)crt_->allocate_write_area(260); | ||||
| 						pixel_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(260)); | ||||
| 					} | ||||
| 				} | ||||
| 				cycles_in_state_++; | ||||
| @@ -323,7 +330,10 @@ template <class T> class MOS6560 { | ||||
| 		/*! | ||||
| 			Causes the 6560 to flush as much pending CRT and speaker communications as possible. | ||||
| 		*/ | ||||
| 		inline void flush() { update_audio(); speaker_->flush(); } | ||||
| 		inline void flush() { | ||||
| 			update_audio(); | ||||
| 			audio_queue_.perform(); | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Writes to a 6560 register. | ||||
| @@ -343,7 +353,7 @@ template <class T> class MOS6560 { | ||||
|  | ||||
| 				case 0x2: | ||||
| 					registers_.number_of_columns = value & 0x7f; | ||||
| 					registers_.video_matrix_start_address = (uint16_t)((registers_.video_matrix_start_address & 0x3c00) | ((value & 0x80) << 2)); | ||||
| 					registers_.video_matrix_start_address = static_cast<uint16_t>((registers_.video_matrix_start_address & 0x3c00) | ((value & 0x80) << 2)); | ||||
| 				break; | ||||
|  | ||||
| 				case 0x3: | ||||
| @@ -352,8 +362,8 @@ template <class T> class MOS6560 { | ||||
| 				break; | ||||
|  | ||||
| 				case 0x5: | ||||
| 					registers_.character_cell_start_address = (uint16_t)((value & 0x0f) << 10); | ||||
| 					registers_.video_matrix_start_address = (uint16_t)((registers_.video_matrix_start_address & 0x0200) | ((value & 0xf0) << 6)); | ||||
| 					registers_.character_cell_start_address = static_cast<uint16_t>((value & 0x0f) << 10); | ||||
| 					registers_.video_matrix_start_address = static_cast<uint16_t>((registers_.video_matrix_start_address & 0x0200) | ((value & 0xf0) << 6)); | ||||
| 				break; | ||||
|  | ||||
| 				case 0xa: | ||||
| @@ -361,13 +371,13 @@ template <class T> class MOS6560 { | ||||
| 				case 0xc: | ||||
| 				case 0xd: | ||||
| 					update_audio(); | ||||
| 					speaker_->set_control(address - 0xa, value); | ||||
| 					audio_generator_.set_control(address - 0xa, value); | ||||
| 				break; | ||||
|  | ||||
| 				case 0xe: | ||||
| 					update_audio(); | ||||
| 					registers_.auxiliary_colour = colours_[value >> 4]; | ||||
| 					speaker_->set_volume(value & 0xf); | ||||
| 					audio_generator_.set_volume(value & 0xf); | ||||
| 				break; | ||||
|  | ||||
| 				case 0xf: { | ||||
| @@ -397,19 +407,21 @@ template <class T> class MOS6560 { | ||||
| 			int current_line = (full_frame_counter_ + timing_.line_counter_increment_offset) / timing_.cycles_per_line; | ||||
| 			switch(address) { | ||||
| 				default: return registers_.direct_values[address]; | ||||
| 				case 0x03: return (uint8_t)(current_line << 7) | (registers_.direct_values[3] & 0x7f); | ||||
| 				case 0x03: return static_cast<uint8_t>(current_line << 7) | (registers_.direct_values[3] & 0x7f); | ||||
| 				case 0x04: return (current_line >> 1) & 0xff; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		std::shared_ptr<Outputs::CRT::CRT> crt_; | ||||
| 		std::unique_ptr<Outputs::CRT::CRT> crt_; | ||||
|  | ||||
| 		std::shared_ptr<Speaker> speaker_; | ||||
| 		unsigned int cycles_since_speaker_update_; | ||||
| 		Concurrency::DeferringAsyncTaskQueue audio_queue_; | ||||
| 		AudioGenerator audio_generator_; | ||||
| 		Outputs::Speaker::LowpassSpeaker<AudioGenerator> speaker_; | ||||
|  | ||||
| 		Cycles cycles_since_speaker_update_; | ||||
| 		void update_audio() { | ||||
| 			speaker_->run_for_cycles(cycles_since_speaker_update_ >> 2); | ||||
| 			cycles_since_speaker_update_ &= 3; | ||||
| 			speaker_.run_for(audio_queue_, Cycles(cycles_since_speaker_update_.divide(Cycles(4)))); | ||||
| 		} | ||||
|  | ||||
| 		// register state | ||||
| @@ -431,7 +443,7 @@ template <class T> class MOS6560 { | ||||
| 		unsigned int cycles_in_state_; | ||||
|  | ||||
| 		// counters that cover an entire field | ||||
| 		int horizontal_counter_, vertical_counter_, full_frame_counter_; | ||||
| 		int horizontal_counter_ = 0, vertical_counter_ = 0, full_frame_counter_; | ||||
|  | ||||
| 		// latches dictating start and length of drawing | ||||
| 		bool vertical_drawing_latch_, horizontal_drawing_latch_; | ||||
| @@ -446,14 +458,14 @@ template <class T> class MOS6560 { | ||||
| 		// data latched from the bus | ||||
| 		uint8_t character_code_, character_colour_, character_value_; | ||||
|  | ||||
| 		bool is_odd_frame_, is_odd_line_; | ||||
| 		bool is_odd_frame_ = false, is_odd_line_ = false; | ||||
|  | ||||
| 		// lookup table from 6560 colour index to appropriate PAL/NTSC value | ||||
| 		uint16_t colours_[16]; | ||||
|  | ||||
| 		uint16_t *pixel_pointer; | ||||
| 		void output_border(unsigned int number_of_cycles) { | ||||
| 			uint16_t *colour_pointer = (uint16_t *)crt_->allocate_write_area(1); | ||||
| 			uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(1)); | ||||
| 			if(colour_pointer) *colour_pointer = registers_.borderColour; | ||||
| 			crt_->output_level(number_of_cycles); | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										275
									
								
								Components/6845/CRTC6845.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								Components/6845/CRTC6845.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | ||||
| // | ||||
| //  CRTC6845.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 31/07/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef CRTC6845_hpp | ||||
| #define CRTC6845_hpp | ||||
|  | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <cstdio> | ||||
|  | ||||
| namespace Motorola { | ||||
| namespace CRTC { | ||||
|  | ||||
| struct BusState { | ||||
| 	bool display_enable = false; | ||||
| 	bool hsync = false; | ||||
| 	bool vsync = false; | ||||
| 	bool cursor = false; | ||||
| 	uint16_t refresh_address = 0; | ||||
| 	uint16_t row_address = 0; | ||||
| }; | ||||
|  | ||||
| class BusHandler { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			Performs the first phase of a 6845 bus cycle; this is the phase in which it is intended that | ||||
| 			systems using the 6845 respect the bus state and produce pixels, sync or whatever they require. | ||||
| 		*/ | ||||
| 		void perform_bus_cycle_phase1(const BusState &) {} | ||||
|  | ||||
| 		/*! | ||||
| 			Performs the second phase of a 6845 bus cycle. Some bus state — including sync — is updated | ||||
| 			directly after phase 1 and hence is visible to an observer during phase 2. Handlers may therefore | ||||
| 			implement @c perform_bus_cycle_phase2 to be notified of the availability of that state without | ||||
| 			having to wait until the next cycle has begun. | ||||
| 		*/ | ||||
| 		void perform_bus_cycle_phase2(const BusState &) {} | ||||
| }; | ||||
|  | ||||
| enum Personality { | ||||
| 	HD6845S,	// Type 0 in CPC parlance. Zero-width HSYNC available, no status, programmable VSYNC length. | ||||
| 				// Considered exactly identical to the UM6845, so this enum covers both. | ||||
| 	UM6845R,	// Type 1 in CPC parlance. Status register, fixed-length VSYNC. | ||||
| 	MC6845,		// Type 2. No status register, fixed-length VSYNC, no zero-length HSYNC. | ||||
| 	AMS40226	// Type 3. Status is get register, fixed-length VSYNC, no zero-length HSYNC. | ||||
| }; | ||||
|  | ||||
| // TODO UM6845R and R12/R13; see http://www.cpcwiki.eu/index.php/CRTC#CRTC_Differences | ||||
|  | ||||
| template <class T> class CRTC6845 { | ||||
| 	public: | ||||
|  | ||||
| 		CRTC6845(Personality p, T &bus_handler) noexcept : | ||||
| 			personality_(p), bus_handler_(bus_handler), status_(0) {} | ||||
|  | ||||
| 		void select_register(uint8_t r) { | ||||
| 			selected_register_ = r; | ||||
| 		} | ||||
|  | ||||
| 		uint8_t get_status() { | ||||
| 			switch(personality_) { | ||||
| 				case UM6845R:	return status_ | (bus_state_.vsync ? 0x20 : 0x00); | ||||
| 				case AMS40226:	return get_register(); | ||||
| 				default:		return 0xff; | ||||
| 			} | ||||
| 			return 0xff; | ||||
| 		} | ||||
|  | ||||
| 		uint8_t get_register() { | ||||
| 			if(selected_register_ == 31) status_ &= ~0x80; | ||||
| 			if(selected_register_ == 16 || selected_register_ == 17) status_ &= ~0x40; | ||||
|  | ||||
| 			if(personality_ == UM6845R && selected_register_ == 31) return dummy_register_; | ||||
| 			if(selected_register_ < 12 || selected_register_ > 17) return 0xff; | ||||
| 			return registers_[selected_register_]; | ||||
| 		} | ||||
|  | ||||
| 		void set_register(uint8_t value) { | ||||
| 			static uint8_t masks[] = { | ||||
| 				0xff, 0xff, 0xff, 0xff, 0x7f, 0x1f, 0x7f, 0x7f, | ||||
| 				0xff, 0x1f, 0x7f, 0x1f, 0x3f, 0xff, 0x3f, 0xff | ||||
| 			}; | ||||
|  | ||||
| 			// Per CPC documentation, skew doesn't work on a "type 1 or 2", i.e. an MC6845 or a UM6845R. | ||||
| 			if(selected_register_ == 8 && personality_ != UM6845R && personality_ != MC6845) { | ||||
| 				switch((value >> 4)&3) { | ||||
| 					default:	display_skew_mask_ = 1;		break; | ||||
| 					case 1:		display_skew_mask_ = 2;		break; | ||||
| 					case 2:		display_skew_mask_ = 4;		break; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if(selected_register_ < 16) { | ||||
| 				registers_[selected_register_] = value & masks[selected_register_]; | ||||
| 			} | ||||
| 			if(selected_register_ == 31 && personality_ == UM6845R) { | ||||
| 				dummy_register_ = value; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		void trigger_light_pen() { | ||||
| 			registers_[17] = bus_state_.refresh_address & 0xff; | ||||
| 			registers_[16] = bus_state_.refresh_address >> 8; | ||||
| 			status_ |= 0x40; | ||||
| 		} | ||||
|  | ||||
| 		void run_for(Cycles cycles) { | ||||
| 			int cyles_remaining = cycles.as_int(); | ||||
| 			while(cyles_remaining--) { | ||||
| 				// check for end of visible characters | ||||
| 				if(character_counter_ == registers_[1]) { | ||||
| 					// TODO: consider skew in character_is_visible_. Or maybe defer until perform_bus_cycle? | ||||
| 					character_is_visible_ = false; | ||||
| 					end_of_line_address_ = bus_state_.refresh_address; | ||||
| 				} | ||||
|  | ||||
| 				perform_bus_cycle_phase1(); | ||||
| 				bus_state_.refresh_address = (bus_state_.refresh_address + 1) & 0x3fff; | ||||
|  | ||||
| 				// check for end-of-line | ||||
| 				if(character_counter_ == registers_[0]) { | ||||
| 					character_counter_ = 0; | ||||
| 					do_end_of_line(); | ||||
| 					character_is_visible_ = true; | ||||
| 				} else { | ||||
| 					// increment counter | ||||
| 					character_counter_++; | ||||
| 				} | ||||
|  | ||||
| 				// check for start of horizontal sync | ||||
| 				if(character_counter_ == registers_[2]) { | ||||
| 					hsync_counter_ = 0; | ||||
| 					bus_state_.hsync = true; | ||||
| 				} | ||||
|  | ||||
| 				// check for end of horizontal sync; note that a sync time of zero will result in an immediate | ||||
| 				// cancellation of the plan to perform sync if this is an HD6845S or UM6845R; otherwise zero | ||||
| 				// will end up counting as 16 as it won't be checked until after overflow. | ||||
| 				if(bus_state_.hsync) { | ||||
| 					switch(personality_) { | ||||
| 						case HD6845S: | ||||
| 						case UM6845R: | ||||
| 							bus_state_.hsync = hsync_counter_ != (registers_[3] & 15); | ||||
| 							hsync_counter_ = (hsync_counter_ + 1) & 15; | ||||
| 						break; | ||||
| 						default: | ||||
| 							hsync_counter_ = (hsync_counter_ + 1) & 15; | ||||
| 							bus_state_.hsync = hsync_counter_ != (registers_[3] & 15); | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				perform_bus_cycle_phase2(); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		const BusState &get_bus_state() const { | ||||
| 			return bus_state_; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		inline void perform_bus_cycle_phase1() { | ||||
| 			// Skew theory of operation: keep a history of the last three states, and apply whichever is selected. | ||||
| 			character_is_visible_shifter_ = (character_is_visible_shifter_ << 1) | static_cast<unsigned int>(character_is_visible_); | ||||
| 			bus_state_.display_enable = (static_cast<int>(character_is_visible_shifter_) & display_skew_mask_) && line_is_visible_; | ||||
| 			bus_handler_.perform_bus_cycle_phase1(bus_state_); | ||||
| 		} | ||||
|  | ||||
| 		inline void perform_bus_cycle_phase2() { | ||||
| 			bus_handler_.perform_bus_cycle_phase2(bus_state_); | ||||
| 		} | ||||
|  | ||||
| 		inline void do_end_of_line() { | ||||
| 			// check for end of vertical sync | ||||
| 			if(bus_state_.vsync) { | ||||
| 				vsync_counter_ = (vsync_counter_ + 1) & 15; | ||||
| 				// on the UM6845R and AMS40226, honour the programmed vertical sync time; on the other CRTCs | ||||
| 				// always use a vertical sync count of 16. | ||||
| 				switch(personality_) { | ||||
| 					case HD6845S: | ||||
| 					case AMS40226: | ||||
| 						bus_state_.vsync = vsync_counter_ != (registers_[3] >> 4); | ||||
| 					break; | ||||
| 					default: | ||||
| 						bus_state_.vsync = vsync_counter_ != 0; | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if(is_in_adjustment_period_) { | ||||
| 				line_counter_++; | ||||
| 				if(line_counter_ == registers_[5]) { | ||||
| 					is_in_adjustment_period_ = false; | ||||
| 					do_end_of_frame(); | ||||
| 				} | ||||
| 			} else { | ||||
| 				// advance vertical counter | ||||
| 				if(bus_state_.row_address == registers_[9]) { | ||||
| 					bus_state_.row_address = 0; | ||||
| 					line_address_ = end_of_line_address_; | ||||
|  | ||||
| 					// check for entry into the overflow area | ||||
| 					if(line_counter_ == registers_[4]) { | ||||
| 						if(registers_[5]) { | ||||
| 							line_counter_ = 0; | ||||
| 							is_in_adjustment_period_ = true; | ||||
| 						} else { | ||||
| 							do_end_of_frame(); | ||||
| 						} | ||||
| 					} else { | ||||
| 						line_counter_ = (line_counter_ + 1) & 0x7f; | ||||
|  | ||||
| 						// check for start of vertical sync | ||||
| 						if(line_counter_ == registers_[7]) { | ||||
| 							bus_state_.vsync = true; | ||||
| 							vsync_counter_ = 0; | ||||
| 						} | ||||
|  | ||||
| 						// check for end of visible lines | ||||
| 						if(line_counter_ == registers_[6]) { | ||||
| 							line_is_visible_ = false; | ||||
| 						} | ||||
| 					} | ||||
| 				} else { | ||||
| 					bus_state_.row_address = (bus_state_.row_address + 1) & 0x1f; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			bus_state_.refresh_address = line_address_; | ||||
| 			character_counter_ = 0; | ||||
| 			character_is_visible_ = (registers_[1] != 0); | ||||
| 		} | ||||
|  | ||||
| 		inline void do_end_of_frame() { | ||||
| 			line_counter_ = 0; | ||||
| 			line_is_visible_ = true; | ||||
| 			line_address_ = static_cast<uint16_t>((registers_[12] << 8) | registers_[13]); | ||||
| 			bus_state_.refresh_address = line_address_; | ||||
| 		} | ||||
|  | ||||
| 		Personality personality_; | ||||
| 		T &bus_handler_; | ||||
| 		BusState bus_state_; | ||||
|  | ||||
| 		uint8_t registers_[18] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; | ||||
| 		uint8_t dummy_register_ = 0; | ||||
| 		int selected_register_ = 0; | ||||
|  | ||||
| 		uint8_t character_counter_ = 0; | ||||
| 		uint8_t line_counter_ = 0; | ||||
|  | ||||
| 		bool character_is_visible_ = false, line_is_visible_ = false; | ||||
|  | ||||
| 		int hsync_counter_ = 0; | ||||
| 		int vsync_counter_ = 0; | ||||
| 		bool is_in_adjustment_period_ = false; | ||||
|  | ||||
| 		uint16_t line_address_ = 0; | ||||
| 		uint16_t end_of_line_address_ = 0; | ||||
| 		uint8_t status_ = 0; | ||||
|  | ||||
| 		int display_skew_mask_ = 1; | ||||
| 		unsigned int character_is_visible_shifter_ = 0; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* CRTC6845_hpp */ | ||||
							
								
								
									
										92
									
								
								Components/8255/i8255.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								Components/8255/i8255.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| // | ||||
| //  i8255.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 01/08/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef i8255_hpp | ||||
| #define i8255_hpp | ||||
|  | ||||
| namespace Intel { | ||||
| namespace i8255 { | ||||
|  | ||||
| class PortHandler { | ||||
| 	public: | ||||
| 		void set_value(int port, uint8_t value) {} | ||||
| 		uint8_t get_value(int port) { return 0xff; } | ||||
| }; | ||||
|  | ||||
| // TODO: Modes 1 and 2. | ||||
| template <class T> class i8255 { | ||||
| 	public: | ||||
| 		i8255(T &port_handler) : control_(0), outputs_{0, 0, 0}, port_handler_(port_handler) {} | ||||
|  | ||||
| 		/*! | ||||
| 			Stores the value @c value to the register at @c address. If this causes a change in 8255 output | ||||
| 			then the PortHandler will be informed. | ||||
| 		*/ | ||||
| 		void set_register(int address, uint8_t value) { | ||||
| 			switch(address & 3) { | ||||
| 				case 0: | ||||
| 					if(!(control_ & 0x10)) { | ||||
| 						// TODO: so what would output be when switching from input to output mode? | ||||
| 						outputs_[0] = value; port_handler_.set_value(0, value); | ||||
| 					} | ||||
| 				break; | ||||
| 				case 1: | ||||
| 					if(!(control_ & 0x02)) { | ||||
| 						outputs_[1] = value; port_handler_.set_value(1, value); | ||||
| 					} | ||||
| 				break; | ||||
| 				case 2:	outputs_[2] = value; port_handler_.set_value(2, value);	break; | ||||
| 				case 3: | ||||
| 					if(value & 0x80) { | ||||
| 						control_ = value; | ||||
| 					} else { | ||||
| 						if(value & 1) { | ||||
| 							outputs_[2] |= 1 << ((value >> 1)&7); | ||||
| 						} else { | ||||
| 							outputs_[2] &= ~(1 << ((value >> 1)&7)); | ||||
| 						} | ||||
| 					} | ||||
| 					update_outputs(); | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Obtains the current value for the register at @c address. If this provides a reading | ||||
| 			of input then the PortHandler will be queried. | ||||
| 		*/ | ||||
| 		uint8_t get_register(int address) { | ||||
| 			switch(address & 3) { | ||||
| 				case 0:	return (control_ & 0x10) ? port_handler_.get_value(0) : outputs_[0]; | ||||
| 				case 1:	return (control_ & 0x02) ? port_handler_.get_value(1) : outputs_[1]; | ||||
| 				case 2:	{ | ||||
| 					if(!(control_ & 0x09)) return outputs_[2]; | ||||
| 					uint8_t input = port_handler_.get_value(2); | ||||
| 					return ((control_ & 0x01) ? (input & 0x0f) : (outputs_[2] & 0x0f)) | ((control_ & 0x08) ? (input & 0xf0) : (outputs_[2] & 0xf0)); | ||||
| 				} | ||||
| 				case 3:	return control_; | ||||
| 			} | ||||
| 			return 0xff; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		void update_outputs() { | ||||
| 			if(!(control_ & 0x10)) port_handler_.set_value(0, outputs_[0]); | ||||
| 			if(!(control_ & 0x02)) port_handler_.set_value(1, outputs_[1]); | ||||
| 			port_handler_.set_value(2, outputs_[2]); | ||||
| 		} | ||||
|  | ||||
| 		uint8_t control_; | ||||
| 		uint8_t outputs_[3]; | ||||
| 		T &port_handler_; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* i8255_hpp */ | ||||
							
								
								
									
										872
									
								
								Components/8272/i8272.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										872
									
								
								Components/8272/i8272.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,872 @@ | ||||
| // | ||||
| //  i8272.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 05/08/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "i8272.hpp" | ||||
| //#include "../../Storage/Disk/Encodings/MFM/Encoder.hpp" | ||||
|  | ||||
| #include <cstdio> | ||||
|  | ||||
| using namespace Intel::i8272; | ||||
|  | ||||
| #define SetDataRequest()				(main_status_ |= 0x80) | ||||
| #define ResetDataRequest()				(main_status_ &= ~0x80) | ||||
| #define DataRequest()					(main_status_ & 0x80) | ||||
|  | ||||
| #define SetDataDirectionToProcessor()	(main_status_ |= 0x40) | ||||
| #define SetDataDirectionFromProcessor()	(main_status_ &= ~0x40) | ||||
| #define DataDirectionToProcessor()		(main_status_ & 0x40) | ||||
|  | ||||
| #define SetNonDMAExecution()			(main_status_ |= 0x20) | ||||
| #define ResetNonDMAExecution()			(main_status_ &= ~0x20) | ||||
|  | ||||
| #define SetBusy()						(main_status_ |= 0x10) | ||||
| #define ResetBusy()						(main_status_ &= ~0x10) | ||||
| #define Busy()							(main_status_ & 0x10) | ||||
|  | ||||
| #define SetAbnormalTermination()		(status_[0] |= 0x40) | ||||
| #define SetInvalidCommand()				(status_[0] |= 0x80) | ||||
| #define SetReadyChanged()				(status_[0] |= 0xc0) | ||||
| #define SetSeekEnd()					(status_[0] |= 0x20) | ||||
| #define SetEquipmentCheck()				(status_[0] |= 0x10) | ||||
| #define SetNotReady()					(status_[0] |= 0x08) | ||||
| #define SetSide2()						(status_[0] |= 0x04) | ||||
|  | ||||
| #define SetEndOfCylinder()				(status_[1] |= 0x80) | ||||
| #define SetDataError()					(status_[1] |= 0x20) | ||||
| #define SetOverrun()					(status_[1] |= 0x10) | ||||
| #define SetNoData()						(status_[1] |= 0x04) | ||||
| #define SetNotWriteable()				(status_[1] |= 0x02) | ||||
| #define SetMissingAddressMark()			(status_[1] |= 0x01) | ||||
|  | ||||
| #define SetControlMark()				(status_[2] |= 0x40) | ||||
| #define ClearControlMark()				(status_[2] &= ~0x40) | ||||
| #define ControlMark()					(status_[2] & 0x40) | ||||
|  | ||||
| #define SetDataFieldDataError()			(status_[2] |= 0x20) | ||||
| #define SetWrongCyinder()				(status_[2] |= 0x10) | ||||
| #define SetScanEqualHit()				(status_[2] |= 0x08) | ||||
| #define SetScanNotSatisfied()			(status_[2] |= 0x04) | ||||
| #define SetBadCylinder()				(status_[2] |= 0x02) | ||||
| #define SetMissingDataAddressMark()		(status_[2] |= 0x01) | ||||
|  | ||||
| namespace { | ||||
| 	const uint8_t CommandReadData = 0x06; | ||||
| 	const uint8_t CommandReadDeletedData = 0x0c; | ||||
|  | ||||
| 	const uint8_t CommandWriteData = 0x05; | ||||
| 	const uint8_t CommandWriteDeletedData = 0x09; | ||||
|  | ||||
| 	const uint8_t CommandReadTrack = 0x02; | ||||
| 	const uint8_t CommandReadID = 0x0a; | ||||
| 	const uint8_t CommandFormatTrack = 0x0d; | ||||
|  | ||||
| 	const uint8_t CommandScanLow = 0x11; | ||||
| 	const uint8_t CommandScanLowOrEqual = 0x19; | ||||
| 	const uint8_t CommandScanHighOrEqual = 0x1d; | ||||
|  | ||||
| 	const uint8_t CommandRecalibrate = 0x07; | ||||
| 	const uint8_t CommandSeek = 0x0f; | ||||
|  | ||||
| 	const uint8_t CommandSenseInterruptStatus = 0x08; | ||||
| 	const uint8_t CommandSpecify = 0x03; | ||||
| 	const uint8_t CommandSenseDriveStatus = 0x04; | ||||
| } | ||||
|  | ||||
| i8272::i8272(BusHandler &bus_handler, Cycles clock_rate) : | ||||
| 	Storage::Disk::MFMController(clock_rate), | ||||
| 	bus_handler_(bus_handler) { | ||||
| 	posit_event(static_cast<int>(Event8272::CommandByte)); | ||||
| } | ||||
|  | ||||
| bool i8272::is_sleeping() { | ||||
| 	return is_sleeping_ && Storage::Disk::MFMController::is_sleeping(); | ||||
| } | ||||
|  | ||||
| void i8272::run_for(Cycles cycles) { | ||||
| 	Storage::Disk::MFMController::run_for(cycles); | ||||
|  | ||||
| 	if(is_sleeping_) return; | ||||
|  | ||||
| 	// check for an expired timer | ||||
| 	if(delay_time_ > 0) { | ||||
| 		if(cycles.as_int() >= delay_time_) { | ||||
| 			delay_time_ = 0; | ||||
| 			posit_event(static_cast<int>(Event8272::Timer)); | ||||
| 		} else { | ||||
| 			delay_time_ -= cycles.as_int(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// update seek status of any drives presently seeking | ||||
| 	if(drives_seeking_) { | ||||
| 		int drives_left = drives_seeking_; | ||||
| 		for(int c = 0; c < 4; c++) { | ||||
| 			if(drives_[c].phase == Drive::Seeking) { | ||||
| 				drives_[c].step_rate_counter += cycles.as_int(); | ||||
| 				int steps = drives_[c].step_rate_counter / (8000 * step_rate_time_); | ||||
| 				drives_[c].step_rate_counter %= (8000 * step_rate_time_); | ||||
| 				while(steps--) { | ||||
| 					// Perform a step. | ||||
| 					int direction = (drives_[c].target_head_position < drives_[c].head_position) ? -1 : 1; | ||||
| 					printf("Target %d versus believed %d\n", drives_[c].target_head_position, drives_[c].head_position); | ||||
| 					select_drive(c); | ||||
| 					get_drive().step(direction); | ||||
| 					if(drives_[c].target_head_position >= 0) drives_[c].head_position += direction; | ||||
|  | ||||
| 					// Check for completion. | ||||
| 					if(seek_is_satisfied(c)) { | ||||
| 						drives_[c].phase = Drive::CompletedSeeking; | ||||
| 						drives_seeking_--; | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				drives_left--; | ||||
| 				if(!drives_left) break; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// check for any head unloads | ||||
| 	if(head_timers_running_) { | ||||
| 		int timers_left = head_timers_running_; | ||||
| 		for(int c = 0; c < 8; c++) { | ||||
| 			int drive = (c >> 1); | ||||
| 			int head = c&1; | ||||
|  | ||||
| 			if(drives_[drive].head_unload_delay[head] > 0) { | ||||
| 				if(cycles.as_int() >= drives_[drive].head_unload_delay[head]) { | ||||
| 					drives_[drive].head_unload_delay[head] = 0; | ||||
| 					drives_[drive].head_is_loaded[head] = false; | ||||
| 					head_timers_running_--; | ||||
| 				} else { | ||||
| 					drives_[drive].head_unload_delay[head] -= cycles.as_int(); | ||||
| 				} | ||||
| 				timers_left--; | ||||
| 				if(!timers_left) break; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// check for busy plus ready disabled | ||||
| 	if(is_executing_ && !get_drive().get_is_ready()) { | ||||
| 		posit_event(static_cast<int>(Event8272::NoLongerReady)); | ||||
| 	} | ||||
|  | ||||
| 	is_sleeping_ = !delay_time_ && !drives_seeking_ && !head_timers_running_; | ||||
| 	if(is_sleeping_) update_sleep_observer(); | ||||
| } | ||||
|  | ||||
| void i8272::set_register(int address, uint8_t value) { | ||||
| 	// don't consider attempted sets to the status register | ||||
| 	if(!address) return; | ||||
|  | ||||
| 	// if not ready for commands, do nothing | ||||
| 	if(!DataRequest() || DataDirectionToProcessor()) return; | ||||
|  | ||||
| 	if(expects_input_) { | ||||
| 		input_ = value; | ||||
| 		has_input_ = true; | ||||
| 		ResetDataRequest(); | ||||
| 	} else { | ||||
| 		// accumulate latest byte in the command byte sequence | ||||
| 		command_.push_back(value); | ||||
| 		posit_event(static_cast<int>(Event8272::CommandByte)); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t i8272::get_register(int address) { | ||||
| 	if(address) { | ||||
| 		if(result_stack_.empty()) return 0xff; | ||||
| 		uint8_t result = result_stack_.back(); | ||||
| 		result_stack_.pop_back(); | ||||
| 		if(result_stack_.empty()) posit_event(static_cast<int>(Event8272::ResultEmpty)); | ||||
|  | ||||
| 		return result; | ||||
| 	} else { | ||||
| 		return main_status_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| #define BEGIN_SECTION()	switch(resume_point_) { default: | ||||
| #define END_SECTION()	} | ||||
|  | ||||
| #define MS_TO_CYCLES(x)			x * 8000 | ||||
| #define WAIT_FOR_EVENT(mask)	resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(mask); return; case __LINE__: | ||||
| #define WAIT_FOR_TIME(ms)		resume_point_ = __LINE__; interesting_event_mask_ = static_cast<int>(Event8272::Timer); delay_time_ = MS_TO_CYCLES(ms); is_sleeping_ = false; update_sleep_observer(); case __LINE__: if(delay_time_) return; | ||||
|  | ||||
| #define PASTE(x, y) x##y | ||||
| #define CONCAT(x, y) PASTE(x, y) | ||||
|  | ||||
| #define FIND_HEADER()	\ | ||||
| 	set_data_mode(DataMode::Scanning);	\ | ||||
| 	CONCAT(find_header, __LINE__): WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); \ | ||||
| 	if(event_type == static_cast<int>(Event::IndexHole)) { index_hole_limit_--; }	\ | ||||
| 	else if(get_latest_token().type == Token::ID) goto CONCAT(header_found, __LINE__);	\ | ||||
| 	\ | ||||
| 	if(index_hole_limit_) goto CONCAT(find_header, __LINE__);	\ | ||||
| 	CONCAT(header_found, __LINE__):	(void)0;\ | ||||
|  | ||||
| #define FIND_DATA()	\ | ||||
| 	set_data_mode(DataMode::Scanning);	\ | ||||
| 	CONCAT(find_data, __LINE__): WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); \ | ||||
| 	if(event_type == static_cast<int>(Event::Token)) { \ | ||||
| 		if(get_latest_token().type == Token::Byte || get_latest_token().type == Token::Sync) goto CONCAT(find_data, __LINE__);	\ | ||||
| 	} | ||||
|  | ||||
| #define READ_HEADER()	\ | ||||
| 	distance_into_section_ = 0;	\ | ||||
| 	set_data_mode(DataMode::Reading);	\ | ||||
| 	CONCAT(read_header, __LINE__): WAIT_FOR_EVENT(Event::Token); \ | ||||
| 	header_[distance_into_section_] = get_latest_token().byte_value;	\ | ||||
| 	distance_into_section_++; \ | ||||
| 	if(distance_into_section_ < 6) goto CONCAT(read_header, __LINE__);	\ | ||||
|  | ||||
| #define SET_DRIVE_HEAD_MFM()	\ | ||||
| 	active_drive_ = command_[1]&3;	\ | ||||
| 	active_head_ = (command_[1] >> 2)&1;	\ | ||||
| 	status_[0] = (command_[1]&7);	\ | ||||
| 	select_drive(active_drive_);	\ | ||||
| 	get_drive().set_head(active_head_);	\ | ||||
| 	set_is_double_density(command_[0] & 0x40); | ||||
|  | ||||
| #define WAIT_FOR_BYTES(n) \ | ||||
| 	distance_into_section_ = 0;	\ | ||||
| 	CONCAT(wait_bytes, __LINE__): WAIT_FOR_EVENT(Event::Token);	\ | ||||
| 	if(get_latest_token().type == Token::Byte) distance_into_section_++;	\ | ||||
| 	if(distance_into_section_ < (n)) goto CONCAT(wait_bytes, __LINE__); | ||||
|  | ||||
| #define LOAD_HEAD()	\ | ||||
| 	if(!drives_[active_drive_].head_is_loaded[active_head_]) {	\ | ||||
| 		drives_[active_drive_].head_is_loaded[active_head_] = true;	\ | ||||
| 		WAIT_FOR_TIME(head_load_time_);	\ | ||||
| 	} else {	\ | ||||
| 		if(drives_[active_drive_].head_unload_delay[active_head_] > 0) {	\ | ||||
| 			drives_[active_drive_].head_unload_delay[active_head_] = 0;	\ | ||||
| 			head_timers_running_--;	\ | ||||
| 		}	\ | ||||
| 	} | ||||
|  | ||||
| #define SCHEDULE_HEAD_UNLOAD()	\ | ||||
| 	if(drives_[active_drive_].head_is_loaded[active_head_]) {\ | ||||
| 		if(drives_[active_drive_].head_unload_delay[active_head_] == 0) {	\ | ||||
| 			head_timers_running_++;	\ | ||||
| 			is_sleeping_ = false;	\ | ||||
| 			update_sleep_observer();	\ | ||||
| 		}	\ | ||||
| 		drives_[active_drive_].head_unload_delay[active_head_] = MS_TO_CYCLES(head_unload_time_);\ | ||||
| 	} | ||||
|  | ||||
| void i8272::posit_event(int event_type) { | ||||
| 	if(event_type == static_cast<int>(Event::IndexHole)) index_hole_count_++; | ||||
| 	if(event_type == static_cast<int>(Event8272::NoLongerReady)) { | ||||
| 		SetNotReady(); | ||||
| 		goto abort; | ||||
| 	} | ||||
| 	if(!(interesting_event_mask_ & event_type)) return; | ||||
| 	interesting_event_mask_ &= ~event_type; | ||||
|  | ||||
| 	BEGIN_SECTION(); | ||||
|  | ||||
| 	// Resets busy and non-DMA execution, clears the command buffer, sets the data mode to scanning and flows | ||||
| 	// into wait_for_complete_command_sequence. | ||||
| 	wait_for_command: | ||||
| 			expects_input_ = false; | ||||
| 			set_data_mode(Storage::Disk::MFMController::DataMode::Scanning); | ||||
| 			ResetBusy(); | ||||
| 			ResetNonDMAExecution(); | ||||
| 			command_.clear(); | ||||
|  | ||||
| 	// Sets the data request bit, and waits for a byte. Then sets the busy bit. Continues accepting bytes | ||||
| 	// until it has a quantity that make up an entire command, then resets the data request bit and | ||||
| 	// branches to that command. | ||||
| 	wait_for_complete_command_sequence: | ||||
| 			SetDataRequest(); | ||||
| 			SetDataDirectionFromProcessor(); | ||||
| 			WAIT_FOR_EVENT(Event8272::CommandByte) | ||||
| 			SetBusy(); | ||||
|  | ||||
| 			static const std::size_t required_lengths[32] = { | ||||
| 				0,	0,	9,	3,	2,	9,	9,	2, | ||||
| 				1,	9,	2,	0,	9,	6,	0,	3, | ||||
| 				0,	9,	0,	0,	0,	0,	0,	0, | ||||
| 				0,	9,	0,	0,	0,	9,	0,	0, | ||||
| 			}; | ||||
|  | ||||
| 			if(command_.size() < required_lengths[command_[0] & 0x1f]) goto wait_for_complete_command_sequence; | ||||
| 			if(command_.size() == 9) { | ||||
| 				cylinder_ = command_[2]; | ||||
| 				head_ = command_[3]; | ||||
| 				sector_ = command_[4]; | ||||
| 				size_ = command_[5]; | ||||
| 			} | ||||
| 			ResetDataRequest(); | ||||
| 			status_[0] = status_[1] = status_[2] = 0; | ||||
|  | ||||
| 			// If this is not clearly a command that's safe to carry out in parallel to a seek, end all seeks. | ||||
| 			switch(command_[0] & 0x1f) { | ||||
| 				case CommandReadData: | ||||
| 				case CommandReadDeletedData: | ||||
| 				case CommandWriteData: | ||||
| 				case CommandWriteDeletedData: | ||||
| 				case CommandReadTrack: | ||||
| 				case CommandReadID: | ||||
| 				case CommandFormatTrack: | ||||
| 				case CommandScanLow: | ||||
| 				case CommandScanLowOrEqual: | ||||
| 				case CommandScanHighOrEqual: | ||||
| 					is_access_command_ = true; | ||||
| 				break; | ||||
|  | ||||
| 				default: | ||||
| 					is_access_command_ = false; | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			if(is_access_command_) { | ||||
| 				for(int c = 0; c < 4; c++) { | ||||
| 					if(drives_[c].phase == Drive::Seeking) { | ||||
| 						drives_[c].phase = Drive::NotSeeking; | ||||
| 						drives_seeking_--; | ||||
| 					} | ||||
| 				} | ||||
| 				// Establishes the drive and head being addressed, and whether in double density mode; populates the internal | ||||
| 				// cylinder, head, sector and size registers from the command stream. | ||||
| 				is_executing_ = true; | ||||
| 				if(!dma_mode_) SetNonDMAExecution(); | ||||
| 				SET_DRIVE_HEAD_MFM(); | ||||
| 				LOAD_HEAD(); | ||||
| 				if(!get_drive().get_is_ready()) { | ||||
| 					SetNotReady(); | ||||
| 					goto abort; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Jump to the proper place. | ||||
| 			switch(command_[0] & 0x1f) { | ||||
| 				case CommandReadData: | ||||
| 				case CommandReadDeletedData: | ||||
| 					goto read_data; | ||||
|  | ||||
| 				case CommandWriteData: | ||||
| 				case CommandWriteDeletedData: | ||||
| 					goto write_data; | ||||
|  | ||||
| 				case CommandReadTrack:				goto read_track; | ||||
| 				case CommandReadID:					goto read_id; | ||||
| 				case CommandFormatTrack:			goto format_track; | ||||
|  | ||||
| 				case CommandScanLow:				goto scan_low; | ||||
| 				case CommandScanLowOrEqual:			goto scan_low_or_equal; | ||||
| 				case CommandScanHighOrEqual:		goto scan_high_or_equal; | ||||
|  | ||||
| 				case CommandRecalibrate:			goto recalibrate; | ||||
| 				case CommandSeek:					goto seek; | ||||
|  | ||||
| 				case CommandSenseInterruptStatus:	goto sense_interrupt_status; | ||||
| 				case CommandSpecify:				goto specify; | ||||
| 				case CommandSenseDriveStatus:		goto sense_drive_status; | ||||
|  | ||||
| 				default:							goto invalid; | ||||
| 			} | ||||
|  | ||||
| 	// Decodes drive, head and density, loads the head, loads the internal cylinder, head, sector and size registers, | ||||
| 	// and searches for a sector that meets those criteria. If one is found, inspects the instruction in use and | ||||
| 	// jumps to an appropriate handler. | ||||
| 	read_write_find_header: | ||||
|  | ||||
| 		// Sets a maximum index hole limit of 2 then performs a find header/read header loop, continuing either until | ||||
| 		// the index hole limit is breached or a sector is found with a cylinder, head, sector and size equal to the | ||||
| 		// values in the internal registers. | ||||
| 			index_hole_limit_ = 2; | ||||
| //			printf("Seeking %02x %02x %02x %02x\n", cylinder_, head_, sector_, size_); | ||||
| 		find_next_sector: | ||||
| 			FIND_HEADER(); | ||||
| 			if(!index_hole_limit_) { | ||||
| 				// Two index holes have passed wihout finding the header sought. | ||||
| //				printf("Not found\n"); | ||||
| 				SetNoData(); | ||||
| 				goto abort; | ||||
| 			} | ||||
| 			index_hole_count_ = 0; | ||||
| //			printf("Header\n"); | ||||
| 			READ_HEADER(); | ||||
| 			if(index_hole_count_) { | ||||
| 				// This implies an index hole was sighted within the header. Error out. | ||||
| 				SetEndOfCylinder(); | ||||
| 				goto abort; | ||||
| 			} | ||||
| 			if(get_crc_generator().get_value()) { | ||||
| 				// This implies a CRC error in the header; mark as such but continue. | ||||
| 				SetDataError(); | ||||
| 			} | ||||
| //			printf("Considering %02x %02x %02x %02x [%04x]\n", header_[0], header_[1], header_[2], header_[3], get_crc_generator().get_value()); | ||||
| 			if(header_[0] != cylinder_ || header_[1] != head_ || header_[2] != sector_ || header_[3] != size_) goto find_next_sector; | ||||
|  | ||||
| 			// Branch to whatever is supposed to happen next | ||||
| //			printf("Proceeding\n"); | ||||
| 			switch(command_[0] & 0x1f) { | ||||
| 				case CommandReadData: | ||||
| 				case CommandReadDeletedData: | ||||
| 				goto read_data_found_header; | ||||
|  | ||||
| 				case CommandWriteData:	// write data | ||||
| 				case CommandWriteDeletedData:	// write deleted data | ||||
| 				goto write_data_found_header; | ||||
| 			} | ||||
|  | ||||
|  | ||||
| 	// Performs the read data or read deleted data command. | ||||
| 	read_data: | ||||
| 			printf("Read [deleted] data [%02x %02x %02x %02x ... %02x %02x]\n", command_[2], command_[3], command_[4], command_[5], command_[6], command_[8]); | ||||
| 		read_next_data: | ||||
| 			goto read_write_find_header; | ||||
|  | ||||
| 		// Finds the next data block and sets data mode to reading, setting an error flag if the on-disk deleted | ||||
| 		// flag doesn't match the sort the command was looking for. | ||||
| 		read_data_found_header: | ||||
| 			FIND_DATA(); | ||||
| 			ClearControlMark(); | ||||
| 			if(event_type == static_cast<int>(Event::Token)) { | ||||
| 				if(get_latest_token().type != Token::Data && get_latest_token().type != Token::DeletedData) { | ||||
| 					// Something other than a data mark came next — impliedly an ID or index mark. | ||||
| 					SetMissingAddressMark(); | ||||
| 					SetMissingDataAddressMark(); | ||||
| 					goto abort;	// TODO: or read_next_data? | ||||
| 				} else { | ||||
| 					if((get_latest_token().type == Token::Data) != ((command_[0] & 0x1f) == CommandReadData)) { | ||||
| 						if(!(command_[0]&0x20)) { | ||||
| 							// SK is not set; set the error flag but read this sector before finishing. | ||||
| 							SetControlMark(); | ||||
| 						} else { | ||||
| 							// SK is set; skip this sector. | ||||
| 							goto read_next_data; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} else { | ||||
| 				// An index hole appeared before the data mark. | ||||
| 				SetEndOfCylinder(); | ||||
| 				goto abort;	// TODO: or read_next_data? | ||||
| 			} | ||||
|  | ||||
| 			distance_into_section_ = 0; | ||||
| 			set_data_mode(Reading); | ||||
|  | ||||
| 		// Waits for the next token, then supplies it to the CPU by: (i) setting data request and direction; and (ii) resetting | ||||
| 		// data request once the byte has been taken. Continues until all bytes have been read. | ||||
| 		// | ||||
| 		// TODO: consider DTL. | ||||
| 		read_data_get_byte: | ||||
| 			WAIT_FOR_EVENT(static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); | ||||
| 			if(event_type == static_cast<int>(Event::Token)) { | ||||
| 				result_stack_.push_back(get_latest_token().byte_value); | ||||
| 				distance_into_section_++; | ||||
| 				SetDataRequest(); | ||||
| 				SetDataDirectionToProcessor(); | ||||
| 				WAIT_FOR_EVENT(static_cast<int>(Event8272::ResultEmpty) | static_cast<int>(Event::Token) | static_cast<int>(Event::IndexHole)); | ||||
| 			} | ||||
| 			switch(event_type) { | ||||
| 				case static_cast<int>(Event8272::ResultEmpty):	// The caller read the byte in time; proceed as normal. | ||||
| 					ResetDataRequest(); | ||||
| 					if(distance_into_section_ < (128 << size_)) goto read_data_get_byte; | ||||
| 				break; | ||||
| 				case static_cast<int>(Event::Token):				// The caller hasn't read the old byte yet and a new one has arrived | ||||
| 					SetOverrun(); | ||||
| 					goto abort; | ||||
| 				break; | ||||
| 				case static_cast<int>(Event::IndexHole): | ||||
| 					SetEndOfCylinder(); | ||||
| 					goto abort; | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 		// read CRC, without transferring it, then check it | ||||
| 			WAIT_FOR_EVENT(Event::Token); | ||||
| 			WAIT_FOR_EVENT(Event::Token); | ||||
| 			if(get_crc_generator().get_value()) { | ||||
| 				// This implies a CRC error in the sector; mark as such and temrinate. | ||||
| 				SetDataError(); | ||||
| 				SetDataFieldDataError(); | ||||
| 				goto abort; | ||||
| 			} | ||||
|  | ||||
| 		// check whether that's it: either the final requested sector has been read, or because | ||||
| 		// a sector that was [/wasn't] marked as deleted when it shouldn't [/should] have been | ||||
| 			if(sector_ != command_[6] && !ControlMark()) { | ||||
| 				sector_++; | ||||
| 				goto read_next_data; | ||||
| 			} | ||||
|  | ||||
| 		// For a final result phase, post the standard ST0, ST1, ST2, C, H, R, N | ||||
| 			goto post_st012chrn; | ||||
|  | ||||
| 	write_data: | ||||
| 			printf("Write [deleted] data [%02x %02x %02x %02x ... %02x %02x]\n", command_[2], command_[3], command_[4], command_[5], command_[6], command_[8]); | ||||
|  | ||||
| 			if(get_drive().get_is_read_only()) { | ||||
| 				SetNotWriteable(); | ||||
| 				goto abort; | ||||
| 			} | ||||
|  | ||||
| 		write_next_data: | ||||
| 			goto read_write_find_header; | ||||
|  | ||||
| 		write_data_found_header: | ||||
| 			WAIT_FOR_BYTES(get_is_double_density() ? 22 : 11); | ||||
| 			begin_writing(true); | ||||
|  | ||||
| 			write_id_data_joiner((command_[0] & 0x1f) == CommandWriteDeletedData, true); | ||||
|  | ||||
| 			SetDataDirectionFromProcessor(); | ||||
| 			SetDataRequest(); | ||||
| 			expects_input_ = true; | ||||
| 			distance_into_section_ = 0; | ||||
|  | ||||
| 		write_loop: | ||||
| 			WAIT_FOR_EVENT(Event::DataWritten); | ||||
| 			if(!has_input_) { | ||||
| 				SetOverrun(); | ||||
| 				goto abort; | ||||
| 			} | ||||
| 			write_byte(input_); | ||||
| 			has_input_ = false; | ||||
| 			distance_into_section_++; | ||||
| 			if(distance_into_section_ < (128 << size_)) { | ||||
| 				SetDataRequest(); | ||||
| 				goto write_loop; | ||||
| 			} | ||||
|  | ||||
| 			printf("Wrote %d bytes\n", distance_into_section_); | ||||
| 			write_crc(); | ||||
| 			expects_input_ = false; | ||||
| 			WAIT_FOR_EVENT(Event::DataWritten); | ||||
| 			end_writing(); | ||||
|  | ||||
| 			if(sector_ != command_[6]) { | ||||
| 				sector_++; | ||||
| 				goto write_next_data; | ||||
| 			} | ||||
|  | ||||
| 		goto post_st012chrn; | ||||
|  | ||||
| 	// Performs the read ID command. | ||||
| 	read_id: | ||||
| 		// Establishes the drive and head being addressed, and whether in double density mode. | ||||
| 			printf("Read ID [%02x %02x]\n", command_[0], command_[1]); | ||||
|  | ||||
| 		// Sets a maximum index hole limit of 2 then waits either until it finds a header mark or sees too many index holes. | ||||
| 		// If a header mark is found, reads in the following bytes that produce a header. Otherwise branches to data not found. | ||||
| 			index_hole_limit_ = 2; | ||||
| 			FIND_HEADER(); | ||||
| 			if(!index_hole_limit_) { | ||||
| 				SetMissingAddressMark(); | ||||
| 				goto abort; | ||||
| 			} | ||||
| 			READ_HEADER(); | ||||
|  | ||||
| 		// Sets internal registers from the discovered header and posts the standard ST0, ST1, ST2, C, H, R, N. | ||||
| 			cylinder_ = header_[0]; | ||||
| 			head_ = header_[1]; | ||||
| 			sector_ = header_[2]; | ||||
| 			size_ = header_[3]; | ||||
|  | ||||
| 			goto post_st012chrn; | ||||
|  | ||||
| 	// Performs read track. | ||||
| 	read_track: | ||||
| 			printf("Read track [%02x %02x %02x %02x]\n", command_[2], command_[3], command_[4], command_[5]); | ||||
|  | ||||
| 			// Wait for the index hole. | ||||
| 			WAIT_FOR_EVENT(Event::IndexHole); | ||||
|  | ||||
| 			sector_ = 0; | ||||
| 			index_hole_limit_ = 2; | ||||
|  | ||||
| 		// While not index hole again, stream all sector contents until EOT sectors have been read. | ||||
| 		read_track_next_sector: | ||||
| 			FIND_HEADER(); | ||||
| 			if(!index_hole_limit_) { | ||||
| 				if(!sector_) { | ||||
| 					SetMissingAddressMark(); | ||||
| 					goto abort; | ||||
| 				} else { | ||||
| 					goto post_st012chrn; | ||||
| 				} | ||||
| 			} | ||||
| 			READ_HEADER(); | ||||
|  | ||||
| 			FIND_DATA(); | ||||
| 			distance_into_section_ = 0; | ||||
| 			SetDataDirectionToProcessor(); | ||||
| 		read_track_get_byte: | ||||
| 			WAIT_FOR_EVENT(Event::Token); | ||||
| 			result_stack_.push_back(get_latest_token().byte_value); | ||||
| 			distance_into_section_++; | ||||
| 			SetDataRequest(); | ||||
| 			// TODO: other possible exit conditions; find a way to merge with the read_data version of this. | ||||
| 			WAIT_FOR_EVENT(static_cast<int>(Event8272::ResultEmpty)); | ||||
| 			ResetDataRequest(); | ||||
| 			if(distance_into_section_ < (128 << header_[2])) goto read_track_get_byte; | ||||
|  | ||||
| 			sector_++; | ||||
| 			if(sector_ < command_[6]) goto read_track_next_sector; | ||||
|  | ||||
| 			goto post_st012chrn; | ||||
|  | ||||
| 	// Performs format [/write] track. | ||||
| 	format_track: | ||||
| 			printf("Format track\n"); | ||||
| 			if(get_drive().get_is_read_only()) { | ||||
| 				SetNotWriteable(); | ||||
| 				goto abort; | ||||
| 			} | ||||
|  | ||||
| 			// Wait for the index hole. | ||||
| 			WAIT_FOR_EVENT(Event::IndexHole); | ||||
| 			index_hole_count_ = 0; | ||||
| 			begin_writing(true); | ||||
|  | ||||
| 			// Write start-of-track. | ||||
| 			write_start_of_track(); | ||||
| 			WAIT_FOR_EVENT(Event::DataWritten); | ||||
| 			sector_ = 0; | ||||
|  | ||||
| 		format_track_write_sector: | ||||
| 			write_id_joiner(); | ||||
|  | ||||
| 			// Write the sector header, obtaining its contents | ||||
| 			// from the processor. | ||||
| 			SetDataDirectionFromProcessor(); | ||||
| 			SetDataRequest(); | ||||
| 			expects_input_ = true; | ||||
| 			distance_into_section_ = 0; | ||||
| 		format_track_write_header: | ||||
| 			WAIT_FOR_EVENT(static_cast<int>(Event::DataWritten) | static_cast<int>(Event::IndexHole)); | ||||
| 			switch(event_type) { | ||||
| 				case static_cast<int>(Event::IndexHole): | ||||
| 					SetOverrun(); | ||||
| 					goto abort; | ||||
| 				break; | ||||
| 				case static_cast<int>(Event::DataWritten): | ||||
| 					header_[distance_into_section_] = input_; | ||||
| 					write_byte(input_); | ||||
| 					has_input_ = false; | ||||
| 					distance_into_section_++; | ||||
| 					if(distance_into_section_ < 4) { | ||||
| 						SetDataRequest(); | ||||
| 						goto format_track_write_header; | ||||
| 					} | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			printf("W: %02x %02x %02x %02x, %04x\n", header_[0], header_[1], header_[2], header_[3], get_crc_generator().get_value()); | ||||
| 			write_crc(); | ||||
|  | ||||
| 			// Write the sector body. | ||||
| 			write_id_data_joiner(false, false); | ||||
| 			write_n_bytes(128 << command_[2], command_[5]); | ||||
| 			write_crc(); | ||||
|  | ||||
| 			// Write the prescribed gap. | ||||
| 			write_n_bytes(command_[4], get_is_double_density() ? 0x4e : 0xff); | ||||
|  | ||||
| 			// Consider repeating. | ||||
| 			sector_++; | ||||
| 			if(sector_ < command_[3] && !index_hole_count_) | ||||
| 				goto format_track_write_sector; | ||||
|  | ||||
| 			// Otherwise, pad out to the index hole. | ||||
| 		format_track_pad: | ||||
| 			write_byte(get_is_double_density() ? 0x4e : 0xff); | ||||
| 			WAIT_FOR_EVENT(static_cast<int>(Event::DataWritten) | static_cast<int>(Event::IndexHole)); | ||||
| 			if(event_type != static_cast<int>(Event::IndexHole)) goto format_track_pad; | ||||
|  | ||||
| 			end_writing(); | ||||
|  | ||||
| 			cylinder_ = header_[0]; | ||||
| 			head_ = header_[1]; | ||||
| 			sector_ = header_[2] + 1; | ||||
| 			size_ = header_[3]; | ||||
|  | ||||
| 		goto post_st012chrn; | ||||
|  | ||||
| 	scan_low: | ||||
| 		printf("Scan low unimplemented!!\n"); | ||||
| 		goto wait_for_command; | ||||
|  | ||||
| 	scan_low_or_equal: | ||||
| 		printf("Scan low or equal unimplemented!!\n"); | ||||
| 		goto wait_for_command; | ||||
|  | ||||
| 	scan_high_or_equal: | ||||
| 		printf("Scan high or equal unimplemented!!\n"); | ||||
| 		goto wait_for_command; | ||||
|  | ||||
| 	// Performs both recalibrate and seek commands. These commands occur asynchronously, so the actual work | ||||
| 	// occurs in ::run_for; this merely establishes that seeking should be ongoing. | ||||
| 	recalibrate: | ||||
| 	seek: | ||||
| 			{ | ||||
| 				int drive = command_[1]&3; | ||||
| 				select_drive(drive); | ||||
|  | ||||
| 				// Increment the seeking count if this drive wasn't already seeking. | ||||
| 				if(drives_[drive].phase != Drive::Seeking) { | ||||
| 					drives_seeking_++; | ||||
| 					is_sleeping_ = false; | ||||
| 					update_sleep_observer(); | ||||
| 				} | ||||
|  | ||||
| 				// Set currently seeking, with a step to occur right now (yes, it sounds like jamming these | ||||
| 				// in could damage your drive motor). | ||||
| 				drives_[drive].phase = Drive::Seeking; | ||||
| 				drives_[drive].step_rate_counter = 8000 * step_rate_time_; | ||||
| 				drives_[drive].steps_taken = 0; | ||||
| 				drives_[drive].seek_failed = false; | ||||
| 				main_status_ |= 1 << (command_[1]&3); | ||||
|  | ||||
| 				// If this is a seek, set the processor-supplied target location; otherwise it is a recalibrate, | ||||
| 				// which means resetting the current state now but aiming to hit '-1' (which the stepping code | ||||
| 				// up in run_for understands to mean 'keep going until track 0 is active'). | ||||
| 				if(command_.size() > 2) { | ||||
| 					drives_[drive].target_head_position = command_[2]; | ||||
| 					printf("Seek to %02x\n", command_[2]); | ||||
| 				} else { | ||||
| 					drives_[drive].target_head_position = -1; | ||||
| 					drives_[drive].head_position = 0; | ||||
| 					printf("Recalibrate\n"); | ||||
| 				} | ||||
|  | ||||
| 				// Check whether any steps are even needed; if not then mark as completed already. | ||||
| 				if(seek_is_satisfied(drive)) { | ||||
| 					drives_[drive].phase = Drive::CompletedSeeking; | ||||
| 					drives_seeking_--; | ||||
| 				} | ||||
| 			} | ||||
| 			goto wait_for_command; | ||||
|  | ||||
| 	// Performs sense interrupt status. | ||||
| 	sense_interrupt_status: | ||||
| 			printf("Sense interrupt status\n"); | ||||
| 			{ | ||||
| 				// Find the first drive that is in the CompletedSeeking state. | ||||
| 				int found_drive = -1; | ||||
| 				for(int c = 0; c < 4; c++) { | ||||
| 					if(drives_[c].phase == Drive::CompletedSeeking) { | ||||
| 						found_drive = c; | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				// If a drive was found, return its results. Otherwise return a single 0x80. | ||||
| 				if(found_drive != -1) { | ||||
| 					drives_[found_drive].phase = Drive::NotSeeking; | ||||
| 					status_[0] = static_cast<uint8_t>(found_drive); | ||||
| 					main_status_ &= ~(1 << found_drive); | ||||
| 					SetSeekEnd(); | ||||
|  | ||||
| 					result_stack_ = { drives_[found_drive].head_position, status_[0]}; | ||||
| 				} else { | ||||
| 					result_stack_ = { 0x80 }; | ||||
| 				} | ||||
| 			} | ||||
| 			goto post_result; | ||||
|  | ||||
| 	// Performs specify. | ||||
| 	specify: | ||||
| 		// Just store the values, and terminate the command. | ||||
| 			printf("Specify\n"); | ||||
| 			step_rate_time_ = 16 - (command_[1] >> 4);			// i.e. 1 to 16ms | ||||
| 			head_unload_time_ = (command_[1] & 0x0f) << 4;		// i.e. 16 to 240ms | ||||
| 			head_load_time_ = command_[2] & ~1;					// i.e. 2 to 254 ms in increments of 2ms | ||||
|  | ||||
| 			if(!head_unload_time_) head_unload_time_ = 16; | ||||
| 			if(!head_load_time_) head_load_time_ = 2; | ||||
| 			dma_mode_ = !(command_[2] & 1); | ||||
| 			goto wait_for_command; | ||||
|  | ||||
| 	sense_drive_status: | ||||
| 			printf("Sense drive status\n"); | ||||
| 			{ | ||||
| 				int drive = command_[1] & 3; | ||||
| 				select_drive(drive); | ||||
| 				result_stack_= { | ||||
| 					static_cast<uint8_t>( | ||||
| 						(command_[1] & 7) |	// drive and head number | ||||
| 						0x08 |				// single sided | ||||
| 						(get_drive().get_is_track_zero() ? 0x10 : 0x00)	| | ||||
| 						(get_drive().get_is_ready() ? 0x20 : 0x00)		| | ||||
| 						(get_drive().get_is_read_only() ? 0x40 : 0x00) | ||||
| 					) | ||||
| 				}; | ||||
| 			} | ||||
| 			goto post_result; | ||||
|  | ||||
| 	// Performs any invalid command. | ||||
| 	invalid: | ||||
| 			// A no-op, but posts ST0 (but which ST0?) | ||||
| 			result_stack_ = {0x80}; | ||||
| 			goto post_result; | ||||
|  | ||||
| 	// Sets abnormal termination of the current command and proceeds to an ST0, ST1, ST2, C, H, R, N result phase. | ||||
| 	abort: | ||||
| 		end_writing(); | ||||
| 		SetAbnormalTermination(); | ||||
| 		goto post_st012chrn; | ||||
|  | ||||
| 	// Posts ST0, ST1, ST2, C, H, R and N as a result phase. | ||||
| 	post_st012chrn: | ||||
| 			SCHEDULE_HEAD_UNLOAD(); | ||||
|  | ||||
| 			result_stack_ = {size_, sector_, head_, cylinder_, status_[2], status_[1], status_[0]}; | ||||
|  | ||||
| 			goto post_result; | ||||
|  | ||||
| 	// Posts whatever is in result_stack_ as a result phase. Be aware that it is a stack — the | ||||
| 	// last thing in it will be returned first. | ||||
| 	post_result: | ||||
| 			printf("Result to %02x, main %02x: ", command_[0] & 0x1f, main_status_); | ||||
| 			for(std::size_t c = 0; c < result_stack_.size(); c++) { | ||||
| 				printf("%02x ", result_stack_[result_stack_.size() - 1 - c]); | ||||
| 			} | ||||
| 			printf("\n"); | ||||
|  | ||||
| 			// Set ready to send data to the processor, no longer in non-DMA execution phase. | ||||
| 			is_executing_ = false; | ||||
| 			ResetNonDMAExecution(); | ||||
| 			SetDataRequest(); | ||||
| 			SetDataDirectionToProcessor(); | ||||
|  | ||||
| 			// The actual stuff of unwinding result_stack_ is handled by ::get_register; wait | ||||
| 			// until the processor has read all result bytes. | ||||
| 			WAIT_FOR_EVENT(Event8272::ResultEmpty); | ||||
|  | ||||
| 			// Reset data direction and end the command. | ||||
| 			goto wait_for_command; | ||||
|  | ||||
| 	END_SECTION() | ||||
| } | ||||
|  | ||||
| bool i8272::seek_is_satisfied(int drive) { | ||||
| 	return	(drives_[drive].target_head_position == drives_[drive].head_position) || | ||||
| 			(drives_[drive].target_head_position == -1 && get_drive().get_is_track_zero()); | ||||
| } | ||||
|  | ||||
| void i8272::set_dma_acknowledge(bool dack) { | ||||
| } | ||||
|  | ||||
| void i8272::set_terminal_count(bool tc) { | ||||
| } | ||||
|  | ||||
| void i8272::set_data_input(uint8_t value) { | ||||
| } | ||||
|  | ||||
| uint8_t i8272::get_data_output() { | ||||
| 	return 0xff; | ||||
| } | ||||
							
								
								
									
										135
									
								
								Components/8272/i8272.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								Components/8272/i8272.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| // | ||||
| //  i8272.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 05/08/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef i8272_hpp | ||||
| #define i8272_hpp | ||||
|  | ||||
| #include "../../Storage/Disk/Controller/MFMDiskController.hpp" | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Intel { | ||||
| namespace i8272 { | ||||
|  | ||||
| class BusHandler { | ||||
| 	public: | ||||
| 		virtual void set_dma_data_request(bool drq) {} | ||||
| 		virtual void set_interrupt(bool irq) {} | ||||
| }; | ||||
|  | ||||
| class i8272: public Storage::Disk::MFMController { | ||||
| 	public: | ||||
| 		i8272(BusHandler &bus_handler, Cycles clock_rate); | ||||
|  | ||||
| 		void run_for(Cycles); | ||||
|  | ||||
| 		void set_data_input(uint8_t value); | ||||
| 		uint8_t get_data_output(); | ||||
|  | ||||
| 		void set_register(int address, uint8_t value); | ||||
| 		uint8_t get_register(int address); | ||||
|  | ||||
| 		void set_dma_acknowledge(bool dack); | ||||
| 		void set_terminal_count(bool tc); | ||||
|  | ||||
| 		bool is_sleeping(); | ||||
|  | ||||
| 	protected: | ||||
| 		virtual void select_drive(int number) = 0; | ||||
|  | ||||
| 	private: | ||||
| 		// The bus handler, for interrupt and DMA-driven usage. | ||||
| 		BusHandler &bus_handler_; | ||||
| 		std::unique_ptr<BusHandler> allocated_bus_handler_; | ||||
|  | ||||
| 		// Status registers. | ||||
| 		uint8_t main_status_ = 0; | ||||
| 		uint8_t status_[3] = {0, 0, 0}; | ||||
|  | ||||
| 		// A buffer for accumulating the incoming command, and one for accumulating the result. | ||||
| 		std::vector<uint8_t> command_; | ||||
| 		std::vector<uint8_t> result_stack_; | ||||
| 		uint8_t input_ = 0; | ||||
| 		bool has_input_ = false; | ||||
| 		bool expects_input_ = false; | ||||
|  | ||||
| 		// Event stream: the 8272-specific events, plus the current event state. | ||||
| 		enum class Event8272: int { | ||||
| 			CommandByte	= (1 << 3), | ||||
| 			Timer = (1 << 4), | ||||
| 			ResultEmpty = (1 << 5), | ||||
| 			NoLongerReady = (1 << 6) | ||||
| 		}; | ||||
| 		void posit_event(int type); | ||||
| 		int interesting_event_mask_ = static_cast<int>(Event8272::CommandByte); | ||||
| 		int resume_point_ = 0; | ||||
| 		bool is_access_command_ = false; | ||||
|  | ||||
| 		// The counter used for ::Timer events. | ||||
| 		int delay_time_ = 0; | ||||
|  | ||||
| 		// The connected drives. | ||||
| 		struct Drive { | ||||
| 			uint8_t head_position = 0; | ||||
|  | ||||
| 			// Seeking: persistent state. | ||||
| 			enum Phase { | ||||
| 				NotSeeking, | ||||
| 				Seeking, | ||||
| 				CompletedSeeking | ||||
| 			} phase = NotSeeking; | ||||
| 			bool did_seek = false; | ||||
| 			bool seek_failed = false; | ||||
|  | ||||
| 			// Seeking: transient state. | ||||
| 			int step_rate_counter = 0; | ||||
| 			int steps_taken = 0; | ||||
| 			int target_head_position = 0;	// either an actual number, or -1 to indicate to step until track zero | ||||
|  | ||||
| 			// Head state. | ||||
| 			int head_unload_delay[2] = {0, 0}; | ||||
| 			bool head_is_loaded[2] = {false, false}; | ||||
|  | ||||
| 		} drives_[4]; | ||||
| 		int drives_seeking_ = 0; | ||||
|  | ||||
| 		/// @returns @c true if the selected drive, which is number @c drive, can stop seeking. | ||||
| 		bool seek_is_satisfied(int drive); | ||||
|  | ||||
| 		// User-supplied parameters; as per the specify command. | ||||
| 		int step_rate_time_ = 1; | ||||
| 		int head_unload_time_ = 1; | ||||
| 		int head_load_time_ = 1; | ||||
| 		bool dma_mode_ = false; | ||||
| 		bool is_executing_ = false; | ||||
|  | ||||
| 		// A count of head unload timers currently running. | ||||
| 		int head_timers_running_ = 0; | ||||
|  | ||||
| 		// Transient storage and counters used while reading the disk. | ||||
| 		uint8_t header_[6] = {0, 0, 0, 0, 0, 0}; | ||||
| 		int distance_into_section_ = 0; | ||||
| 		int index_hole_count_ = 0, index_hole_limit_ = 0; | ||||
|  | ||||
| 		// Keeps track of the drive and head in use during commands. | ||||
| 		int active_drive_ = 0; | ||||
| 		int active_head_ = 0; | ||||
|  | ||||
| 		// Internal registers. | ||||
| 		uint8_t cylinder_ = 0, head_ = 0, sector_ = 0, size_ = 0; | ||||
|  | ||||
| 		// Master switch on not performing any work. | ||||
| 		bool is_sleeping_ = false; | ||||
| }; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* i8272_hpp */ | ||||
							
								
								
									
										712
									
								
								Components/9918/9918.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										712
									
								
								Components/9918/9918.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,712 @@ | ||||
| // | ||||
| //  9918.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 25/11/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "9918.hpp" | ||||
|  | ||||
| #include <cassert> | ||||
| #include <cstring> | ||||
|  | ||||
| using namespace TI; | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| const uint32_t palette_pack(uint8_t r, uint8_t g, uint8_t b) { | ||||
| 	uint32_t result = 0; | ||||
| 	uint8_t *const result_ptr = reinterpret_cast<uint8_t *>(&result); | ||||
| 	result_ptr[0] = r; | ||||
| 	result_ptr[1] = g; | ||||
| 	result_ptr[2] = b; | ||||
| 	result_ptr[3] = 0; | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| const uint32_t palette[16] = { | ||||
| 	palette_pack(0, 0, 0), | ||||
| 	palette_pack(0, 0, 0), | ||||
| 	palette_pack(33, 200, 66), | ||||
| 	palette_pack(94, 220, 120), | ||||
|  | ||||
| 	palette_pack(84, 85, 237), | ||||
| 	palette_pack(125, 118, 252), | ||||
| 	palette_pack(212, 82, 77), | ||||
| 	palette_pack(66, 235, 245), | ||||
|  | ||||
| 	palette_pack(252, 85, 84), | ||||
| 	palette_pack(255, 121, 120), | ||||
| 	palette_pack(212, 193, 84), | ||||
| 	palette_pack(230, 206, 128), | ||||
|  | ||||
| 	palette_pack(33, 176, 59), | ||||
| 	palette_pack(201, 91, 186), | ||||
| 	palette_pack(204, 204, 204), | ||||
| 	palette_pack(255, 255, 255) | ||||
| }; | ||||
|  | ||||
| const uint8_t StatusInterrupt = 0x80; | ||||
| const uint8_t StatusFifthSprite = 0x40; | ||||
|  | ||||
| const int StatusSpriteCollisionShift = 5; | ||||
| const uint8_t StatusSpriteCollision = 0x20; | ||||
|  | ||||
| struct ReverseTable { | ||||
| 	std::uint8_t map[256]; | ||||
|  | ||||
| 	ReverseTable() { | ||||
| 		for(int c = 0; c < 256; ++c) { | ||||
| 			map[c] = static_cast<uint8_t>( | ||||
| 				((c & 0x80) >> 7) | | ||||
| 				((c & 0x40) >> 5) | | ||||
| 				((c & 0x20) >> 3) | | ||||
| 				((c & 0x10) >> 1) | | ||||
| 				((c & 0x08) << 1) | | ||||
| 				((c & 0x04) << 3) | | ||||
| 				((c & 0x02) << 5) | | ||||
| 				((c & 0x01) << 7) | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
| } reverse_table; | ||||
|  | ||||
| // Bits are reversed in the internal mode value; they're stored | ||||
| // in the order M1 M2 M3. Hence the definitions below. | ||||
| enum ScreenMode { | ||||
| 	Text = 4, | ||||
| 	MultiColour = 2, | ||||
| 	ColouredText = 0, | ||||
| 	Graphics = 1 | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| TMS9918Base::TMS9918Base() : | ||||
| 	// 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole | ||||
| 	// line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. | ||||
| 	crt_(new Outputs::CRT::CRT(1365, 4, Outputs::CRT::DisplayType::NTSC60, 4)) {} | ||||
|  | ||||
| TMS9918::TMS9918(Personality p) { | ||||
| 	// Unimaginatively, this class just passes RGB through to the shader. Investigation is needed | ||||
| 	// into whether there's a more natural form. | ||||
| 	crt_->set_rgb_sampling_function( | ||||
| 		"vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)" | ||||
| 		"{" | ||||
| 			"return texture(sampler, coordinate).rgb / vec3(255.0);" | ||||
| 		"}"); | ||||
| 	crt_->set_output_device(Outputs::CRT::OutputDevice::Monitor); | ||||
| 	crt_->set_visible_area(Outputs::CRT::Rect(0.055f, 0.025f, 0.9f, 0.9f)); | ||||
| 	crt_->set_input_gamma(2.8f); | ||||
|  | ||||
| 	// The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement | ||||
| 	// intended to produce the correct relationship between the hard edges between pixels and | ||||
| 	// the colour clock. It was eyeballed rather than derived from any knowledge of the TMS | ||||
| 	// colour burst generator because I've yet to find any. | ||||
| 	crt_->set_immediate_default_phase(0.85f); | ||||
| } | ||||
|  | ||||
| Outputs::CRT::CRT *TMS9918::get_crt() { | ||||
| 	return crt_.get(); | ||||
| } | ||||
|  | ||||
| void TMS9918Base::test_sprite(int sprite_number, int screen_row) { | ||||
| 	if(!(status_ & StatusFifthSprite)) { | ||||
| 		status_ = static_cast<uint8_t>((status_ & ~31) | sprite_number); | ||||
| 	} | ||||
| 	if(sprites_stopped_) | ||||
| 		return; | ||||
|  | ||||
| 	const int sprite_position = ram_[sprite_attribute_table_address_ + (sprite_number << 2)]; | ||||
| 	// A sprite Y of 208 means "don't scan the list any further". | ||||
| 	if(sprite_position == 208) { | ||||
| 		sprites_stopped_ = true; | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	const int sprite_row = (screen_row - sprite_position)&255; | ||||
| 	if(sprite_row < 0 || sprite_row >= sprite_height_) return; | ||||
|  | ||||
| 	const int active_sprite_slot = sprite_sets_[active_sprite_set_].active_sprite_slot; | ||||
| 	if(active_sprite_slot == 4) { | ||||
| 		status_ |= StatusFifthSprite; | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	SpriteSet::ActiveSprite &sprite = sprite_sets_[active_sprite_set_].active_sprites[active_sprite_slot]; | ||||
| 	sprite.index = sprite_number; | ||||
| 	sprite.row = sprite_row >> (sprites_magnified_ ? 1 : 0); | ||||
| 	sprite_sets_[active_sprite_set_].active_sprite_slot++; | ||||
| } | ||||
|  | ||||
| void TMS9918Base::get_sprite_contents(int field, int cycles_left, int screen_row) { | ||||
| 	int sprite_id = field / 6; | ||||
| 	field %= 6; | ||||
|  | ||||
| 	while(true) { | ||||
| 		const int cycles_in_sprite = std::min(cycles_left, 6 - field); | ||||
| 		cycles_left -= cycles_in_sprite; | ||||
| 		const int final_field = cycles_in_sprite + field; | ||||
|  | ||||
| 		assert(sprite_id < 4); | ||||
| 		SpriteSet::ActiveSprite &sprite = sprite_sets_[active_sprite_set_].active_sprites[sprite_id]; | ||||
|  | ||||
| 		if(field < 4) { | ||||
| 			std::memcpy( | ||||
| 				&sprite.info[field], | ||||
| 				&ram_[sprite_attribute_table_address_ + (sprite.index << 2) + field], | ||||
| 				static_cast<size_t>(std::min(4, final_field) - field)); | ||||
| 		} | ||||
|  | ||||
| 		field = std::min(4, final_field); | ||||
| 		const int sprite_offset = sprite.info[2] & ~(sprites_16x16_ ? 3 : 0); | ||||
| 		const int sprite_address = sprite_generator_table_address_ + (sprite_offset << 3) + sprite.row; // TODO: recalclate sprite.row from screen_row (?) | ||||
| 		while(field < final_field) { | ||||
| 			sprite.image[field - 4] = ram_[sprite_address + ((field - 4) << 4)]; | ||||
| 			field++; | ||||
| 		} | ||||
|  | ||||
| 		if(!cycles_left) return; | ||||
| 		field = 0; | ||||
| 		sprite_id++; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void TMS9918::run_for(const HalfCycles cycles) { | ||||
| 	// As specific as I've been able to get: | ||||
| 	// Scanline time is always 228 cycles. | ||||
| 	// PAL output is 313 lines total. NTSC output is 262 lines total. | ||||
| 	// Interrupt is signalled upon entering the lower border. | ||||
|  | ||||
| 	// Keep a count of cycles separate from internal counts to avoid | ||||
| 	// potential errors mapping back and forth. | ||||
| 	half_cycles_into_frame_ = (half_cycles_into_frame_ + cycles) % HalfCycles(frame_lines_ * 228 * 2); | ||||
|  | ||||
| 	// Convert 456 clocked half cycles per line to 342 internal cycles per line; | ||||
| 	// the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised | ||||
| 	// for this part. So multiply by three quarters. | ||||
| 	int int_cycles = (cycles.as_int() * 3) + cycles_error_; | ||||
| 	cycles_error_ = int_cycles & 3; | ||||
| 	int_cycles >>= 2; | ||||
| 	if(!int_cycles) return; | ||||
|  | ||||
| 	while(int_cycles) { | ||||
| 		// Determine how much time has passed in the remainder of this line, and proceed. | ||||
| 		int cycles_left = std::min(342 - column_, int_cycles); | ||||
|  | ||||
|  | ||||
|  | ||||
| 		// ------------------------------------ | ||||
| 		// Potentially perform a memory access. | ||||
| 		// ------------------------------------ | ||||
| 		if(queued_access_ != MemoryAccess::None) { | ||||
| 			int time_until_access_slot = 0; | ||||
| 			switch(line_mode_) { | ||||
| 				case LineMode::Refresh: | ||||
| 					if(column_ < 53 || column_ >= 307) time_until_access_slot = column_&1; | ||||
| 					else time_until_access_slot = 3 - ((column_ - 53)&3); | ||||
| 					// i.e. 53 -> 3, 52 -> 2, 51 -> 1, 50 -> 0, etc | ||||
| 				break; | ||||
| 				case LineMode::Text: | ||||
| 					if(column_ < 59 || column_ >= 299) time_until_access_slot = column_&1; | ||||
| 					else time_until_access_slot = 5 - ((column_ + 3)%6); | ||||
| 					// i.e. 59 -> 3, 60 -> 2, 61 -> 1, etc | ||||
| 				break; | ||||
| 				case LineMode::Character: | ||||
| 					if(column_ < 9) time_until_access_slot = column_&1; | ||||
| 					else if(column_ < 30) time_until_access_slot = 30 - column_; | ||||
| 					else if(column_ < 37) time_until_access_slot = column_&1; | ||||
| 					else if(column_ < 311) time_until_access_slot = 31 - ((column_ + 7)&31); | ||||
| 					// i.e. 53 -> 3, 54 -> 2, 55 -> 1, 56 -> 0, 57 -> 31, etc | ||||
| 					else if(column_ < 313) time_until_access_slot = column_&1; | ||||
| 					else time_until_access_slot = 342 - column_; | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			if(cycles_left >= time_until_access_slot) { | ||||
| 				if(queued_access_ == MemoryAccess::Write) { | ||||
| 					ram_[ram_pointer_ & 16383] = read_ahead_buffer_; | ||||
| 				} else { | ||||
| 					read_ahead_buffer_ = ram_[ram_pointer_ & 16383]; | ||||
| 				} | ||||
| 				ram_pointer_++; | ||||
| 				queued_access_ = MemoryAccess::None; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|  | ||||
|  | ||||
| 		column_ += cycles_left;		// column_ is now the column that has been reached in this line. | ||||
| 		int_cycles -= cycles_left;	// Count down duration to run for. | ||||
|  | ||||
|  | ||||
|  | ||||
| 		// ------------------------------ | ||||
| 		// Perform video memory accesses. | ||||
| 		// ------------------------------ | ||||
| 		if(((row_ < 192) || (row_ == frame_lines_-1)) && !blank_screen_) { | ||||
| 			const int sprite_row = (row_ < 192) ? row_ : -1; | ||||
| 			const int access_slot = column_ >> 1;	// There are only 171 available memory accesses per line. | ||||
| 			switch(line_mode_) { | ||||
| 				default: break; | ||||
|  | ||||
| 				case LineMode::Text: | ||||
| 					access_pointer_ = std::min(30, access_slot); | ||||
| 					if(access_pointer_ >= 30 && access_pointer_ < 150) { | ||||
| 						const int row_base = pattern_name_address_ + (row_ >> 3) * 40; | ||||
| 						const int end = std::min(150, access_slot); | ||||
|  | ||||
| 						// Pattern names are collected every third window starting from window 30. | ||||
| 						const int pattern_names_start = (access_pointer_ - 30 + 2) / 3; | ||||
| 						const int pattern_names_end = (end - 30 + 2) / 3; | ||||
| 						std::memcpy(&pattern_names_[pattern_names_start], &ram_[row_base + pattern_names_start], static_cast<size_t>(pattern_names_end - pattern_names_start)); | ||||
|  | ||||
| 						// Patterns are collected every third window starting from window 32. | ||||
| 						const int pattern_buffer_start = (access_pointer_ - 32 + 2) / 3; | ||||
| 						const int pattern_buffer_end = (end - 32 + 2) / 3; | ||||
| 						for(int column = pattern_buffer_start; column < pattern_buffer_end; ++column) { | ||||
| 							pattern_buffer_[column] = ram_[pattern_generator_table_address_ + (pattern_names_[column] << 3) + (row_ & 7)]; | ||||
| 						} | ||||
| 					} | ||||
| 				break; | ||||
|  | ||||
| 				case LineMode::Character: | ||||
| 					// Four access windows: no collection. | ||||
| 					if(access_pointer_ < 5) | ||||
| 						access_pointer_ = std::min(5, access_slot); | ||||
|  | ||||
| 					// Then ten access windows are filled with collection of sprite 3 and 4 details. | ||||
| 					if(access_pointer_ >= 5 && access_pointer_ < 15) { | ||||
| 						int end = std::min(15, access_slot); | ||||
| 						get_sprite_contents(access_pointer_ - 5 + 14, end - access_pointer_, sprite_row - 1); | ||||
| 						access_pointer_ = std::min(15, access_slot); | ||||
| 					} | ||||
|  | ||||
| 					// Four more access windows: no collection. | ||||
| 					if(access_pointer_ >= 15 && access_pointer_ < 19) { | ||||
| 						access_pointer_ = std::min(19, access_slot); | ||||
|  | ||||
| 						// Start new sprite set if this is location 19. | ||||
| 						if(access_pointer_ == 19) { | ||||
| 							active_sprite_set_ ^= 1; | ||||
| 							sprite_sets_[active_sprite_set_].active_sprite_slot = 0; | ||||
| 							sprites_stopped_ = false; | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					// Then eight access windows fetch the y position for the first eight sprites. | ||||
| 					while(access_pointer_ < 27 && access_pointer_ < access_slot) { | ||||
| 						test_sprite(access_pointer_ - 19, sprite_row); | ||||
| 						access_pointer_++; | ||||
| 					} | ||||
|  | ||||
| 					// The next 128 access slots are video and sprite collection interleaved. | ||||
| 					if(access_pointer_ >= 27 && access_pointer_ < 155) { | ||||
| 						int end = std::min(155, access_slot); | ||||
|  | ||||
| 						int row_base = pattern_name_address_; | ||||
| 						int pattern_base = pattern_generator_table_address_; | ||||
| 						int colour_base = colour_table_address_; | ||||
| 						if(screen_mode_ == ScreenMode::Graphics) { | ||||
| 							// If this is high resolution mode, allow the row number to affect the pattern and colour addresses. | ||||
| 							pattern_base &= 0x2000 | ((row_ & 0xc0) << 5); | ||||
| 							colour_base &= 0x2000 | ((row_ & 0xc0) << 5); | ||||
| 						} | ||||
| 						row_base += (row_ << 2)&~31; | ||||
|  | ||||
| 						// Pattern names are collected every fourth window starting from window 27. | ||||
| 						const int pattern_names_start = (access_pointer_ - 27 + 3) >> 2; | ||||
| 						const int pattern_names_end = (end - 27 + 3) >> 2; | ||||
| 						std::memcpy(&pattern_names_[pattern_names_start], &ram_[row_base + pattern_names_start], static_cast<size_t>(pattern_names_end - pattern_names_start)); | ||||
|  | ||||
| 						// Colours are collected every fourth window starting from window 29. | ||||
| 						const int colours_start = (access_pointer_ - 29 + 3) >> 2; | ||||
| 						const int colours_end = (end - 29 + 3) >> 2; | ||||
| 						if(screen_mode_ != 1) { | ||||
| 							for(int column = colours_start; column < colours_end; ++column) { | ||||
| 								colour_buffer_[column] = ram_[colour_base + (pattern_names_[column] >> 3)]; | ||||
| 							} | ||||
| 						} else { | ||||
| 							for(int column = colours_start; column < colours_end; ++column) { | ||||
| 								colour_buffer_[column] = ram_[colour_base + (pattern_names_[column] << 3) + (row_ & 7)]; | ||||
| 							} | ||||
| 						} | ||||
|  | ||||
| 						// Patterns are collected ever fourth window starting from window 30. | ||||
| 						const int pattern_buffer_start = (access_pointer_ - 30 + 3) >> 2; | ||||
| 						const int pattern_buffer_end = (end - 30 + 3) >> 2; | ||||
|  | ||||
| 						// Multicolour mode uss a different function of row to pick bytes | ||||
| 						const int row = (screen_mode_ != 2) ? (row_ & 7) : ((row_ >> 2) & 7); | ||||
| 						for(int column = pattern_buffer_start; column < pattern_buffer_end; ++column) { | ||||
| 							pattern_buffer_[column] = ram_[pattern_base + (pattern_names_[column] << 3) + row]; | ||||
| 						} | ||||
|  | ||||
| 						// Sprite slots occur in three quarters of ever fourth window starting from window 28. | ||||
| 						const int sprite_start = (access_pointer_ - 28 + 3) >> 2; | ||||
| 						const int sprite_end = (end - 28 + 3) >> 2; | ||||
| 						for(int column = sprite_start; column < sprite_end; ++column) { | ||||
| 							if(column&3) { | ||||
| 								test_sprite(7 + column - (column >> 2), sprite_row); | ||||
| 							} | ||||
| 						} | ||||
|  | ||||
| 						access_pointer_ = std::min(155, access_slot); | ||||
| 					} | ||||
|  | ||||
| 					// Two access windows: no collection. | ||||
| 					if(access_pointer_ < 157) | ||||
| 						access_pointer_ = std::min(157, access_slot); | ||||
|  | ||||
| 					// Fourteen access windows: collect initial sprite information. | ||||
| 					if(access_pointer_ >= 157 && access_pointer_ < 171) { | ||||
| 						int end = std::min(171, access_slot); | ||||
| 						get_sprite_contents(access_pointer_ - 157, end - access_pointer_, sprite_row); | ||||
| 						access_pointer_ = std::min(171, access_slot); | ||||
| 					} | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
| 		// -------------------------- | ||||
| 		// End video memory accesses. | ||||
| 		// -------------------------- | ||||
|  | ||||
|  | ||||
|  | ||||
| 		// -------------------- | ||||
| 		// Output video stream. | ||||
| 		// -------------------- | ||||
| 		if(row_	< 192 && !blank_screen_) { | ||||
| 			// ---------------------- | ||||
| 			// Output horizontal sync | ||||
| 			// ---------------------- | ||||
| 			if(!output_column_ && column_ >= 26) { | ||||
| 				crt_->output_sync(13 * 4); | ||||
| 				crt_->output_default_colour_burst(13 * 4); | ||||
| 				output_column_ = 26; | ||||
| 			} | ||||
|  | ||||
| 			// ------------------- | ||||
| 			// Output left border. | ||||
| 			// ------------------- | ||||
| 			if(output_column_ >= 26) { | ||||
| 				int pixels_end = std::min(first_pixel_column_, column_); | ||||
| 				if(output_column_ < pixels_end) { | ||||
| 					output_border(pixels_end - output_column_); | ||||
| 					output_column_ = pixels_end; | ||||
|  | ||||
| 					// Grab a pointer for drawing pixels to, if the moment has arrived. | ||||
| 					if(pixels_end == first_pixel_column_) { | ||||
| 						pixel_base_ = pixel_target_ = reinterpret_cast<uint32_t *>(crt_->allocate_write_area(static_cast<unsigned int>(first_right_border_column_ - first_pixel_column_))); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// -------------- | ||||
| 			// Output pixels. | ||||
| 			// -------------- | ||||
| 			if(output_column_ >= first_pixel_column_) { | ||||
| 				int pixels_end = std::min(first_right_border_column_, column_); | ||||
|  | ||||
| 				if(output_column_ < pixels_end) { | ||||
| 					switch(line_mode_) { | ||||
| 						default: break; | ||||
|  | ||||
| 						case LineMode::Text: { | ||||
| 							const uint32_t colours[2] = { palette[background_colour_], palette[text_colour_] }; | ||||
|  | ||||
| 							const int shift = (output_column_ - first_pixel_column_) % 6; | ||||
| 							int byte_column = (output_column_ - first_pixel_column_) / 6; | ||||
| 							int pattern = reverse_table.map[pattern_buffer_[byte_column]] >> shift; | ||||
| 							int pixels_left = pixels_end - output_column_; | ||||
| 							int length = std::min(pixels_left, 6 - shift); | ||||
| 							while(true) { | ||||
| 								pixels_left -= length; | ||||
| 								for(int c = 0; c < length; ++c) { | ||||
| 									pixel_target_[c] = colours[pattern&0x01]; | ||||
| 									pattern >>= 1; | ||||
| 								} | ||||
| 								pixel_target_ += length; | ||||
|  | ||||
| 								if(!pixels_left) break; | ||||
| 								length = std::min(6, pixels_left); | ||||
| 								byte_column++; | ||||
| 								pattern = reverse_table.map[pattern_buffer_[byte_column]]; | ||||
| 							} | ||||
| 							output_column_ = pixels_end; | ||||
| 						} break; | ||||
|  | ||||
| 						case LineMode::Character: { | ||||
| 							// If this is the start of the visible area, seed sprite shifter positions. | ||||
| 							SpriteSet &sprite_set = sprite_sets_[active_sprite_set_ ^ 1]; | ||||
| 							if(output_column_ == first_pixel_column_) { | ||||
| 								int c = sprite_set.active_sprite_slot; | ||||
| 								while(c--) { | ||||
| 									SpriteSet::ActiveSprite &sprite = sprite_set.active_sprites[c]; | ||||
| 									sprite.shift_position = -sprite.info[1]; | ||||
| 									if(sprite.info[3] & 0x80) { | ||||
| 										sprite.shift_position += 32; | ||||
| 										if(sprite.shift_position > 0 && !sprites_magnified_) | ||||
| 											sprite.shift_position *= 2; | ||||
| 									} | ||||
| 								} | ||||
| 							} | ||||
|  | ||||
| 							// Paint the background tiles. | ||||
| 							const int pixels_left = pixels_end - output_column_; | ||||
| 							if(screen_mode_ == ScreenMode::MultiColour) { | ||||
| 								int pixel_location = output_column_ - first_pixel_column_; | ||||
| 								for(int c = 0; c < pixels_left; ++c) { | ||||
| 									pixel_target_[c] = palette[ | ||||
| 										(pattern_buffer_[(pixel_location + c) >> 3] >> (((pixel_location + c) & 4)^4)) & 15 | ||||
| 									]; | ||||
| 								} | ||||
| 								pixel_target_ += pixels_left; | ||||
| 							} else { | ||||
| 								const int shift = (output_column_ - first_pixel_column_) & 7; | ||||
| 								int byte_column = (output_column_ - first_pixel_column_) >> 3; | ||||
|  | ||||
| 								int length = std::min(pixels_left, 8 - shift); | ||||
|  | ||||
| 								int pattern = reverse_table.map[pattern_buffer_[byte_column]] >> shift; | ||||
| 								uint8_t colour = colour_buffer_[byte_column]; | ||||
| 								uint32_t colours[2] = { | ||||
| 									palette[(colour & 15) ? (colour & 15) : background_colour_], | ||||
| 									palette[(colour >> 4) ? (colour >> 4) : background_colour_] | ||||
| 								}; | ||||
|  | ||||
| 								int background_pixels_left = pixels_left; | ||||
| 								while(true) { | ||||
| 									background_pixels_left -= length; | ||||
| 									for(int c = 0; c < length; ++c) { | ||||
| 										pixel_target_[c] = colours[pattern&0x01]; | ||||
| 										pattern >>= 1; | ||||
| 									} | ||||
| 									pixel_target_ += length; | ||||
|  | ||||
| 									if(!background_pixels_left) break; | ||||
| 									length = std::min(8, background_pixels_left); | ||||
| 									byte_column++; | ||||
|  | ||||
| 									pattern = reverse_table.map[pattern_buffer_[byte_column]]; | ||||
| 									colour = colour_buffer_[byte_column]; | ||||
| 									colours[0] = palette[(colour & 15) ? (colour & 15) : background_colour_]; | ||||
| 									colours[1] = palette[(colour >> 4) ? (colour >> 4) : background_colour_]; | ||||
| 								} | ||||
| 							} | ||||
|  | ||||
| 							// Paint sprites and check for collisions. | ||||
| 							if(sprite_set.active_sprite_slot) { | ||||
| 								int sprite_pixels_left = pixels_left; | ||||
| 								const int shift_advance = sprites_magnified_ ? 1 : 2; | ||||
|  | ||||
| 								const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; | ||||
| 								const int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; | ||||
|  | ||||
| 								while(sprite_pixels_left--) { | ||||
| 									uint32_t sprite_colour = pixel_base_[output_column_ - first_pixel_column_]; | ||||
| 									int sprite_mask = 0; | ||||
| 									int c = sprite_set.active_sprite_slot; | ||||
| 									while(c--) { | ||||
| 										SpriteSet::ActiveSprite &sprite = sprite_set.active_sprites[c]; | ||||
|  | ||||
| 										if(sprite.shift_position < 0) { | ||||
| 											sprite.shift_position++; | ||||
| 											continue; | ||||
| 										} else if(sprite.shift_position < 32) { | ||||
| 											int mask = sprite.image[sprite.shift_position >> 4] << ((sprite.shift_position&15) >> 1); | ||||
| 											mask = (mask >> 7) & 1; | ||||
| 											status_ |= (mask & sprite_mask) << StatusSpriteCollisionShift; | ||||
| 											sprite_mask |= mask; | ||||
| 											sprite.shift_position += shift_advance; | ||||
|  | ||||
| 											mask &= colour_masks[sprite.info[3]&15]; | ||||
| 											sprite_colour = (sprite_colour & sprite_colour_selection_masks[mask^1]) | (palette[sprite.info[3]&15] & sprite_colour_selection_masks[mask]); | ||||
| 										} | ||||
| 									} | ||||
|  | ||||
| 									pixel_base_[output_column_ - first_pixel_column_] = sprite_colour; | ||||
| 									output_column_++; | ||||
| 								} | ||||
| 							} | ||||
|  | ||||
| 							output_column_ = pixels_end; | ||||
| 						} break; | ||||
| 					} | ||||
|  | ||||
| 					if(output_column_ == first_right_border_column_) { | ||||
| 						crt_->output_data(static_cast<unsigned int>(first_right_border_column_ - first_pixel_column_) * 4, 4); | ||||
| 						pixel_target_ = nullptr; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// -------------------- | ||||
| 			// Output right border. | ||||
| 			// -------------------- | ||||
| 			if(output_column_ >= first_right_border_column_) { | ||||
| 				output_border(column_ - output_column_); | ||||
| 				output_column_ = column_; | ||||
| 			} | ||||
| 		} else if(row_ >= first_vsync_line_ && row_ < first_vsync_line_+3) { | ||||
| 			// Vertical sync. | ||||
| 			if(column_ == 342) { | ||||
| 				crt_->output_sync(342 * 4); | ||||
| 			} | ||||
| 		} else { | ||||
| 			// Blank. | ||||
| 			if(!output_column_ && column_ >= 26) { | ||||
| 				crt_->output_sync(13 * 4); | ||||
| 				crt_->output_default_colour_burst(13 * 4); | ||||
| 				output_column_ = 26; | ||||
| 			} | ||||
| 			if(output_column_ >= 26) { | ||||
| 				output_border(column_ - output_column_); | ||||
| 				output_column_ = column_; | ||||
| 			} | ||||
| 		} | ||||
| 		// ----------------- | ||||
| 		// End video stream. | ||||
| 		// ----------------- | ||||
|  | ||||
|  | ||||
|  | ||||
| 		// ----------------------------------- | ||||
| 		// Prepare for next line, potentially. | ||||
| 		// ----------------------------------- | ||||
| 		if(column_ == 342) { | ||||
| 			access_pointer_ = column_ = output_column_ = 0; | ||||
| 			row_ = (row_ + 1) % frame_lines_; | ||||
| 			if(row_ == 192) status_ |= StatusInterrupt; | ||||
|  | ||||
| 			screen_mode_ = next_screen_mode_; | ||||
| 			blank_screen_ = next_blank_screen_; | ||||
| 			switch(screen_mode_) { | ||||
| 				case ScreenMode::Text: | ||||
| 					line_mode_ = LineMode::Text; | ||||
| 					first_pixel_column_ = 69; | ||||
| 					first_right_border_column_ = 309; | ||||
| 				break; | ||||
| 				default: | ||||
| 					line_mode_ = LineMode::Character; | ||||
| 					first_pixel_column_ = 63; | ||||
| 					first_right_border_column_ = 319; | ||||
| 				break; | ||||
| 			} | ||||
| 			if(blank_screen_ || (row_ >= 192 && row_ != frame_lines_-1)) line_mode_ = LineMode::Refresh; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void TMS9918Base::output_border(int cycles) { | ||||
| 	pixel_target_ = reinterpret_cast<uint32_t *>(crt_->allocate_write_area(1)); | ||||
| 	if(pixel_target_) *pixel_target_ = palette[background_colour_]; | ||||
| 	crt_->output_level(static_cast<unsigned int>(cycles) * 4); | ||||
| } | ||||
|  | ||||
| void TMS9918::set_register(int address, uint8_t value) { | ||||
| 	// Writes to address 0 are writes to the video RAM. Store | ||||
| 	// the value and return. | ||||
| 	if(!(address & 1)) { | ||||
| 		write_phase_ = false; | ||||
|  | ||||
| 		// Enqueue the write to occur at the next available slot. | ||||
| 		read_ahead_buffer_ = value; | ||||
| 		queued_access_ = MemoryAccess::Write; | ||||
|  | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	// Writes to address 1 are performed in pairs; if this is the | ||||
| 	// low byte of a value, store it and wait for the high byte. | ||||
| 	if(!write_phase_) { | ||||
| 		low_write_ = value; | ||||
| 		write_phase_ = true; | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	write_phase_ = false; | ||||
| 	if(value & 0x80) { | ||||
| 		// This is a write to a register. | ||||
| 		switch(value & 7) { | ||||
| 			case 0: | ||||
| 				next_screen_mode_ = (next_screen_mode_ & 6) | ((low_write_ & 2) >> 1); | ||||
| 			break; | ||||
|  | ||||
| 			case 1: | ||||
| 				next_blank_screen_ = !(low_write_ & 0x40); | ||||
| 				generate_interrupts_ = !!(low_write_ & 0x20); | ||||
| 				next_screen_mode_ = (next_screen_mode_ & 1) | ((low_write_ & 0x18) >> 2); | ||||
| 				sprites_16x16_ = !!(low_write_ & 0x02); | ||||
| 				sprites_magnified_ = !!(low_write_ & 0x01); | ||||
|  | ||||
| 				sprite_height_ = 8; | ||||
| 				if(sprites_16x16_) sprite_height_ <<= 1; | ||||
| 				if(sprites_magnified_) sprite_height_ <<= 1; | ||||
| 			break; | ||||
|  | ||||
| 			case 2: | ||||
| 				pattern_name_address_ = static_cast<uint16_t>((low_write_ & 0xf) << 10); | ||||
| 			break; | ||||
|  | ||||
| 			case 3: | ||||
| 				colour_table_address_ = static_cast<uint16_t>(low_write_ << 6); | ||||
| 			break; | ||||
|  | ||||
| 			case 4: | ||||
| 				pattern_generator_table_address_ = static_cast<uint16_t>((low_write_ & 0x07) << 11); | ||||
| 			break; | ||||
|  | ||||
| 			case 5: | ||||
| 				sprite_attribute_table_address_ = static_cast<uint16_t>((low_write_ & 0x7f) << 7); | ||||
| 			break; | ||||
|  | ||||
| 			case 6: | ||||
| 				sprite_generator_table_address_ = static_cast<uint16_t>((low_write_ & 0x07) << 11); | ||||
| 			break; | ||||
|  | ||||
| 			case 7: | ||||
| 				text_colour_ = low_write_ >> 4; | ||||
| 				background_colour_ = low_write_ & 0xf; | ||||
| 			break; | ||||
| 		} | ||||
| 	} else { | ||||
| 		// This is a write to the RAM pointer. | ||||
| 		ram_pointer_ = static_cast<uint16_t>(low_write_ | (value << 8)); | ||||
| 		if(!(value & 0x40)) { | ||||
| 			// Officially a 'read' set, so perform lookahead. | ||||
| 			queued_access_ = MemoryAccess::Read; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t TMS9918::get_register(int address) { | ||||
| 	write_phase_ = false; | ||||
|  | ||||
| 	// Reads from address 0 read video RAM, via the read-ahead buffer. | ||||
| 	if(!(address & 1)) { | ||||
| 		// Enqueue the write to occur at the next available slot. | ||||
| 		uint8_t result = read_ahead_buffer_; | ||||
| 		queued_access_ = MemoryAccess::Read; | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	// Reads from address 1 get the status register. | ||||
| 	uint8_t result = status_; | ||||
| 	status_ &= ~(StatusInterrupt | StatusFifthSprite | StatusSpriteCollision); | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
|  HalfCycles TMS9918::get_time_until_interrupt() { | ||||
| 	if(!generate_interrupts_) return HalfCycles(-1); | ||||
| 	if(get_interrupt_line()) return HalfCycles(0); | ||||
|  | ||||
| 	const int half_cycles_per_frame = frame_lines_ * 228 * 2; | ||||
| 	int half_cycles_remaining = (192 * 228 * 2 + half_cycles_per_frame - half_cycles_into_frame_.as_int()) % half_cycles_per_frame; | ||||
| 	return HalfCycles(half_cycles_remaining ? half_cycles_remaining : half_cycles_per_frame); | ||||
| } | ||||
|  | ||||
| bool TMS9918::get_interrupt_line() { | ||||
| 	return (status_ & StatusInterrupt) && generate_interrupts_; | ||||
| } | ||||
							
								
								
									
										86
									
								
								Components/9918/9918.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								Components/9918/9918.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| // | ||||
| //  9918.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 25/11/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef TMS9918_hpp | ||||
| #define TMS9918_hpp | ||||
|  | ||||
| #include "../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
|  | ||||
| #include "Implementation/9918Base.hpp" | ||||
|  | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace TI { | ||||
|  | ||||
| /*! | ||||
| 	Provides emulation of the TMS9918a, TMS9928 and TMS9929. Likely in the future to be the | ||||
| 	vessel for emulation of sufficiently close derivatives, such as the Master System VDP. | ||||
|  | ||||
| 	The TMS9918 and descendants are video display generators that own their own RAM, making it | ||||
| 	accessible through an implicitly-timed register interface, and (depending on model) can generate | ||||
| 	PAL and NTSC component and composite video. | ||||
|  | ||||
| 	These chips have only one non-on-demand interaction with the outside world: an interrupt line. | ||||
| 	See get_time_until_interrupt and get_interrupt_line for asynchronous operation options. | ||||
| */ | ||||
| class TMS9918: public TMS9918Base { | ||||
| 	public: | ||||
| 		enum Personality { | ||||
| 			TMS9918A,	// includes the 9928 and 9929; set TV standard and output device as desired. | ||||
| 		}; | ||||
|  | ||||
| 		/*! | ||||
| 			Constructs an instance of the drive controller that behaves according to personality @c p. | ||||
| 			@param p The type of controller to emulate. | ||||
| 		*/ | ||||
| 		TMS9918(Personality p); | ||||
|  | ||||
| 		enum TVStandard { | ||||
| 			/*! i.e. 50Hz output at around 312.5 lines/field */ | ||||
| 			PAL, | ||||
| 			/*! i.e. 60Hz output at around 262.5 lines/field */ | ||||
| 			NTSC | ||||
| 		}; | ||||
|  | ||||
| 		/*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */ | ||||
| 		void set_tv_standard(TVStandard standard); | ||||
|  | ||||
| 		/*! Provides the CRT this TMS is connected to. */ | ||||
| 		Outputs::CRT::CRT *get_crt(); | ||||
|  | ||||
| 		/*! | ||||
| 			Runs the VCP for the number of cycles indicate; it is an implicit assumption of the code | ||||
| 			that the input clock rate is 3579545 Hz — the NTSC colour clock rate. | ||||
| 		*/ | ||||
| 		void run_for(const HalfCycles cycles); | ||||
|  | ||||
| 		/*! Sets a register value. */ | ||||
| 		void set_register(int address, uint8_t value); | ||||
|  | ||||
| 		/*! Gets a register value. */ | ||||
| 		uint8_t get_register(int address); | ||||
|  | ||||
| 		/*! | ||||
| 			Returns the amount of time until get_interrupt_line would next return true if | ||||
| 			there are no interceding calls to set_register or get_register. | ||||
|  | ||||
| 			If get_interrupt_line is true now, returns zero. If get_interrupt_line would | ||||
| 			never return true, returns -1. | ||||
| 		*/ | ||||
| 		HalfCycles get_time_until_interrupt(); | ||||
|  | ||||
| 		/*! | ||||
| 			@returns @c true if the interrupt line is currently active; @c false otherwise. | ||||
| 		*/ | ||||
| 		bool get_interrupt_line(); | ||||
| }; | ||||
|  | ||||
| }; | ||||
|  | ||||
| #endif /* TMS9918_hpp */ | ||||
							
								
								
									
										101
									
								
								Components/9918/Implementation/9918Base.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								Components/9918/Implementation/9918Base.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| // | ||||
| //  9918Base.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 14/12/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef TMS9918Base_hpp | ||||
| #define TMS9918Base_hpp | ||||
|  | ||||
| #include "../../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
|  | ||||
| namespace TI { | ||||
|  | ||||
| class TMS9918Base { | ||||
| 	protected: | ||||
| 		TMS9918Base(); | ||||
|  | ||||
| 		std::unique_ptr<Outputs::CRT::CRT> crt_; | ||||
|  | ||||
| 		uint8_t ram_[16384]; | ||||
|  | ||||
| 		uint16_t ram_pointer_ = 0; | ||||
| 		uint8_t read_ahead_buffer_ = 0; | ||||
| 		enum class MemoryAccess { | ||||
| 			Read, Write, None | ||||
| 		} queued_access_ = MemoryAccess::None; | ||||
|  | ||||
| 		uint8_t status_ = 0; | ||||
|  | ||||
| 		bool write_phase_ = false; | ||||
| 		uint8_t low_write_ = 0; | ||||
|  | ||||
| 		// The various register flags. | ||||
| 		int next_screen_mode_ = 0, screen_mode_ = 0; | ||||
| 		bool next_blank_screen_ = true, blank_screen_ = true; | ||||
| 		bool sprites_16x16_ = false; | ||||
| 		bool sprites_magnified_ = false; | ||||
| 		bool generate_interrupts_ = false; | ||||
| 		int sprite_height_ = 8; | ||||
| 		uint16_t pattern_name_address_ = 0; | ||||
| 		uint16_t colour_table_address_ = 0; | ||||
| 		uint16_t pattern_generator_table_address_ = 0; | ||||
| 		uint16_t sprite_attribute_table_address_ = 0; | ||||
| 		uint16_t sprite_generator_table_address_ = 0; | ||||
|  | ||||
| 		uint8_t text_colour_ = 0; | ||||
| 		uint8_t background_colour_ = 0; | ||||
|  | ||||
| 		HalfCycles half_cycles_into_frame_; | ||||
| 		int column_ = 0, row_ = 0, output_column_ = 0; | ||||
| 		int cycles_error_ = 0; | ||||
| 		uint32_t *pixel_target_ = nullptr, *pixel_base_ = nullptr; | ||||
|  | ||||
| 		void output_border(int cycles); | ||||
|  | ||||
| 		// Vertical timing details. | ||||
| 		int frame_lines_ = 262; | ||||
| 		int first_vsync_line_ = 227; | ||||
|  | ||||
| 		// Horizontal selections. | ||||
| 		enum class LineMode { | ||||
| 			Text = 0, | ||||
| 			Character = 1, | ||||
| 			Refresh = 2 | ||||
| 		} line_mode_ = LineMode::Text; | ||||
| 		int first_pixel_column_, first_right_border_column_; | ||||
|  | ||||
| 		uint8_t pattern_names_[40]; | ||||
| 		uint8_t pattern_buffer_[40]; | ||||
| 		uint8_t colour_buffer_[40]; | ||||
|  | ||||
| 		struct SpriteSet { | ||||
| 			struct ActiveSprite { | ||||
| 				int index = 0; | ||||
| 				int row = 0; | ||||
|  | ||||
| 				uint8_t info[4]; | ||||
| 				uint8_t image[2]; | ||||
|  | ||||
| 				int shift_position = 0; | ||||
| 			} active_sprites[4]; | ||||
| 			int active_sprite_slot = 0; | ||||
| 		} sprite_sets_[2]; | ||||
| 		int active_sprite_set_ = 0; | ||||
| 		bool sprites_stopped_ = false; | ||||
|  | ||||
| 		int access_pointer_ = 0; | ||||
|  | ||||
| 		inline void test_sprite(int sprite_number, int screen_row); | ||||
| 		inline void get_sprite_contents(int start, int cycles, int screen_row); | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* TMS9918Base_hpp */ | ||||
| @@ -8,17 +8,11 @@ | ||||
|  | ||||
| #include "AY38910.hpp" | ||||
|  | ||||
| using namespace GI; | ||||
| #include <cmath> | ||||
|  | ||||
| AY38910::AY38910() : | ||||
| 		selected_register_(0), | ||||
| 		tone_counters_{0, 0, 0}, tone_periods_{0, 0, 0}, tone_outputs_{0, 0, 0}, | ||||
| 		noise_shift_register_(0xffff), noise_period_(0), noise_counter_(0), noise_output_(0), | ||||
| 		envelope_divider_(0), envelope_period_(0), envelope_position_(0), | ||||
| 		master_divider_(0), | ||||
| 		output_registers_{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} { | ||||
| 	output_registers_[8] = output_registers_[9] = output_registers_[10] = 0; | ||||
| using namespace GI::AY38910; | ||||
|  | ||||
| AY38910::AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) { | ||||
| 	// set up envelope lookup tables | ||||
| 	for(int c = 0; c < 16; c++) { | ||||
| 		for(int p = 0; p < 32; p++) { | ||||
| @@ -66,17 +60,13 @@ AY38910::AY38910() : | ||||
| 	float max_volume = 8192; | ||||
| 	float root_two = sqrtf(2.0f); | ||||
| 	for(int v = 0; v < 16; v++) { | ||||
| 		volumes_[v] = (int)(max_volume / powf(root_two, (float)(v ^ 0xf))); | ||||
| 		volumes_[v] = static_cast<int>(max_volume / powf(root_two, static_cast<float>(v ^ 0xf))); | ||||
| 	} | ||||
| 	volumes_[0] = 0; | ||||
| } | ||||
|  | ||||
| void AY38910::set_clock_rate(double clock_rate) { | ||||
| 	set_input_rate((float)clock_rate); | ||||
| } | ||||
|  | ||||
| void AY38910::get_samples(unsigned int number_of_samples, int16_t *target) { | ||||
| 	int c = 0; | ||||
| void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| 	std::size_t c = 0; | ||||
| 	while((master_divider_&7) && c < number_of_samples) { | ||||
| 		target[c] = output_volume_; | ||||
| 		master_divider_++; | ||||
| @@ -159,22 +149,30 @@ void AY38910::evaluate_output_volume() { | ||||
| #undef channel_volume | ||||
|  | ||||
| 	// Mix additively. | ||||
| 	output_volume_ = (int16_t)( | ||||
| 	output_volume_ = static_cast<int16_t>( | ||||
| 		volumes_[volumes[0]] * channel_levels[0] + | ||||
| 		volumes_[volumes[1]] * channel_levels[1] + | ||||
| 		volumes_[volumes[2]] * channel_levels[2] | ||||
| 	); | ||||
| } | ||||
|  | ||||
| bool AY38910::is_zero_level() { | ||||
| 	// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero. | ||||
| 	return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0; | ||||
| } | ||||
|  | ||||
| // MARK: - Register manipulation | ||||
|  | ||||
| void AY38910::select_register(uint8_t r) { | ||||
| 	selected_register_ = r & 0xf; | ||||
| 	selected_register_ = r; | ||||
| } | ||||
|  | ||||
| void AY38910::set_register_value(uint8_t value) { | ||||
| 	if(selected_register_ > 15) return; | ||||
| 	registers_[selected_register_] = value; | ||||
| 	if(selected_register_ < 14) { | ||||
| 		int selected_register = selected_register_; | ||||
| 		enqueue([=] () { | ||||
| 		task_queue_.defer([=] () { | ||||
| 			uint8_t masked_value = value; | ||||
| 			switch(selected_register) { | ||||
| 				case 0: case 2: case 4: | ||||
| @@ -182,26 +180,22 @@ void AY38910::set_register_value(uint8_t value) { | ||||
| 					int channel = selected_register >> 1; | ||||
|  | ||||
| 					if(selected_register & 1) | ||||
| 						tone_periods_[channel] = (tone_periods_[channel] & 0xff) | (uint16_t)((value&0xf) << 8); | ||||
| 						tone_periods_[channel] = (tone_periods_[channel] & 0xff) | static_cast<uint16_t>((value&0xf) << 8); | ||||
| 					else | ||||
| 						tone_periods_[channel] = (tone_periods_[channel] & ~0xff) | value; | ||||
| 					tone_counters_[channel] = tone_periods_[channel]; | ||||
| 				} | ||||
| 				break; | ||||
|  | ||||
| 				case 6: | ||||
| 					noise_period_ = value & 0x1f; | ||||
| 					noise_counter_ = noise_period_; | ||||
| 				break; | ||||
|  | ||||
| 				case 11: | ||||
| 					envelope_period_ = (envelope_period_ & ~0xff) | value; | ||||
| 					envelope_divider_ = envelope_period_; | ||||
| 				break; | ||||
|  | ||||
| 				case 12: | ||||
| 					envelope_period_ = (envelope_period_ & 0xff) | (int)(value << 8); | ||||
| 					envelope_divider_ = envelope_period_; | ||||
| 					envelope_period_ = (envelope_period_ & 0xff) | static_cast<int>(value << 8); | ||||
| 				break; | ||||
|  | ||||
| 				case 13: | ||||
| @@ -212,53 +206,75 @@ void AY38910::set_register_value(uint8_t value) { | ||||
| 			output_registers_[selected_register] = masked_value; | ||||
| 			evaluate_output_volume(); | ||||
| 		}); | ||||
| 	} else { | ||||
| 		if(port_handler_) port_handler_->set_port_output(selected_register_ == 15, value); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| uint8_t AY38910::get_register_value() { | ||||
| 	// This table ensures that bits that aren't defined within the AY are returned as 1s | ||||
| 	// when read. I can't find documentation on this and don't have a machine to test, so | ||||
| 	// this is provisionally a guess. TODO: investigate. | ||||
| 	// This table ensures that bits that aren't defined within the AY are returned as 0s | ||||
| 	// when read, conforming to CPC-sourced unit tests. | ||||
| 	const uint8_t register_masks[16] = { | ||||
| 		0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0xe0, 0x00, | ||||
| 		0xe0, 0xe0, 0xe0, 0x00, 0x00, 0xf0, 0x00, 0x00 | ||||
| 		0xff, 0x0f, 0xff, 0x0f, 0xff, 0x0f, 0x1f, 0xff, | ||||
| 		0x1f, 0x1f, 0x1f, 0xff, 0xff, 0x0f, 0xff, 0xff | ||||
| 	}; | ||||
|  | ||||
| 	return registers_[selected_register_] | register_masks[selected_register_]; | ||||
| 	if(selected_register_ > 15) return 0xff; | ||||
| 	switch(selected_register_) { | ||||
| 		default:	return registers_[selected_register_] & register_masks[selected_register_]; | ||||
| 		case 14:	return (registers_[0x7] & 0x40) ? registers_[14] : port_inputs_[0]; | ||||
| 		case 15:	return (registers_[0x7] & 0x80) ? registers_[15] : port_inputs_[1]; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // MARK: - Port handling | ||||
|  | ||||
| uint8_t AY38910::get_port_output(bool port_b) { | ||||
| 	return registers_[port_b ? 15 : 14]; | ||||
| } | ||||
|  | ||||
| // MARK: - Bus handling | ||||
|  | ||||
| void AY38910::set_port_handler(PortHandler *handler) { | ||||
| 	port_handler_ = handler; | ||||
| } | ||||
|  | ||||
| void AY38910::set_data_input(uint8_t r) { | ||||
| 	data_input_ = r; | ||||
| 	update_bus(); | ||||
| } | ||||
|  | ||||
| uint8_t AY38910::get_data_output() { | ||||
| 	if(control_state_ == Read && selected_register_ >= 14) { | ||||
| 		if(port_handler_) { | ||||
| 			return port_handler_->get_port_input(selected_register_ == 15); | ||||
| 		} else { | ||||
| 			return 0xff; | ||||
| 		} | ||||
| 	} | ||||
| 	return data_output_; | ||||
| } | ||||
|  | ||||
| void AY38910::set_control_lines(ControlLines control_lines) { | ||||
| 	ControlState new_state; | ||||
| 	switch((int)control_lines) { | ||||
| 		default:					new_state = Inactive;		break; | ||||
| 	switch(static_cast<int>(control_lines)) { | ||||
| 		default:					control_state_ = Inactive;		break; | ||||
|  | ||||
| 		case (int)(BCDIR | BC2 | BC1): | ||||
| 		case BCDIR: | ||||
| 		case BC1:					new_state = LatchAddress;	break; | ||||
| 		case static_cast<int>(BDIR | BC2 | BC1): | ||||
| 		case BDIR: | ||||
| 		case BC1:					control_state_ = LatchAddress;	break; | ||||
|  | ||||
| 		case (int)(BC2 | BC1):		new_state = Read;			break; | ||||
| 		case (int)(BCDIR | BC2):	new_state = Write;			break; | ||||
| 		case static_cast<int>(BC2 | BC1):		control_state_ = Read;			break; | ||||
| 		case static_cast<int>(BDIR | BC2):		control_state_ = Write;			break; | ||||
| 	} | ||||
|  | ||||
| 	if(new_state != control_state_) { | ||||
| 		control_state_ = new_state; | ||||
| 		switch(new_state) { | ||||
| 	update_bus(); | ||||
| } | ||||
|  | ||||
| void AY38910::update_bus() { | ||||
| 	switch(control_state_) { | ||||
| 		default: break; | ||||
| 		case LatchAddress:	select_register(data_input_);			break; | ||||
| 		case Write:			set_register_value(data_input_);		break; | ||||
| 		case Read:			data_output_ = get_register_value();	break; | ||||
| 	} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -9,28 +9,57 @@ | ||||
| #ifndef AY_3_8910_hpp | ||||
| #define AY_3_8910_hpp | ||||
|  | ||||
| #include "../../Outputs/Speaker.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
|  | ||||
| namespace GI { | ||||
| namespace AY38910 { | ||||
|  | ||||
| /*! | ||||
| 	A port handler provides all input for an AY's two 8-bit ports, and may optionally receive | ||||
| 	active notification of changes in output. | ||||
|  | ||||
| 	Machines with an AY without ports or with nothing wired to them need not supply a port handler. | ||||
| 	Machines that use the AY ports as output but for which polling for changes is acceptable can | ||||
| 	instead use AY38910.get_port_output. | ||||
| */ | ||||
| class PortHandler { | ||||
| 	public: | ||||
| 		/*! | ||||
| 			Requests the current input on an AY port. | ||||
|  | ||||
| 			@param port_b @c true if the input being queried is Port B. @c false if it is Port A. | ||||
| 		*/ | ||||
| 		virtual uint8_t get_port_input(bool port_b) { | ||||
| 			return 0xff; | ||||
| 		} | ||||
|  | ||||
| 		/*! | ||||
| 			Requests the current input on an AY port. | ||||
|  | ||||
| 			@param port_b @c true if the input being queried is Port B. @c false if it is Port A. | ||||
| 		*/ | ||||
| 		virtual void set_port_output(bool port_b, uint8_t value) {} | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Names the control lines used as input to the AY, which uses CP1600 bus semantics. | ||||
| */ | ||||
| enum ControlLines { | ||||
| 	BC1		= (1 << 0), | ||||
| 	BC2		= (1 << 1), | ||||
| 	BDIR	= (1 << 2) | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Provides emulation of an AY-3-8910 / YM2149, which is a three-channel sound chip with a | ||||
| 	noise generator and a volume envelope generator, which also provides two bidirectional | ||||
| 	interface ports. | ||||
| */ | ||||
| class AY38910: public ::Outputs::Filter<AY38910> { | ||||
| class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
| 	public: | ||||
| 		/// Creates a new AY38910. | ||||
| 		AY38910(); | ||||
|  | ||||
| 		/// Sets the clock rate at which this AY38910 will be run. | ||||
| 		void set_clock_rate(double clock_rate); | ||||
|  | ||||
| 		enum ControlLines { | ||||
| 			BC1		= (1 << 0), | ||||
| 			BC2		= (1 << 1), | ||||
| 			BCDIR	= (1 << 2) | ||||
| 		}; | ||||
| 		AY38910(Concurrency::DeferringAsyncTaskQueue &task_queue); | ||||
|  | ||||
| 		/// Sets the value the AY would read from its data lines if it were not outputting. | ||||
| 		void set_data_input(uint8_t r); | ||||
| @@ -38,7 +67,7 @@ class AY38910: public ::Outputs::Filter<AY38910> { | ||||
| 		/// Gets the value that would appear on the data lines if only the AY is outputting. | ||||
| 		uint8_t get_data_output(); | ||||
|  | ||||
| 		/// Sets the | ||||
| 		/// Sets the current control line state, as a bit field. | ||||
| 		void set_control_lines(ControlLines control_lines); | ||||
|  | ||||
| 		/*! | ||||
| @@ -47,27 +76,39 @@ class AY38910: public ::Outputs::Filter<AY38910> { | ||||
| 		*/ | ||||
| 		uint8_t get_port_output(bool port_b); | ||||
|  | ||||
| 		/*! | ||||
| 			Sets the port handler, which will receive a call every time the AY either wants to sample | ||||
| 			input or else declare new output. As a convenience, current port output can be obtained | ||||
| 			without installing a port handler via get_port_output. | ||||
| 		*/ | ||||
| 		void set_port_handler(PortHandler *); | ||||
|  | ||||
| 		// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter; not for public consumption | ||||
| 		void get_samples(unsigned int number_of_samples, int16_t *target); | ||||
| 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||
| 		bool is_zero_level(); | ||||
|  | ||||
| 	private: | ||||
| 		int selected_register_; | ||||
| 		uint8_t registers_[16], output_registers_[16]; | ||||
| 		Concurrency::DeferringAsyncTaskQueue &task_queue_; | ||||
|  | ||||
| 		int master_divider_; | ||||
| 		int selected_register_ = 0; | ||||
| 		uint8_t registers_[16]; | ||||
| 		uint8_t output_registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; | ||||
| 		uint8_t port_inputs_[2]; | ||||
|  | ||||
| 		int tone_periods_[3]; | ||||
| 		int tone_counters_[3]; | ||||
| 		int tone_outputs_[3]; | ||||
| 		int master_divider_ = 0; | ||||
|  | ||||
| 		int noise_period_; | ||||
| 		int noise_counter_; | ||||
| 		int noise_shift_register_; | ||||
| 		int noise_output_; | ||||
| 		int tone_periods_[3] = {0, 0, 0}; | ||||
| 		int tone_counters_[3] = {0, 0, 0}; | ||||
| 		int tone_outputs_[3] = {0, 0, 0}; | ||||
|  | ||||
| 		int envelope_period_; | ||||
| 		int envelope_divider_; | ||||
| 		int envelope_position_; | ||||
| 		int noise_period_ = 0; | ||||
| 		int noise_counter_ = 0; | ||||
| 		int noise_shift_register_ = 0xffff; | ||||
| 		int noise_output_ = 0; | ||||
|  | ||||
| 		int envelope_period_ = 0; | ||||
| 		int envelope_divider_ = 0; | ||||
| 		int envelope_position_ = 0; | ||||
| 		int envelope_shapes_[16][32]; | ||||
| 		int envelope_overflow_masks_[16]; | ||||
|  | ||||
| @@ -88,8 +129,12 @@ class AY38910: public ::Outputs::Filter<AY38910> { | ||||
|  | ||||
| 		int16_t output_volume_; | ||||
| 		inline void evaluate_output_volume(); | ||||
|  | ||||
| 		inline void update_bus(); | ||||
| 		PortHandler *port_handler_ = nullptr; | ||||
| }; | ||||
|  | ||||
| }; | ||||
| } | ||||
| } | ||||
|  | ||||
| #endif /* AY_3_8910_hpp */ | ||||
|   | ||||
							
								
								
									
										107
									
								
								Components/KonamiSCC/KonamiSCC.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								Components/KonamiSCC/KonamiSCC.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| // | ||||
| //  KonamiSCC.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 06/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "KonamiSCC.hpp" | ||||
|  | ||||
| #include <cstring> | ||||
|  | ||||
| using namespace Konami; | ||||
|  | ||||
| SCC::SCC(Concurrency::DeferringAsyncTaskQueue &task_queue) : | ||||
| 	task_queue_(task_queue) {} | ||||
|  | ||||
| bool SCC::is_zero_level() { | ||||
| 	return !(channel_enable_ & 0x1f); | ||||
| } | ||||
|  | ||||
| void SCC::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| 	if(is_zero_level()) { | ||||
| 		std::memset(target, 0, sizeof(std::int16_t) * number_of_samples); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	std::size_t c = 0; | ||||
| 	while((master_divider_&7) && c < number_of_samples) { | ||||
| 		target[c] = output_volume_; | ||||
| 		master_divider_++; | ||||
| 		c++; | ||||
| 	} | ||||
|  | ||||
| 	while(c < number_of_samples) { | ||||
| 		for(int channel = 0; channel < 5; ++channel) { | ||||
| 			if(channels_[channel].tone_counter) channels_[channel].tone_counter--; | ||||
| 			else { | ||||
| 				channels_[channel].offset = (channels_[channel].offset + 1) & 0x1f; | ||||
| 				channels_[channel].tone_counter = channels_[channel].period; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		evaluate_output_volume(); | ||||
|  | ||||
| 		for(int ic = 0; ic < 8 && c < number_of_samples; ++ic) { | ||||
| 			target[c] = output_volume_; | ||||
| 			c++; | ||||
| 			master_divider_++; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void SCC::write(uint16_t address, uint8_t value) { | ||||
| 	address &= 0xff; | ||||
| 	if(address < 0x80) ram_[address] = value; | ||||
|  | ||||
| 	task_queue_.defer([=] { | ||||
| 		// Check for a write into waveform memory. | ||||
| 		if(address < 0x80) { | ||||
| 			waves_[address >> 5].samples[address & 0x1f] = value; | ||||
| 		} else switch(address) { | ||||
| 			default: break; | ||||
|  | ||||
| 			case 0x80: case 0x82: case 0x84: case 0x86: case 0x88: { | ||||
| 				int channel = (address - 0x80) >> 1; | ||||
| 				channels_[channel].period = (channels_[channel].period & ~0xff) | value; | ||||
| 			} break; | ||||
|  | ||||
| 			case 0x81: case 0x83: case 0x85: case 0x87: case 0x89: { | ||||
| 				int channel = (address - 0x80) >> 1; | ||||
| 				channels_[channel].period = (channels_[channel].period & 0xff) | ((value & 0xf) << 8); | ||||
| 			} break; | ||||
|  | ||||
| 			case 0x8a: case 0x8b: case 0x8c: case 0x8d: case 0x8e: | ||||
| 				channels_[address - 0x8a].amplitude = value & 0xf; | ||||
| 			break; | ||||
|  | ||||
| 			case 0x8f: | ||||
| 				channel_enable_ = value; | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		evaluate_output_volume(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void SCC::evaluate_output_volume() { | ||||
| 	output_volume_ = | ||||
| 		static_cast<int16_t>( | ||||
| 			( | ||||
| 				(channel_enable_ & 0x01) ? static_cast<int8_t>(waves_[0].samples[channels_[0].offset]) * channels_[0].amplitude : 0 + | ||||
| 				(channel_enable_ & 0x02) ? static_cast<int8_t>(waves_[1].samples[channels_[1].offset]) * channels_[1].amplitude : 0 + | ||||
| 				(channel_enable_ & 0x04) ? static_cast<int8_t>(waves_[2].samples[channels_[2].offset]) * channels_[2].amplitude : 0 + | ||||
| 				(channel_enable_ & 0x08) ? static_cast<int8_t>(waves_[3].samples[channels_[3].offset]) * channels_[3].amplitude : 0 + | ||||
| 				(channel_enable_ & 0x10) ? static_cast<int8_t>(waves_[3].samples[channels_[4].offset]) * channels_[4].amplitude : 0 | ||||
| 			) | ||||
| 		); | ||||
| } | ||||
|  | ||||
| uint8_t SCC::read(uint16_t address) { | ||||
| 	address &= 0xff; | ||||
| 	if(address < 0x80) { | ||||
| 		return ram_[address]; | ||||
| 	} | ||||
| 	return 0xff; | ||||
| } | ||||
							
								
								
									
										72
									
								
								Components/KonamiSCC/KonamiSCC.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								Components/KonamiSCC/KonamiSCC.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| // | ||||
| //  KonamiSCC.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 06/01/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef KonamiSCC_hpp | ||||
| #define KonamiSCC_hpp | ||||
|  | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
|  | ||||
| namespace Konami { | ||||
|  | ||||
| /*! | ||||
| 	Provides an emulation of Konami's Sound Creative Chip ('SCC'). | ||||
|  | ||||
| 	The SCC is a primitive wavetable synthesis chip, offering 32-sample tables, | ||||
| 	and five channels of output. The original SCC uses the same wave for channels | ||||
| 	four and five, the SCC+ supports different waves for the two channels. | ||||
| */ | ||||
| class SCC: public ::Outputs::Speaker::SampleSource { | ||||
| 	public: | ||||
| 		/// Creates a new SCC. | ||||
| 		SCC(Concurrency::DeferringAsyncTaskQueue &task_queue); | ||||
|  | ||||
| 		/// As per ::SampleSource; provides a broadphase test for silence. | ||||
| 		bool is_zero_level(); | ||||
|  | ||||
| 		/// As per ::SampleSource; provides audio output. | ||||
| 		void get_samples(std::size_t number_of_samples, std::int16_t *target); | ||||
|  | ||||
| 		/// Writes to the SCC. | ||||
| 		void write(uint16_t address, uint8_t value); | ||||
|  | ||||
| 		/// Reads from the SCC. | ||||
| 		uint8_t read(uint16_t address); | ||||
|  | ||||
| 	private: | ||||
| 		Concurrency::DeferringAsyncTaskQueue &task_queue_; | ||||
|  | ||||
| 		// State from here on down is accessed ony from the audio thread. | ||||
| 		int master_divider_ = 0; | ||||
| 		int16_t output_volume_ = 0; | ||||
|  | ||||
| 		struct Channel { | ||||
| 			int period = 0; | ||||
| 			int amplitude = 0; | ||||
|  | ||||
| 			int tone_counter = 0; | ||||
| 			int offset = 0; | ||||
| 		} channels_[5]; | ||||
|  | ||||
| 		struct Wavetable { | ||||
| 			std::uint8_t samples[32]; | ||||
| 		} waves_[4]; | ||||
|  | ||||
| 		std::uint8_t channel_enable_ = 0; | ||||
| 		std::uint8_t test_register_ = 0; | ||||
|  | ||||
| 		void evaluate_output_volume(); | ||||
|  | ||||
| 		// This keeps a copy of wave memory that is accessed from the | ||||
| 		// main emulation thread. | ||||
| 		std::uint8_t ram_[128]; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* KonamiSCC_hpp */ | ||||
							
								
								
									
										157
									
								
								Components/SN76489/SN76489.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								Components/SN76489/SN76489.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| // | ||||
| //  SN76489.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 26/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "SN76489.hpp" | ||||
|  | ||||
| #include <cassert> | ||||
| #include <cmath> | ||||
|  | ||||
| using namespace TI; | ||||
|  | ||||
| SN76489::SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider) : task_queue_(task_queue) { | ||||
| 	// Build a volume table. | ||||
| 	double multiplier = pow(10.0, -0.1); | ||||
| 	double volume = 8191.0f; | ||||
| 	for(int c = 0; c < 16; ++c) { | ||||
| 		volumes_[c] = (int)round(volume); | ||||
| 		volume *= multiplier; | ||||
| 	} | ||||
| 	volumes_[15] = 0; | ||||
| 	evaluate_output_volume(); | ||||
|  | ||||
| 	switch(personality) { | ||||
| 		case Personality::SN76494: | ||||
| 			master_divider_period_ = 2; | ||||
| 			shifter_is_16bit_ = false; | ||||
| 		break; | ||||
| 		case Personality::SN76489: | ||||
| 			master_divider_period_ = 16; | ||||
| 			shifter_is_16bit_ = false; | ||||
| 		break; | ||||
| 		case Personality::SMS: | ||||
| 			master_divider_period_ = 16; | ||||
| 			shifter_is_16bit_ = true; | ||||
| 		break; | ||||
| 	} | ||||
|  | ||||
| 	assert((master_divider_period_ % additional_divider) == 0); | ||||
| 	assert(additional_divider < master_divider_period_); | ||||
| 	master_divider_period_ /= additional_divider; | ||||
| } | ||||
|  | ||||
| void SN76489::set_register(uint8_t value) { | ||||
| 	task_queue_.defer([value, this] () { | ||||
| 		if(value & 0x80) { | ||||
| 			active_register_ = value; | ||||
| 		} | ||||
|  | ||||
| 		const int channel = (active_register_ >> 5)&3; | ||||
| 		if(active_register_ & 0x10) { | ||||
| 			// latch for volume | ||||
| 			channels_[channel].volume = value & 0xf; | ||||
| 			evaluate_output_volume(); | ||||
| 		} else { | ||||
| 			// latch for tone/data | ||||
| 			if(channel < 3) { | ||||
| 				if(value & 0x80) { | ||||
| 					channels_[channel].divider = (channels_[channel].divider & ~0xf) | (value & 0xf); | ||||
| 				} else { | ||||
| 					channels_[channel].divider = static_cast<uint16_t>((channels_[channel].divider & 0xf) | ((value & 0x3f) << 4)); | ||||
| 				} | ||||
| 			} else { | ||||
| 				// writes to the noise register always reset the shifter | ||||
| 				noise_shifter_ = shifter_is_16bit_ ? 0x8000 : 0x4000; | ||||
|  | ||||
| 				if(value & 4) { | ||||
| 					noise_mode_ = shifter_is_16bit_ ? Noise16 : Noise15; | ||||
| 				} else { | ||||
| 					noise_mode_ = shifter_is_16bit_ ? Periodic16 : Periodic15; | ||||
| 				} | ||||
|  | ||||
| 				channels_[3].divider = static_cast<uint16_t>(0x10 << (value & 3)); | ||||
| 				// Special case: if these bits are both set, the noise channel should track channel 2, | ||||
| 				// which is marked with a divider of 0xffff. | ||||
| 				if(channels_[3].divider == 0x80) channels_[3].divider = 0xffff; | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| bool SN76489::is_zero_level() { | ||||
| 	return channels_[0].volume == 0xf && channels_[1].volume == 0xf && channels_[2].volume == 0xf && channels_[3].volume == 0xf; | ||||
| } | ||||
|  | ||||
| void SN76489::evaluate_output_volume() { | ||||
| 	output_volume_ = static_cast<int16_t>( | ||||
| 		channels_[0].level * volumes_[channels_[0].volume] + | ||||
| 		channels_[1].level * volumes_[channels_[1].volume] + | ||||
| 		channels_[2].level * volumes_[channels_[2].volume] + | ||||
| 		channels_[3].level * volumes_[channels_[3].volume] | ||||
| 	); | ||||
| } | ||||
|  | ||||
| void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| 	std::size_t c = 0; | ||||
| 	while((master_divider_& (master_divider_period_ - 1)) && c < number_of_samples) { | ||||
| 		target[c] = output_volume_; | ||||
| 		master_divider_++; | ||||
| 		c++; | ||||
| 	} | ||||
|  | ||||
| 	while(c < number_of_samples) { | ||||
| 		bool did_flip = false; | ||||
|  | ||||
| #define step_channel(x, s) \ | ||||
| 		if(channels_[x].counter) channels_[x].counter--;\ | ||||
| 		else {\ | ||||
| 			channels_[x].level ^= 1;\ | ||||
| 			channels_[x].counter = channels_[x].divider;\ | ||||
| 			s;\ | ||||
| 		} | ||||
|  | ||||
| 		step_channel(0, /**/); | ||||
| 		step_channel(1, /**/); | ||||
| 		step_channel(2, did_flip = true); | ||||
|  | ||||
| #undef step_channel | ||||
|  | ||||
| 		if(channels_[3].divider != 0xffff) { | ||||
| 			if(channels_[3].counter) channels_[3].counter--; | ||||
| 			else { | ||||
| 				did_flip = true; | ||||
| 				channels_[3].counter = channels_[3].divider; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if(did_flip) { | ||||
| 			channels_[3].level = noise_shifter_ & 1; | ||||
| 			int new_bit = channels_[3].level; | ||||
| 			switch(noise_mode_) { | ||||
| 				default: break; | ||||
| 				case Noise15: | ||||
| 					new_bit ^= (noise_shifter_ >> 1); | ||||
| 				break; | ||||
| 				case Noise16: | ||||
| 					new_bit ^= (noise_shifter_ >> 3); | ||||
| 				break; | ||||
| 			} | ||||
| 			noise_shifter_ >>= 1; | ||||
| 			noise_shifter_ |= (new_bit & 1) << (shifter_is_16bit_ ? 15 : 14); | ||||
| 		} | ||||
|  | ||||
| 		evaluate_output_volume(); | ||||
|  | ||||
| 		for(int ic = 0; ic < master_divider_period_ && c < number_of_samples; ++ic) { | ||||
| 			target[c] = output_volume_; | ||||
| 			c++; | ||||
| 			master_divider_++; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	master_divider_ &= (master_divider_period_ - 1); | ||||
| } | ||||
							
								
								
									
										67
									
								
								Components/SN76489/SN76489.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								Components/SN76489/SN76489.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| // | ||||
| //  SN76489.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 26/02/2018. | ||||
| //  Copyright © 2018 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef SN76489_hpp | ||||
| #define SN76489_hpp | ||||
|  | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
|  | ||||
| namespace TI { | ||||
|  | ||||
| class SN76489: public Outputs::Speaker::SampleSource { | ||||
| 	public: | ||||
| 		enum class Personality { | ||||
| 			SN76489, | ||||
| 			SN76494, | ||||
| 			SMS | ||||
| 		}; | ||||
|  | ||||
| 		/// Creates a new SN76489. | ||||
| 		SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1); | ||||
|  | ||||
| 		/// Writes a new value to the SN76489. | ||||
| 		void set_register(uint8_t value); | ||||
|  | ||||
| 		// As per SampleSource. | ||||
| 		void get_samples(std::size_t number_of_samples, std::int16_t *target); | ||||
| 		bool is_zero_level(); | ||||
|  | ||||
| 	private: | ||||
| 		int master_divider_ = 0; | ||||
| 		int master_divider_period_ = 16; | ||||
| 		int16_t output_volume_ = 0; | ||||
| 		void evaluate_output_volume(); | ||||
| 		int volumes_[16]; | ||||
|  | ||||
| 		Concurrency::DeferringAsyncTaskQueue &task_queue_; | ||||
|  | ||||
| 		struct ToneChannel { | ||||
| 			// Programmatically-set state; updated by the processor. | ||||
| 			uint16_t divider = 0; | ||||
| 			uint8_t volume = 0xf; | ||||
|  | ||||
| 			// Active state; self-evolving as a function of time. | ||||
| 			uint16_t counter = 0; | ||||
| 			int level = 0; | ||||
| 		} channels_[4]; | ||||
| 		enum { | ||||
| 			Periodic15, | ||||
| 			Periodic16, | ||||
| 			Noise15, | ||||
| 			Noise16 | ||||
| 		} noise_mode_ = Periodic15; | ||||
| 		uint16_t noise_shifter_ = 0; | ||||
| 		int active_register_ = 0; | ||||
|  | ||||
| 		bool shifter_is_16bit_ = false; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* SN76489_hpp */ | ||||
| @@ -46,6 +46,7 @@ AsyncTaskQueue::AsyncTaskQueue() | ||||
| AsyncTaskQueue::~AsyncTaskQueue() { | ||||
| #ifdef __APPLE__ | ||||
| 	dispatch_release(serial_dispatch_queue_); | ||||
| 	serial_dispatch_queue_ = nullptr; | ||||
| #else | ||||
| 	should_destruct_ = true; | ||||
| 	enqueue([](){}); | ||||
| @@ -78,3 +79,25 @@ void AsyncTaskQueue::flush() { | ||||
| 	flush_condition->wait(lock); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() { | ||||
| 	perform(); | ||||
| } | ||||
|  | ||||
| void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) { | ||||
| 	if(!deferred_tasks_) { | ||||
| 		deferred_tasks_.reset(new std::list<std::function<void(void)>>); | ||||
| 	} | ||||
| 	deferred_tasks_->push_back(function); | ||||
| } | ||||
|  | ||||
| void DeferringAsyncTaskQueue::perform() { | ||||
| 	if(!deferred_tasks_) return; | ||||
| 	std::shared_ptr<std::list<std::function<void(void)>>> deferred_tasks = deferred_tasks_; | ||||
| 	deferred_tasks_.reset(); | ||||
| 	enqueue([deferred_tasks] { | ||||
| 		for(auto &function : *deferred_tasks) { | ||||
| 			function(); | ||||
| 		} | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -9,10 +9,12 @@ | ||||
| #ifndef AsyncTaskQueue_hpp | ||||
| #define AsyncTaskQueue_hpp | ||||
|  | ||||
| #include <atomic> | ||||
| #include <condition_variable> | ||||
| #include <functional> | ||||
| #include <list> | ||||
| #include <memory> | ||||
| #include <thread> | ||||
| #include <list> | ||||
| #include <condition_variable> | ||||
|  | ||||
| #ifdef __APPLE__ | ||||
| #include <dispatch/dispatch.h> | ||||
| @@ -26,10 +28,9 @@ namespace Concurrency { | ||||
| 	causing it to block until all previously-enqueued functions are complete. | ||||
| */ | ||||
| class AsyncTaskQueue { | ||||
|  | ||||
| 	public: | ||||
| 		AsyncTaskQueue(); | ||||
| 		~AsyncTaskQueue(); | ||||
| 		virtual ~AsyncTaskQueue(); | ||||
|  | ||||
| 		/*! | ||||
| 			Adds @c function to the queue. | ||||
| @@ -58,6 +59,39 @@ class AsyncTaskQueue { | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	A deferring async task queue is one that accepts a list of functions to be performed but defers | ||||
| 	any action until told to perform. It performs them by enquing a single asynchronous task that will | ||||
| 	perform the deferred tasks in order. | ||||
|  | ||||
| 	It therefore offers similar semantics to an asynchronous task queue, but allows for management of | ||||
| 	synchronisation costs, since neither defer nor perform make any effort to be thread safe. | ||||
| */ | ||||
| class DeferringAsyncTaskQueue: public AsyncTaskQueue { | ||||
| 	public: | ||||
| 		~DeferringAsyncTaskQueue(); | ||||
|  | ||||
| 		/*! | ||||
| 			Adds a function to the deferral list. | ||||
|  | ||||
| 			This is not thread safe; it should be serialised with other calls to itself and to perform. | ||||
| 		*/ | ||||
| 		void defer(std::function<void(void)> function); | ||||
|  | ||||
| 		/*! | ||||
| 			Enqueues a function that will perform all currently deferred functions, in the | ||||
| 			order that they were deferred. | ||||
|  | ||||
| 			This is not thread safe; it should be serialised with other calls to itself and to defer. | ||||
| 		*/ | ||||
| 		void perform(); | ||||
|  | ||||
| 	private: | ||||
| 		// TODO: this is a shared_ptr because of the issues capturing moveables in C++11; | ||||
| 		// switch to a unique_ptr if/when adapting to C++14 | ||||
| 		std::shared_ptr<std::list<std::function<void(void)>>> deferred_tasks_; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* Concurrency_hpp */ | ||||
|   | ||||
							
								
								
									
										77
									
								
								Concurrency/BestEffortUpdater.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								Concurrency/BestEffortUpdater.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| // | ||||
| //  BestEffortUpdater.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 04/10/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "BestEffortUpdater.hpp" | ||||
|  | ||||
| #include <cmath> | ||||
|  | ||||
| using namespace Concurrency; | ||||
|  | ||||
| BestEffortUpdater::BestEffortUpdater() { | ||||
| 	// ATOMIC_FLAG_INIT isn't necessarily safe to use, so establish default state by other means. | ||||
| 	update_is_ongoing_.clear(); | ||||
| } | ||||
|  | ||||
| BestEffortUpdater::~BestEffortUpdater() { | ||||
| 	// Don't allow further deconstruction until the task queue is stopped. | ||||
| 	flush(); | ||||
| } | ||||
|  | ||||
| void BestEffortUpdater::update() { | ||||
| 	// Perform an update only if one is not currently ongoing. | ||||
| 	if(!update_is_ongoing_.test_and_set()) { | ||||
| 		async_task_queue_.enqueue([this]() { | ||||
| 			// Get time now using the highest-resolution clock provided by the implementation, and determine | ||||
| 			// the duration since the last time this section was entered. | ||||
| 			const std::chrono::time_point<std::chrono::high_resolution_clock> now = std::chrono::high_resolution_clock::now(); | ||||
| 			const auto elapsed = now - previous_time_point_; | ||||
| 			previous_time_point_ = now; | ||||
|  | ||||
| 			if(has_previous_time_point_) { | ||||
| 				// If the duration is valid, convert it to integer cycles, maintaining a rolling error and call the delegate | ||||
| 				// if there is one. Proceed only if the number of cycles is positive, and cap it to the per-second maximum — | ||||
| 				// it's possible this is an adjustable clock so be ready to swallow unexpected adjustments. | ||||
| 				const int64_t duration = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count(); | ||||
| 				if(duration > 0) { | ||||
| 					double cycles = ((static_cast<double>(duration) * clock_rate_) / 1e9) + error_; | ||||
| 					error_ = fmod(cycles, 1.0); | ||||
|  | ||||
| 					if(delegate_) { | ||||
| 						delegate_->update(this, static_cast<int>(std::min(cycles, clock_rate_)), has_skipped_); | ||||
| 					} | ||||
| 					has_skipped_ = false; | ||||
| 				} | ||||
| 			} else { | ||||
| 				has_previous_time_point_ = true; | ||||
| 			} | ||||
|  | ||||
| 			// Allow furthers updates to occur. | ||||
| 			update_is_ongoing_.clear(); | ||||
| 		}); | ||||
| 	} else { | ||||
| 		async_task_queue_.enqueue([this]() { | ||||
| 			has_skipped_ = true; | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void BestEffortUpdater::flush() { | ||||
| 	async_task_queue_.flush(); | ||||
| } | ||||
|  | ||||
| void BestEffortUpdater::set_delegate(Delegate *const delegate) { | ||||
| 	async_task_queue_.enqueue([this, delegate]() { | ||||
| 		delegate_ = delegate; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void BestEffortUpdater::set_clock_rate(const double clock_rate) { | ||||
| 	async_task_queue_.enqueue([this, clock_rate]() { | ||||
| 		this->clock_rate_ = clock_rate; | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										66
									
								
								Concurrency/BestEffortUpdater.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Concurrency/BestEffortUpdater.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| // | ||||
| //  BestEffortUpdater.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 04/10/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef BestEffortUpdater_hpp | ||||
| #define BestEffortUpdater_hpp | ||||
|  | ||||
| #include <atomic> | ||||
| #include <chrono> | ||||
|  | ||||
| #include "AsyncTaskQueue.hpp" | ||||
|  | ||||
| namespace Concurrency { | ||||
|  | ||||
| /*! | ||||
| 	Accepts timing cues from multiple threads and ensures that a delegate receives calls to total | ||||
| 	a certain number of cycles per second, that those calls are strictly serialised, and that no | ||||
| 	backlog of calls accrues. | ||||
|  | ||||
| 	No guarantees about the thread that the delegate will be called on are made. | ||||
| */ | ||||
| class BestEffortUpdater { | ||||
| 	public: | ||||
| 		BestEffortUpdater(); | ||||
| 		~BestEffortUpdater(); | ||||
|  | ||||
| 		/// A delegate receives timing cues. | ||||
| 		struct Delegate { | ||||
| 			virtual void update(BestEffortUpdater *updater, int cycles, bool did_skip_previous_update) = 0; | ||||
| 		}; | ||||
|  | ||||
| 		/// Sets the current delegate. | ||||
| 		void set_delegate(Delegate *); | ||||
|  | ||||
| 		/// Sets the clock rate of the delegate. | ||||
| 		void set_clock_rate(double clock_rate); | ||||
|  | ||||
| 		/*! | ||||
| 			If the delegate is not currently in the process of an `update` call, calls it now to catch up to the current time. | ||||
| 			The call is asynchronous; this method will return immediately. | ||||
| 		*/ | ||||
| 		void update(); | ||||
|  | ||||
| 		/// Blocks until any ongoing update is complete. | ||||
| 		void flush(); | ||||
|  | ||||
| 	private: | ||||
| 		std::atomic_flag update_is_ongoing_; | ||||
| 		AsyncTaskQueue async_task_queue_; | ||||
|  | ||||
| 		std::chrono::time_point<std::chrono::high_resolution_clock> previous_time_point_; | ||||
| 		bool has_previous_time_point_ = false; | ||||
| 		double error_ = 0.0; | ||||
| 		bool has_skipped_ = false; | ||||
|  | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| 		double clock_rate_ = 1.0; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* BestEffortUpdater_hpp */ | ||||
							
								
								
									
										27
									
								
								Configurable/Configurable.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Configurable/Configurable.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  Configurable.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 18/11/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Configurable.hpp" | ||||
|  | ||||
| using namespace Configurable; | ||||
|  | ||||
| ListSelection *BooleanSelection::list_selection() { | ||||
| 	return new ListSelection(value ? "yes" : "no"); | ||||
| } | ||||
|  | ||||
| ListSelection *ListSelection::list_selection() { | ||||
| 	return new ListSelection(value); | ||||
| } | ||||
|  | ||||
| BooleanSelection *ListSelection::boolean_selection() { | ||||
| 	return new BooleanSelection(value != "no" && value != "n"); | ||||
| } | ||||
|  | ||||
| BooleanSelection *BooleanSelection::boolean_selection() { | ||||
| 	return new BooleanSelection(value); | ||||
| } | ||||
							
								
								
									
										98
									
								
								Configurable/Configurable.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								Configurable/Configurable.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| // | ||||
| //  Configurable.h | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 17/11/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Configurable_h | ||||
| #define Configurable_h | ||||
|  | ||||
| #include <map> | ||||
| #include <memory> | ||||
| #include <string> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Configurable { | ||||
|  | ||||
| /*! | ||||
| 	The Option class hierarchy provides a way for components, machines, etc, to provide a named | ||||
| 	list of typed options to which they can respond. | ||||
| */ | ||||
| struct Option { | ||||
| 	std::string long_name; | ||||
| 	std::string short_name; | ||||
| 	virtual ~Option() {} | ||||
|  | ||||
| 	Option(const std::string &long_name, const std::string &short_name) : long_name(long_name), short_name(short_name) {} | ||||
|  | ||||
| 	virtual bool operator==(const Option &rhs) { | ||||
| 		return long_name == rhs.long_name && short_name == rhs.short_name; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| struct BooleanOption: public Option { | ||||
| 	BooleanOption(const std::string &long_name, const std::string &short_name) : Option(long_name, short_name) {} | ||||
| }; | ||||
|  | ||||
| struct ListOption: public Option { | ||||
| 	std::vector<std::string> options; | ||||
| 	ListOption(const std::string &long_name, const std::string &short_name, const std::vector<std::string> &options) : Option(long_name, short_name), options(options) {} | ||||
|  | ||||
| 	virtual bool operator==(const Option &rhs) { | ||||
| 		const ListOption *list_rhs = dynamic_cast<const ListOption *>(&rhs); | ||||
| 		if(!list_rhs) return false; | ||||
| 		return long_name == rhs.long_name && short_name == rhs.short_name && options == list_rhs->options; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| struct BooleanSelection; | ||||
| struct ListSelection; | ||||
|  | ||||
| /*! | ||||
| 	Selections are responses to Options. | ||||
| */ | ||||
| struct Selection { | ||||
| 	virtual ~Selection() {} | ||||
| 	virtual ListSelection *list_selection() = 0; | ||||
| 	virtual BooleanSelection *boolean_selection() = 0; | ||||
| }; | ||||
|  | ||||
| struct BooleanSelection: public Selection { | ||||
| 	bool value; | ||||
|  | ||||
| 	ListSelection *list_selection(); | ||||
| 	BooleanSelection *boolean_selection(); | ||||
| 	BooleanSelection(bool value) : value(value) {} | ||||
| }; | ||||
|  | ||||
| struct ListSelection: public Selection { | ||||
| 	std::string value; | ||||
| 	 | ||||
| 	ListSelection *list_selection(); | ||||
| 	BooleanSelection *boolean_selection(); | ||||
| 	ListSelection(const std::string value) : value(value) {} | ||||
| }; | ||||
|  | ||||
| using SelectionSet = std::map<std::string, std::unique_ptr<Selection>>; | ||||
|  | ||||
| /*! | ||||
| 	A Configuratble provides the options that it responds to and allows selections to be set. | ||||
| */ | ||||
| struct Device { | ||||
| 	virtual std::vector<std::unique_ptr<Option>> get_options() = 0; | ||||
| 	virtual void set_selections(const SelectionSet &selection_by_option) = 0; | ||||
| 	virtual SelectionSet get_accurate_selections() = 0; | ||||
| 	virtual SelectionSet get_user_friendly_selections() = 0; | ||||
| }; | ||||
|  | ||||
| template <typename T> T *selection(const Configurable::SelectionSet &selections_by_option, const std::string &name) { | ||||
| 	auto selection = selections_by_option.find(name); | ||||
| 	if(selection == selections_by_option.end()) return nullptr; | ||||
| 	return dynamic_cast<T *>(selection->second.get()); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* Configurable_h */ | ||||
							
								
								
									
										76
									
								
								Configurable/StandardOptions.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								Configurable/StandardOptions.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| // | ||||
| //  StandardOptions.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 20/11/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "StandardOptions.hpp" | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| /*! | ||||
| 	Appends a Boolean selection of @c selection for option @c name to @c selection_set. | ||||
| */ | ||||
| void append_bool(Configurable::SelectionSet &selection_set, const std::string &name, bool selection) { | ||||
| 	selection_set[name] = std::unique_ptr<Configurable::Selection>(new Configurable::BooleanSelection(selection)); | ||||
| } | ||||
|  | ||||
| /*! | ||||
| 	Enquires for a Boolean selection for option @c name from @c selections_by_option, storing it to @c result if found. | ||||
| */ | ||||
| bool get_bool(const Configurable::SelectionSet &selections_by_option, const std::string &name, bool &result) { | ||||
| 	auto quickload = Configurable::selection<Configurable::BooleanSelection>(selections_by_option, "quickload"); | ||||
| 	if(!quickload) return false; | ||||
| 	result = quickload->value; | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| // MARK: - Standard option list builder | ||||
| std::vector<std::unique_ptr<Configurable::Option>> Configurable::standard_options(Configurable::StandardOptions mask) { | ||||
| 	std::vector<std::unique_ptr<Configurable::Option>> options; | ||||
| 	if(mask & QuickLoadTape)				options.emplace_back(new Configurable::BooleanOption("Load Tapes Quickly", "quickload")); | ||||
| 	if(mask & DisplayRGBComposite)			options.emplace_back(new Configurable::ListOption("Display", "display", {"composite", "rgb"})); | ||||
| 	if(mask & AutomaticTapeMotorControl)	options.emplace_back(new Configurable::BooleanOption("Automatic Tape Motor Control", "autotapemotor")); | ||||
| 	return options; | ||||
| } | ||||
|  | ||||
| // MARK: - Selection appenders | ||||
| void Configurable::append_quick_load_tape_selection(Configurable::SelectionSet &selection_set, bool selection) { | ||||
| 	append_bool(selection_set, "quickload", selection); | ||||
| } | ||||
|  | ||||
| void Configurable::append_automatic_tape_motor_control_selection(SelectionSet &selection_set, bool selection) { | ||||
| 	append_bool(selection_set, "autotapemotor", selection); | ||||
| } | ||||
|  | ||||
| void Configurable::append_display_selection(Configurable::SelectionSet &selection_set, Display selection) { | ||||
| 	selection_set["display"] = std::unique_ptr<Configurable::Selection>(new Configurable::ListSelection((selection == Display::RGB) ? "rgb" : "composite")); | ||||
| } | ||||
|  | ||||
| // MARK: - Selection parsers | ||||
| bool Configurable::get_quick_load_tape(const Configurable::SelectionSet &selections_by_option, bool &result) { | ||||
| 	return get_bool(selections_by_option, "quickload", result); | ||||
| } | ||||
|  | ||||
| bool Configurable::get_automatic_tape_motor_control_selection(const SelectionSet &selections_by_option, bool &result) { | ||||
| 	return get_bool(selections_by_option, "autotapemotor", result); | ||||
| } | ||||
|  | ||||
| bool Configurable::get_display(const Configurable::SelectionSet &selections_by_option, Configurable::Display &result) { | ||||
| 	auto display = Configurable::selection<Configurable::ListSelection>(selections_by_option, "display"); | ||||
| 	if(display) { | ||||
| 		if(display->value == "rgb") { | ||||
| 			result = Configurable::Display::RGB; | ||||
| 			return true; | ||||
| 		} | ||||
| 		if(display->value == "composite") { | ||||
| 			result = Configurable::Display::Composite; | ||||
| 			return true; | ||||
| 		} | ||||
| 	} | ||||
| 	return false; | ||||
| } | ||||
							
								
								
									
										76
									
								
								Configurable/StandardOptions.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								Configurable/StandardOptions.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| // | ||||
| //  StandardOptions.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 20/11/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef StandardOptions_hpp | ||||
| #define StandardOptions_hpp | ||||
|  | ||||
| #include "Configurable.hpp" | ||||
|  | ||||
| namespace Configurable { | ||||
|  | ||||
| enum StandardOptions { | ||||
| 	DisplayRGBComposite			= (1 << 0), | ||||
| 	QuickLoadTape				= (1 << 1), | ||||
| 	AutomaticTapeMotorControl	= (1 << 2) | ||||
| }; | ||||
|  | ||||
| enum class Display { | ||||
| 	RGB, | ||||
| 	Composite | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	@returns An option list comprised of the standard names for all the options indicated by @c mask. | ||||
| */ | ||||
| std::vector<std::unique_ptr<Option>> standard_options(StandardOptions mask); | ||||
|  | ||||
| /*! | ||||
| 	Appends to @c selection_set a selection of @c selection for QuickLoadTape. | ||||
| */ | ||||
| void append_quick_load_tape_selection(SelectionSet &selection_set, bool selection); | ||||
|  | ||||
| /*! | ||||
| 	Appends to @c selection_set a selection of @c selection for AutomaticTapeMotorControl. | ||||
| */ | ||||
| void append_automatic_tape_motor_control_selection(SelectionSet &selection_set, bool selection); | ||||
|  | ||||
| /*! | ||||
| 	Appends to @c selection_set a selection of @c selection for DisplayRGBComposite. | ||||
| */ | ||||
| void append_display_selection(SelectionSet &selection_set, Display selection); | ||||
|  | ||||
| /*! | ||||
| 	Attempts to discern a QuickLoadTape selection from @c selections_by_option. | ||||
|   | ||||
| 	@param selections_by_option The user selections. | ||||
| 	@param result The location to which the selection will be stored if found. | ||||
| 	@returns @c true if a selection is found; @c false otherwise. | ||||
| */ | ||||
| bool get_quick_load_tape(const SelectionSet &selections_by_option, bool &result); | ||||
|  | ||||
| /*! | ||||
| 	Attempts to discern an AutomaticTapeMotorControl selection from @c selections_by_option. | ||||
|   | ||||
| 	@param selections_by_option The user selections. | ||||
| 	@param result The location to which the selection will be stored if found. | ||||
| 	@returns @c true if a selection is found; @c false otherwise. | ||||
| */ | ||||
| bool get_automatic_tape_motor_control_selection(const SelectionSet &selections_by_option, bool &result); | ||||
|  | ||||
| /*! | ||||
| 	Attempts to discern a display RGB/composite selection from @c selections_by_option. | ||||
|   | ||||
| 	@param selections_by_option The user selections. | ||||
| 	@param result The location to which the selection will be stored if found. | ||||
| 	@returns @c true if a selection is found; @c false otherwise. | ||||
| */ | ||||
| bool get_display(const SelectionSet &selections_by_option, Display &result); | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* StandardOptions_hpp */ | ||||
							
								
								
									
										68
									
								
								Inputs/Joystick.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								Inputs/Joystick.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| // | ||||
| //  Joystick.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 14/10/2017. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Joystick_hpp | ||||
| #define Joystick_hpp | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace Inputs { | ||||
|  | ||||
| /*! | ||||
| 	Provides an intermediate idealised model of a simple joystick, allowing a host | ||||
| 	machine to toggle states, while an interested party either observes or polls. | ||||
| */ | ||||
| class Joystick { | ||||
| 	public: | ||||
| 		virtual ~Joystick() {} | ||||
|  | ||||
| 		struct DigitalInput { | ||||
| 			enum Type { | ||||
| 				Up, Down, Left, Right, Fire, | ||||
| 				Key | ||||
| 			} type; | ||||
| 			union { | ||||
| 				struct { | ||||
| 					int index; | ||||
| 				} control; | ||||
| 				struct { | ||||
| 					wchar_t symbol; | ||||
| 				} key; | ||||
| 			} info; | ||||
|  | ||||
| 			DigitalInput(Type type, int index = 0) : type(type) { | ||||
| 				info.control.index = index; | ||||
| 			} | ||||
| 			DigitalInput(wchar_t symbol) : type(Key) { | ||||
| 				info.key.symbol = symbol; | ||||
| 			} | ||||
|  | ||||
| 			bool operator == (const DigitalInput &rhs) { | ||||
| 				if(rhs.type != type) return false; | ||||
| 				if(rhs.type == Key) { | ||||
| 					return rhs.info.key.symbol == info.key.symbol; | ||||
| 				} else { | ||||
| 					return rhs.info.control.index == info.control.index; | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		virtual std::vector<DigitalInput> get_inputs() = 0; | ||||
|  | ||||
| 		// Host interface. | ||||
| 		virtual void set_digital_input(const DigitalInput &digital_input, bool is_active) = 0; | ||||
| 		virtual void reset_all_inputs() { | ||||
| 			for(const auto &input: get_inputs()) { | ||||
| 				set_digital_input(input, false); | ||||
| 			} | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* Joystick_hpp */ | ||||
							
								
								
									
										38
									
								
								Inputs/Keyboard.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								Inputs/Keyboard.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| // | ||||
| //  Keyboard.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/9/17. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Keyboard.hpp" | ||||
|  | ||||
| using namespace Inputs; | ||||
|  | ||||
| Keyboard::Keyboard() {} | ||||
|  | ||||
| void Keyboard::set_key_pressed(Key key, bool is_pressed) { | ||||
| 	std::size_t key_offset = static_cast<std::size_t>(key); | ||||
| 	if(key_offset >= key_states_.size()) { | ||||
| 		key_states_.resize(key_offset+1, false); | ||||
| 	} | ||||
| 	key_states_[key_offset] = is_pressed; | ||||
|  | ||||
| 	if(delegate_) delegate_->keyboard_did_change_key(this, key, is_pressed); | ||||
| } | ||||
|  | ||||
| void Keyboard::reset_all_keys() { | ||||
| 	std::fill(key_states_.begin(), key_states_.end(), false); | ||||
| 	if(delegate_) delegate_->reset_all_keys(this); | ||||
| } | ||||
|  | ||||
| void Keyboard::set_delegate(Delegate *delegate) { | ||||
| 	delegate_ = delegate; | ||||
| } | ||||
|  | ||||
| bool Keyboard::get_key_state(Key key) { | ||||
| 	std::size_t key_offset = static_cast<std::size_t>(key); | ||||
| 	if(key_offset >= key_states_.size()) return false; | ||||
| 	return key_states_[key_offset]; | ||||
| } | ||||
							
								
								
									
										61
									
								
								Inputs/Keyboard.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								Inputs/Keyboard.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| // | ||||
| //  Keyboard.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 10/9/17. | ||||
| //  Copyright © 2017 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef Keyboard_hpp | ||||
| #define Keyboard_hpp | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace Inputs { | ||||
|  | ||||
| /*! | ||||
| 	Provides an intermediate idealised model of a modern-era computer keyboard | ||||
| 	(so, heavily indebted to the current Windows and Mac layouts), allowing a host | ||||
| 	machine to toggle states, while an interested party either observes or polls. | ||||
| */ | ||||
| class Keyboard { | ||||
| 	public: | ||||
| 		Keyboard(); | ||||
|  | ||||
| 		enum class Key { | ||||
| 			Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, PrintScreen, ScrollLock, Pause, | ||||
| 			BackTick, k1, k2, k3, k4, k5, k6, k7, k8, k9, k0, Hyphen, Equals, BackSpace, | ||||
| 			Tab, Q, W, E, R, T, Y, U, I, O, P, OpenSquareBracket, CloseSquareBracket, BackSlash, | ||||
| 			CapsLock, A, S, D, F, G, H, J, K, L, Semicolon, Quote, Hash, Enter, | ||||
| 			LeftShift, Z, X, C, V, B, N, M, Comma, FullStop, ForwardSlash, RightShift, | ||||
| 			LeftControl, LeftOption, LeftMeta, Space, RightMeta, RightOption, RightControl, | ||||
| 			Left, Right, Up, Down, | ||||
| 			Insert, Home, PageUp, Delete, End, PageDown, | ||||
| 			NumLock, KeyPadSlash, KeyPadAsterisk, KeyPadDelete, | ||||
| 			KeyPad7, KeyPad8, KeyPad9, KeyPadPlus, | ||||
| 			KeyPad4, KeyPad5, KeyPad6, KeyPadMinus, | ||||
| 			KeyPad1, KeyPad2, KeyPad3, KeyPadEnter, | ||||
| 			KeyPad0, KeyPadDecimalPoint, KeyPadEquals, | ||||
| 			Help | ||||
| 		}; | ||||
|  | ||||
| 		// Host interface. | ||||
| 		virtual void set_key_pressed(Key key, bool is_pressed); | ||||
| 		virtual void reset_all_keys(); | ||||
|  | ||||
| 		// Delegate interface. | ||||
| 		struct Delegate { | ||||
| 			virtual void keyboard_did_change_key(Keyboard *keyboard, Key key, bool is_pressed) = 0; | ||||
| 			virtual void reset_all_keys(Keyboard *keyboard) = 0; | ||||
| 		}; | ||||
| 		void set_delegate(Delegate *delegate); | ||||
| 		bool get_key_state(Key key); | ||||
|  | ||||
| 	private: | ||||
| 		std::vector<bool> key_states_; | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| #endif /* Keyboard_hpp */ | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user