mirror of
				https://github.com/TomHarte/CLK.git
				synced 2025-10-30 14:16:04 +00:00 
			
		
		
		
	Compare commits
	
		
			475 Commits
		
	
	
		
			Z80Tests
			...
			2024-05-05
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e6724a701a | ||
|  | d90eedfc8c | ||
|  | 63009d00b4 | ||
|  | 6a2261d217 | ||
|  | c3ad2154b5 | ||
|  | 3d61861737 | ||
|  | 7545786436 | ||
|  | a997b6c677 | ||
|  | 72d4f638aa | ||
|  | b15ff6d442 | ||
|  | cb70967971 | ||
|  | 42aea2663c | ||
|  | a882faa7f6 | ||
|  | 5da01e4fd8 | ||
|  | 71c5a1d419 | ||
|  | 03c3da7338 | ||
|  | 47b276ca0b | ||
|  | c7747ec5a0 | ||
|  | 5a84e98256 | ||
|  | b66d69b60c | ||
|  | dfaea5a922 | ||
|  | f4da417c3a | ||
|  | fa7fff86eb | ||
|  | d480f9eae2 | ||
|  | 4f1aef90b8 | ||
|  | 24f4538eb7 | ||
|  | 38d096cad6 | ||
|  | b82af9c471 | ||
|  | 0bff2089c4 | ||
|  | 36d9c40d7b | ||
|  | becb6ce2e0 | ||
|  | 56b65780d2 | ||
|  | 265d151879 | ||
|  | c485097eed | ||
|  | f86e9fe086 | ||
|  | c91ce4cfea | ||
|  | 8e64a854fc | ||
|  | 7c9383cd6b | ||
|  | 82d03e3980 | ||
|  | 0775e3ad58 | ||
|  | ea3eef3817 | ||
|  | 83eac172c9 | ||
|  | 5b13d3e893 | ||
|  | 807835b9fe | ||
|  | 4bf02122ee | ||
|  | e6c4454059 | ||
|  | d464ce831a | ||
|  | 018f0e097f | ||
|  | 2acb853021 | ||
|  | acd477df39 | ||
|  | da520de9ef | ||
|  | e680a973b0 | ||
|  | 07984a2f8b | ||
|  | 16532136e9 | ||
|  | 30c2c65b77 | ||
|  | b63178132d | ||
|  | 6d66c90aad | ||
|  | c807c75412 | ||
|  | f6feaddfe6 | ||
|  | 87d1a476a4 | ||
|  | f7337f2400 | ||
|  | fac94a5d36 | ||
|  | 140228a936 | ||
|  | 06fd91f002 | ||
|  | c3d4d0ee38 | ||
|  | 30cca54e6c | ||
|  | 6ac6e48b95 | ||
|  | 779794632e | ||
|  | 88bb16f261 | ||
|  | c134c7bdc2 | ||
|  | 6c6cda3db5 | ||
|  | a29f246536 | ||
|  | d9d675a74f | ||
|  | d62ea95889 | ||
|  | e2e951ad0b | ||
|  | a5a653d684 | ||
|  | 6123350895 | ||
|  | ec73c00c3b | ||
|  | dd24f5f4f3 | ||
|  | 82a2e802ea | ||
|  | 3b75eeb70a | ||
|  | ee4575b70f | ||
|  | 46a4ec1cb1 | ||
|  | 8ab77a3260 | ||
|  | b875e349c1 | ||
|  | 169298af42 | ||
|  | 5e502df48b | ||
|  | 4f58664f97 | ||
|  | ffd298218c | ||
|  | d2b077c573 | ||
|  | 547dc29a60 | ||
|  | 69aeca5c0e | ||
|  | ed7cd4b277 | ||
|  | 7bf831e1a6 | ||
|  | 0092cb8c36 | ||
|  | 543b1c644a | ||
|  | cfaea7a90c | ||
|  | b821645644 | ||
|  | 2865190499 | ||
|  | 3f40e409c5 | ||
|  | 002e235d90 | ||
|  | 7d8a364658 | ||
|  | 41c471ca52 | ||
|  | dd127f64fe | ||
|  | b19dcfd6dc | ||
|  | 55369464ad | ||
|  | 609c117267 | ||
|  | 3b62a2fe7a | ||
|  | 7c9715f00c | ||
|  | 7de92a9457 | ||
|  | 0866caf934 | ||
|  | 914b88d115 | ||
|  | cc122a7a68 | ||
|  | 31979649c6 | ||
|  | 335d13d06d | ||
|  | ec785f3a8a | ||
|  | 1f83a5425e | ||
|  | 4882d6d0f2 | ||
|  | 722743659b | ||
|  | 6e64a79b52 | ||
|  | 8a6bf84cff | ||
|  | a0fdd8f4eb | ||
|  | bda1783624 | ||
|  | 2a14557478 | ||
|  | 0ddbc67b1f | ||
|  | ffb5149890 | ||
|  | bb339d619f | ||
|  | 2ed11877e8 | ||
|  | ea6b83815b | ||
|  | 740b0e35d5 | ||
|  | 2e7c1acb88 | ||
|  | 4fcb85d132 | ||
|  | f175dcea58 | ||
|  | c04c708a9d | ||
|  | f4cf1e5313 | ||
|  | 0e17f382a1 | ||
|  | f38bca37a2 | ||
|  | 166793ebe6 | ||
|  | 8b04d0e3ef | ||
|  | a3931674dc | ||
|  | bd4ef5ec57 | ||
|  | 3ba12630ab | ||
|  | 342d90c929 | ||
|  | 9078fc994b | ||
|  | f46af4b702 | ||
|  | b112987556 | ||
|  | fc880ac130 | ||
|  | a2d95cb982 | ||
|  | d2776071e4 | ||
|  | 72a645ec1e | ||
|  | 1154ffd072 | ||
|  | 8ba9708942 | ||
|  | 521fca6089 | ||
|  | ae684edbe1 | ||
|  | fa0a9aa611 | ||
|  | 5da9e0486a | ||
|  | 6980fd760c | ||
|  | 3549488b7a | ||
|  | c1602cc8fe | ||
|  | 189dd176de | ||
|  | 3cf262d1f7 | ||
|  | ccfc389274 | ||
|  | 0e07f802ac | ||
|  | 55f92e2411 | ||
|  | c720f3910a | ||
|  | 4215edd11b | ||
|  | 09a61cf1a7 | ||
|  | 5967ad0865 | ||
|  | eb34c38332 | ||
|  | 5ccb18225a | ||
|  | 58bbce1a15 | ||
|  | 9ea3e547ee | ||
|  | fb5fdc9f10 | ||
|  | de7b7818f4 | ||
|  | c4e6b18294 | ||
|  | ae6cf69449 | ||
|  | 4a2dcff028 | ||
|  | aa6acec8fa | ||
|  | 4ac4da908c | ||
|  | 66e62857c4 | ||
|  | bbc0d8b050 | ||
|  | 0f8bc416d1 | ||
|  | 2ec235170e | ||
|  | 2de1a2dd0d | ||
|  | 1f49c3b113 | ||
|  | 5c645fb3c2 | ||
|  | 40b5227f0b | ||
|  | 847dba8f07 | ||
|  | 417c6c4629 | ||
|  | 2d6a4d490e | ||
|  | a6ec870872 | ||
|  | 389541be6d | ||
|  | 208f3e24de | ||
|  | f7e36a1e03 | ||
|  | 1341816791 | ||
|  | b986add74a | ||
|  | 08673ff021 | ||
|  | 3a2d9c6082 | ||
|  | 43a3959b8f | ||
|  | 85a738acff | ||
|  | 17dbdce230 | ||
|  | 9d084782ae | ||
|  | 106937b679 | ||
|  | 623eda7162 | ||
|  | 2ad6bb099b | ||
|  | 9d858bc61b | ||
|  | 612c9ce49a | ||
|  | 64e025484a | ||
|  | 7b1f800387 | ||
|  | 2712d50e05 | ||
|  | 47e9279bd4 | ||
|  | 635efd0212 | ||
|  | 1c1d2891c7 | ||
|  | 1979d2e5ba | ||
|  | c25d0e8843 | ||
|  | 3a899ea4be | ||
|  | 9d08282e28 | ||
|  | 18154278d1 | ||
|  | 9063852857 | ||
|  | bc27e3998d | ||
|  | 19fa0b8945 | ||
|  | 4987bdfec9 | ||
|  | 0e4615564d | ||
|  | 7aeea535a1 | ||
|  | 6b18d775ab | ||
|  | 2ed031e440 | ||
|  | 5d6bb11eb7 | ||
|  | c6b91559e1 | ||
|  | 6efc41ded7 | ||
|  | e9c5582fe1 | ||
|  | 8b3c0abe93 | ||
|  | a5ebac1b29 | ||
|  | 1ccfae885c | ||
|  | 971bfb2ecb | ||
|  | e7457461ba | ||
|  | e8c1e8fd3f | ||
|  | ca779bc841 | ||
|  | a28c97c0de | ||
|  | db49146efe | ||
|  | 830d70d3aa | ||
|  | 336292bc49 | ||
|  | bd62228cc6 | ||
|  | ccdd340c9a | ||
|  | 0b42f5fb30 | ||
|  | e9e1db7a05 | ||
|  | 21278d028c | ||
|  | fbc273f114 | ||
|  | 06a5df029d | ||
|  | e17700b495 | ||
|  | 655b1e516c | ||
|  | 4e7a63f792 | ||
|  | a2896b9bd0 | ||
|  | a4cf86268e | ||
|  | d059e7c5d8 | ||
|  | d6f882a8bb | ||
|  | 08f50f3eff | ||
|  | 47f7340dfc | ||
|  | fdef8901ab | ||
|  | ca1c3dc005 | ||
|  | 9406a97141 | ||
|  | a46ec4cffb | ||
|  | 9bb5dc3c2b | ||
|  | f6ea442606 | ||
|  | fa8fcd2218 | ||
|  | 2a36d0fcbc | ||
|  | 0e92885ed5 | ||
|  | f5225b69e5 | ||
|  | 15ee84b2eb | ||
|  | d380cecdb7 | ||
|  | ae3cd924e8 | ||
|  | a0f0f73bde | ||
|  | 7cdceb7b4f | ||
|  | 38b5624639 | ||
|  | 3405b3b287 | ||
|  | 173fc9329a | ||
|  | 691a42d81e | ||
|  | 4059905f85 | ||
|  | bbb520fd12 | ||
|  | 108a056f1c | ||
|  | ed92e98ca2 | ||
|  | 0d666f9935 | ||
|  | fe467be124 | ||
|  | ba5f142515 | ||
|  | ed586e80bc | ||
|  | 871c5467d7 | ||
|  | 387791635e | ||
|  | b7a1363add | ||
|  | 341b705bef | ||
|  | 0b65aa39cd | ||
|  | 1b7c3644f4 | ||
|  | 0cdca12e06 | ||
|  | 61d4c69e45 | ||
|  | 79865e295b | ||
|  | 1f43047de8 | ||
|  | 6f0ad0ab71 | ||
|  | 447734b1e9 | ||
|  | 3e80651a0e | ||
|  | 692a9da2e4 | ||
|  | e27312c980 | ||
|  | eae92a0cdb | ||
|  | 95cc34ba23 | ||
|  | 7532b461cd | ||
|  | 230e9c6327 | ||
|  | 11c4d2f09e | ||
|  | f2db1b4aae | ||
|  | b42a6e447d | ||
|  | 8a83d71560 | ||
|  | 9fd7d5c10f | ||
|  | 7a5ed6c427 | ||
|  | 4e7963ee81 | ||
|  | 945b7e90da | ||
|  | 99f0233b76 | ||
|  | 62da0dee7f | ||
|  | 1663d3d9d1 | ||
|  | 37499d493a | ||
|  | c0dd96eb7c | ||
|  | 2abae4c8bf | ||
|  | c865da67e0 | ||
|  | e6f77a9b80 | ||
|  | 7b28b3d634 | ||
|  | 42ba6d1281 | ||
|  | 85b7afd530 | ||
|  | f2f59a4de5 | ||
|  | 5759798ad7 | ||
|  | ab1dd7f57e | ||
|  | 53a2ea3a57 | ||
|  | 1f1e7236be | ||
|  | fd2c5b6679 | ||
|  | 0b287c55d5 | ||
|  | 0de8240238 | ||
|  | 1449b2a2a6 | ||
|  | 0f691766ee | ||
|  | 3ce05e9de1 | ||
|  | 98f5d0cdb7 | ||
|  | 93b4008f81 | ||
|  | 904462b881 | ||
|  | 3b320bcdef | ||
|  | 3368bdb99f | ||
|  | 4d400c3cb7 | ||
|  | 474f9da3c2 | ||
|  | c49b26701f | ||
|  | 9b42d35d56 | ||
|  | 645152a1fd | ||
|  | 487ade56ed | ||
|  | 60d1b36e9a | ||
|  | 5a48c15e46 | ||
|  | d6bf1808f9 | ||
|  | b676153d21 | ||
|  | a3339cf882 | ||
|  | b4e0b46bac | ||
|  | 09c1b2d7db | ||
|  | 4255283e33 | ||
|  | 16e827bb2c | ||
|  | def69ce6d5 | ||
|  | 054a799699 | ||
|  | 580f402bb6 | ||
|  | 030dda34f0 | ||
|  | cd21b39f44 | ||
|  | 481b6d0e69 | ||
|  | a88d41bf00 | ||
|  | 0ee3b628e8 | ||
|  | 45628ba9df | ||
|  | c843c395ea | ||
|  | 9bdaf31d04 | ||
|  | 4ac2baeb9d | ||
|  | c56e82207a | ||
|  | 82abebec6e | ||
|  | 60286c3a15 | ||
|  | 8460fe2118 | ||
|  | 4b5456c9ba | ||
|  | ddf136556d | ||
|  | 4c45f4468e | ||
|  | 73d2acca12 | ||
|  | 56a5df3783 | ||
|  | d205e538e1 | ||
|  | f9cbec668b | ||
|  | 6577f68efc | ||
|  | e986ae2878 | ||
|  | b2696450d5 | ||
|  | 2bbaf73aa2 | ||
|  | 0fe2c1406b | ||
|  | 954d920b9e | ||
|  | 57b45076c5 | ||
|  | d639dc8bcb | ||
|  | 9a74ab6a8e | ||
|  | 4c53414cc3 | ||
|  | c36288dd6b | ||
|  | bc5727af14 | ||
|  | bd0a15c054 | ||
|  | a758112084 | ||
|  | 3981f3a874 | ||
|  | e8036127fe | ||
|  | 17abd87791 | ||
|  | 35545451fe | ||
|  | 64bec0cc3d | ||
|  | fadd3bc6fc | ||
|  | d9ec11c62e | ||
|  | 093a029b8c | ||
|  | b4a3b23571 | ||
|  | be99183f1d | ||
|  | dda5f41487 | ||
|  | a09457dab5 | ||
|  | ac171d166e | ||
|  | 51de1892c0 | ||
|  | e1fdda928a | ||
|  | 1c8261dc09 | ||
|  | cb22278c7f | ||
|  | 809bc9d6a8 | ||
|  | be11f31d5d | ||
|  | 0103761b7b | ||
|  | 3ac5fdafab | ||
|  | 1e877c7563 | ||
|  | 07c11e8268 | ||
|  | 0dcceff410 | ||
|  | 2a684ab302 | ||
|  | 27059233b3 | ||
|  | 7f84d5ac6f | ||
|  | d43f050922 | ||
|  | 3ba2618547 | ||
|  | a3e104f8e2 | ||
|  | 1bb82189e9 | ||
|  | e06a66644c | ||
|  | 6dcc13921f | ||
|  | fd45745600 | ||
|  | 507c3da927 | ||
|  | f14e45f93e | ||
|  | 3d2d9ac45e | ||
|  | 1895b4ee5d | ||
|  | d49c07687c | ||
|  | 3a208460e2 | ||
|  | 472297e411 | ||
|  | 25085cb5af | ||
|  | 9909146c59 | ||
|  | 609d81d75d | ||
|  | c105acf1c7 | ||
|  | f3d0827d14 | ||
|  | a4a983eb81 | ||
|  | 48be7c677e | ||
|  | 147d817977 | ||
|  | d481f335b8 | ||
|  | 228012cd0c | ||
|  | f4d8c04f3c | ||
|  | c6c9be0b08 | ||
|  | 3827929a15 | ||
|  | ca7e4b3a0e | ||
|  | fd73c24fc3 | ||
|  | ce0d53b277 | ||
|  | 7dc3b5ba06 | ||
|  | 17cad73177 | ||
|  | b28e3eb419 | ||
|  | d811501421 | ||
|  | c68dd50fde | ||
|  | ad8abf2e05 | ||
|  | 01d9455897 | ||
|  | cbf8849004 | ||
|  | f3a7d82dc1 | ||
|  | 017674de35 | ||
|  | 4f211334b5 | ||
|  | 31e261f7e5 | ||
|  | 15b5a62e01 | ||
|  | f5800aa004 | ||
|  | 584aa78695 | ||
|  | 030f49db83 | ||
|  | cc165b65be | ||
|  | cb125e6336 | ||
|  | a3337ea90f | ||
|  | ae31f85f0c | ||
|  | 5e9f484662 | ||
|  | f2e29357bf | ||
|  | 8a1a14ba4c | ||
|  | 31cbcb206f | ||
|  | bf9e913a7b | ||
|  | d3cea4a10f | ||
|  | a6df20ff84 | ||
|  | 7122a9ee16 | 
							
								
								
									
										46
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										46
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,11 +1,11 @@ | ||||
| name: Build | ||||
| on: [pull_request] | ||||
| jobs: | ||||
|   build-mac: | ||||
|     name: Mac UI on ${{ matrix.os }} | ||||
|   build-mac-xcodebuild: | ||||
|     name: Mac UI / xcodebuild / ${{ matrix.os }} | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [macos-latest] | ||||
|         os: [macos-11, macos-12, macos-13, macos-14] | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     steps: | ||||
|     - name: Checkout | ||||
| @@ -13,13 +13,49 @@ jobs: | ||||
|     - name: Make | ||||
|       working-directory: OSBindings/Mac | ||||
|       run: xcodebuild CODE_SIGN_IDENTITY=- | ||||
|   build-sdl: | ||||
|     name: SDL UI on ${{ matrix.os }} | ||||
|   build-sdl-cmake: | ||||
|     name: SDL UI / cmake / ${{ matrix.os }} | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [macos-latest, ubuntu-latest] | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     steps: | ||||
|     - name: Checkout | ||||
|       uses: actions/checkout@v4 | ||||
|     - name: Install dependencies | ||||
|       shell: bash | ||||
|       run: | | ||||
|         case $RUNNER_OS in | ||||
|           Linux) | ||||
|             sudo apt-get --allow-releaseinfo-change update | ||||
|             sudo apt-get --fix-missing install cmake gcc-10 libsdl2-dev | ||||
|             ;; | ||||
|           macOS) | ||||
|             brew install cmake sdl2 | ||||
|             ;; | ||||
|         esac | ||||
|     - name: Make | ||||
|       shell: bash | ||||
|       run: | | ||||
|         case $RUNNER_OS in | ||||
|           Linux) | ||||
|             jobs=$(nproc --all) | ||||
|             ;; | ||||
|           macOS) | ||||
|             jobs=$(sysctl -n hw.activecpu) | ||||
|             ;; | ||||
|           *) | ||||
|             jobs=1 | ||||
|         esac | ||||
|         cmake -S. -Bbuild -DCLK_UI=SDL -DCMAKE_BUILD_TYPE=Release | ||||
|         cmake --build build -v -j"$jobs" | ||||
|   build-sdl-scons: | ||||
|     name: SDL UI / scons / ${{ matrix.os }} | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [macos-14, ubuntu-latest] | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     steps: | ||||
|     - name: Checkout | ||||
|       uses: actions/checkout@v4 | ||||
|     - name: Install dependencies | ||||
|   | ||||
| @@ -23,7 +23,7 @@ namespace Activity { | ||||
| */ | ||||
| class Observer { | ||||
| 	public: | ||||
| 		virtual ~Observer() {} | ||||
| 		virtual ~Observer() = default; | ||||
|  | ||||
| 		/// Provides hints as to the sort of information presented on an LED. | ||||
| 		enum LEDPresentation: uint8_t { | ||||
|   | ||||
| @@ -17,6 +17,7 @@ enum class Machine { | ||||
| 	Atari2600, | ||||
| 	AtariST, | ||||
| 	Amiga, | ||||
| 	Archimedes, | ||||
| 	ColecoVision, | ||||
| 	Electron, | ||||
| 	Enterprise, | ||||
|   | ||||
| @@ -13,6 +13,7 @@ | ||||
| #include "../../../Numeric/CRC.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cstring> | ||||
|  | ||||
| using namespace Analyser::Static::Acorn; | ||||
|  | ||||
| @@ -86,34 +87,56 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std:: | ||||
| 	auto catalogue = std::make_unique<Catalogue>(); | ||||
| 	Storage::Encodings::MFM::Parser parser(Storage::Encodings::MFM::Density::Double, disk); | ||||
|  | ||||
| 	// Grab the second half of the free-space map because it has the boot option in it. | ||||
| 	const Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.sector(0, 0, 1); | ||||
| 	if(!free_space_map_second_half) return nullptr; | ||||
| 	catalogue->has_large_sectors = free_space_map_second_half->samples[0].size() == 1024; | ||||
|  | ||||
| 	// Possibility: this is a large-sector disk with an old-style free space map. In which | ||||
| 	// case the above just read the start of the root directory. | ||||
| 	uint8_t first_directory_sector = 2; | ||||
| 	if(catalogue->has_large_sectors && !memcmp(&free_space_map_second_half->samples[0][1], "Hugo", 4)) { | ||||
| 		free_space_map_second_half = parser.sector(0, 0, 0); | ||||
| 		if(!free_space_map_second_half) return nullptr; | ||||
| 		first_directory_sector = 1; | ||||
| 	} | ||||
|  | ||||
| 	std::vector<uint8_t> root_directory; | ||||
| 	root_directory.reserve(5 * 256); | ||||
| 	for(uint8_t c = 2; c < 7; c++) { | ||||
| 	root_directory.reserve(catalogue->has_large_sectors ? 2*1024 : 5*256); | ||||
|  | ||||
| 	for(uint8_t c = first_directory_sector; c < first_directory_sector + (catalogue->has_large_sectors ? 2 : 5); c++) { | ||||
| 		const Storage::Encodings::MFM::Sector *const sector = parser.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; | ||||
| 	// Check for end of directory marker. | ||||
| 	if(root_directory[catalogue->has_large_sectors ? 0x7d7 : 0x4cb]) return nullptr; | ||||
|  | ||||
| 	// Check for both directory identifiers. | ||||
| 	const uint8_t *const start_id = &root_directory[1]; | ||||
| 	const uint8_t *const end_id = &root_directory[root_directory.size() - 5]; | ||||
| 	catalogue->is_hugo = !memcmp(start_id, "Hugo", 4) && !memcmp(end_id, "Hugo", 4); | ||||
| 	const bool is_nick = !memcmp(start_id, "Nick", 4) && !memcmp(end_id, "Nick", 4); | ||||
| 	if(!catalogue->is_hugo && !is_nick) { | ||||
| 		return nullptr; | ||||
| 	} | ||||
|  | ||||
| 	if(!catalogue->has_large_sectors) { | ||||
| 		// TODO: I don't know where the boot option rests with large sectors. | ||||
| 		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; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Parse the root directory, at least. | ||||
| 	for(std::size_t file_offset = 0x005; file_offset < 0x4cb; file_offset += 0x1a) { | ||||
| 	for(std::size_t file_offset = 0x005; file_offset < (catalogue->has_large_sectors ? 0x7d7 : 0x4cb); file_offset += 0x1a) { | ||||
| 		// Obtain the name, which will be at most ten characters long, and will | ||||
| 		// be terminated by either a NULL character or a \r. | ||||
| 		char name[11]; | ||||
| 		char name[11]{}; | ||||
| 		std::size_t c = 0; | ||||
| 		for(; c < 10; c++) { | ||||
| 			const char next = root_directory[file_offset + c] & 0x7f; | ||||
|   | ||||
| @@ -15,6 +15,8 @@ namespace Analyser::Static::Acorn { | ||||
|  | ||||
| /// Describes a DFS- or ADFS-format catalogue(/directory): the list of files available and the catalogue's boot option. | ||||
| struct Catalogue { | ||||
| 	bool is_hugo = false; | ||||
| 	bool has_large_sectors = false; | ||||
| 	std::string name; | ||||
| 	std::vector<File> files; | ||||
| 	enum class BootOption { | ||||
|   | ||||
| @@ -60,12 +60,13 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> | ||||
| } | ||||
|  | ||||
| Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) { | ||||
| 	auto target = std::make_unique<Target>(); | ||||
| 	auto target8bit = std::make_unique<Target>(); | ||||
| 	auto targetArchimedes = std::make_unique<Analyser::Static::Target>(Machine::Archimedes); | ||||
|  | ||||
| 	// strip out inappropriate cartridges | ||||
| 	target->media.cartridges = AcornCartridgesFrom(media.cartridges); | ||||
| 	// Copy appropriate cartridges to the 8-bit target. | ||||
| 	target8bit->media.cartridges = AcornCartridgesFrom(media.cartridges); | ||||
|  | ||||
| 	// if there are any tapes, attempt to get data from the first | ||||
| 	// If there are any tapes, attempt to get data from the first. | ||||
| 	if(!media.tapes.empty()) { | ||||
| 		std::shared_ptr<Storage::Tape::Tape> tape = media.tapes.front(); | ||||
| 		std::vector<File> files = GetFiles(tape); | ||||
| @@ -94,30 +95,34 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me | ||||
|  | ||||
| 			// Inspect first file. If it's protected or doesn't look like BASIC | ||||
| 			// then the loading command is *RUN. Otherwise it's CHAIN"". | ||||
| 			target->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n"; | ||||
| 			target8bit->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n"; | ||||
|  | ||||
| 			target->media.tapes = media.tapes; | ||||
| 			target8bit->media.tapes = media.tapes; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if(!media.disks.empty()) { | ||||
| 		// TODO: below requires an [8-bit compatible] 'Hugo' ADFS catalogue, disallowing | ||||
| 		// [Archimedes-exclusive] 'Nick' catalogues. | ||||
| 		// | ||||
| 		// Would be better to form the appropriate target in the latter case. | ||||
| 		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) { | ||||
| 		if(dfs_catalogue || (adfs_catalogue && !adfs_catalogue->has_large_sectors && adfs_catalogue->is_hugo)) { | ||||
| 			// Accept the disk and determine whether DFS or ADFS ROMs are implied. | ||||
| 			// Use the Pres ADFS if using an ADFS, as it leaves Page at &EOO. | ||||
| 			target->media.disks = media.disks; | ||||
| 			target->has_dfs = bool(dfs_catalogue); | ||||
| 			target->has_pres_adfs = bool(adfs_catalogue); | ||||
| 			target8bit->media.disks = media.disks; | ||||
| 			target8bit->has_dfs = bool(dfs_catalogue); | ||||
| 			target8bit->has_pres_adfs = bool(adfs_catalogue); | ||||
|  | ||||
| 			// Check whether a simple shift+break will do for loading this disk. | ||||
| 			Catalogue::BootOption bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption; | ||||
| 			if(bootOption != Catalogue::BootOption::None) { | ||||
| 				target->should_shift_restart = true; | ||||
| 				target8bit->should_shift_restart = true; | ||||
| 			} else { | ||||
| 				target->loading_command = "*CAT\n"; | ||||
| 				target8bit->loading_command = "*CAT\n"; | ||||
| 			} | ||||
|  | ||||
| 			// Check whether adding the AP6 ROM is justified. | ||||
| @@ -133,39 +138,44 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me | ||||
| 					"VERIFY", "ZERO" | ||||
| 				}) { | ||||
| 					if(std::search(file.data.begin(), file.data.end(), command, command+strlen(command)) != file.data.end()) { | ||||
| 						target->has_ap6_rom = true; | ||||
| 						target->has_sideways_ram = true; | ||||
| 						target8bit->has_ap6_rom = true; | ||||
| 						target8bit->has_sideways_ram = true; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} else if(adfs_catalogue) { | ||||
| 			targetArchimedes->media.disks = media.disks; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Enable the Acorn ADFS if a mass-storage device is attached; | ||||
| 	// unlike the Pres ADFS it retains SCSI logic. | ||||
| 	if(!media.mass_storage_devices.empty()) { | ||||
| 		target->has_pres_adfs = false;	// To override a floppy selection, if one was made. | ||||
| 		target->has_acorn_adfs = true; | ||||
| 		target8bit->has_pres_adfs = false;	// To override a floppy selection, if one was made. | ||||
| 		target8bit->has_acorn_adfs = true; | ||||
|  | ||||
| 		// Assume some sort of later-era Acorn work is likely to happen; | ||||
| 		// so ensure *TYPE, etc are present. | ||||
| 		target->has_ap6_rom = true; | ||||
| 		target->has_sideways_ram = true; | ||||
| 		target8bit->has_ap6_rom = true; | ||||
| 		target8bit->has_sideways_ram = true; | ||||
|  | ||||
| 		target->media.mass_storage_devices = media.mass_storage_devices; | ||||
| 		target8bit->media.mass_storage_devices = media.mass_storage_devices; | ||||
|  | ||||
| 		// Check for a boot option. | ||||
| 		const auto sector = target->media.mass_storage_devices.front()->get_block(1); | ||||
| 		const auto sector = target8bit->media.mass_storage_devices.front()->get_block(1); | ||||
| 		if(sector[0xfd]) { | ||||
| 			target->should_shift_restart = true; | ||||
| 			target8bit->should_shift_restart = true; | ||||
| 		} else { | ||||
| 			target->loading_command = "*CAT\n"; | ||||
| 			target8bit->loading_command = "*CAT\n"; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	TargetList targets; | ||||
| 	if(!target->media.empty()) { | ||||
| 		targets.push_back(std::move(target)); | ||||
| 	if(!target8bit->media.empty()) { | ||||
| 		targets.push_back(std::move(target8bit)); | ||||
| 	} | ||||
| 	if(!targetArchimedes->media.empty()) { | ||||
| 		targets.push_back(std::move(targetArchimedes)); | ||||
| 	} | ||||
| 	return targets; | ||||
| } | ||||
|   | ||||
| @@ -20,7 +20,9 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage:: | ||||
| 	int shift_register = 0; | ||||
|  | ||||
| 	// TODO: move this into the parser | ||||
| #define shift()	shift_register = (shift_register >> 1) | (parser.get_next_bit(tape) << 9) | ||||
| 	const auto shift = [&] { | ||||
| 		shift_register = (shift_register >> 1) | (parser.get_next_bit(tape) << 9); | ||||
| 	}; | ||||
|  | ||||
| 	// find next area of high tone | ||||
| 	while(!tape->is_at_end() && (shift_register != 0x3ff)) { | ||||
| @@ -32,8 +34,6 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage:: | ||||
| 		shift(); | ||||
| 	} | ||||
|  | ||||
| #undef shift | ||||
|  | ||||
| 	parser.reset_crc(); | ||||
| 	parser.reset_error_flag(); | ||||
|  | ||||
|   | ||||
| @@ -34,12 +34,14 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta | ||||
| 	Model model = Model::IIe; | ||||
| 	DiskController disk_controller = DiskController::None; | ||||
| 	SCSIController scsi_controller = SCSIController::None; | ||||
| 	bool has_mockingboard = true; | ||||
|  | ||||
| 	Target() : Analyser::Static::Target(Machine::AppleII) { | ||||
| 		if(needs_declare()) { | ||||
| 			DeclareField(model); | ||||
| 			DeclareField(disk_controller); | ||||
| 			DeclareField(scsi_controller); | ||||
| 			DeclareField(has_mockingboard); | ||||
|  | ||||
| 			AnnounceEnum(Model); | ||||
| 			AnnounceEnum(DiskController); | ||||
| @@ -48,4 +50,8 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| constexpr bool is_iie(Target::Model model) { | ||||
| 	return model == Target::Model::IIe || model == Target::Model::EnhancedIIe; | ||||
| } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -38,12 +38,15 @@ struct Media { | ||||
| 	} | ||||
|  | ||||
| 	Media &operator +=(const Media &rhs) { | ||||
| #define append(name)	name.insert(name.end(), rhs.name.begin(), rhs.name.end()); | ||||
| 		append(disks); | ||||
| 		append(tapes); | ||||
| 		append(cartridges); | ||||
| 		append(mass_storage_devices); | ||||
| #undef append | ||||
| 		const auto append = [&](auto &destination, auto &source) { | ||||
| 			destination.insert(destination.end(), source.begin(), source.end()); | ||||
| 		}; | ||||
|  | ||||
| 		append(disks, rhs.disks); | ||||
| 		append(tapes, rhs.tapes); | ||||
| 		append(cartridges, rhs.cartridges); | ||||
| 		append(mass_storage_devices, rhs.mass_storage_devices); | ||||
|  | ||||
| 		return *this; | ||||
| 	} | ||||
| }; | ||||
| @@ -54,7 +57,7 @@ struct Media { | ||||
| */ | ||||
| struct Target { | ||||
| 	Target(Machine machine) : machine(machine) {} | ||||
| 	virtual ~Target() {} | ||||
| 	virtual ~Target() = default; | ||||
|  | ||||
| 	// This field is entirely optional. | ||||
| 	std::unique_ptr<Reflection::Struct> state; | ||||
|   | ||||
							
								
								
									
										99
									
								
								BUILD.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								BUILD.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
|  | ||||
| # Building Clock Signal | ||||
|  | ||||
| Clock Signal is available as [a macOS native application using | ||||
| Metal](#macos-app) or as [a cross-platform command-line-driven SDL executable | ||||
| using OpenGL](#sdl-app). | ||||
|  | ||||
| ## macOS app | ||||
|  | ||||
| The macOS native application requires a Metal-capable Mac running macOS 10.13 or | ||||
| later and has no prerequisites beyond the normal system libraries. It can be | ||||
| built [using Xcode](#building-the-macos-app-using-xcode) or on the command line | ||||
| [using `xcodebuild`](#building-the-macos-app-using-xcodebuild). | ||||
|  | ||||
| Machine ROMs are intended to be built into the application bundle; populate the | ||||
| dummy folders below ROMImages before building. | ||||
|  | ||||
| The Xcode project is configured to sign the application using the developer's | ||||
| certificate, but if you are not the developer then you will get a "No signing | ||||
| certificate" error. To avoid this, you'll specify that you want to sign the | ||||
| application to run locally. | ||||
|  | ||||
| ### Building the macOS app using Xcode | ||||
|  | ||||
| Open the Clock Signal Xcode project in OSBindings/Mac. | ||||
|  | ||||
| To avoid signing errors, edit the project, select the Signing & Capabilities | ||||
| tab, and change the Signing Certificate drop-down menu from "Development" to | ||||
| "Sign to Run Locally". | ||||
|  | ||||
| To avoid crashes when running Clock Signal via Xcode on older Macs due to | ||||
| "unrecognized selector sent to instance" errors, edit the scheme, and in the Run | ||||
| section, scroll down to the Metal heading and uncheck the "API Validation" | ||||
| checkbox. | ||||
|  | ||||
| To build, choose "Build" from Xcode's Product menu or press | ||||
| <kbd>Command</kbd> + <kbd>B</kbd>. | ||||
|  | ||||
| To build and run, choose "Run" from the Product menu or press | ||||
| <kbd>Command</kbd> + <kbd>R</kbd>. | ||||
|  | ||||
| To see the folder where the Clock Signal application was built, choose "Show | ||||
| Build Folder in Finder" from the Product menu. Look in the "Products" folder for | ||||
| a folder named after the configuration (e.g. "Debug" or "Release"). | ||||
|  | ||||
| ### Building the macOS app using `xcodebuild` | ||||
|  | ||||
| To build, change to the OSBindings/Mac directory in the Terminal, then run | ||||
| `xcodebuild`, specifying `-` as the code sign identity to sign the application | ||||
| to run locally to avoid signing errors: | ||||
|  | ||||
| 	cd OSBindings/Mac | ||||
| 	xcodebuild CODE_SIGN_IDENTITY=- | ||||
|  | ||||
| `xcodebuild` will create a "build" folder in this directory which is where you | ||||
| can find the Clock Signal application after it's compiled, in a directory named | ||||
| after the configuration (e.g. "Debug" or "Release"). | ||||
|  | ||||
| ## SDL app | ||||
|  | ||||
| The SDL app can be built on Linux, BSD, macOS, and other Unix-like operating | ||||
| systems. Prerequisites are SDL 2, ZLib and OpenGL (or Mesa). OpenGL 3.2 or | ||||
| better is required at runtime. It can be built [using | ||||
| SCons](#building-the-sdl-app-using-scons). | ||||
|  | ||||
| ### Building the SDL app using SCons | ||||
|  | ||||
| To build, change to the OSBindings/SDL directory and run `scons`. You can add a | ||||
| `-j` flag to build in parallel. For example, if you have 8 processor cores: | ||||
|  | ||||
| 	cd OSBindings/SDL | ||||
| 	scons -j8 | ||||
|  | ||||
| The `clksignal` executable will be created in this directory. You can run it | ||||
| from here or install it by copying it where you want it, for example: | ||||
|  | ||||
| 	cp clksignal /usr/local/bin | ||||
|  | ||||
| To start an emulator with a particular disk image `file`, if you've installed | ||||
| `clksignal` to a directory in your `PATH`, run: | ||||
|  | ||||
| 	clksignal file | ||||
|  | ||||
| Or if you're running it from the current directory: | ||||
|  | ||||
| 	./clksignal file | ||||
|  | ||||
| Other options are availble. Run `clksignal` or `./clksignal` with no arguments | ||||
| to learn more. | ||||
|  | ||||
| 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. | ||||
							
								
								
									
										30
									
								
								BUILD.txt
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								BUILD.txt
									
									
									
									
									
								
							| @@ -1,30 +0,0 @@ | ||||
| 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. | ||||
							
								
								
									
										70
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| cmake_minimum_required(VERSION 3.19 FATAL_ERROR) | ||||
|  | ||||
| project(CLK | ||||
| 	LANGUAGES CXX | ||||
| 	VERSION 24.01.22 | ||||
| ) | ||||
|  | ||||
| set(CMAKE_CXX_STANDARD 17) | ||||
| set(CMAKE_CXX_STANDARD_REQUIRED ON) | ||||
| set(CMAKE_CXX_EXTENSIONS OFF) | ||||
|  | ||||
| set(CLK_UIS "SDL") | ||||
| #list(PREPEND CLK_UIS "QT") | ||||
| #if(APPLE) | ||||
| #	list(PREPEND CLK_UIS "MAC") | ||||
| #	set(CLK_DEFAULT_UI "MAC") | ||||
| #else() | ||||
| 	set(CLK_DEFAULT_UI "SDL") | ||||
| #endif() | ||||
|  | ||||
| set(CLK_UI ${CLK_DEFAULT_UI} CACHE STRING "User interface") | ||||
| set_property(CACHE CLK_UI PROPERTY STRINGS ${CLK_UIS}) | ||||
|  | ||||
| if(NOT CLK_UI IN_LIST CLK_UIS) | ||||
| 	list(JOIN CLK_UIS ", " CLK_UIS_PRETTY) | ||||
| 	message(FATAL_ERROR "Invalid value for 'CLK_UI'; must be one of ${CLK_UIS_PRETTY}") | ||||
| endif() | ||||
|  | ||||
| message(STATUS "Configuring for ${CLK_UI} UI") | ||||
|  | ||||
| list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") | ||||
| include("CLK_SOURCES") | ||||
|  | ||||
| add_executable(clksignal ${CLK_SOURCES}) | ||||
|  | ||||
| if(MSVC) | ||||
| 	target_compile_options(clksignal PRIVATE /W4) | ||||
| else() | ||||
| 	# TODO: Add -Wpedandic. | ||||
| 	target_compile_options(clksignal PRIVATE -Wall -Wextra) | ||||
| endif() | ||||
|  | ||||
| find_package(ZLIB REQUIRED) | ||||
| target_link_libraries(clksignal PRIVATE ZLIB::ZLIB) | ||||
|  | ||||
| if(CLK_UI STREQUAL "MAC") | ||||
| 	enable_language(OBJC OBJCXX SWIFT) | ||||
| 	# TODO: Build the Mac version. | ||||
| else() | ||||
| 	find_package(OpenGL REQUIRED) | ||||
| 	target_link_libraries(clksignal PRIVATE OpenGL::GL) | ||||
| 	if(APPLE) | ||||
| 		target_compile_definitions(clksignal PRIVATE "GL_SILENCE_DEPRECATION" "IGNORE_APPLE") | ||||
| 	endif() | ||||
| endif() | ||||
|  | ||||
| if(CLK_UI STREQUAL "QT") | ||||
| 	# TODO: Build the Qt version. | ||||
| elseif(APPLE) | ||||
| 	set(BLA_VENDOR Apple) | ||||
| 	find_package(BLAS REQUIRED) | ||||
| 	target_link_libraries(clksignal PRIVATE BLAS::BLAS) | ||||
| endif() | ||||
|  | ||||
| if(CLK_UI STREQUAL "SDL") | ||||
| 	find_package(SDL2 REQUIRED CONFIG REQUIRED COMPONENTS SDL2) | ||||
| 	target_link_libraries(clksignal PRIVATE SDL2::SDL2) | ||||
| endif() | ||||
|  | ||||
| # TODO: Investigate building on Windows. | ||||
| @@ -73,6 +73,11 @@ template <typename TimeUnit> class DeferredQueue { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/// @returns @c true if no actions are enqueued; @c false otherwise. | ||||
| 		bool empty() const { | ||||
| 			return pending_actions_.empty(); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		// The list of deferred actions. | ||||
| 		struct DeferredAction { | ||||
|   | ||||
| @@ -30,7 +30,7 @@ class WD1770: public Storage::Disk::MFMController { | ||||
| 			@param p The type of controller to emulate. | ||||
| 		*/ | ||||
| 		WD1770(Personality p); | ||||
| 		virtual ~WD1770() {} | ||||
| 		virtual ~WD1770() = default; | ||||
|  | ||||
| 		/// 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; | ||||
|   | ||||
| @@ -68,7 +68,7 @@ class IRQDelegatePortHandler: public PortHandler { | ||||
| 		/// 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. | ||||
| 		/// Overrides @c PortHandler::set_interrupt_status, notifying the delegate if one is set. | ||||
| 		void set_interrupt_status(bool new_status); | ||||
|  | ||||
| 	private: | ||||
|   | ||||
| @@ -82,6 +82,8 @@ template <typename PortHandlerT, Personality personality> class MOS6526: | ||||
| 		void advance_counters(int); | ||||
|  | ||||
| 		bool serial_line_did_produce_bit(Serial::Line<true> *line, int bit) final; | ||||
|  | ||||
| 		Log::Logger<Log::Source::MOS6526> log; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -8,9 +8,6 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <cassert> | ||||
| #include <cstdio> | ||||
|  | ||||
| namespace MOS::MOS6526 { | ||||
|  | ||||
| enum Interrupts: uint8_t { | ||||
| @@ -132,13 +129,11 @@ void MOS6526<BusHandlerT, personality>::write(int address, uint8_t value) { | ||||
|  | ||||
| 		// Shift control. | ||||
| 		case 12: | ||||
| 			printf("TODO: write to shift register\n"); | ||||
| 			log.error().append("TODO: write to shift register"); | ||||
| 		break; | ||||
|  | ||||
| 		default: | ||||
| 			printf("Unhandled 6526 write: %02x to %d\n", value, address); | ||||
| 			assert(false); | ||||
| 		break; | ||||
| 		// Logically unreachable. | ||||
| 		default: break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -179,10 +174,8 @@ uint8_t MOS6526<BusHandlerT, personality>::read(int address) { | ||||
| 		// Shift register. | ||||
| 		case 12:	return shift_data_; | ||||
|  | ||||
| 		default: | ||||
| 			printf("Unhandled 6526 read from %d\n", address); | ||||
| 			assert(false); | ||||
| 		break; | ||||
| 		// Logically unreachable. | ||||
| 		default: break; | ||||
| 	} | ||||
| 	return 0xff; | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ | ||||
| #include <array> | ||||
|  | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| namespace MOS::MOS6526 { | ||||
|  | ||||
| @@ -220,7 +221,8 @@ struct MOS6526Storage { | ||||
| 			control = v; | ||||
|  | ||||
| 			if(v&2) { | ||||
| 				printf("UNIMPLEMENTED: PB strobe\n"); | ||||
| 				Log::Logger<Log::Source::MOS6526> log; | ||||
| 				log.error().append("UNIMPLEMENTED: PB strobe"); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -19,6 +19,7 @@ AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) | ||||
| void AudioGenerator::set_volume(uint8_t volume) { | ||||
| 	audio_queue_.enqueue([this, volume]() { | ||||
| 		volume_ = int16_t(volume) * range_multiplier_; | ||||
| 		dc_offset_ = volume_ >> 4; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| @@ -105,7 +106,8 @@ static uint8_t noise_pattern[] = { | ||||
| // 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 AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| template <Outputs::Speaker::Action action> | ||||
| void AudioGenerator::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { | ||||
| 	for(unsigned int c = 0; c < number_of_samples; ++c) { | ||||
| 		update(0, 2, shift); | ||||
| 		update(1, 1, shift); | ||||
| @@ -114,23 +116,22 @@ void AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target) | ||||
|  | ||||
| 		// this sums the output of all three sounds channels plus a DC offset for volume; | ||||
| 		// TODO: what's the real ratio of this stuff? | ||||
| 		target[c] = int16_t( | ||||
| 		const int16_t sample = | ||||
| 			(shift_registers_[0]&1) + | ||||
| 			(shift_registers_[1]&1) + | ||||
| 			(shift_registers_[2]&1) + | ||||
| 			((noise_pattern[shift_registers_[3] >> 3] >> (shift_registers_[3]&7))&(control_registers_[3] >> 7)&1) | ||||
| 		) * volume_ + (volume_ >> 4); | ||||
| 	} | ||||
| } | ||||
| 			((noise_pattern[shift_registers_[3] >> 3] >> (shift_registers_[3]&7))&(control_registers_[3] >> 7)&1); | ||||
|  | ||||
| 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); | ||||
| 		update(2, 0, shift); | ||||
| 		update(3, 1, increment); | ||||
| 		Outputs::Speaker::apply<action>( | ||||
| 			target[c], | ||||
| 			Outputs::Speaker::MonoSample( | ||||
| 				sample * volume_ + dc_offset_ | ||||
| 			)); | ||||
| 	} | ||||
| } | ||||
| template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
|  | ||||
| void AudioGenerator::set_sample_volume_range(std::int16_t range) { | ||||
| 	range_multiplier_ = int16_t(range / 64); | ||||
|   | ||||
| @@ -12,12 +12,12 @@ | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
| #include "../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/BufferSource.hpp" | ||||
|  | ||||
| namespace MOS::MOS6560 { | ||||
|  | ||||
| // audio state | ||||
| class AudioGenerator: public ::Outputs::Speaker::SampleSource { | ||||
| class AudioGenerator: public Outputs::Speaker::BufferSource<AudioGenerator, false> { | ||||
| 	public: | ||||
| 		AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue); | ||||
|  | ||||
| @@ -25,10 +25,9 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource { | ||||
| 		void set_control(int channel, uint8_t value); | ||||
|  | ||||
| 		// For ::SampleSource. | ||||
| 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||
| 		void skip_samples(std::size_t number_of_samples); | ||||
| 		template <Outputs::Speaker::Action action> | ||||
| 			void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| 		static constexpr bool get_is_stereo() { return false; } | ||||
|  | ||||
| 	private: | ||||
| 		Concurrency::AsyncTaskQueue<false> &audio_queue_; | ||||
| @@ -37,6 +36,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource { | ||||
| 		unsigned int shift_registers_[4] = {0, 0, 0, 0}; | ||||
| 		uint8_t control_registers_[4] = {0, 0, 0, 0}; | ||||
| 		int16_t volume_ = 0; | ||||
| 		int16_t dc_offset_ = 0; | ||||
| 		int16_t range_multiplier_ = 1; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -21,7 +21,7 @@ namespace Intel::i8272 { | ||||
|  | ||||
| class BusHandler { | ||||
| 	public: | ||||
| 		virtual ~BusHandler() {} | ||||
| 		virtual ~BusHandler() = default; | ||||
| 		virtual void set_dma_data_request([[maybe_unused]] bool drq) {} | ||||
| 		virtual void set_interrupt([[maybe_unused]] bool irq) {} | ||||
| }; | ||||
|   | ||||
| @@ -163,7 +163,7 @@ void Base<personality>::posit_sprite(int sprite_number, int sprite_position, uin | ||||
| 	} | ||||
|  | ||||
| 	const auto sprite_row = uint8_t(screen_row - sprite_position); | ||||
| 	if(sprite_row < 0 || sprite_row >= sprite_height_) return; | ||||
| 	if(sprite_row >= sprite_height_) return;	// The less-than-zero case is dealt with by the cast to unsigned. | ||||
|  | ||||
| 	if(fetch_sprite_buffer_->active_sprite_slot == mode_timing_.maximum_visible_sprites) { | ||||
| 		status_ |= StatusSpriteOverflow; | ||||
|   | ||||
| @@ -38,6 +38,7 @@ constexpr size_t memory_size(Personality p) { | ||||
| 		case TI::TMS::V9938:	return 128 * 1024; | ||||
| 		case TI::TMS::V9958:	return 192 * 1024; | ||||
| 	} | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| constexpr size_t memory_mask(Personality p) { | ||||
|   | ||||
| @@ -59,7 +59,7 @@ struct YamahaFetcher { | ||||
| 				type(type), | ||||
| 				id(id) {} | ||||
|  | ||||
| 			constexpr Event() noexcept {} | ||||
| 			constexpr Event() noexcept = default; | ||||
| 		}; | ||||
|  | ||||
| 		// State that tracks fetching position within a line. | ||||
|   | ||||
| @@ -132,7 +132,7 @@ struct Command { | ||||
| 	CommandContext &context; | ||||
| 	ModeDescription &mode_description; | ||||
| 	Command(CommandContext &context, ModeDescription &mode_description) : context(context), mode_description(mode_description) {} | ||||
| 	virtual ~Command() {} | ||||
| 	virtual ~Command() = default; | ||||
|  | ||||
| 	/// @returns @c true if all output from this command is done; @c false otherwise. | ||||
| 	virtual bool done() = 0; | ||||
|   | ||||
| @@ -10,17 +10,24 @@ | ||||
|  | ||||
| #include "AY38910.hpp" | ||||
|  | ||||
| //namespace GI { | ||||
| //namespace AY38910 { | ||||
|  | ||||
| using namespace GI::AY38910; | ||||
|  | ||||
| // Note on dividers: the real AY has a built-in divider of 8 | ||||
| // prior to applying its tone and noise dividers. But the YM fills the | ||||
| // same total periods for noise and tone with double-precision envelopes. | ||||
| // Therefore this class implements a divider of 4 and doubles the tone | ||||
| // and noise periods. The envelope ticks along at the divide-by-four rate, | ||||
| // but if this is an AY rather than a YM then its lowest bit is forced to 1, | ||||
| // matching the YM datasheet's depiction of envelope level 31 as equal to | ||||
| // programmatic volume 15, envelope level 29 as equal to programmatic 14, etc. | ||||
|  | ||||
| template <bool is_stereo> | ||||
| AY38910<is_stereo>::AY38910(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) { | ||||
| AY38910SampleSource<is_stereo>::AY38910SampleSource(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) { | ||||
| 	// Don't use the low bit of the envelope position if this is an AY. | ||||
| 	envelope_position_mask_ |= personality == Personality::AY38910; | ||||
|  | ||||
| 	// Set up envelope lookup tables. | ||||
| 	// Set up envelope lookup tables; these are based on 32 volume levels as used by the YM2149F. | ||||
| 	// The AY38910 will just use only even table entries, and therefore only even volumes. | ||||
| 	for(int c = 0; c < 16; c++) { | ||||
| 		for(int p = 0; p < 64; p++) { | ||||
| 			switch(c) { | ||||
| @@ -74,7 +81,8 @@ AY38910<is_stereo>::AY38910(Personality personality, Concurrency::AsyncTaskQueue | ||||
| 	set_sample_volume_range(0); | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::int16_t range) { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::set_sample_volume_range(std::int16_t range) { | ||||
| 	// Set up volume lookup table; the function below is based on a combination of the graph | ||||
| 	// from the YM's datasheet, showing a clear power curve, and fitting that to observed | ||||
| 	// values reported elsewhere. | ||||
| @@ -92,7 +100,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std:: | ||||
| 	evaluate_output_volume(); | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) { | ||||
| 	a_left_ = uint8_t(a_left * 255.0f); | ||||
| 	b_left_ = uint8_t(b_left * 255.0f); | ||||
| 	c_left_ = uint8_t(c_left * 255.0f); | ||||
| @@ -101,45 +110,24 @@ template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_lef | ||||
| 	c_right_ = uint8_t(c_right * 255.0f); | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t number_of_samples, int16_t *target) { | ||||
| 	// Note on structure below: the real AY has a built-in divider of 8 | ||||
| 	// prior to applying its tone and noise dividers. But the YM fills the | ||||
| 	// same total periods for noise and tone with double-precision envelopes. | ||||
| 	// Therefore this class implements a divider of 4 and doubles the tone | ||||
| 	// and noise periods. The envelope ticks along at the divide-by-four rate, | ||||
| 	// but if this is an AY rather than a YM then its lowest bit is forced to 1, | ||||
| 	// matching the YM datasheet's depiction of envelope level 31 as equal to | ||||
| 	// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc. | ||||
|  | ||||
| 	std::size_t c = 0; | ||||
| 	while((master_divider_&3) && c < number_of_samples) { | ||||
| 		if constexpr (is_stereo) { | ||||
| 			reinterpret_cast<uint32_t *>(target)[c] = output_volume_; | ||||
| 		} else { | ||||
| 			target[c] = int16_t(output_volume_); | ||||
| 		} | ||||
| 		master_divider_++; | ||||
| 		c++; | ||||
| 	} | ||||
|  | ||||
| 	while(c < number_of_samples) { | ||||
| #define step_channel(c) \ | ||||
| 	if(tone_counters_[c]) tone_counters_[c]--;\ | ||||
| 	else {\ | ||||
| 		tone_outputs_[c] ^= 1;\ | ||||
| 		tone_counters_[c] = tone_periods_[c] << 1;\ | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::advance() { | ||||
| 	const auto step_channel = [&](int c) { | ||||
| 		if(tone_counters_[c]) --tone_counters_[c]; | ||||
| 		else { | ||||
| 			tone_outputs_[c] ^= 1; | ||||
| 			tone_counters_[c] = tone_periods_[c] << 1; | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	// Update the tone channels. | ||||
| 	step_channel(0); | ||||
| 	step_channel(1); | ||||
| 	step_channel(2); | ||||
|  | ||||
| #undef step_channel | ||||
|  | ||||
| 	// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting | ||||
| 	// it into the official 17 upon divider underflow. | ||||
| 		if(noise_counter_) noise_counter_--; | ||||
| 	if(noise_counter_) --noise_counter_; | ||||
| 	else { | ||||
| 		noise_counter_ = noise_period_ << 1;		// To cover the double resolution of envelopes. | ||||
| 		noise_output_ ^= noise_shift_register_&1; | ||||
| @@ -149,30 +137,23 @@ template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t numbe | ||||
|  | ||||
| 	// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of | ||||
| 	// implementing non-repeating patterns by locking them to the final table position. | ||||
| 		if(envelope_divider_) envelope_divider_--; | ||||
| 	if(envelope_divider_) --envelope_divider_; | ||||
| 	else { | ||||
| 			envelope_divider_ = envelope_period_; | ||||
| 			envelope_position_ ++; | ||||
| 		envelope_divider_ = envelope_period_ << 1; | ||||
| 		++envelope_position_; | ||||
| 		if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]]; | ||||
| 	} | ||||
|  | ||||
| 	evaluate_output_volume(); | ||||
|  | ||||
| 		for(int ic = 0; ic < 4 && c < number_of_samples; ic++) { | ||||
| 			if constexpr (is_stereo) { | ||||
| 				reinterpret_cast<uint32_t *>(target)[c] = output_volume_; | ||||
| 			} else { | ||||
| 				target[c] = int16_t(output_volume_); | ||||
| 			} | ||||
| 			c++; | ||||
| 			master_divider_++; | ||||
| 		} | ||||
| } | ||||
|  | ||||
| 	master_divider_ &= 3; | ||||
| template <bool is_stereo> | ||||
| typename Outputs::Speaker::SampleT<is_stereo>::type AY38910SampleSource<is_stereo>::level() const { | ||||
| 	return output_volume_; | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::evaluate_output_volume() { | ||||
| 	int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_]; | ||||
|  | ||||
| 	// The output level for a channel is: | ||||
| @@ -214,19 +195,18 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() { | ||||
|  | ||||
| 	// Mix additively, weighting if in stereo. | ||||
| 	if constexpr (is_stereo) { | ||||
| 		int16_t *const output_volumes = reinterpret_cast<int16_t *>(&output_volume_); | ||||
| 		output_volumes[0] = int16_t(( | ||||
| 		output_volume_.left = int16_t(( | ||||
| 			volumes_[volumes[0]] * channel_levels[0] * a_left_ + | ||||
| 			volumes_[volumes[1]] * channel_levels[1] * b_left_ + | ||||
| 			volumes_[volumes[2]] * channel_levels[2] * c_left_ | ||||
| 		) >> 8); | ||||
| 		output_volumes[1] = int16_t(( | ||||
| 		output_volume_.right = int16_t(( | ||||
| 			volumes_[volumes[0]] * channel_levels[0] * a_right_ + | ||||
| 			volumes_[volumes[1]] * channel_levels[1] * b_right_ + | ||||
| 			volumes_[volumes[2]] * channel_levels[2] * c_right_ | ||||
| 		) >> 8); | ||||
| 	} else { | ||||
| 		output_volume_ = uint32_t( | ||||
| 		output_volume_ = int16_t( | ||||
| 			volumes_[volumes[0]] * channel_levels[0] + | ||||
| 			volumes_[volumes[1]] * channel_levels[1] + | ||||
| 			volumes_[volumes[2]] * channel_levels[2] | ||||
| @@ -234,18 +214,21 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() const { | ||||
| template <bool is_stereo> | ||||
| bool AY38910SampleSource<is_stereo>::is_zero_level() const { | ||||
| 	// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero. | ||||
| 	return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0; | ||||
| } | ||||
|  | ||||
| // MARK: - Register manipulation | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::select_register(uint8_t r) { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::select_register(uint8_t r) { | ||||
| 	selected_register_ = r; | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t value) { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::set_register_value(uint8_t value) { | ||||
| 	// There are only 16 registers. | ||||
| 	if(selected_register_ > 15) return; | ||||
|  | ||||
| @@ -314,7 +297,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t va | ||||
| 	if(update_port_a) set_port_output(false); | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() { | ||||
| template <bool is_stereo> | ||||
| uint8_t AY38910SampleSource<is_stereo>::get_register_value() { | ||||
| 	// This table ensures that bits that aren't defined within the AY are returned as 0s | ||||
| 	// when read, conforming to CPC-sourced unit tests. | ||||
| 	const uint8_t register_masks[16] = { | ||||
| @@ -328,24 +312,28 @@ template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() { | ||||
|  | ||||
| // MARK: - Port querying | ||||
|  | ||||
| template <bool is_stereo> uint8_t AY38910<is_stereo>::get_port_output(bool port_b) { | ||||
| template <bool is_stereo> | ||||
| uint8_t AY38910SampleSource<is_stereo>::get_port_output(bool port_b) { | ||||
| 	return registers_[port_b ? 15 : 14]; | ||||
| } | ||||
|  | ||||
| // MARK: - Bus handling | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_port_handler(PortHandler *handler) { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::set_port_handler(PortHandler *handler) { | ||||
| 	port_handler_ = handler; | ||||
| 	set_port_output(true); | ||||
| 	set_port_output(false); | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_data_input(uint8_t r) { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::set_data_input(uint8_t r) { | ||||
| 	data_input_ = r; | ||||
| 	update_bus(); | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b) { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::set_port_output(bool port_b) { | ||||
| 	// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor, | ||||
| 	// so that when in the "input" mode, all pins will read normally high". Therefore, | ||||
| 	// report programmer selection of input mode as creating an output of 0xff. | ||||
| @@ -355,7 +343,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() { | ||||
| template <bool is_stereo> | ||||
| uint8_t AY38910SampleSource<is_stereo>::get_data_output() { | ||||
| 	if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) { | ||||
| 		// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the | ||||
| 		// value returned to the CPU when reading it is the and of the output value and any input. | ||||
| @@ -371,7 +360,8 @@ template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() { | ||||
| 	return data_output_; | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::set_control_lines(ControlLines control_lines) { | ||||
| 	switch(int(control_lines)) { | ||||
| 		default:					control_state_ = Inactive;		break; | ||||
|  | ||||
| @@ -386,7 +376,32 @@ template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLine | ||||
| 	update_bus(); | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> void AY38910<is_stereo>::update_bus() { | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::set_reset(bool active) { | ||||
| 	if(active == reset_) return; | ||||
| 	reset_ = active; | ||||
|  | ||||
| 	// Reset upon the leading edge; TODO: is this right? | ||||
| 	if(reset_) { | ||||
| 		reset(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::reset() { | ||||
| 	// TODO: the below is a guess. Look up real answers. | ||||
|  | ||||
| 	selected_register_ = 0; | ||||
| 	std::fill(registers_, registers_ + 16, 0); | ||||
|  | ||||
| 	task_queue_.enqueue([&] { | ||||
| 		std::fill(output_registers_, output_registers_ + 16, 0); | ||||
| 		evaluate_output_volume(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| template <bool is_stereo> | ||||
| void AY38910SampleSource<is_stereo>::update_bus() { | ||||
| 	// Assume no output, unless this turns out to be a read. | ||||
| 	data_output_ = 0xff; | ||||
| 	switch(control_state_) { | ||||
| @@ -398,5 +413,10 @@ template <bool is_stereo> void AY38910<is_stereo>::update_bus() { | ||||
| } | ||||
|  | ||||
| // Ensure both mono and stereo versions of the AY are built. | ||||
| template class GI::AY38910::AY38910<true>; | ||||
| template class GI::AY38910::AY38910<false>; | ||||
| template class GI::AY38910::AY38910SampleSource<true>; | ||||
| template class GI::AY38910::AY38910SampleSource<false>; | ||||
|  | ||||
| // Perform an explicit instantiation of the BufferSource to hope for | ||||
| // appropriate inlining of advance() and level(). | ||||
| template struct GI::AY38910::AY38910<true>; | ||||
| template struct GI::AY38910::AY38910<false>; | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/BufferSource.hpp" | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
|  | ||||
| #include "../../Reflection/Struct.hpp" | ||||
| @@ -66,10 +66,11 @@ enum class Personality { | ||||
|  | ||||
| 	This AY has an attached mono or stereo mixer. | ||||
| */ | ||||
| template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource { | ||||
| template <bool stereo> class AY38910SampleSource { | ||||
| 	public: | ||||
| 		/// Creates a new AY38910. | ||||
| 		AY38910(Personality, Concurrency::AsyncTaskQueue<false> &); | ||||
| 		AY38910SampleSource(Personality, Concurrency::AsyncTaskQueue<false> &); | ||||
| 		AY38910SampleSource(const AY38910SampleSource &) = delete; | ||||
|  | ||||
| 		/// Sets the value the AY would read from its data lines if it were not outputting. | ||||
| 		void set_data_input(uint8_t r); | ||||
| @@ -80,6 +81,12 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource | ||||
| 		/// Sets the current control line state, as a bit field. | ||||
| 		void set_control_lines(ControlLines control_lines); | ||||
|  | ||||
| 		/// Strobes the reset line. | ||||
| 		void reset(); | ||||
|  | ||||
| 		/// Sets the current value of the reset line. | ||||
| 		void set_reset(bool reset); | ||||
|  | ||||
| 		/*! | ||||
| 			Gets the value that would appear on the requested interface port if it were in output mode. | ||||
| 			@parameter port_b @c true to get the value for Port B, @c false to get the value for Port A. | ||||
| @@ -105,24 +112,24 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource | ||||
| 		*/ | ||||
| 		void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0); | ||||
|  | ||||
| 		// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter. | ||||
| 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||
| 		// Sample generation. | ||||
| 		typename Outputs::Speaker::SampleT<stereo>::type level() const; | ||||
| 		void advance(); | ||||
| 		bool is_zero_level() const; | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| 		static constexpr bool get_is_stereo() { return is_stereo; } | ||||
|  | ||||
| 	private: | ||||
| 		Concurrency::AsyncTaskQueue<false> &task_queue_; | ||||
|  | ||||
| 		bool reset_ = false; | ||||
|  | ||||
| 		int selected_register_ = 0; | ||||
| 		uint8_t registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; | ||||
| 		uint8_t output_registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; | ||||
| 		uint8_t registers_[16]{}; | ||||
| 		uint8_t output_registers_[16]{}; | ||||
|  | ||||
| 		int master_divider_ = 0; | ||||
|  | ||||
| 		int tone_periods_[3] = {0, 0, 0}; | ||||
| 		int tone_counters_[3] = {0, 0, 0}; | ||||
| 		int tone_outputs_[3] = {0, 0, 0}; | ||||
| 		int tone_periods_[3]{}; | ||||
| 		int tone_counters_[3]{}; | ||||
| 		int tone_outputs_[3]{}; | ||||
|  | ||||
| 		int noise_period_ = 0; | ||||
| 		int noise_counter_ = 0; | ||||
| @@ -150,7 +157,7 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource | ||||
|  | ||||
| 		uint8_t data_input_, data_output_; | ||||
|  | ||||
| 		uint32_t output_volume_; | ||||
| 		typename Outputs::Speaker::SampleT<stereo>::type output_volume_; | ||||
|  | ||||
| 		void update_bus(); | ||||
| 		PortHandler *port_handler_ = nullptr; | ||||
| @@ -166,6 +173,20 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource | ||||
| 		friend struct State; | ||||
| }; | ||||
|  | ||||
| /// Defines a default AY to be the sample source with a master divider of 4; | ||||
| /// real AYs have a divide-by-8 step built in but YMs apply only a divide by 4. | ||||
| /// | ||||
| /// The implementation of AY38910SampleSource combines those two worlds | ||||
| /// by always applying a divide by four and scaling other things as appropriate. | ||||
| template <bool stereo> struct AY38910: | ||||
| 	public AY38910SampleSource<stereo>, | ||||
| 	public Outputs::Speaker::SampleSource<AY38910<stereo>, stereo, 4> { | ||||
|  | ||||
| 	// Use the same constructor as `AY38910SampleSource` (along with inheriting | ||||
| 	// the rest of its interface). | ||||
| 	using AY38910SampleSource<stereo>::AY38910SampleSource; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
| 	Provides helper code, to provide something closer to the interface exposed by many | ||||
| 	AY-deploying machines of the era. | ||||
|   | ||||
| @@ -21,8 +21,6 @@ namespace Apple::Clock { | ||||
| */ | ||||
| class ClockStorage { | ||||
| 	public: | ||||
| 		ClockStorage() {} | ||||
|  | ||||
| 		/*! | ||||
| 			Advances the clock by 1 second. | ||||
|  | ||||
|   | ||||
| @@ -8,27 +8,23 @@ | ||||
|  | ||||
| #include "AudioToggle.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| using namespace Audio; | ||||
|  | ||||
| Audio::Toggle::Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue) : | ||||
| 	audio_queue_(audio_queue) {} | ||||
|  | ||||
| void Toggle::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| 	for(std::size_t sample = 0; sample < number_of_samples; ++sample) { | ||||
| 		target[sample] = level_; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void Toggle::set_sample_volume_range(std::int16_t range) { | ||||
| 	volume_ = range; | ||||
| 	level_ = level_active_ ? volume_ : 0; | ||||
| } | ||||
|  | ||||
| void Toggle::skip_samples(std::size_t) {} | ||||
|  | ||||
| void Toggle::set_output(bool enabled) { | ||||
| 	if(is_enabled_ == enabled) return; | ||||
| 	is_enabled_ = enabled; | ||||
| 	audio_queue_.enqueue([this, enabled] { | ||||
| 		level_active_ = enabled; | ||||
| 		level_ = enabled ? volume_ : 0; | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/BufferSource.hpp" | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
|  | ||||
| namespace Audio { | ||||
| @@ -16,13 +16,18 @@ namespace Audio { | ||||
| /*! | ||||
| 	Provides a sample source that can programmatically be set to one of two values. | ||||
| */ | ||||
| class Toggle: public Outputs::Speaker::SampleSource { | ||||
| class Toggle: public Outputs::Speaker::BufferSource<Toggle, false> { | ||||
| 	public: | ||||
| 		Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue); | ||||
|  | ||||
| 		void get_samples(std::size_t number_of_samples, std::int16_t *target); | ||||
| 		template <Outputs::Speaker::Action action> | ||||
| 		void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { | ||||
| 			Outputs::Speaker::fill<action>(target, target + number_of_samples, level_); | ||||
| 		} | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| 		void skip_samples(const std::size_t number_of_samples); | ||||
| 		bool is_zero_level() const { | ||||
| 			return !level_; | ||||
| 		} | ||||
|  | ||||
| 		void set_output(bool enabled); | ||||
| 		bool get_output() const; | ||||
| @@ -34,6 +39,7 @@ class Toggle: public Outputs::Speaker::SampleSource { | ||||
|  | ||||
| 		// Accessed on the audio thread. | ||||
| 		int16_t level_ = 0, volume_ = 0; | ||||
| 		bool level_active_ = false; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -55,10 +55,6 @@ uint8_t IWM::read(int address) { | ||||
| 			logger.info().append("Invalid read\n"); | ||||
| 		return 0xff; | ||||
|  | ||||
| 		// "Read all 1s". | ||||
| //			printf("Reading all 1s\n"); | ||||
| //		return 0xff; | ||||
|  | ||||
| 		case 0: | ||||
| 		case ENABLE: {				/* Read data register. Zeroing afterwards is a guess. */ | ||||
| 			const auto result = data_register_; | ||||
|   | ||||
| @@ -30,7 +30,6 @@ DoubleDensityDrive::DoubleDensityDrive(int input_clock_rate, bool is_800k) : | ||||
| // MARK: - Speed Selection | ||||
|  | ||||
| void DoubleDensityDrive::did_step(Storage::Disk::HeadPosition to_position) { | ||||
| //	printf("At track %d\n", to_position.as_int()); | ||||
| 	// The 800kb drive automatically selects rotation speed as a function of | ||||
| 	// head position; the 400kb drive doesn't do so. | ||||
| 	if(is_800k_) { | ||||
|   | ||||
							
								
								
									
										226
									
								
								Components/I2C/I2C.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								Components/I2C/I2C.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | ||||
| // | ||||
| //  I2C.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 16/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "I2C.hpp" | ||||
|  | ||||
| #include "../../Outputs/Log.hpp" | ||||
|  | ||||
| using namespace I2C; | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| Log::Logger<Log::Source::I2C> logger; | ||||
|  | ||||
| } | ||||
|  | ||||
| void Bus::set_data(bool pulled) { | ||||
| 	set_clock_data(clock_, pulled); | ||||
| } | ||||
| bool Bus::data() { | ||||
| 	bool result = data_; | ||||
| 	if(peripheral_bits_) { | ||||
| 		result |= !(peripheral_response_ & 0x80); | ||||
| 	} | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| void Bus::set_clock(bool pulled) { | ||||
| 	set_clock_data(pulled, data_); | ||||
| } | ||||
| bool Bus::clock() { | ||||
| 	return clock_; | ||||
| } | ||||
|  | ||||
| void Bus::set_clock_data(bool clock_pulled, bool data_pulled) { | ||||
| 	// Proceed only if changes are evidenced. | ||||
| 	if(clock_pulled == clock_ && data_pulled == data_) { | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	const bool prior_clock = clock_; | ||||
| 	const bool prior_data = data_; | ||||
| 	clock_ = clock_pulled; | ||||
| 	data_ = data_pulled; | ||||
|  | ||||
| 	// If currently serialising from a peripheral then shift onwards on | ||||
| 	// every clock trailing edge. | ||||
| 	if(peripheral_bits_) { | ||||
| 		// Trailing edge of clock => bit has been consumed. | ||||
| 		if(!prior_clock && clock_) { | ||||
| 			logger.info().append("<< %d", (peripheral_response_ >> 7) & 1); | ||||
| 			--peripheral_bits_; | ||||
| 			peripheral_response_ <<= 1; | ||||
|  | ||||
| 			if(!peripheral_bits_) { | ||||
| 				signal(Event::FinishedOutput); | ||||
| 			} | ||||
| 		} | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	// Not currently serialising implies listening. | ||||
| 	if(!clock_ && prior_data != data_) { | ||||
| 		// A data transition outside of a clock cycle implies a start or stop. | ||||
| 		in_bit_ = false; | ||||
| 		if(data_) { | ||||
| 			logger.info().append("S"); | ||||
| 			signal(Event::Start); | ||||
| 		} else { | ||||
| 			logger.info().append("W"); | ||||
| 			signal(Event::Stop); | ||||
| 		} | ||||
| 	} else if(clock_ != prior_clock) { | ||||
| 		// Bits: wait until the falling edge of the cycle. | ||||
| 		if(!clock_) { | ||||
| 			// Rising edge: clock period begins. | ||||
| 			in_bit_ = true; | ||||
| 		} else if(in_bit_) { | ||||
| 			// Falling edge: clock period ends (assuming it began; otherwise this is a preparatory | ||||
| 			// clock transition only, immediately after a start bit). | ||||
| 			in_bit_ = false; | ||||
|  | ||||
| 			if(data_) { | ||||
| 				logger.info().append("0"); | ||||
| 				signal(Event::Zero); | ||||
| 			} else { | ||||
| 				logger.info().append("1"); | ||||
| 				signal(Event::One); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void Bus::signal(Event event) { | ||||
| 	const auto capture_bit = [&]() { | ||||
| 		input_ = uint16_t((input_ << 1) | (event == Event::Zero ? 0 : 1)); | ||||
| 		++input_count_; | ||||
| 	}; | ||||
|  | ||||
| 	const auto acknowledge = [&]() { | ||||
| 		// Post an acknowledgement bit. | ||||
| 		peripheral_response_ = 0; | ||||
| 		peripheral_bits_ = 1; | ||||
| 	}; | ||||
|  | ||||
| 	const auto set_state = [&](State state) { | ||||
| 		state_ = state; | ||||
| 		input_count_ = 0; | ||||
| 		input_ = 0; | ||||
| 	}; | ||||
|  | ||||
| 	const auto enqueue = [&](std::optional<uint8_t> next) { | ||||
| 		if(next) { | ||||
| 			peripheral_response_ = static_cast<uint16_t>(*next); | ||||
| 			peripheral_bits_ = 8; | ||||
| 			set_state(State::AwaitingByteAcknowledge); | ||||
| 		} else { | ||||
| 			set_state(State::AwaitingAddress); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const auto stop = [&]() { | ||||
| 		set_state(State::AwaitingAddress); | ||||
| 		active_peripheral_ = nullptr; | ||||
| 	}; | ||||
|  | ||||
| 	// Allow start and stop conditions at any time. | ||||
| 	if(event == Event::Start) { | ||||
| 		set_state(State::CollectingAddress); | ||||
| 		active_peripheral_ = nullptr; | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	if(event == Event::Stop) { | ||||
| 		if(active_peripheral_) { | ||||
| 			active_peripheral_->stop(); | ||||
| 		} | ||||
| 		stop(); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	switch(state_) { | ||||
| 		// While waiting for an address, don't respond to anything other than a | ||||
| 		// start bit, which is actually dealt with above. | ||||
| 		case State::AwaitingAddress:	break; | ||||
|  | ||||
| 		// To collect an address: shift in eight bits, and if there's a device | ||||
| 		// at that address then acknowledge the address and segue into a read | ||||
| 		// or write loop. | ||||
| 		case State::CollectingAddress: | ||||
| 			capture_bit(); | ||||
| 			if(input_count_ == 8) { | ||||
| 				auto pair = peripherals_.find(uint8_t(input_) & 0xfe); | ||||
| 				if(pair != peripherals_.end()) { | ||||
| 					active_peripheral_ = pair->second; | ||||
| 					active_peripheral_->start(input_ & 1); | ||||
|  | ||||
| 					if(input_&1) { | ||||
| 						acknowledge(); | ||||
| 						set_state(State::CompletingReadAcknowledge); | ||||
| 					} else { | ||||
| 						acknowledge(); | ||||
| 						set_state(State::ReceivingByte); | ||||
| 					} | ||||
| 				} else { | ||||
| 					state_ = State::AwaitingAddress; | ||||
| 				} | ||||
| 			} | ||||
| 		break; | ||||
|  | ||||
| 		// Receiving byte: wait until a scheduled acknowledgment has | ||||
| 		// happened, then collect eight bits, then see whether the | ||||
| 		// active peripheral will accept them. If so, acknowledge and repeat. | ||||
| 		// Otherwise fall silent. | ||||
| 		case State::ReceivingByte: | ||||
| 			if(event == Event::FinishedOutput) { | ||||
| 				return; | ||||
| 			} | ||||
| 			capture_bit(); | ||||
| 			if(input_count_ == 8) { | ||||
| 				if(active_peripheral_->write(static_cast<uint8_t>(input_))) { | ||||
| 					acknowledge(); | ||||
| 					set_state(State::ReceivingByte); | ||||
| 				} else { | ||||
| 					stop(); | ||||
| 				} | ||||
| 			} | ||||
| 		break; | ||||
|  | ||||
| 		// The initial state immediately after a peripheral has been started | ||||
| 		// in read mode and the address-select acknowledgement is still | ||||
| 		// being serialised. | ||||
| 		// | ||||
| 		// Once that is completed, enqueues the first byte from the peripheral. | ||||
| 		case State::CompletingReadAcknowledge: | ||||
| 			if(event != Event::FinishedOutput) { | ||||
| 				break; | ||||
| 			} | ||||
| 			enqueue(active_peripheral_->read()); | ||||
| 		break; | ||||
|  | ||||
| 		// Repeating state during reading; waits until the previous byte has | ||||
| 		// been fully serialised, and if the host acknowledged it then posts | ||||
| 		// the next. If the host didn't acknowledge, stops the connection. | ||||
| 		case State::AwaitingByteAcknowledge: | ||||
| 			if(event == Event::FinishedOutput) { | ||||
| 				break; | ||||
| 			} | ||||
| 			if(event != Event::Zero) { | ||||
| 				stop(); | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			// Add a new byte if there is one. | ||||
| 			enqueue(active_peripheral_->read()); | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void Bus::add_peripheral(Peripheral *peripheral, int address) { | ||||
| 	peripherals_[address] = peripheral; | ||||
| } | ||||
							
								
								
									
										82
									
								
								Components/I2C/I2C.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								Components/I2C/I2C.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| // | ||||
| //  I2C.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 16/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <optional> | ||||
| #include <unordered_map> | ||||
|  | ||||
| namespace I2C { | ||||
|  | ||||
| /// Provides the virtual interface for an I2C peripheral; attaching this to a bus | ||||
| /// provides automatic protocol handling. | ||||
| struct Peripheral { | ||||
| 	/// Indicates that the host signalled the start condition and addressed this | ||||
| 	/// peripheral, along with whether it indicated a read or write. | ||||
| 	virtual void start([[maybe_unused]] bool is_read) {} | ||||
|  | ||||
| 	/// Indicates that the host signalled a stop. | ||||
| 	virtual void stop() {} | ||||
|  | ||||
| 	/// Requests the next byte to serialise onto the I2C bus after this peripheral has | ||||
| 	/// been started in read mode. | ||||
| 	/// | ||||
| 	/// @returns A byte to serialise or std::nullopt if the peripheral declines to | ||||
| 	/// continue to communicate. | ||||
| 	virtual std::optional<uint8_t> read() { return std::nullopt; } | ||||
|  | ||||
| 	/// Provides a byte received from the bus after this peripheral has been started | ||||
| 	/// in write mode. | ||||
| 	/// | ||||
| 	/// @returns @c true if the write should be acknowledged; @c false otherwise. | ||||
| 	virtual bool write(uint8_t) { return false; } | ||||
| }; | ||||
|  | ||||
| class Bus { | ||||
| public: | ||||
| 	void set_data(bool pulled); | ||||
| 	bool data(); | ||||
|  | ||||
| 	void set_clock(bool pulled); | ||||
| 	bool clock(); | ||||
|  | ||||
| 	void set_clock_data(bool clock_pulled, bool data_pulled); | ||||
|  | ||||
| 	void add_peripheral(Peripheral *, int address); | ||||
|  | ||||
| private: | ||||
| 	bool data_ = false; | ||||
| 	bool clock_ = false; | ||||
| 	bool in_bit_ = false; | ||||
| 	std::unordered_map<int, Peripheral *> peripherals_; | ||||
|  | ||||
| 	uint16_t input_ = 0xffff; | ||||
| 	int input_count_ = -1; | ||||
|  | ||||
| 	Peripheral *active_peripheral_ = nullptr; | ||||
| 	uint16_t peripheral_response_ = 0xffff; | ||||
| 	int peripheral_bits_ = 0; | ||||
|  | ||||
| 	enum class Event { | ||||
| 		Zero, One, Start, Stop, FinishedOutput, | ||||
| 	}; | ||||
| 	void signal(Event); | ||||
|  | ||||
| 	enum class State { | ||||
| 		AwaitingAddress, | ||||
| 		CollectingAddress, | ||||
|  | ||||
| 		CompletingReadAcknowledge, | ||||
| 		AwaitingByteAcknowledge, | ||||
|  | ||||
| 		ReceivingByte, | ||||
| 	} state_ = State::AwaitingAddress; | ||||
| }; | ||||
|  | ||||
| } | ||||
| @@ -19,15 +19,16 @@ bool SCC::is_zero_level() const { | ||||
| 	return !(channel_enable_ & 0x1f); | ||||
| } | ||||
|  | ||||
| void SCC::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| template <Outputs::Speaker::Action action> | ||||
| void SCC::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { | ||||
| 	if(is_zero_level()) { | ||||
| 		std::memset(target, 0, sizeof(std::int16_t) * number_of_samples); | ||||
| 		Outputs::Speaker::fill<action>(target, target + number_of_samples, Outputs::Speaker::MonoSample()); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	std::size_t c = 0; | ||||
| 	while((master_divider_&7) && c < number_of_samples) { | ||||
| 		target[c] = transient_output_level_; | ||||
| 		Outputs::Speaker::apply<action>(target[c], transient_output_level_); | ||||
| 		master_divider_++; | ||||
| 		c++; | ||||
| 	} | ||||
| @@ -44,12 +45,15 @@ void SCC::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| 		evaluate_output_volume(); | ||||
|  | ||||
| 		for(int ic = 0; ic < 8 && c < number_of_samples; ++ic) { | ||||
| 			target[c] = transient_output_level_; | ||||
| 			Outputs::Speaker::apply<action>(target[c], transient_output_level_); | ||||
| 			c++; | ||||
| 			master_divider_++; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| template void SCC::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void SCC::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void SCC::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
|  | ||||
| void SCC::write(uint16_t address, uint8_t value) { | ||||
| 	address &= 0xff; | ||||
| @@ -111,5 +115,3 @@ uint8_t SCC::read(uint16_t address) { | ||||
| 	} | ||||
| 	return 0xff; | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/BufferSource.hpp" | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
|  | ||||
| namespace Konami { | ||||
| @@ -20,7 +20,7 @@ namespace Konami { | ||||
| 	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 { | ||||
| class SCC: public ::Outputs::Speaker::BufferSource<SCC, false> { | ||||
| 	public: | ||||
| 		/// Creates a new SCC. | ||||
| 		SCC(Concurrency::AsyncTaskQueue<false> &task_queue); | ||||
| @@ -29,9 +29,9 @@ class SCC: public ::Outputs::Speaker::SampleSource { | ||||
| 		bool is_zero_level() const; | ||||
|  | ||||
| 		/// As per ::SampleSource; provides audio output. | ||||
| 		void get_samples(std::size_t number_of_samples, std::int16_t *target); | ||||
| 		template <Outputs::Speaker::Action action> | ||||
| 		void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| 		static constexpr bool get_is_stereo() { return false; } | ||||
|  | ||||
| 		/// Writes to the SCC. | ||||
| 		void write(uint16_t address, uint8_t value); | ||||
| @@ -45,7 +45,7 @@ class SCC: public ::Outputs::Speaker::SampleSource { | ||||
| 		// State from here on down is accessed ony from the audio thread. | ||||
| 		int master_divider_ = 0; | ||||
| 		std::int16_t master_volume_ = 0; | ||||
| 		int16_t transient_output_level_ = 0; | ||||
| 		Outputs::Speaker::MonoSample transient_output_level_ = 0; | ||||
|  | ||||
| 		struct Channel { | ||||
| 			int period = 0; | ||||
|   | ||||
| @@ -8,12 +8,12 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/BufferSource.hpp" | ||||
| #include "../../../Concurrency/AsyncTaskQueue.hpp" | ||||
|  | ||||
| namespace Yamaha::OPL { | ||||
|  | ||||
| template <typename Child> class OPLBase: public ::Outputs::Speaker::SampleSource { | ||||
| template <typename Child, bool stereo> class OPLBase: public ::Outputs::Speaker::BufferSource<Child, stereo> { | ||||
| 	public: | ||||
| 		void write(uint16_t address, uint8_t value) { | ||||
| 			if(address & 1) { | ||||
|   | ||||
| @@ -278,7 +278,8 @@ void OPLL::set_sample_volume_range(std::int16_t range) { | ||||
| 	total_volume_ = range; | ||||
| } | ||||
|  | ||||
| void OPLL::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| template <Outputs::Speaker::Action action> | ||||
| void OPLL::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { | ||||
| 	// Both the OPLL and the OPL2 divide the input clock by 72 to get the base tick frequency; | ||||
| 	// unlike the OPL2 the OPLL time-divides the output for 'mixing'. | ||||
|  | ||||
| @@ -289,12 +290,16 @@ void OPLL::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| 	while(number_of_samples--) { | ||||
| 		if(!audio_offset_) update_all_channels(); | ||||
|  | ||||
| 		*target = output_levels_[audio_offset_ / channel_output_period]; | ||||
| 		Outputs::Speaker::apply<action>(*target, output_levels_[audio_offset_ / channel_output_period]); | ||||
| 		++target; | ||||
| 		audio_offset_ = (audio_offset_ + 1) % update_period; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| template void OPLL::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void OPLL::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void OPLL::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
|  | ||||
| void OPLL::update_all_channels() { | ||||
| 	oscillator_.update(); | ||||
|  | ||||
|   | ||||
| @@ -19,24 +19,26 @@ | ||||
|  | ||||
| namespace Yamaha::OPL { | ||||
|  | ||||
| class OPLL: public OPLBase<OPLL> { | ||||
| class OPLL: public OPLBase<OPLL, false> { | ||||
| 	public: | ||||
| 		/// Creates a new OPLL or VRC7. | ||||
| 		OPLL(Concurrency::AsyncTaskQueue<false> &task_queue, int audio_divider = 1, bool is_vrc7 = false); | ||||
|  | ||||
| 		/// As per ::SampleSource; provides audio output. | ||||
| 		void get_samples(std::size_t number_of_samples, std::int16_t *target); | ||||
| 		template <Outputs::Speaker::Action action> | ||||
| 		void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| 		bool is_zero_level() const { return false; }	// TODO. | ||||
|  | ||||
| 		// The OPLL is generally 'half' as loud as it's told to be. This won't strictly be true in | ||||
| 		// rhythm mode, but it's correct for melodic output. | ||||
| 		double get_average_output_peak() const { return 0.5; } | ||||
| 		double average_output_peak() const { return 0.5; } | ||||
|  | ||||
| 		/// Reads from the OPL. | ||||
| 		uint8_t read(uint16_t address); | ||||
|  | ||||
| 	private: | ||||
| 		friend OPLBase<OPLL>; | ||||
| 		friend OPLBase<OPLL, false>; | ||||
| 		void write_register(uint8_t address, uint8_t value); | ||||
|  | ||||
| 		int audio_divider_ = 0; | ||||
|   | ||||
| @@ -99,10 +99,11 @@ void SN76489::evaluate_output_volume() { | ||||
| 	); | ||||
| } | ||||
|  | ||||
| void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| template <Outputs::Speaker::Action action> | ||||
| void SN76489::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { | ||||
| 	std::size_t c = 0; | ||||
| 	while((master_divider_& (master_divider_period_ - 1)) && c < number_of_samples) { | ||||
| 		target[c] = output_volume_; | ||||
| 		Outputs::Speaker::apply<action>(target[c], output_volume_); | ||||
| 		master_divider_++; | ||||
| 		c++; | ||||
| 	} | ||||
| @@ -151,7 +152,7 @@ void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| 		evaluate_output_volume(); | ||||
|  | ||||
| 		for(int ic = 0; ic < master_divider_period_ && c < number_of_samples; ++ic) { | ||||
| 			target[c] = output_volume_; | ||||
| 			Outputs::Speaker::apply<action>(target[c], output_volume_); | ||||
| 			c++; | ||||
| 			master_divider_++; | ||||
| 		} | ||||
| @@ -159,3 +160,6 @@ void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
|  | ||||
| 	master_divider_ &= (master_divider_period_ - 1); | ||||
| } | ||||
| template void SN76489::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void SN76489::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void SN76489::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
|   | ||||
| @@ -8,12 +8,12 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/BufferSource.hpp" | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
|  | ||||
| namespace TI { | ||||
|  | ||||
| class SN76489: public Outputs::Speaker::SampleSource { | ||||
| class SN76489: public Outputs::Speaker::BufferSource<SN76489, false> { | ||||
| 	public: | ||||
| 		enum class Personality { | ||||
| 			SN76489, | ||||
| @@ -28,10 +28,10 @@ class SN76489: public Outputs::Speaker::SampleSource { | ||||
| 		void write(uint8_t value); | ||||
|  | ||||
| 		// As per SampleSource. | ||||
| 		void get_samples(std::size_t number_of_samples, std::int16_t *target); | ||||
| 		template <Outputs::Speaker::Action action> | ||||
| 		void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); | ||||
| 		bool is_zero_level() const; | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| 		static constexpr bool get_is_stereo() { return false; } | ||||
|  | ||||
| 	private: | ||||
| 		int master_divider_ = 0; | ||||
|   | ||||
| @@ -161,9 +161,7 @@ void Line<include_clock>::update_delegate(bool level) { | ||||
| 		// Forward as many bits as occur. | ||||
| 		Storage::Time time_left(cycles_to_forward, int(clock_rate_.as_integral())); | ||||
| 		const int bit = level ? 1 : 0; | ||||
| 		int bits = 0; | ||||
| 		while(time_left >= time_left_in_bit_) { | ||||
| 			++bits; | ||||
| 			if(!read_delegate_->serial_line_did_produce_bit(this, bit)) { | ||||
| 				read_delegate_phase_ = ReadDelegatePhase::WaitingForZero; | ||||
| 				if(bit) return; | ||||
|   | ||||
| @@ -19,7 +19,7 @@ namespace Inputs { | ||||
| */ | ||||
| class Joystick { | ||||
| 	public: | ||||
| 		virtual ~Joystick() {} | ||||
| 		virtual ~Joystick() = default; | ||||
|  | ||||
| 		/*! | ||||
| 			Defines a single input, any individually-measured thing — a fire button or | ||||
|   | ||||
| @@ -45,7 +45,7 @@ class Keyboard { | ||||
| 		/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys. | ||||
| 		Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers); | ||||
|  | ||||
| 		virtual ~Keyboard() {} | ||||
| 		virtual ~Keyboard() = default; | ||||
|  | ||||
| 		// Host interface. | ||||
|  | ||||
|   | ||||
							
								
								
									
										133
									
								
								InstructionSets/ARM/BarrelShifter.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								InstructionSets/ARM/BarrelShifter.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| // | ||||
| //  BarrelShifter.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 28/02/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| namespace InstructionSet::ARM { | ||||
|  | ||||
| enum class ShiftType { | ||||
| 	LogicalLeft = 0b00, | ||||
| 	LogicalRight = 0b01, | ||||
| 	ArithmeticRight = 0b10, | ||||
| 	RotateRight = 0b11, | ||||
| }; | ||||
|  | ||||
| template <bool writeable> struct Carry; | ||||
| template <> struct Carry<true> { | ||||
| 	using type = uint32_t &; | ||||
| }; | ||||
| template <> struct Carry<false> { | ||||
| 	using type = const uint32_t; | ||||
| }; | ||||
|  | ||||
| /// Apply a rotation of @c type to @c source of @c amount; @c carry should be either @c 1 or @c 0 | ||||
| /// at call to represent the current value of the carry flag. If @c set_carry is @c true then @c carry will | ||||
| /// receive the new value of the carry flag following the rotation — @c 0 for no carry, @c non-0 for carry. | ||||
| /// | ||||
| /// Shift amounts of 0 are given the meaning attributed to them for immediate shift counts. | ||||
| template <ShiftType type, bool set_carry, bool is_immediate_shift> | ||||
| void shift(uint32_t &source, uint32_t amount, typename Carry<set_carry>::type carry) { | ||||
| 	switch(type) { | ||||
| 		case ShiftType::LogicalLeft: | ||||
| 			if(amount > 32) { | ||||
| 				if constexpr (set_carry) carry = 0; | ||||
| 				source = 0; | ||||
| 			} else if(amount == 32) { | ||||
| 				if constexpr (set_carry) carry = source & 1; | ||||
| 				source = 0; | ||||
| 			} else if(amount > 0) { | ||||
| 				if constexpr (set_carry) carry = source & (0x8000'0000 >> (amount - 1)); | ||||
| 				source <<= amount; | ||||
| 			} | ||||
| 		break; | ||||
|  | ||||
| 		case ShiftType::LogicalRight: | ||||
| 			if(!amount && is_immediate_shift) { | ||||
| 				// An immediate logical shift right by '0' is treated as a shift by 32; | ||||
| 				// assemblers are supposed to map LSR #0 to LSL #0. | ||||
| 				amount = 32; | ||||
| 			} | ||||
|  | ||||
| 			if(amount > 32) { | ||||
| 				if constexpr (set_carry) carry = 0; | ||||
| 				source = 0; | ||||
| 			} else if(amount == 32) { | ||||
| 				if constexpr (set_carry) carry = source & 0x8000'0000; | ||||
| 				source = 0; | ||||
| 			} else if(amount > 0) { | ||||
| 				if constexpr (set_carry) carry = source & (1 << (amount - 1)); | ||||
| 				source >>= amount; | ||||
| 			} | ||||
| 		break; | ||||
|  | ||||
| 		case ShiftType::ArithmeticRight: { | ||||
| 			if(!amount && is_immediate_shift) { | ||||
| 				// An immediate arithmetic shift of '0' is treated as a shift by 32. | ||||
| 				amount = 32; | ||||
| 			} | ||||
|  | ||||
| 			const uint32_t sign = (source & 0x8000'0000) ? 0xffff'ffff : 0x0000'0000; | ||||
|  | ||||
| 			if(amount >= 32) { | ||||
| 				if constexpr (set_carry) carry = source & 0x8000'0000; | ||||
| 				source = sign; | ||||
| 			} else if(amount > 0) { | ||||
| 				if constexpr (set_carry) carry = source & (1 << (amount - 1)); | ||||
| 				source = (source >> amount) | (sign << (32 - amount)); | ||||
| 			} | ||||
| 		} break; | ||||
|  | ||||
| 		case ShiftType::RotateRight: { | ||||
| 			if(!amount) { | ||||
| 				if(is_immediate_shift) { | ||||
| 					// Immediate rotate right by 0 is treated as a rotate right by 1 through carry. | ||||
| 					const uint32_t high = carry << 31; | ||||
| 					if constexpr (set_carry) carry = source & 1; | ||||
| 					source = (source >> 1) | high; | ||||
| 				} | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			// "ROR by 32 has result equal to Rm, carry out equal to bit 31 ... | ||||
| 			// [for] ROR by n where n is greater than 32 ... repeatedly subtract 32 from n | ||||
| 			// until the amount is in the range 1 to 32" | ||||
| 			amount &= 31; | ||||
| 			if(amount) { | ||||
| 				if constexpr (set_carry) carry = source & (1 << (amount - 1)); | ||||
| 				source = (source >> amount) | (source << (32 - amount)); | ||||
| 			} else { | ||||
| 				if constexpr (set_carry) carry = source & 0x8000'0000; | ||||
| 			} | ||||
| 		} break; | ||||
|  | ||||
| 		// TODO: upon adoption of C++20, use std::rotr. | ||||
|  | ||||
| 		default: break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /// Acts as per @c shift above, but applies runtime shift-type selection. | ||||
| template <bool set_carry, bool is_immediate_shift> | ||||
| void shift(ShiftType type, uint32_t &source, uint32_t amount, typename Carry<set_carry>::type carry) { | ||||
| 	switch(type) { | ||||
| 		case ShiftType::LogicalLeft: | ||||
| 			shift<ShiftType::LogicalLeft, set_carry, is_immediate_shift>(source, amount, carry); | ||||
| 		break; | ||||
| 		case ShiftType::LogicalRight: | ||||
| 			shift<ShiftType::LogicalRight, set_carry, is_immediate_shift>(source, amount, carry); | ||||
| 		break; | ||||
| 		case ShiftType::ArithmeticRight: | ||||
| 			shift<ShiftType::ArithmeticRight, set_carry, is_immediate_shift>(source, amount, carry); | ||||
| 		break; | ||||
| 		case ShiftType::RotateRight: | ||||
| 			shift<ShiftType::RotateRight, set_carry, is_immediate_shift>(source, amount, carry); | ||||
| 		break; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										270
									
								
								InstructionSets/ARM/Disassembler.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								InstructionSets/ARM/Disassembler.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,270 @@ | ||||
| // | ||||
| //  Disassembler.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 19/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "OperationMapper.hpp" | ||||
|  | ||||
| #include <string> | ||||
| #include <sstream> | ||||
|  | ||||
| namespace InstructionSet::ARM { | ||||
|  | ||||
| /// Holds a single ARM operand, whether a source/destination or immediate value, potentially including a shift. | ||||
| struct Operand { | ||||
| 	enum class Type { | ||||
| 		Immediate, Register, RegisterList, None | ||||
| 	} type = Type::None; | ||||
| 	uint32_t value = 0; | ||||
|  | ||||
| 	// TODO: encode shifting | ||||
|  | ||||
| 	operator std::string() const { | ||||
| 		switch(type) { | ||||
| 			default:	return ""; | ||||
| 			case Type::Register:	return std::string("r") + std::to_string(value); | ||||
| 			case Type::RegisterList: { | ||||
| 				std::stringstream stream; | ||||
| 				stream << '['; | ||||
| 				bool first = true; | ||||
| 				for(int c = 0; c < 16; c++) { | ||||
| 					if(value & (1 << c)) { | ||||
| 						if(!first) stream << ", "; | ||||
| 						first = false; | ||||
|  | ||||
| 						stream << 'r' << c; | ||||
| 					} | ||||
| 				} | ||||
| 				stream << ']'; | ||||
|  | ||||
| 				return stream.str(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| /// Describes a single ARM instruction, suboptimally but such that all relevant detail has been extracted | ||||
| /// by the OperationMapper and is now easy to inspect or to turn into a string. | ||||
| struct Instruction { | ||||
| 	Condition condition = Condition::AL; | ||||
| 	enum class Operation { | ||||
| 		AND,	EOR,	SUB,	RSB, | ||||
| 		ADD,	ADC,	SBC,	RSC, | ||||
| 		TST,	TEQ,	CMP,	CMN, | ||||
| 		ORR,	MOV,	BIC,	MVN, | ||||
|  | ||||
| 		LDR,	STR, | ||||
| 		LDM,	STM, | ||||
|  | ||||
| 		B, BL, | ||||
|  | ||||
| 		SWI, | ||||
|  | ||||
| 		MRC, MCR, | ||||
|  | ||||
| 		Undefined, | ||||
| 	} operation = Operation::Undefined; | ||||
|  | ||||
| 	Operand destination, operand1, operand2; | ||||
| 	bool sets_flags = false; | ||||
| 	bool is_byte = false; | ||||
|  | ||||
| 	std::string to_string(uint32_t address) const { | ||||
| 		std::ostringstream result; | ||||
|  | ||||
| 		// Treat all nevers as nops. | ||||
| 		if(condition == Condition::NV) { | ||||
| 			return "nop"; | ||||
| 		} | ||||
|  | ||||
| 		// Print operation. | ||||
| 		switch(operation) { | ||||
| 			case Operation::Undefined:	return "undefined"; | ||||
| 			case Operation::SWI:		return "swi"; | ||||
|  | ||||
| 			case Operation::B:		result << "b";		break; | ||||
| 			case Operation::BL:		result << "bl";		break; | ||||
|  | ||||
| 			case Operation::AND:	result << "and";	break; | ||||
| 			case Operation::EOR:	result << "eor";	break; | ||||
| 			case Operation::SUB:	result << "sub";	break; | ||||
| 			case Operation::RSB:	result << "rsb";	break; | ||||
| 			case Operation::ADD:	result << "add";	break; | ||||
| 			case Operation::ADC:	result << "adc";	break; | ||||
| 			case Operation::SBC:	result << "sbc";	break; | ||||
| 			case Operation::RSC:	result << "rsc";	break; | ||||
| 			case Operation::TST:	result << "tst";	break; | ||||
| 			case Operation::TEQ:	result << "teq";	break; | ||||
| 			case Operation::CMP:	result << "cmp";	break; | ||||
| 			case Operation::CMN:	result << "cmn";	break; | ||||
| 			case Operation::ORR:	result << "orr";	break; | ||||
| 			case Operation::MOV:	result << "mov";	break; | ||||
| 			case Operation::BIC:	result << "bic";	break; | ||||
| 			case Operation::MVN:	result << "mvn";	break; | ||||
|  | ||||
| 			case Operation::LDR:	result << "ldr";	break; | ||||
| 			case Operation::STR:	result << "str";	break; | ||||
| 			case Operation::LDM:	result << "ldm";	break; | ||||
| 			case Operation::STM:	result << "stm";	break; | ||||
|  | ||||
| 			case Operation::MRC:	result << "mrc";	break; | ||||
| 			case Operation::MCR:	result << "mcr";	break; | ||||
| 		} | ||||
|  | ||||
| 		// Append the sets-flags modifier if applicable. | ||||
| 		if(sets_flags) result << 's'; | ||||
|  | ||||
| 		// Possibly a condition code. | ||||
| 		switch(condition) { | ||||
| 			case Condition::EQ:	result << "eq";	break; | ||||
| 			case Condition::NE:	result << "ne";	break; | ||||
| 			case Condition::CS:	result << "cs";	break; | ||||
| 			case Condition::CC:	result << "cc";	break; | ||||
| 			case Condition::MI:	result << "mi";	break; | ||||
| 			case Condition::PL:	result << "pl";	break; | ||||
| 			case Condition::VS:	result << "vs";	break; | ||||
| 			case Condition::VC:	result << "vc";	break; | ||||
| 			case Condition::HI:	result << "hi";	break; | ||||
| 			case Condition::LS:	result << "ls";	break; | ||||
| 			case Condition::GE:	result << "ge";	break; | ||||
| 			case Condition::LT:	result << "lt";	break; | ||||
| 			case Condition::GT:	result << "gt";	break; | ||||
| 			case Condition::LE:	result << "le";	break; | ||||
| 			default: break; | ||||
| 		} | ||||
|  | ||||
| 		// If this is a branch, append the target. | ||||
| 		if(operation == Operation::B || operation == Operation::BL) { | ||||
| 			result << " 0x" << std::hex << ((address + 8 + operand1.value) & 0x3fffffc); | ||||
| 		} | ||||
|  | ||||
| 		if( | ||||
| 			operation == Operation::LDR || operation == Operation::STR || | ||||
| 			operation == Operation::LDM || operation == Operation::STM | ||||
| 		) { | ||||
| 			if(is_byte) result << 'b'; | ||||
| 			result << ' ' << static_cast<std::string>(destination); | ||||
| 			result << ", [" << static_cast<std::string>(operand1) << "]"; | ||||
| 			// TODO: learn how ARM shifts/etc are normally presented. | ||||
| 		} | ||||
|  | ||||
| 		return result.str(); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| /// A target for @c dispatch that merely captures a description of the decoded instruction, being | ||||
| /// able to vend it later via @c last(). | ||||
| template <Model model> | ||||
| struct Disassembler { | ||||
| 	Instruction last() { | ||||
| 		return instruction_; | ||||
| 	} | ||||
|  | ||||
| 	bool should_schedule(Condition condition) { | ||||
| 		instruction_ = Instruction(); | ||||
| 		instruction_.condition = condition; | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	template <Flags f> void perform(DataProcessing fields) { | ||||
| 		constexpr DataProcessingFlags flags(f); | ||||
|  | ||||
| 		instruction_.operand1.type = Operand::Type::Register; | ||||
| 		instruction_.operand1.value = fields.operand1(); | ||||
|  | ||||
| 		instruction_.destination.type = Operand::Type::Register; | ||||
| 		instruction_.destination.value = fields.destination(); | ||||
|  | ||||
| 		if(flags.operand2_is_immediate()) { | ||||
| 			instruction_.operand2.type = Operand::Type::Immediate; | ||||
| //			instruction_.operand2.value = fields.immediate(), fields.rotate(); | ||||
| 			// TODO: decode immediate. | ||||
|  | ||||
| 		} else { | ||||
| 			instruction_.operand2.type = Operand::Type::Register; | ||||
| 			instruction_.operand2.value = fields.operand2(); | ||||
| 			// TODO: capture shift_type(), etc. | ||||
| 		} | ||||
|  | ||||
| 		instruction_.sets_flags = flags.set_condition_codes(); | ||||
|  | ||||
| 		switch(flags.operation()) { | ||||
| 			case DataProcessingOperation::AND:	instruction_.operation = Instruction::Operation::AND;	break; | ||||
| 			case DataProcessingOperation::EOR:	instruction_.operation = Instruction::Operation::EOR;	break; | ||||
| 			case DataProcessingOperation::ORR:	instruction_.operation = Instruction::Operation::ORR;	break; | ||||
| 			case DataProcessingOperation::BIC:	instruction_.operation = Instruction::Operation::BIC;	break; | ||||
| 			case DataProcessingOperation::MOV:	instruction_.operation = Instruction::Operation::MOV;	break; | ||||
| 			case DataProcessingOperation::MVN:	instruction_.operation = Instruction::Operation::MVN;	break; | ||||
| 			case DataProcessingOperation::TST:	instruction_.operation = Instruction::Operation::TST;	break; | ||||
| 			case DataProcessingOperation::TEQ:	instruction_.operation = Instruction::Operation::TEQ;	break; | ||||
| 			case DataProcessingOperation::ADD:	instruction_.operation = Instruction::Operation::ADD;	break; | ||||
| 			case DataProcessingOperation::ADC:	instruction_.operation = Instruction::Operation::ADC;	break; | ||||
| 			case DataProcessingOperation::CMN:	instruction_.operation = Instruction::Operation::CMN;	break; | ||||
| 			case DataProcessingOperation::SUB:	instruction_.operation = Instruction::Operation::SUB;	break; | ||||
| 			case DataProcessingOperation::SBC:	instruction_.operation = Instruction::Operation::SBC;	break; | ||||
| 			case DataProcessingOperation::CMP:	instruction_.operation = Instruction::Operation::CMP;	break; | ||||
| 			case DataProcessingOperation::RSB:	instruction_.operation = Instruction::Operation::RSB;	break; | ||||
| 			case DataProcessingOperation::RSC:	instruction_.operation = Instruction::Operation::RSC;	break; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	template <Flags> void perform(Multiply) {} | ||||
| 	template <Flags f> void perform(SingleDataTransfer fields) { | ||||
| 		constexpr SingleDataTransferFlags flags(f); | ||||
| 		instruction_.operation = | ||||
| 			(flags.operation() == SingleDataTransferFlags::Operation::STR) ? | ||||
| 				Instruction::Operation::STR : Instruction::Operation::LDR; | ||||
|  | ||||
| 		instruction_.destination.type = Operand::Type::Register; | ||||
| 		instruction_.destination.value = fields.destination(); | ||||
|  | ||||
| 		instruction_.operand1.type = Operand::Type::Register; | ||||
| 		instruction_.operand1.value = fields.base(); | ||||
| 	} | ||||
| 	template <Flags f> void perform(BlockDataTransfer fields) { | ||||
| 		constexpr BlockDataTransferFlags flags(f); | ||||
| 		instruction_.operation = | ||||
| 			(flags.operation() == BlockDataTransferFlags::Operation::STM) ? | ||||
| 				Instruction::Operation::STM : Instruction::Operation::LDM; | ||||
|  | ||||
| 		instruction_.destination.type = Operand::Type::Register; | ||||
| 		instruction_.destination.value = fields.base(); | ||||
|  | ||||
| 		instruction_.operand1.type = Operand::Type::RegisterList; | ||||
| 		instruction_.operand1.value = fields.register_list(); | ||||
| 	} | ||||
| 	template <Flags f> void perform(Branch fields) { | ||||
| 		constexpr BranchFlags flags(f); | ||||
| 		instruction_.operation = | ||||
| 			(flags.operation() == BranchFlags::Operation::BL) ? | ||||
| 				Instruction::Operation::BL : Instruction::Operation::B; | ||||
| 		instruction_.operand1.type = Operand::Type::Immediate; | ||||
| 		instruction_.operand1.value = fields.offset(); | ||||
| 	} | ||||
| 	template <Flags f> void perform(CoprocessorRegisterTransfer) { | ||||
| 		constexpr CoprocessorRegisterTransferFlags flags(f); | ||||
| 		instruction_.operation = | ||||
| 			(flags.operation() == CoprocessorRegisterTransferFlags::Operation::MRC) ? | ||||
| 				Instruction::Operation::MRC : Instruction::Operation::MCR; | ||||
| 	} | ||||
| 	template <Flags> void perform(CoprocessorDataOperation) {} | ||||
| 	template <Flags> void perform(CoprocessorDataTransfer) {} | ||||
|  | ||||
| 	void software_interrupt(SoftwareInterrupt) { | ||||
| 		instruction_.operation = Instruction::Operation::SWI; | ||||
| 	} | ||||
| 	void unknown() { | ||||
| 		instruction_.operation = Instruction::Operation::Undefined; | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	Instruction instruction_; | ||||
|  | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										669
									
								
								InstructionSets/ARM/Executor.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										669
									
								
								InstructionSets/ARM/Executor.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,669 @@ | ||||
| // | ||||
| //  Executor.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 01/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "BarrelShifter.hpp" | ||||
| #include "OperationMapper.hpp" | ||||
| #include "Registers.hpp" | ||||
| #include "../../Numeric/Carry.hpp" | ||||
|  | ||||
| namespace InstructionSet::ARM { | ||||
|  | ||||
| /// Maps from a semantic ARM read of type @c SourceT to either the 8- or 32-bit value observed | ||||
| /// by watching the low 8 bits or all 32 bits of the data bus. | ||||
| template <typename DestinationT, typename SourceT> | ||||
| DestinationT read_bus(SourceT value) { | ||||
| 	if constexpr (std::is_same_v<DestinationT, SourceT>) { | ||||
| 		return value; | ||||
| 	} | ||||
| 	if constexpr (std::is_same_v<DestinationT, uint8_t>) { | ||||
| 		return uint8_t(value); | ||||
| 	} else { | ||||
| 		return value | (value << 8) | (value << 16) | (value << 24); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| struct NullControlFlowHandler { | ||||
| 	/// Indicates that a potential pipeline-affecting status flag change occurred, | ||||
| 	/// i.e. a change to processor mode or interrupt flags. | ||||
| 	void did_set_status() {} | ||||
|  | ||||
| 	/// Indicates that the PC was altered by the instruction. | ||||
| 	void did_set_pc() {} | ||||
|  | ||||
| 	/// Provides notification that an SWI is about to happen along with the option of skipping it; this gives handlers the | ||||
| 	/// chance to substitute a high-level reimplementation of the service call. | ||||
| 	bool should_swi([[maybe_unused]] uint32_t comment) { return true; } | ||||
| }; | ||||
|  | ||||
| /// A class compatible with the @c OperationMapper definition of a scheduler which applies all actions | ||||
| /// immediately, updating either a set of @c Registers or using the templated @c MemoryT to access | ||||
| /// memory. No hooks are currently provided for applying realistic timing. | ||||
| /// | ||||
| /// If a ControlFlowHandlerT is specified, it'll receive calls as defined in the NullControlFlowHandler above. | ||||
| template <Model model, typename MemoryT, typename ControlFlowHandlerT = NullControlFlowHandler> | ||||
| struct Executor { | ||||
| 	template <typename... Args> | ||||
| 	Executor(ControlFlowHandlerT &handler, Args &&...args) : bus(std::forward<Args>(args)...), control_flow_handler_(handler) {} | ||||
|  | ||||
| 	template <typename... Args> | ||||
| 	Executor(Args &&...args) : bus(std::forward<Args>(args)...) {} | ||||
|  | ||||
| 	/// @returns @c true if @c condition implies an appropriate perform call should be made for this instruction, | ||||
| 	/// @c false otherwise. | ||||
| 	bool should_schedule(Condition condition) { | ||||
| 		// This short-circuit of registers_.test provides the necessary compiler clue that | ||||
| 		// Condition::AL is not only [[likely]] but [[exceedingly likely]]. | ||||
| 		return condition == Condition::AL ? true : registers_.test(condition); | ||||
| 	} | ||||
|  | ||||
| 	template <bool allow_register, bool set_carry, typename FieldsT> | ||||
| 	uint32_t decode_shift(FieldsT fields, uint32_t &rotate_carry, uint32_t pc_offset) { | ||||
| 		// "When R15 appears in the Rm position it will give the value of the PC together | ||||
| 		// with the PSR flags to the barrel shifter. ... | ||||
| 		// | ||||
| 		// If the shift amount is specified in the instruction, the PC will be 8 bytes ahead. | ||||
| 		// If a register is used to specify the shift amount, the PC will be ... 12 bytes ahead | ||||
| 		// when used as Rn or Rm." | ||||
| 		uint32_t operand2; | ||||
| 		if(fields.operand2() == 15) { | ||||
| 			operand2 = registers_.pc_status(pc_offset); | ||||
| 		} else { | ||||
| 			operand2 = registers_[fields.operand2()]; | ||||
| 		} | ||||
|  | ||||
| 		// TODO: in C++20, a quick `if constexpr (requires` can eliminate the `allow_register` parameter. | ||||
| 		if constexpr (allow_register) { | ||||
| 			if(fields.shift_count_is_register()) { | ||||
| 				uint32_t shift_amount; | ||||
|  | ||||
| 				// "When R15 appears in either of the Rn or Rs positions it will give the value | ||||
| 				// of the PC alone, with the PSR bits replaced by zeroes. ... | ||||
| 				// | ||||
| 				// If a register is used to specify the shift amount, the | ||||
| 				// PC will be 8 bytes ahead when used as Rs." | ||||
| 				shift_amount = | ||||
| 					fields.shift_register() == 15 ? | ||||
| 						registers_.pc(4) : | ||||
| 						registers_[fields.shift_register()]; | ||||
|  | ||||
| 				// "The amount by which the register should be shifted may be contained in | ||||
| 				// ... **the bottom byte** of another register". | ||||
| 				shift_amount &= 0xff; | ||||
| 				shift<set_carry, false>(fields.shift_type(), operand2, shift_amount, rotate_carry); | ||||
| 				return operand2; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		shift<set_carry, true>(fields.shift_type(), operand2, fields.shift_amount(), rotate_carry); | ||||
| 		return operand2; | ||||
| 	} | ||||
|  | ||||
| 	template <Flags f> void perform(DataProcessing fields) { | ||||
| 		constexpr DataProcessingFlags flags(f); | ||||
| 		const bool shift_by_register = !flags.operand2_is_immediate() && fields.shift_count_is_register(); | ||||
|  | ||||
| 		// Write a raw result into the PC proxy if the target is R15; it'll be stored properly later. | ||||
| 		uint32_t pc_proxy = 0; | ||||
| 		auto &destination = fields.destination() == 15 ? pc_proxy : registers_[fields.destination()]; | ||||
|  | ||||
| 		// "When R15 appears in either of the Rn or Rs positions it will give the value | ||||
| 		// of the PC alone, with the PSR bits replaced by zeroes. ... | ||||
| 		// | ||||
| 		// If the shift amount is specified in the instruction, the PC will be 8 bytes ahead. | ||||
| 		// If a register is used to specify the shift amount, the PC will be ... 12 bytes ahead | ||||
| 		// when used as Rn or Rm." | ||||
| 		const uint32_t operand1 = | ||||
| 			(fields.operand1() == 15) ? | ||||
| 				registers_.pc(shift_by_register ? 8 : 4) : | ||||
| 				registers_[fields.operand1()]; | ||||
|  | ||||
| 		uint32_t operand2; | ||||
| 		uint32_t rotate_carry = registers_.c(); | ||||
|  | ||||
| 		// Populate carry from the shift only if it'll be used. | ||||
| 		constexpr bool shift_sets_carry = is_logical(flags.operation()) && flags.set_condition_codes(); | ||||
|  | ||||
| 		// Get operand 2. | ||||
| 		if constexpr (flags.operand2_is_immediate()) { | ||||
| 			operand2 = fields.immediate(); | ||||
| 			shift<ShiftType::RotateRight, shift_sets_carry, false>(operand2, fields.rotate(), rotate_carry); | ||||
| 		} else { | ||||
| 			operand2 = decode_shift<true, shift_sets_carry>(fields, rotate_carry, shift_by_register ? 8 : 4); | ||||
| 		} | ||||
|  | ||||
| 		uint32_t conditions = 0; | ||||
| 		const auto sub = [&](uint32_t lhs, uint32_t rhs) { | ||||
| 			conditions = lhs - rhs; | ||||
|  | ||||
| 			if constexpr (flags.operation() == DataProcessingOperation::SBC || flags.operation() == DataProcessingOperation::RSC) { | ||||
| 				conditions += registers_.c() - 1; | ||||
| 			} | ||||
|  | ||||
| 			if constexpr (flags.set_condition_codes()) { | ||||
| 				// "For a subtraction, including the comparison instruction CMP, C is set to 0 if | ||||
| 				// the subtraction produced a borrow (that is, an unsigned underflow), and to 1 otherwise." | ||||
| 				registers_.set_c(!Numeric::carried_out<false, 31>(lhs, rhs, conditions)); | ||||
| 				registers_.set_v(Numeric::overflow<false>(lhs, rhs, conditions)); | ||||
| 			} | ||||
|  | ||||
| 			if constexpr (!is_comparison(flags.operation())) { | ||||
| 				destination = conditions; | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		// Perform the data processing operation. | ||||
| 		switch(flags.operation()) { | ||||
| 			// Logical operations. | ||||
| 			case DataProcessingOperation::AND:	conditions = destination = operand1 & operand2;		break; | ||||
| 			case DataProcessingOperation::EOR:	conditions = destination = operand1 ^ operand2;		break; | ||||
| 			case DataProcessingOperation::ORR:	conditions = destination = operand1 | operand2;		break; | ||||
| 			case DataProcessingOperation::BIC:	conditions = destination = operand1 & ~operand2;	break; | ||||
|  | ||||
| 			case DataProcessingOperation::MOV:	conditions = destination = operand2;	break; | ||||
| 			case DataProcessingOperation::MVN:	conditions = destination = ~operand2;	break; | ||||
|  | ||||
| 			case DataProcessingOperation::TST:	conditions = operand1 & operand2;	break; | ||||
| 			case DataProcessingOperation::TEQ:	conditions = operand1 ^ operand2;	break; | ||||
|  | ||||
| 			case DataProcessingOperation::ADD: | ||||
| 			case DataProcessingOperation::ADC: | ||||
| 			case DataProcessingOperation::CMN: | ||||
| 				conditions = operand1 + operand2; | ||||
|  | ||||
| 				if constexpr (flags.operation() == DataProcessingOperation::ADC) { | ||||
| 					conditions += registers_.c(); | ||||
| 				} | ||||
|  | ||||
| 				if constexpr (flags.set_condition_codes()) { | ||||
| 					registers_.set_c(Numeric::carried_out<true, 31>(operand1, operand2, conditions)); | ||||
| 					registers_.set_v(Numeric::overflow<true>(operand1, operand2, conditions)); | ||||
| 				} | ||||
|  | ||||
| 				if constexpr (!is_comparison(flags.operation())) { | ||||
| 					destination = conditions; | ||||
| 				} | ||||
| 			break; | ||||
|  | ||||
| 			case DataProcessingOperation::SUB: | ||||
| 			case DataProcessingOperation::SBC: | ||||
| 			case DataProcessingOperation::CMP: | ||||
| 				sub(operand1, operand2); | ||||
| 			break; | ||||
|  | ||||
| 			case DataProcessingOperation::RSB: | ||||
| 			case DataProcessingOperation::RSC: | ||||
| 				sub(operand2, operand1); | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		if(!is_comparison(flags.operation()) && fields.destination() == 15) { | ||||
| 			set_pc<true>(pc_proxy); | ||||
| 		} | ||||
| 		if constexpr (flags.set_condition_codes()) { | ||||
| 			// "When Rd is R15 and the S flag in the instruction is set, the PSR is overwritten by the | ||||
| 			// corresponding bits in the ALU result... [even] if the instruction is of a type that does not | ||||
| 			// normally produce a result (CMP, CMN, TST, TEQ) ... the result will be used to update those | ||||
| 			// PSR flags which are not protected by virtue of the processor mode" | ||||
| 			if(fields.destination() == 15) { | ||||
| 				set_status(conditions); | ||||
| 			} else { | ||||
| 				// Set N and Z in a unified way. | ||||
| 				registers_.set_nz(conditions); | ||||
|  | ||||
| 				// Set C from the barrel shifter if applicable. | ||||
| 				if constexpr (shift_sets_carry) { | ||||
| 					registers_.set_c(rotate_carry); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	template <Flags f> void perform(Multiply fields) { | ||||
| 		constexpr MultiplyFlags flags(f); | ||||
|  | ||||
| 		// R15 rules: | ||||
| 		// | ||||
| 		//	* Rs: no PSR, 8 bytes ahead; | ||||
| 		//	* Rn: with PSR, 8 bytes ahead; | ||||
| 		//	* Rm: with PSR, 12 bytes ahead. | ||||
|  | ||||
| 		const uint32_t multiplicand = fields.multiplicand() == 15 ? registers_.pc(4) : registers_[fields.multiplicand()]; | ||||
| 		const uint32_t multiplier = fields.multiplier() == 15 ? registers_.pc_status(4) : registers_[fields.multiplier()]; | ||||
| 		const uint32_t accumulator = | ||||
| 			flags.operation() == MultiplyFlags::Operation::MUL ? 0 : | ||||
| 				(fields.multiplicand() == 15 ? registers_.pc_status(8) : registers_[fields.accumulator()]); | ||||
|  | ||||
| 		const uint32_t result = multiplicand * multiplier + accumulator; | ||||
|  | ||||
| 		if constexpr (flags.set_condition_codes()) { | ||||
| 			registers_.set_nz(result); | ||||
| 			// V is unaffected; C is undefined. | ||||
| 		} | ||||
|  | ||||
| 		if(fields.destination() != 15) { | ||||
| 			registers_[fields.destination()] = result; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	template <Flags f> void perform(Branch branch) { | ||||
| 		constexpr BranchFlags flags(f); | ||||
|  | ||||
| 		if constexpr (flags.operation() == BranchFlags::Operation::BL) { | ||||
| 			registers_[14] = registers_.pc_status(0); | ||||
| 		} | ||||
| 		set_pc<true>(registers_.pc(4) + branch.offset()); | ||||
| 	} | ||||
|  | ||||
| 	template <Flags f> void perform(SingleDataTransfer transfer) { | ||||
| 		constexpr SingleDataTransferFlags flags(f); | ||||
|  | ||||
| 		// Calculate offset. | ||||
| 		uint32_t offset; | ||||
| 		if constexpr (flags.offset_is_register()) { | ||||
| 			// The 8 shift control bits are described in 6.2.3, but | ||||
| 			// the register specified shift amounts are not available | ||||
| 			// in this instruction class. | ||||
| 			uint32_t carry = registers_.c(); | ||||
| 			offset = decode_shift<false, false>(transfer, carry, 4); | ||||
| 		} else { | ||||
| 			offset = transfer.immediate(); | ||||
| 		} | ||||
|  | ||||
| 		// Obtain base address. | ||||
| 		uint32_t address = | ||||
| 			transfer.base() == 15 ? | ||||
| 				registers_.pc(4) : | ||||
| 				registers_[transfer.base()]; | ||||
|  | ||||
| 		// Determine what the address will be after offsetting. | ||||
| 		uint32_t offsetted_address = address; | ||||
| 		if constexpr (flags.add_offset()) { | ||||
| 			offsetted_address += offset; | ||||
| 		} else { | ||||
| 			offsetted_address -= offset; | ||||
| 		} | ||||
|  | ||||
| 		// If preindexing, apply now. | ||||
| 		if constexpr (flags.pre_index()) { | ||||
| 			address = offsetted_address; | ||||
| 		} | ||||
|  | ||||
| 		// Check for an address exception. | ||||
| 		if(is_invalid_address(address)) { | ||||
| 			exception<Registers::Exception::Address>(); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Decide whether to write back — when either postindexing or else write back is requested. | ||||
| 		// | ||||
| 		// Note to future self on write-back: | ||||
| 		// | ||||
| 		// It's currently unclear what to do in the case of e.g. `str r13, [r13, #0x10]!`. Is the value | ||||
| 		// written r13 as modified or the original r13? If it's as modified, does that imply that | ||||
| 		// write back has occurred regardless of a data abort? | ||||
| 		// | ||||
| 		// TODO: resolve uncertainty. | ||||
| 		constexpr bool should_write_back = !flags.pre_index() || flags.write_back_address(); | ||||
|  | ||||
| 		// "... post-indexed data transfers always write back the modified base. The only use of the [write-back address] | ||||
| 		// bit in a post-indexed data transfer is in non-user mode code, where setting the W bit forces the /TRANS pin | ||||
| 		// to go LOW for the transfer" | ||||
| 		const bool trans = (registers_.mode() == Mode::User) || (!flags.pre_index() && flags.write_back_address()); | ||||
| 		if constexpr (flags.operation() == SingleDataTransferFlags::Operation::STR) { | ||||
| 			const uint32_t source = | ||||
| 				transfer.source() == 15 ? | ||||
| 					registers_.pc_status(8) : | ||||
| 					registers_[transfer.source()]; | ||||
|  | ||||
| 			bool did_write; | ||||
| 			if constexpr (flags.transfer_byte()) { | ||||
| 				did_write = bus.template write<uint8_t>(address, uint8_t(source), registers_.mode(), trans); | ||||
| 			} else { | ||||
| 				// "The data presented to the data bus are not affected if the address is not word aligned". | ||||
| 				did_write = bus.template write<uint32_t>(address, source, registers_.mode(), trans); | ||||
| 			} | ||||
|  | ||||
| 			if(!did_write) { | ||||
| 				exception<Registers::Exception::DataAbort>(); | ||||
| 				return; | ||||
| 			} | ||||
| 		} else { | ||||
| 			bool did_read; | ||||
| 			uint32_t value = 0; | ||||
|  | ||||
| 			if constexpr (flags.transfer_byte()) { | ||||
| 				uint8_t target = 0;	// Value should never be used; this avoids a spurious GCC warning. | ||||
| 				did_read = bus.template read<uint8_t>(address, target, registers_.mode(), trans); | ||||
| 				if(did_read) { | ||||
| 					value = target; | ||||
| 				} | ||||
| 			} else { | ||||
| 				did_read = bus.template read<uint32_t>(address, value, registers_.mode(), trans); | ||||
|  | ||||
| 				if constexpr (model != Model::ARMv2with32bitAddressing) { | ||||
| 					// "An address offset from a word boundary will cause the data to be rotated into the | ||||
| 					// register so that the addressed byte occuplies bits 0 to 7." | ||||
| 					// | ||||
| 					// (though the test set that inspired 'ARMv2with32bitAddressing' appears not to honour this; | ||||
| 					// test below assumes it went away by the version of ARM that set supports) | ||||
| 					switch(address & 3) { | ||||
| 						case 0:	break; | ||||
| 						case 1:	value = (value >> 8) | (value << 24);	break; | ||||
| 						case 2:	value = (value >> 16) | (value << 16);	break; | ||||
| 						case 3:	value = (value >> 24) | (value << 8);	break; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if(!did_read) { | ||||
| 				exception<Registers::Exception::DataAbort>(); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			if(transfer.destination() == 15) { | ||||
| 				set_pc<true>(value); | ||||
| 			} else { | ||||
| 				registers_[transfer.destination()] = value; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if constexpr (should_write_back) { | ||||
| 			// Empirically: I think order of operations for a load is: (i) write back; (ii) store value from bus. | ||||
| 			// So if this is a load, don't allow write back to overwrite what was loaded. | ||||
| 			if(flags.operation() == SingleDataTransferFlags::Operation::STR || transfer.base() != transfer.destination()) { | ||||
| 				if(transfer.base() == 15) { | ||||
| 					set_pc<true>(offsetted_address); | ||||
| 				} else { | ||||
| 					registers_[transfer.base()] = offsetted_address; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	template <Flags f> void perform(BlockDataTransfer transfer) { | ||||
| 		constexpr BlockDataTransferFlags flags(f); | ||||
| 		constexpr bool is_ldm = flags.operation() == BlockDataTransferFlags::Operation::LDM; | ||||
|  | ||||
| 		// Ensure that *base points to the base register if it can be written back; | ||||
| 		// also set address to the base. | ||||
| 		uint32_t *base = nullptr; | ||||
| 		uint32_t address; | ||||
| 		if(transfer.base() == 15) { | ||||
| 			address = registers_.pc_status(4); | ||||
| 		} else { | ||||
| 			base = ®isters_[transfer.base()]; | ||||
| 			address = *base; | ||||
| 		} | ||||
|  | ||||
| 		// For an LDM pc_proxy will receive any read R15 value; | ||||
| 		// for an STM it'll hold the value to be written. | ||||
| 		uint32_t pc_proxy = 0; | ||||
|  | ||||
| 		// Read the base address and take a copy in case a data abort means that | ||||
| 		// it has to be restored later. | ||||
| 		uint32_t initial_address = address; | ||||
|  | ||||
| 		// Grab the register list and decide whether user registers are being used. | ||||
| 		const uint16_t list = transfer.register_list(); | ||||
| 		const bool adopt_user_mode = flags.load_psr() && (!is_ldm || !(list & (1 << 15))); | ||||
|  | ||||
| 		// Write back will prima facie occur if: | ||||
| 		//	(i) the instruction asks for it; and | ||||
| 		//	(ii) the write-back register isn't R15. | ||||
| 		bool write_back = base && flags.write_back_address(); | ||||
|  | ||||
| 		// Collate a transfer list; this is a very long-winded way of implementing STM | ||||
| 		// and LDM but right now the objective is merely correctness. | ||||
| 		// | ||||
| 		// If this is LDM and it turns out that base is also in the transfer list, | ||||
| 		// disable write back. | ||||
| 		uint32_t *transfer_sources[16]; | ||||
| 		uint32_t total = 0; | ||||
| 		for(uint32_t c = 0; c < 15; c++) { | ||||
| 			if(list & (1 << c)) { | ||||
| 				uint32_t *const next = ®isters_.reg(adopt_user_mode, c); | ||||
| 				if(is_ldm && next == base) write_back = false; | ||||
| 				transfer_sources[total++] = next; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// If the last thing in the list is R15, redirect it to the PC proxy, | ||||
| 		// possibly populating with a meaningful value. | ||||
| 		if(list & (1 << 15)) { | ||||
| 			if(!is_ldm) { | ||||
| 				pc_proxy = registers_.pc_status(8); | ||||
| 			} | ||||
| 			transfer_sources[total++] = &pc_proxy; | ||||
| 		} | ||||
|  | ||||
| 		// If this is STM and the first thing in the list is the same as base, | ||||
| 		// point it at initial_address instead. | ||||
| 		if(!is_ldm && total && transfer_sources[0] == base) { | ||||
| 			transfer_sources[0] = &initial_address; | ||||
| 		} | ||||
|  | ||||
| 		// Calculate final_address, which is what will be written back if required; | ||||
| 		// update address to point to the low end of the transfer block. | ||||
| 		// | ||||
| 		// Writes are always ordered from lowest address to highest; adjust the | ||||
| 		// start address if this write is supposed to fill memory downward from | ||||
| 		// the base. | ||||
| 		uint32_t final_address; | ||||
| 		if constexpr (!flags.add_offset()) { | ||||
| 			// Decrementing mode; final_address is the value the base register should | ||||
| 			// have after this operation if writeback is enabled, so it's below | ||||
| 			// the original address. But also writes always occur from lowest address | ||||
| 			// to highest, so push the current address to the bottom. | ||||
| 			final_address = address - total * 4; | ||||
| 			address = final_address; | ||||
| 		} else { | ||||
| 			final_address = address + total * 4; | ||||
| 		} | ||||
|  | ||||
| 		// Write back if enabled. | ||||
| 		if(write_back) { | ||||
| 			*base = final_address; | ||||
| 		} | ||||
|  | ||||
| 		// Update address in advance for: | ||||
| 		//	* pre-indexed upward stores; and | ||||
| 		//	* post-indxed downward stores. | ||||
| 		if constexpr (flags.pre_index() == flags.add_offset()) { | ||||
| 			address += 4; | ||||
| 		} | ||||
|  | ||||
| 		// Perform all memory accesses, tracking whether either kind of abort will be | ||||
| 		// required. | ||||
| 		const bool trans = registers_.mode() == Mode::User; | ||||
| 		const bool address_error = is_invalid_address(address); | ||||
| 		bool accesses_succeeded = true; | ||||
|  | ||||
| 		if constexpr (is_ldm) { | ||||
| 			// Keep a record of the value replaced by the last load and | ||||
| 			// where it came from. A data abort cancels both the current load and | ||||
| 			// the one before it, so this might be used by this implementation to | ||||
| 			// undo the previous load. | ||||
| 			struct { | ||||
| 				uint32_t *target = nullptr; | ||||
| 				uint32_t value = 0; | ||||
| 			} last_replacement; | ||||
|  | ||||
| 			for(uint32_t c = 0; c < total; c++) { | ||||
| 				uint32_t &value = *transfer_sources[c]; | ||||
|  | ||||
| 				// When ARM detects a data abort during a load multiple instruction, it modifies the operation of | ||||
| 				// the instruction to ensure that recovery is possible. | ||||
| 				// | ||||
| 				//	*	Overwriting of registers stops when the abort happens. The aborting load will not | ||||
| 				//		take place, nor will the preceding one ... | ||||
| 				//	*	The base register is restored, to its modified value if write-back was requested. | ||||
| 				if(accesses_succeeded) { | ||||
| 					const uint32_t replaced = value; | ||||
| 					accesses_succeeded &= bus.template read<uint32_t>(address, value, registers_.mode(), trans); | ||||
|  | ||||
| 					// Update the last-modified slot if the access succeeded; otherwise | ||||
| 					// undo the last modification if there was one, and undo the base | ||||
| 					// address change. | ||||
| 					if(accesses_succeeded) { | ||||
| 						last_replacement.value = replaced; | ||||
| 						last_replacement.target = transfer_sources[c]; | ||||
| 					} else { | ||||
| 						if(last_replacement.target) { | ||||
| 							*last_replacement.target = last_replacement.value; | ||||
| 						} | ||||
|  | ||||
| 						// Also restore the base register, including to its original value | ||||
| 						// if write back was disabled. | ||||
| 						if(base) { | ||||
| 							if(write_back) { | ||||
| 								*base = final_address; | ||||
| 							} else { | ||||
| 								*base = initial_address; | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				} else { | ||||
| 					// Implicitly: do the access anyway, but don't store the value. I think. | ||||
| 					uint32_t throwaway; | ||||
| 					bus.template read<uint32_t>(address, throwaway, registers_.mode(), trans); | ||||
| 				} | ||||
|  | ||||
| 				// Advance. | ||||
| 				address += 4; | ||||
| 			} | ||||
| 		} else { | ||||
| 			for(uint32_t c = 0; c < total; c++) { | ||||
| 				uint32_t &value = *transfer_sources[c]; | ||||
|  | ||||
| 				if(!address_error) { | ||||
| 					// "If the abort occurs during a store multiple instruction, ARM takes little action until | ||||
| 					// the instruction completes, whereupon it enters the data abort trap. The memory manager is | ||||
| 					// responsible for preventing erroneous writes to the memory." | ||||
| 					accesses_succeeded &= bus.template write<uint32_t>(address, value, registers_.mode(), trans); | ||||
| 				} else { | ||||
| 					// Do a throwaway read. | ||||
| 					uint32_t throwaway; | ||||
| 					bus.template read<uint32_t>(address, throwaway, registers_.mode(), trans); | ||||
| 				} | ||||
|  | ||||
| 				// Advance. | ||||
| 				address += 4; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Finally throw an exception if necessary. | ||||
| 		if(address_error) { | ||||
| 			exception<Registers::Exception::Address>(); | ||||
| 		} else if(!accesses_succeeded) { | ||||
| 			exception<Registers::Exception::DataAbort>(); | ||||
| 		} else { | ||||
| 			// If this was an LDM to R15 then apply it appropriately. | ||||
| 			if(is_ldm && list & (1 << 15)) { | ||||
| 				set_pc<true>(pc_proxy); | ||||
| 				if constexpr (flags.load_psr()) { | ||||
| 					set_status(pc_proxy); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	void software_interrupt(SoftwareInterrupt swi) { | ||||
| 		if(control_flow_handler_.should_swi(swi.comment())) { | ||||
| 			exception<Registers::Exception::SoftwareInterrupt>(); | ||||
| 		} | ||||
| 	} | ||||
| 	void unknown() { | ||||
| 		exception<Registers::Exception::UndefinedInstruction>(); | ||||
| 	} | ||||
|  | ||||
| 	// Act as if no coprocessors present. | ||||
| 	template <Flags> void perform(CoprocessorRegisterTransfer) { | ||||
| 		exception<Registers::Exception::UndefinedInstruction>(); | ||||
| 	} | ||||
| 	template <Flags> void perform(CoprocessorDataOperation) { | ||||
| 		exception<Registers::Exception::UndefinedInstruction>(); | ||||
| 	} | ||||
| 	template <Flags> void perform(CoprocessorDataTransfer) { | ||||
| 		exception<Registers::Exception::UndefinedInstruction>(); | ||||
| 	} | ||||
|  | ||||
| 	/// @returns The current registers state. | ||||
| 	const Registers ®isters() const { | ||||
| 		return registers_; | ||||
| 	} | ||||
|  | ||||
| 	// Included primarily for testing; my full opinion on this is still | ||||
| 	// incompletely-formed. | ||||
| 	Registers ®isters() { | ||||
| 		return registers_; | ||||
| 	} | ||||
|  | ||||
| 	/// Indicates a prefetch abort exception. | ||||
| 	void prefetch_abort() { | ||||
| 		exception<Registers::Exception::PrefetchAbort>(); | ||||
| 	} | ||||
|  | ||||
| 	/// Sets the expected address of the instruction after whichever  is about to be executed. | ||||
| 	/// So it's PC+4 compared to most other systems. | ||||
| 	/// | ||||
| 	/// By default this is not forwarded to the control-flow handler. | ||||
| 	template <bool notify = false> | ||||
| 	void set_pc(uint32_t pc) { | ||||
| 		registers_.set_pc(pc); | ||||
| 		if constexpr (notify) { | ||||
| 			control_flow_handler_.did_set_pc(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/// @returns The address of the instruction that should be fetched next. So as execution of each instruction | ||||
| 	/// begins, this will be +4 from the instruction being executed; at the end of the instruction it'll either still be +4 | ||||
| 	/// or else be some other address if a branch or exception has occurred. | ||||
| 	uint32_t pc() const { | ||||
| 		return registers_.pc(0); | ||||
| 	} | ||||
|  | ||||
| 	MemoryT bus; | ||||
|  | ||||
| private: | ||||
| 	template <Registers::Exception type> | ||||
| 	void exception() { | ||||
| 		registers_.exception<type>(); | ||||
| 		control_flow_handler_.did_set_pc(); | ||||
| 	} | ||||
|  | ||||
| 	void set_status(uint32_t status) { | ||||
| 		registers_.set_status(status); | ||||
| 		control_flow_handler_.did_set_status(); | ||||
| 	} | ||||
|  | ||||
| 	using ControlFlowHandlerTStorage = | ||||
| 		typename std::conditional< | ||||
| 			std::is_same_v<ControlFlowHandlerT, NullControlFlowHandler>, | ||||
| 			ControlFlowHandlerT, | ||||
| 			ControlFlowHandlerT &>::type; | ||||
| 	ControlFlowHandlerTStorage control_flow_handler_; | ||||
| 	Registers registers_; | ||||
|  | ||||
| 	static bool is_invalid_address(uint32_t address) { | ||||
| 		if constexpr (model == Model::ARMv2with32bitAddressing) { | ||||
| 			return false; | ||||
| 		} | ||||
| 		return address >= 1 << 26; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| /// Executes the instruction @c instruction which should have been fetched from @c executor.pc(), | ||||
| /// modifying @c executor. | ||||
| template <Model model, typename MemoryT, typename StatusObserverT> | ||||
| void execute(uint32_t instruction, Executor<model, MemoryT, StatusObserverT> &executor) { | ||||
| 	executor.set_pc(executor.pc() + 4); | ||||
| 	dispatch<model>(instruction, executor); | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										553
									
								
								InstructionSets/ARM/OperationMapper.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										553
									
								
								InstructionSets/ARM/OperationMapper.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,553 @@ | ||||
| // | ||||
| //  OperationMapper.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 16/02/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../Reflection/Dispatcher.hpp" | ||||
| #include "BarrelShifter.hpp" | ||||
|  | ||||
| namespace InstructionSet::ARM { | ||||
|  | ||||
| enum class Model { | ||||
| 	ARMv2, | ||||
|  | ||||
| 	/// Like an ARMv2 but all non-PC addressing is 64-bit. Primarily useful for a particular set of test | ||||
| 	/// cases that I want to apply retroactively; not a real iteration. | ||||
| 	ARMv2with32bitAddressing, | ||||
| }; | ||||
|  | ||||
| enum class Condition { | ||||
| 	EQ,	NE,	CS,	CC, | ||||
| 	MI,	PL,	VS,	VC, | ||||
| 	HI,	LS,	GE,	LT, | ||||
| 	GT,	LE,	AL,	NV, | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Implementation details. | ||||
| // | ||||
| static constexpr int FlagsStartBit = 20; | ||||
| using Flags = uint8_t; | ||||
|  | ||||
| template <int position> | ||||
| constexpr bool flag_bit(uint8_t flags) { | ||||
| 	static_assert(position >= 20 && position < 28); | ||||
| 	return flags & (1 << (position - FlagsStartBit)); | ||||
| } | ||||
|  | ||||
| // | ||||
| // Methods common to data processing and data transfer. | ||||
| // | ||||
| struct WithShiftControlBits { | ||||
| 	constexpr WithShiftControlBits(uint32_t opcode) noexcept : opcode_(opcode) {} | ||||
|  | ||||
| 	/// The operand 2 register index if @c operand2_is_immediate() is @c false; meaningless otherwise. | ||||
| 	uint32_t operand2() const				{	return opcode_ & 0xf;					} | ||||
| 	/// The type of shift to apply to operand 2 if @c operand2_is_immediate() is @c false; meaningless otherwise. | ||||
| 	ShiftType shift_type() const			{	return ShiftType((opcode_ >> 5) & 3);	} | ||||
| 	/// @returns @c true if the amount to shift by should be taken from a register; @c false if it is an immediate value. | ||||
| 	bool shift_count_is_register() const	{	return opcode_ & (1 << 4);				} | ||||
| 	/// The shift amount register index if @c shift_count_is_register() is @c true; meaningless otherwise. | ||||
| 	uint32_t shift_register() const			{	return (opcode_ >> 8) & 0xf;			} | ||||
| 	/// The amount to shift by if @c shift_count_is_register() is @c false; meaningless otherwise. | ||||
| 	uint32_t shift_amount() const			{	return (opcode_ >> 7) & 0x1f;			} | ||||
|  | ||||
| protected: | ||||
| 	uint32_t opcode_; | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Branch (i.e. B and BL). | ||||
| // | ||||
| struct BranchFlags { | ||||
| 	constexpr BranchFlags(uint8_t flags) noexcept : flags_(flags) {} | ||||
|  | ||||
| 	enum class Operation { | ||||
| 		B,		/// Add offset to PC; programmer allows for PC being two words ahead. | ||||
| 		BL,		/// Copy PC and PSR to R14, then branch. Copied PC points to next instruction. | ||||
| 	}; | ||||
|  | ||||
| 	/// @returns The operation to apply. | ||||
| 	constexpr Operation operation() const { | ||||
| 		return flag_bit<24>(flags_) ? Operation::BL : Operation::B; | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	uint8_t flags_; | ||||
| }; | ||||
|  | ||||
| struct Branch { | ||||
| 	constexpr Branch(uint32_t opcode) noexcept : opcode_(opcode) {} | ||||
|  | ||||
| 	/// The 26-bit offset to add to the PC. | ||||
| 	uint32_t offset() const				{	return (opcode_ & 0xff'ffff) << 2;	} | ||||
|  | ||||
| private: | ||||
| 	uint32_t opcode_; | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Data processing (i.e. AND to MVN). | ||||
| // | ||||
| enum class DataProcessingOperation { | ||||
| 	AND,	/// Rd = Op1 AND Op2. | ||||
| 	EOR,	/// Rd = Op1 EOR Op2. | ||||
| 	SUB,	/// Rd = Op1 - Op2. | ||||
| 	RSB,	/// Rd = Op2 - Op1. | ||||
| 	ADD,	/// Rd = Op1 + Op2. | ||||
| 	ADC,	/// Rd = Op1 + Ord2 + C. | ||||
| 	SBC,	/// Rd = Op1 - Op2 + C. | ||||
| 	RSC,	/// Rd = Op2 - Op1 + C. | ||||
| 	TST,	/// Set condition codes on Op1 AND Op2. | ||||
| 	TEQ,	/// Set condition codes on Op1 EOR Op2. | ||||
| 	CMP,	/// Set condition codes on Op1 - Op2. | ||||
| 	CMN,	/// Set condition codes on Op1 + Op2. | ||||
| 	ORR,	/// Rd = Op1 OR Op2. | ||||
| 	MOV,	/// Rd = Op2 | ||||
| 	BIC,	/// Rd = Op1 AND NOT Op2. | ||||
| 	MVN,	/// Rd = NOT Op2. | ||||
| }; | ||||
|  | ||||
| constexpr bool is_logical(DataProcessingOperation operation) { | ||||
| 	switch(operation) { | ||||
| 		case DataProcessingOperation::AND: | ||||
| 		case DataProcessingOperation::EOR: | ||||
| 		case DataProcessingOperation::TST: | ||||
| 		case DataProcessingOperation::TEQ: | ||||
| 		case DataProcessingOperation::ORR: | ||||
| 		case DataProcessingOperation::MOV: | ||||
| 		case DataProcessingOperation::BIC: | ||||
| 		case DataProcessingOperation::MVN: | ||||
| 			return true; | ||||
|  | ||||
| 		default: return false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| constexpr bool is_comparison(DataProcessingOperation operation) { | ||||
| 	switch(operation) { | ||||
| 		case DataProcessingOperation::TST: | ||||
| 		case DataProcessingOperation::TEQ: | ||||
| 		case DataProcessingOperation::CMP: | ||||
| 		case DataProcessingOperation::CMN: | ||||
| 			return true; | ||||
|  | ||||
| 		default: return false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| struct DataProcessingFlags { | ||||
| 	constexpr DataProcessingFlags(uint8_t flags) noexcept : flags_(flags) {} | ||||
|  | ||||
| 	/// @returns The operation to apply. | ||||
| 	constexpr DataProcessingOperation operation() const { | ||||
| 		return DataProcessingOperation((flags_ >> (21 - FlagsStartBit)) & 0xf); | ||||
| 	} | ||||
|  | ||||
| 	/// @returns @c true if operand 2 is defined by the @c rotate() and @c immediate() fields; | ||||
| 	///		@c false if it is defined by the @c shift_*() and @c operand2() fields. | ||||
| 	constexpr bool operand2_is_immediate() const	{	return flag_bit<25>(flags_);	} | ||||
|  | ||||
| 	/// @c true if the status register should be updated; @c false otherwise. | ||||
| 	constexpr bool set_condition_codes() const		{	return flag_bit<20>(flags_);	} | ||||
|  | ||||
| private: | ||||
| 	uint8_t flags_; | ||||
| }; | ||||
|  | ||||
| struct DataProcessing: public WithShiftControlBits { | ||||
| 	using WithShiftControlBits::WithShiftControlBits; | ||||
|  | ||||
| 	/// The destination register index. i.e. Rd. | ||||
| 	uint32_t destination() const		{	return (opcode_ >> 12) & 0xf;	} | ||||
|  | ||||
| 	/// The operand 1 register index. i.e. Rn. | ||||
| 	uint32_t operand1() const			{	return (opcode_ >> 16) & 0xf;	} | ||||
|  | ||||
| 	// | ||||
| 	// Immediate values for operand 2. | ||||
| 	// | ||||
|  | ||||
| 	/// An 8-bit value to rotate right @c rotate() places if @c operand2_is_immediate() is @c true; meaningless otherwise. | ||||
| 	uint32_t immediate() const			{	return opcode_ & 0xff;			} | ||||
| 	/// The number of bits to rotate @c immediate()  by to the right if @c operand2_is_immediate() is @c true; meaningless otherwise. | ||||
| 	uint32_t rotate() const				{	return (opcode_ >> 7) & 0x1e;	} | ||||
| }; | ||||
|  | ||||
| // | ||||
| // MUL and MLA. | ||||
| // | ||||
| struct MultiplyFlags { | ||||
| 	constexpr MultiplyFlags(uint8_t flags) noexcept : flags_(flags) {} | ||||
|  | ||||
| 	/// @c true if the status register should be updated; @c false otherwise. | ||||
| 	constexpr bool set_condition_codes() const	{	return flag_bit<20>(flags_);	} | ||||
|  | ||||
| 	enum class Operation { | ||||
| 		MUL,	/// Rd = Rm * Rs | ||||
| 		MLA,	/// Rd = Rm * Rs + Rn | ||||
| 	}; | ||||
|  | ||||
| 	/// @returns The operation to apply. | ||||
| 	constexpr Operation operation() const { | ||||
| 		return flag_bit<21>(flags_) ? Operation::MLA : Operation::MUL; | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	uint8_t flags_; | ||||
| }; | ||||
|  | ||||
| struct Multiply { | ||||
| 	constexpr Multiply(uint32_t opcode) noexcept : opcode_(opcode) {} | ||||
|  | ||||
| 	/// The destination register index. i.e. 'Rd'. | ||||
| 	uint32_t destination() const	{	return (opcode_ >> 16) & 0xf;	} | ||||
|  | ||||
| 	/// The accumulator register index for multiply-add. i.e. 'Rn'. | ||||
| 	uint32_t accumulator() const	{	return (opcode_ >> 12) & 0xf;	} | ||||
|  | ||||
| 	/// The multiplicand register index. i.e. 'Rs'. | ||||
| 	uint32_t multiplicand() const	{	return (opcode_ >> 8) & 0xf;	} | ||||
|  | ||||
| 	/// The multiplier register index. i.e. 'Rm'. | ||||
| 	uint32_t multiplier() const		{	return opcode_ & 0xf;			} | ||||
|  | ||||
| private: | ||||
| 	uint32_t opcode_; | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Single data transfer (LDR, STR). | ||||
| // | ||||
| struct SingleDataTransferFlags { | ||||
| 	constexpr SingleDataTransferFlags(uint8_t flags) noexcept : flags_(flags) {} | ||||
|  | ||||
| 	enum class Operation { | ||||
| 		LDR,	/// Read single byte or word from [base + offset], possibly mutating the base. | ||||
| 		STR,	/// Write a single byte or word to [base + offset], possibly mutating the base. | ||||
| 	}; | ||||
|  | ||||
| 	constexpr Operation operation() const { | ||||
| 		return flag_bit<20>(flags_) ? Operation::LDR : Operation::STR; | ||||
| 	} | ||||
|  | ||||
| 	constexpr bool offset_is_register() const	{	return flag_bit<25>(flags_);	} | ||||
| 	constexpr bool pre_index() const			{	return flag_bit<24>(flags_);	} | ||||
| 	constexpr bool add_offset() const			{	return flag_bit<23>(flags_);	} | ||||
| 	constexpr bool transfer_byte() const		{	return flag_bit<22>(flags_);	} | ||||
| 	constexpr bool write_back_address() const	{	return flag_bit<21>(flags_);	} | ||||
|  | ||||
| private: | ||||
| 	uint8_t flags_; | ||||
| }; | ||||
|  | ||||
| struct SingleDataTransfer: public WithShiftControlBits { | ||||
| 	using WithShiftControlBits::WithShiftControlBits; | ||||
|  | ||||
| 	/// The destination register index. i.e. 'Rd' for LDR. | ||||
| 	uint32_t destination() const		{	return (opcode_ >> 12) & 0xf;	} | ||||
|  | ||||
| 	/// The destination register index. i.e. 'Rd' for STR. | ||||
| 	uint32_t source() const				{	return (opcode_ >> 12) & 0xf;	} | ||||
|  | ||||
| 	/// The base register index. i.e. 'Rn'. | ||||
| 	uint32_t base() const				{	return (opcode_ >> 16) & 0xf;	} | ||||
|  | ||||
| 	/// The immediate offset, if @c offset_is_register() was @c false; meaningless otherwise. | ||||
| 	uint32_t immediate() const			{	return opcode_ & 0xfff;			} | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Block data transfer (LDR, STR). | ||||
| // | ||||
| struct BlockDataTransferFlags { | ||||
| 	constexpr BlockDataTransferFlags(uint8_t flags) noexcept : flags_(flags) {} | ||||
|  | ||||
| 	enum class Operation { | ||||
| 		LDM,	/// Read 1–16 words from [base], possibly mutating it. | ||||
| 		STM,	/// Write 1-16 words to [base], possibly mutating it. | ||||
| 	}; | ||||
|  | ||||
| 	constexpr Operation operation() const { | ||||
| 		return flag_bit<20>(flags_) ? Operation::LDM : Operation::STM; | ||||
| 	} | ||||
|  | ||||
| 	constexpr bool pre_index() const			{	return flag_bit<24>(flags_);	} | ||||
| 	constexpr bool add_offset() const			{	return flag_bit<23>(flags_);	} | ||||
| 	constexpr bool load_psr() const				{	return flag_bit<22>(flags_);	} | ||||
| 	constexpr bool write_back_address() const	{	return flag_bit<21>(flags_);	} | ||||
|  | ||||
| private: | ||||
| 	uint8_t flags_; | ||||
| }; | ||||
|  | ||||
| struct BlockDataTransfer: public WithShiftControlBits { | ||||
| 	using WithShiftControlBits::WithShiftControlBits; | ||||
|  | ||||
| 	/// The base register index. i.e. 'Rn'. | ||||
| 	uint32_t base() const				{	return (opcode_ >> 16) & 0xf;			} | ||||
|  | ||||
| 	/// A bitfield indicating which registers to load or store. | ||||
| 	uint16_t register_list() const		{	return static_cast<uint16_t>(opcode_);	} | ||||
| 	uint32_t popcount() const { | ||||
| 		const uint16_t list = register_list(); | ||||
|  | ||||
| 		// TODO: just use std::popcount when adopting C++20. | ||||
| 		uint32_t total = ((list & 0xaaaa) >> 1) + (list & 0x5555); | ||||
| 		total = ((total & 0xcccc) >> 2) + (total & 0x3333); | ||||
| 		total = ((total & 0xf0f0) >> 4) + (total & 0x0f0f); | ||||
| 		total = ((total & 0xff00) >> 8) + (total & 0x00ff); | ||||
|  | ||||
| 		return total; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Coprocessor data operation. | ||||
| // | ||||
| struct CoprocessorDataOperationFlags { | ||||
| 	constexpr CoprocessorDataOperationFlags(uint8_t flags) noexcept : flags_(flags) {} | ||||
|  | ||||
| 	constexpr int coprocessor_operation() const		{	return (flags_ >> (FlagsStartBit - 20)) & 0xf;	} | ||||
|  | ||||
| private: | ||||
| 	uint8_t flags_; | ||||
| }; | ||||
|  | ||||
| struct CoprocessorDataOperation { | ||||
| 	constexpr CoprocessorDataOperation(uint32_t opcode) noexcept : opcode_(opcode) {} | ||||
|  | ||||
| 	uint32_t operand1() const		{ return (opcode_ >> 16) & 0xf;	} | ||||
| 	uint32_t operand2() const		{ return opcode_ & 0xf; 		} | ||||
| 	uint32_t destination() const	{ return (opcode_ >> 12) & 0xf;	} | ||||
| 	uint32_t coprocessor() const	{ return (opcode_ >> 8) & 0xf;	} | ||||
| 	uint32_t information() const	{ return (opcode_ >> 5) & 0x7;	} | ||||
|  | ||||
| private: | ||||
| 	uint32_t opcode_; | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Coprocessor register transfer. | ||||
| // | ||||
| struct CoprocessorRegisterTransferFlags { | ||||
| 	constexpr CoprocessorRegisterTransferFlags(uint8_t flags) noexcept : flags_(flags) {} | ||||
|  | ||||
| 	enum class Operation { | ||||
| 		MRC,	///	Move from coprocessor register to ARM register. | ||||
| 		MCR,	/// Move from ARM register to coprocessor register. | ||||
| 	}; | ||||
|  | ||||
| 	constexpr Operation operation() const { | ||||
| 		return flag_bit<20>(flags_) ? Operation::MRC : Operation::MCR; | ||||
| 	} | ||||
| 	constexpr int coprocessor_operation() const		{	return (flags_ >> (FlagsStartBit - 20)) & 0x7;	} | ||||
|  | ||||
| private: | ||||
| 	uint8_t flags_; | ||||
| }; | ||||
|  | ||||
| struct CoprocessorRegisterTransfer { | ||||
| 	constexpr CoprocessorRegisterTransfer(uint32_t opcode) noexcept : opcode_(opcode) {} | ||||
|  | ||||
| 	uint32_t operand1() const		{ return (opcode_ >> 16) & 0xf;	} | ||||
| 	uint32_t operand2() const		{ return opcode_ & 0xf; 		} | ||||
| 	uint32_t destination() const	{ return (opcode_ >> 12) & 0xf;	} | ||||
| 	uint32_t coprocessor() const	{ return (opcode_ >> 8) & 0xf;	} | ||||
| 	uint32_t information() const	{ return (opcode_ >> 5) & 0x7;	} | ||||
|  | ||||
| private: | ||||
| 	uint32_t opcode_; | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Coprocessor data transfer. | ||||
| // | ||||
| struct CoprocessorDataTransferFlags { | ||||
| 	constexpr CoprocessorDataTransferFlags(uint8_t flags) noexcept : flags_(flags) {} | ||||
|  | ||||
| 	enum class Operation { | ||||
| 		LDC,	/// Coprocessor data transfer load. | ||||
| 		STC,	/// Coprocessor data transfer store. | ||||
| 	}; | ||||
|  | ||||
| 	constexpr Operation operation() const { | ||||
| 		return flag_bit<20>(flags_) ? Operation::LDC : Operation::STC; | ||||
| 	} | ||||
| 	constexpr bool pre_index() const			{	return flag_bit<24>(flags_);	} | ||||
| 	constexpr bool add_offset() const			{	return flag_bit<23>(flags_);	} | ||||
| 	constexpr bool transfer_length() const		{	return flag_bit<22>(flags_);	} | ||||
| 	constexpr bool write_back_address() const	{	return flag_bit<21>(flags_);	} | ||||
|  | ||||
| private: | ||||
| 	uint8_t flags_; | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Software interrupt. | ||||
| // | ||||
| struct SoftwareInterrupt { | ||||
| 	constexpr SoftwareInterrupt(uint32_t opcode) noexcept : opcode_(opcode) {} | ||||
|  | ||||
| 	/// The 24-bit comment field, often decoded by the receiver of an SWI. | ||||
| 	uint32_t comment() const				{	return opcode_ & 0xff'ffff;	} | ||||
|  | ||||
| private: | ||||
| 	uint32_t opcode_; | ||||
| }; | ||||
|  | ||||
| struct CoprocessorDataTransfer { | ||||
| 	constexpr CoprocessorDataTransfer(uint32_t opcode) noexcept : opcode_(opcode) {} | ||||
|  | ||||
| 	int base() const		{ return (opcode_ >> 16) & 0xf;	} | ||||
|  | ||||
| 	int source() const		{ return (opcode_ >> 12) & 0xf; } | ||||
| 	int destination() const	{ return (opcode_ >> 12) & 0xf;	} | ||||
|  | ||||
| 	int coprocessor() const	{ return (opcode_ >> 8) & 0xf;	} | ||||
| 	int offset() const		{ return opcode_ & 0xff;		} | ||||
|  | ||||
| private: | ||||
| 	uint32_t opcode_; | ||||
| }; | ||||
|  | ||||
| /// Operation mapper; use the free function @c dispatch as defined below. | ||||
| template <Model> | ||||
| struct OperationMapper { | ||||
| 	static Condition condition(uint32_t instruction) { | ||||
| 		return Condition(instruction >> 28); | ||||
| 	} | ||||
|  | ||||
| 	template <int i, typename SchedulerT> | ||||
| 	static void dispatch(uint32_t instruction, SchedulerT &scheduler) { | ||||
| 		// Put the 8-bit segment of instruction back into its proper place; | ||||
| 		// this allows all the tests below to be written so as to coordinate | ||||
| 		// properly with the data sheet, and since it's all compile-time work | ||||
| 		// it doesn't cost anything. | ||||
| 		constexpr auto partial = uint32_t(i << 20); | ||||
|  | ||||
| 		// Cf. the ARM2 datasheet, p.45. Tests below match its ordering | ||||
| 		// other than that 'undefined' is the fallthrough case. More specific | ||||
| 		// page references are provided were more detailed versions of the | ||||
| 		// decoding are depicted. | ||||
|  | ||||
| 		// Multiply and multiply-accumulate (MUL, MLA); cf. p.23. | ||||
| 		// | ||||
| 		// This usurps a potential data processing decoding, so needs priority. | ||||
| 		if constexpr (((partial >> 22) & 0b111'111) == 0b000'000) { | ||||
| 			// This implementation provides only eight bits baked into the template parameters so | ||||
| 			// an additional dynamic test is required to check whether this is really, really MUL or MLA. | ||||
| 			if((instruction & 0b1111'0000) == 0b1001'0000) { | ||||
| 				scheduler.template perform<i>(Multiply(instruction)); | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Data processing; cf. p.17. | ||||
| 		if constexpr (((partial >> 26) & 0b11) == 0b00) { | ||||
| 			scheduler.template perform<i>(DataProcessing(instruction)); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Single data transfer (LDR, STR); cf. p.25. | ||||
| 		if constexpr (((partial >> 26) & 0b11) == 0b01) { | ||||
| 			scheduler.template perform<i>(SingleDataTransfer(instruction)); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Block data transfer (LDM, STM); cf. p.29. | ||||
| 		if constexpr (((partial >> 25) & 0b111) == 0b100) { | ||||
| 			scheduler.template perform<i>(BlockDataTransfer(instruction)); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Branch and branch with link (B, BL); cf. p.15. | ||||
| 		if constexpr (((partial >> 25) & 0b111) == 0b101) { | ||||
| 			scheduler.template perform<i>(Branch(instruction)); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Software interreupt; cf. p.35. | ||||
| 		if constexpr (((partial >> 24) & 0b1111) == 0b1111) { | ||||
| 			scheduler.software_interrupt(SoftwareInterrupt(instruction)); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Both: | ||||
| 		// Coprocessor data operation; cf. p. 37; and | ||||
| 		// Coprocessor register transfers; cf. p. 42. | ||||
| 		if constexpr (((partial >> 24) & 0b1111) == 0b1110) { | ||||
| 			if(instruction & (1 << 4)) { | ||||
| 				// Register transfer. | ||||
| 				scheduler.template perform<i>(CoprocessorRegisterTransfer(instruction)); | ||||
| 			} else { | ||||
| 				// Data operation. | ||||
| 				scheduler.template perform<i>(CoprocessorDataOperation(instruction)); | ||||
| 			} | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Coprocessor data transfers; cf. p.39. | ||||
| 		if constexpr (((partial >> 25) & 0b111) == 0b110) { | ||||
| 			scheduler.template perform<i>(CoprocessorDataTransfer(instruction)); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Fallback position. | ||||
| 		scheduler.unknown(); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| /// A brief documentation of the interface expected by @c dispatch below; will be a concept if/when this project adopts C++20. | ||||
| struct SampleScheduler { | ||||
| 	/// @returns @c true if the rest of the instruction should be decoded and supplied | ||||
| 	/// to the scheduler as defined below; @c false otherwise. | ||||
| 	bool should_schedule(Condition condition); | ||||
|  | ||||
| 	// Template argument: | ||||
| 	// | ||||
| 	//		Flags, an opaque type which can be converted into a DataProcessingFlags, MultiplyFlags, etc, | ||||
| 	//		by simple construction, to provide all flags that can be baked into the template parameters. | ||||
| 	// | ||||
| 	// Function argument: | ||||
| 	// | ||||
| 	//		An operation-specific encapsulation of the operation code for decoding of fields that didn't | ||||
| 	//		fit into the template parameters. | ||||
| 	// | ||||
| 	// Either or both may be omitted if unnecessary. | ||||
| 	template <Flags> void perform(DataProcessing); | ||||
| 	template <Flags> void perform(Multiply); | ||||
| 	template <Flags> void perform(SingleDataTransfer); | ||||
| 	template <Flags> void perform(BlockDataTransfer); | ||||
| 	template <Flags> void perform(Branch); | ||||
| 	template <Flags> void perform(CoprocessorRegisterTransfer); | ||||
| 	template <Flags> void perform(CoprocessorDataOperation); | ||||
| 	template <Flags> void perform(CoprocessorDataTransfer); | ||||
|  | ||||
| 	// Irregular operations. | ||||
| 	void software_interrupt(SoftwareInterrupt); | ||||
| 	void unknown(); | ||||
| }; | ||||
|  | ||||
| /// Decodes @c instruction, making an appropriate call into @c scheduler. | ||||
| /// | ||||
| /// In lieu of C++20, see the sample definition of SampleScheduler above for the expected interface. | ||||
| template <Model model, typename SchedulerT> void dispatch(uint32_t instruction, SchedulerT &scheduler) { | ||||
| 	OperationMapper<model> mapper; | ||||
|  | ||||
| 	// Test condition. | ||||
| 	const auto condition = mapper.condition(instruction); | ||||
| 	if(!scheduler.should_schedule(condition)) { | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	// Dispatch body. | ||||
| 	Reflection::dispatch(mapper, (instruction >> FlagsStartBit) & 0xff, instruction, scheduler); | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										388
									
								
								InstructionSets/ARM/Registers.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								InstructionSets/ARM/Registers.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,388 @@ | ||||
| // | ||||
| //  Status.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 25/02/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "OperationMapper.hpp" | ||||
|  | ||||
| #include <array> | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace InstructionSet::ARM { | ||||
|  | ||||
| namespace ConditionCode { | ||||
|  | ||||
| static constexpr uint32_t Negative		= 1u << 31; | ||||
| static constexpr uint32_t Zero			= 1u << 30; | ||||
| static constexpr uint32_t Carry			= 1u << 29; | ||||
| static constexpr uint32_t Overflow		= 1u << 28; | ||||
| static constexpr uint32_t IRQDisable	= 1u << 27; | ||||
| static constexpr uint32_t FIQDisable	= 1u << 26; | ||||
| static constexpr uint32_t Mode			= (1u << 1) | (1u << 0); | ||||
|  | ||||
| static constexpr uint32_t Address		= FIQDisable - Mode - 1; | ||||
|  | ||||
| } | ||||
|  | ||||
| enum class Mode { | ||||
| 	User = 0b00, | ||||
| 	FIQ = 0b01, | ||||
| 	IRQ = 0b10, | ||||
| 	Supervisor = 0b11, | ||||
| }; | ||||
|  | ||||
| /// Combines the ARM registers and status flags into a single whole, given that the architecture | ||||
| /// doesn't have the same degree of separation as others. | ||||
| /// | ||||
| /// The PC contained here is always taken to be **the address of the current instruction + 4**, | ||||
| /// i.e. whatever should be executed next, disregarding pipeline differences. | ||||
| /// | ||||
| /// Appropriate prefetch offsets are left to other code to handle. | ||||
| /// This is to try to keep this structure independent of a specific ARM implementation. | ||||
| struct Registers { | ||||
| 	public: | ||||
| 		// Don't allow copying. | ||||
| 		Registers(const Registers &) = delete; | ||||
| 		Registers &operator =(const Registers &) = delete; | ||||
| 		Registers() = default; | ||||
|  | ||||
| 		/// Sets the N and Z flags according to the value of @c result. | ||||
| 		void set_nz(uint32_t value) { | ||||
| 			zero_result_ = negative_flag_ = value; | ||||
| 		} | ||||
|  | ||||
| 		/// Sets C if @c value is non-zero; resets it otherwise. | ||||
| 		void set_c(uint32_t value) { | ||||
| 			carry_flag_ = value; | ||||
| 		} | ||||
|  | ||||
| 		/// @returns @c 1 if carry is set; @c 0 otherwise. | ||||
| 		uint32_t c() const { | ||||
| 			return carry_flag_ ? 1 : 0; | ||||
| 		} | ||||
|  | ||||
| 		/// Sets V if the highest bit of @c value is set; resets it otherwise. | ||||
| 		void set_v(uint32_t value) { | ||||
| 			overflow_flag_ = value; | ||||
| 		} | ||||
|  | ||||
| 		/// @returns The current status bits, separate from the PC — mode, NVCZ and the two interrupt flags. | ||||
| 		uint32_t status() const { | ||||
| 			return | ||||
| 				uint32_t(mode_) | | ||||
| 				(negative_flag_ & ConditionCode::Negative) | | ||||
| 				(zero_result_ ? 0 : ConditionCode::Zero) | | ||||
| 				(carry_flag_ ? ConditionCode::Carry : 0) | | ||||
| 				((overflow_flag_ >> 3) & ConditionCode::Overflow) | | ||||
| 				interrupt_flags_; | ||||
| 		} | ||||
|  | ||||
| 		/// @returns The full PC + status bits. | ||||
| 		uint32_t pc_status(uint32_t offset) const { | ||||
| 			return | ||||
| 				((active_[15] + offset) & ConditionCode::Address) | | ||||
| 				status(); | ||||
| 		} | ||||
|  | ||||
| 		/// Sets status bits only, subject to mode. | ||||
| 		void set_status(uint32_t status) { | ||||
| 			// ... in user mode the other flags (I, F, M1, M0) are protected from direct change | ||||
| 			// but in non-user modes these will also be affected, accepting copies of bits 27, 26, | ||||
| 			// 1 and 0 of the result respectively. | ||||
|  | ||||
| 			negative_flag_ = status; | ||||
| 			overflow_flag_ = status << 3; | ||||
| 			carry_flag_ = status & ConditionCode::Carry; | ||||
| 			zero_result_ = ~status & ConditionCode::Zero; | ||||
|  | ||||
| 			if(mode_ != Mode::User) { | ||||
| 				set_mode(Mode(status & 3)); | ||||
| 				interrupt_flags_ = status & (ConditionCode::IRQDisable | ConditionCode::FIQDisable); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/// @returns The current mode. | ||||
| 		Mode mode() const { | ||||
| 			return mode_; | ||||
| 		} | ||||
|  | ||||
| 		/// Sets a new PC. | ||||
| 		void set_pc(uint32_t value) { | ||||
| 			active_[15] = value & ConditionCode::Address; | ||||
| 		} | ||||
|  | ||||
| 		/// @returns The stored PC plus @c offset limited to 26 bits. | ||||
| 		uint32_t pc(uint32_t offset) const { | ||||
| 			return (active_[15] + offset) & ConditionCode::Address; | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - Exceptions. | ||||
|  | ||||
| 		enum class Exception { | ||||
| 			/// Reset line went from high to low. | ||||
| 			Reset = 0x00, | ||||
| 			/// Either an undefined instruction or a coprocessor instruction for which no coprocessor was found. | ||||
| 			UndefinedInstruction = 0x04, | ||||
| 			/// Code executed a software interrupt. | ||||
| 			SoftwareInterrupt = 0x08, | ||||
| 			/// The memory subsystem indicated an abort during prefetch and that instruction has now come to the head of the queue. | ||||
| 			PrefetchAbort = 0x0c, | ||||
| 			/// The memory subsystem indicated an abort during an instruction; if it is an LDR or STR then this should be signalled | ||||
| 			/// before any instruction execution. If it was an LDM then loading stops upon a data abort but both an LDM and STM | ||||
| 			/// otherwise complete, including pointer writeback. | ||||
| 			DataAbort = 0x10, | ||||
| 			/// The first data transfer attempted within an instruction was above address 0x3ff'ffff. | ||||
| 			Address = 0x14, | ||||
| 			/// The IRQ line was low at the end of an instruction and ConditionCode::IRQDisable was not set. | ||||
| 			IRQ = 0x18, | ||||
| 			/// The FIQ went low at least one cycle ago and ConditionCode::FIQDisable was not set. | ||||
| 			FIQ = 0x1c, | ||||
| 		}; | ||||
| 		static constexpr uint32_t pc_offset_during(Exception exception) { | ||||
| 			// The below is somewhat convoluted by the assumed execution model: | ||||
| 			// | ||||
| 			//	*	exceptions occuring during execution of an instruction are taken | ||||
| 			//		to occur after R15 has already been incremented by 4; but | ||||
| 			//	*	exceptions occurring instead of execution of an instruction are | ||||
| 			//		taken to occur with R15 pointing to an instruction that hasn't begun. | ||||
| 			// | ||||
| 			// i.e. in net R15 always refers to the next instruction | ||||
| 			// that has not yet started. | ||||
| 			switch(exception) { | ||||
| 				// "To return normally from FIQ use SUBS PC, R14_fiq, #4". | ||||
| 				case Exception::FIQ:					return 4; | ||||
|  | ||||
| 				// "To return normally from IRQ use SUBS PC, R14_irq, #4". | ||||
| 				case Exception::IRQ:					return 4; | ||||
|  | ||||
| 				// "If a return is required from [address exception traps], use | ||||
| 				// SUBS PC, R14_svc, #4. This will return to the instruction after | ||||
| 				// the one causing the trap". | ||||
| 				case Exception::Address:				return 4; | ||||
|  | ||||
| 				// "A Data Abort requires [work before a return], the return being | ||||
| 				// done by SUBS PC, R14_svc, #8" (and returning to the instruction | ||||
| 				// that aborted). | ||||
| 				case Exception::DataAbort:				return 4; | ||||
|  | ||||
| 				// "To continue after a Prefetch Abort use SUBS PC, R14_svc #4. | ||||
| 				// This will attempt to re-execute the aborting instruction." | ||||
| 				case Exception::PrefetchAbort:			return 4; | ||||
|  | ||||
| 				// "To return from a SWI, use MOVS PC, R14_svc. This returns to the instruction | ||||
| 				// following the SWI". | ||||
| 				case Exception::SoftwareInterrupt:		return 0; | ||||
|  | ||||
| 				// "To return from [an undefined instruction trap] use MOVS PC, R14_svc. | ||||
| 				// This returns to the instruction following the undefined instruction". | ||||
| 				case Exception::UndefinedInstruction:	return 0; | ||||
|  | ||||
| 				// Unspecified; a guess. | ||||
| 				case Exception::Reset:					return 0; | ||||
| 			} | ||||
| 			return 4; | ||||
| 		} | ||||
|  | ||||
| 		/// Updates the program counter, interupt flags and link register if applicable to begin @c exception. | ||||
| 		template <Exception type> | ||||
| 		void exception() { | ||||
| 			const auto r14 = pc_status(pc_offset_during(type)); | ||||
| 			switch(type) { | ||||
| 				case Exception::IRQ:	set_mode(Mode::IRQ);		break; | ||||
| 				case Exception::FIQ: 	set_mode(Mode::FIQ);		break; | ||||
| 				default:				set_mode(Mode::Supervisor);	break; | ||||
| 			} | ||||
| 			active_[14] = r14; | ||||
|  | ||||
| 			interrupt_flags_ |= ConditionCode::IRQDisable; | ||||
| 			if constexpr (type == Exception::Reset || type == Exception::FIQ) { | ||||
| 				interrupt_flags_ |= ConditionCode::FIQDisable; | ||||
| 			} | ||||
| 			set_pc(uint32_t(type)); | ||||
| 		} | ||||
|  | ||||
| 		/// Returns @c true if: (i) the exception type is IRQ or FIQ; and (ii) the processor is currently accepting such interrupts. | ||||
| 		/// Otherwise returns @c false. | ||||
| 		template <Exception type> | ||||
| 		bool would_interrupt() { | ||||
| 			switch(type) { | ||||
| 				case Exception::IRQ: | ||||
| 					if(interrupt_flags_ & ConditionCode::IRQDisable) { | ||||
| 						return false; | ||||
| 					} | ||||
| 				break; | ||||
|  | ||||
| 				case Exception::FIQ: | ||||
| 					if(interrupt_flags_ & ConditionCode::FIQDisable) { | ||||
| 						return false; | ||||
| 					} | ||||
| 				break; | ||||
|  | ||||
| 				default: return false; | ||||
| 			} | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - Condition tests. | ||||
|  | ||||
| 		/// @returns @c true if @c condition tests as true; @c false otherwise. | ||||
| 		bool test(Condition condition) const { | ||||
| 			const auto ne = [&]() -> bool { | ||||
| 				return zero_result_; | ||||
| 			}; | ||||
| 			const auto cs = [&]() -> bool { | ||||
| 				return carry_flag_; | ||||
| 			}; | ||||
| 			const auto mi = [&]() -> bool { | ||||
| 				return negative_flag_ & ConditionCode::Negative; | ||||
| 			}; | ||||
| 			const auto vs = [&]() -> bool { | ||||
| 				return overflow_flag_ & ConditionCode::Negative; | ||||
| 			}; | ||||
| 			const auto hi = [&]() -> bool { | ||||
| 				return carry_flag_ && zero_result_; | ||||
| 			}; | ||||
| 			const auto lt = [&]() -> bool { | ||||
| 				return (negative_flag_ ^ overflow_flag_) & ConditionCode::Negative; | ||||
| 			}; | ||||
| 			const auto le = [&]() -> bool { | ||||
| 				return !zero_result_ || lt(); | ||||
| 			}; | ||||
|  | ||||
| 			switch(condition) { | ||||
| 				case Condition::EQ:	return !ne(); | ||||
| 				case Condition::NE:	return ne(); | ||||
| 				case Condition::CS:	return cs(); | ||||
| 				case Condition::CC:	return !cs(); | ||||
| 				case Condition::MI:	return mi(); | ||||
| 				case Condition::PL:	return !mi(); | ||||
| 				case Condition::VS:	return vs(); | ||||
| 				case Condition::VC:	return !vs(); | ||||
|  | ||||
| 				case Condition::HI:	return hi(); | ||||
| 				case Condition::LS:	return !hi(); | ||||
| 				case Condition::GE:	return !lt(); | ||||
| 				case Condition::LT:	return lt(); | ||||
| 				case Condition::GT:	return !le(); | ||||
| 				case Condition::LE:	return le(); | ||||
|  | ||||
| 				case Condition::AL:	return true; | ||||
| 				case Condition::NV:	return false; | ||||
| 			} | ||||
|  | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		/// Sets current execution mode. | ||||
| 		void set_mode(Mode target_mode) { | ||||
| 			if(mode_ == target_mode) { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			// For outgoing modes other than FIQ, only save the final two registers for now; | ||||
| 			// if the incoming mode is FIQ then the other five will be saved in the next switch. | ||||
| 			// For FIQ, save all seven up front. | ||||
| 			switch(mode_) { | ||||
| 				// FIQ outgoing: save R8 to R14. | ||||
| 				case Mode::FIQ: | ||||
| 					std::copy(active_.begin() + 8, active_.begin() + 15, fiq_registers_.begin()); | ||||
| 				break; | ||||
|  | ||||
| 				// Non-FIQ outgoing: save R13 and R14. If saving to the user registers, | ||||
| 				// use only the final two slots. | ||||
| 				case Mode::User: | ||||
| 					std::copy(active_.begin() + 13, active_.begin() + 15, user_registers_.begin() + 5); | ||||
| 				break; | ||||
| 				case Mode::Supervisor: | ||||
| 					std::copy(active_.begin() + 13, active_.begin() + 15, supervisor_registers_.begin()); | ||||
| 				break; | ||||
| 				case Mode::IRQ: | ||||
| 					std::copy(active_.begin() + 13, active_.begin() + 15, irq_registers_.begin()); | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			// For all modes except FIQ: restore the final two registers to their appropriate values. | ||||
| 			// For FIQ: save an additional five, then overwrite seven. | ||||
| 			switch(target_mode) { | ||||
| 				case Mode::FIQ: | ||||
| 					// FIQ is incoming, so save registers 8 to 12 to the first five slots of the user registers. | ||||
| 					std::copy(active_.begin() + 8, active_.begin() + 13, user_registers_.begin()); | ||||
|  | ||||
| 					// Replace R8 to R14. | ||||
| 					std::copy(fiq_registers_.begin(), fiq_registers_.end(), active_.begin() + 8); | ||||
| 				break; | ||||
| 				case Mode::User: | ||||
| 					std::copy(user_registers_.begin() + 5, user_registers_.end(), active_.begin() + 13); | ||||
| 				break; | ||||
| 				case Mode::Supervisor: | ||||
| 					std::copy(supervisor_registers_.begin(), supervisor_registers_.end(), active_.begin() + 13); | ||||
| 				break; | ||||
| 				case Mode::IRQ: | ||||
| 					std::copy(irq_registers_.begin(), irq_registers_.end(), active_.begin() + 13); | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			// If FIQ is outgoing then there's another five registers to restore. | ||||
| 			if(mode_ == Mode::FIQ) { | ||||
| 				std::copy(user_registers_.begin(), user_registers_.begin() + 5, active_.begin() + 8); | ||||
| 			} | ||||
|  | ||||
| 			mode_ = target_mode; | ||||
| 		} | ||||
|  | ||||
| 		uint32_t &operator[](uint32_t offset) { | ||||
| 			return active_[static_cast<size_t>(offset)]; | ||||
| 		} | ||||
|  | ||||
| 		uint32_t operator[](uint32_t offset) const { | ||||
| 			return active_[static_cast<size_t>(offset)]; | ||||
| 		} | ||||
|  | ||||
| 		/// @returns A reference to the register at @c offset. If @c force_user_mode is true, | ||||
| 		/// this will the the user-mode register. Otherwise it'll be that for the current mode. These references | ||||
| 		/// are guaranteed to remain valid only until the next mode change. | ||||
| 		uint32_t ®(bool force_user_mode, uint32_t offset) { | ||||
| 			switch(mode_) { | ||||
| 				default: | ||||
| 				case Mode::User: return active_[offset]; | ||||
|  | ||||
| 				case Mode::Supervisor: | ||||
| 				case Mode::IRQ: | ||||
| 					if(force_user_mode && (offset == 13 || offset == 14)) { | ||||
| 						return user_registers_[offset - 8]; | ||||
| 					} | ||||
| 					return active_[offset]; | ||||
|  | ||||
| 				case Mode::FIQ: | ||||
| 					if(force_user_mode && (offset >= 8 && offset < 15)) { | ||||
| 						return user_registers_[offset - 8]; | ||||
| 					} | ||||
| 					return active_[offset]; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		Mode mode_ = Mode::Supervisor; | ||||
|  | ||||
| 		uint32_t zero_result_ = 1; | ||||
| 		uint32_t negative_flag_ = 0; | ||||
| 		uint32_t interrupt_flags_ = ConditionCode::IRQDisable | ConditionCode::FIQDisable; | ||||
| 		uint32_t carry_flag_ = 0; | ||||
| 		uint32_t overflow_flag_ = 0; | ||||
|  | ||||
| 		// Various shadow registers. | ||||
| 		std::array<uint32_t, 7> user_registers_{}; | ||||
| 		std::array<uint32_t, 7> fiq_registers_{}; | ||||
| 		std::array<uint32_t, 2> irq_registers_{}; | ||||
| 		std::array<uint32_t, 2> supervisor_registers_{}; | ||||
|  | ||||
| 		// The active register set. | ||||
| 		std::array<uint32_t, 16> active_{}; | ||||
| }; | ||||
|  | ||||
| } | ||||
| @@ -222,7 +222,7 @@ struct Instruction { | ||||
|  | ||||
| 	Instruction(Operation operation, AddressingMode addressing_mode, uint8_t opcode) : operation(operation), addressing_mode(addressing_mode), opcode(opcode) {} | ||||
| 	Instruction(uint8_t opcode) : opcode(opcode) {} | ||||
| 	Instruction() {} | ||||
| 	Instruction() = default; | ||||
| }; | ||||
|  | ||||
| /*! | ||||
|   | ||||
| @@ -478,7 +478,7 @@ class Preinstruction { | ||||
| 			static constexpr int SizeShift						= 0; | ||||
| 		}; | ||||
|  | ||||
| 		Preinstruction() {} | ||||
| 		Preinstruction() = default; | ||||
|  | ||||
| 		/// Produces a string description of this instruction; if @c opcode | ||||
| 		/// is supplied then any quick fields in this instruction will be decoded; | ||||
|   | ||||
| @@ -1362,9 +1362,9 @@ struct Instruction { | ||||
| 	bool is_supervisor = false; | ||||
| 	uint32_t opcode = 0; | ||||
|  | ||||
| 	Instruction() noexcept {} | ||||
| 	Instruction(uint32_t opcode) noexcept : opcode(opcode) {} | ||||
| 	Instruction(Operation operation, uint32_t opcode, bool is_supervisor = false) noexcept : operation(operation), is_supervisor(is_supervisor), opcode(opcode) {} | ||||
| 	constexpr Instruction() noexcept = default; | ||||
| 	constexpr Instruction(uint32_t opcode) noexcept : opcode(opcode) {} | ||||
| 	constexpr Instruction(Operation operation, uint32_t opcode, bool is_supervisor = false) noexcept : operation(operation), is_supervisor(is_supervisor), opcode(opcode) {} | ||||
|  | ||||
| 	// Instruction fields are decoded below; naming is a compromise between | ||||
| 	// Motorola's documentation and IBM's. | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| Code in here provides the means to disassemble, and to execute code for certain instruction sets. | ||||
|  | ||||
| It **does not seek to emulate specific processors** other than in terms of implementing their instruction sets. So: | ||||
|  | ||||
| * it doesn't involve itself in the actual bus signalling of real processors; and | ||||
| * instruction-level timing (e.g. total cycle counts) may be unimplemented, and is likely to be incomplete. | ||||
|  | ||||
| @@ -13,6 +14,7 @@ This part of CLK is intended primarily to provide disassembly services for stati | ||||
| A decoder extracts fully-decoded instructions from a data stream for its associated architecture. | ||||
|  | ||||
| The meaning of 'fully-decoded' is flexible but it means that a caller can easily discern at least: | ||||
|  | ||||
| * the operation in use; | ||||
| * its addressing mode; and | ||||
| * relevant registers. | ||||
| @@ -20,6 +22,7 @@ The meaning of 'fully-decoded' is flexible but it means that a caller can easily | ||||
| It may be assumed that callers will have access to the original data stream for immediate values, if it is sensible to do so. | ||||
|  | ||||
| In deciding what to expose, what to store ahead of time and what to obtain just-in-time a decoder should have an eye on two principal consumers: | ||||
|  | ||||
| 1. disassemblers; and | ||||
| 2. instruction executors. | ||||
|  | ||||
| @@ -50,6 +53,7 @@ A sample interface: | ||||
|     std::pair<int, Instruction> decode(word_type *stream, size_t length) { ... } | ||||
|  | ||||
| In this sample the returned pair provides an `int` size that is one of: | ||||
|  | ||||
| * a positive number, indicating a completed decoding that consumed that many `word_type`s; or | ||||
| * a negative number, indicating the [negatived] minimum number of `word_type`s that the caller should try to get hold of before calling `decode` again. | ||||
|  | ||||
| @@ -58,6 +62,7 @@ A caller is permitted to react in any way it prefers to negative numbers; they'r | ||||
| ## Parsers | ||||
|  | ||||
| A parser sits one level above a decoder; it is handed: | ||||
|  | ||||
| * a start address; | ||||
| * a closing bound; and | ||||
| * a target. | ||||
| @@ -65,6 +70,7 @@ A parser sits one level above a decoder; it is handed: | ||||
| It is responsible for parsing the instruction stream from the start address up to and not beyond the closing bound, and no further than any unconditional branches. | ||||
|  | ||||
| It should post to the target: | ||||
|  | ||||
| * any instructions fully decoded; | ||||
| * any conditional branch destinations encountered; | ||||
| * any immediately-knowable accessed addresses; and | ||||
| @@ -75,6 +81,7 @@ So a parser has the same two primary potential recipients as a decoder: diassemb | ||||
| ## Executors | ||||
|  | ||||
| An executor is responsible for only one thing: | ||||
|  | ||||
| * mapping from decoded instructions to objects that can perform those instructions. | ||||
|  | ||||
| An executor is assumed to bundle all the things that go into instruction set execution: processor state and memory, alongside a parser. | ||||
|   | ||||
| @@ -564,7 +564,7 @@ constexpr Operation rep_operation(Operation operation, Repetition repetition) { | ||||
| /// It cannot natively describe a base of ::None. | ||||
| class ScaleIndexBase { | ||||
| 	public: | ||||
| 		constexpr ScaleIndexBase() noexcept {} | ||||
| 		constexpr ScaleIndexBase() noexcept = default; | ||||
| 		constexpr ScaleIndexBase(uint8_t sib) noexcept : sib_(sib) {} | ||||
| 		constexpr ScaleIndexBase(int scale, Source index, Source base) noexcept : | ||||
| 			sib_(uint8_t( | ||||
| @@ -703,7 +703,7 @@ template<bool is_32bit> class Instruction { | ||||
| 		using ImmediateT = typename std::conditional<is_32bit, uint32_t, uint16_t>::type; | ||||
| 		using AddressT = ImmediateT; | ||||
|  | ||||
| 		constexpr Instruction() noexcept {} | ||||
| 		constexpr Instruction() noexcept = default; | ||||
| 		constexpr Instruction(Operation operation) noexcept : | ||||
| 			Instruction(operation, Source::None, Source::None, ScaleIndexBase(), false, AddressSize::b16, Source::None, DataSize::None, 0, 0) {} | ||||
| 		constexpr Instruction( | ||||
|   | ||||
							
								
								
									
										636
									
								
								Machines/Acorn/Archimedes/Archimedes.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										636
									
								
								Machines/Acorn/Archimedes/Archimedes.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,636 @@ | ||||
| // | ||||
| //  Archimedes.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 04/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "Archimedes.hpp" | ||||
|  | ||||
| #include "HalfDuplexSerial.hpp" | ||||
| #include "InputOutputController.hpp" | ||||
| #include "Keyboard.hpp" | ||||
| #include "KeyboardMapper.hpp" | ||||
| #include "MemoryController.hpp" | ||||
| #include "Sound.hpp" | ||||
|  | ||||
| #include "../../AudioProducer.hpp" | ||||
| #include "../../KeyboardMachine.hpp" | ||||
| #include "../../MediaTarget.hpp" | ||||
| #include "../../MouseMachine.hpp" | ||||
| #include "../../ScanProducer.hpp" | ||||
| #include "../../TimedMachine.hpp" | ||||
|  | ||||
| #include "../../../Activity/Source.hpp" | ||||
|  | ||||
| #include "../../../InstructionSets/ARM/Disassembler.hpp" | ||||
| #include "../../../InstructionSets/ARM/Executor.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
| #include "../../../Components/I2C/I2C.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <array> | ||||
| #include <set> | ||||
| #include <vector> | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| #ifndef NDEBUG | ||||
| namespace { | ||||
| Log::Logger<Log::Source::Archimedes> logger; | ||||
| } | ||||
|  | ||||
| template <InstructionSet::ARM::Model model, typename Executor> | ||||
| struct HackyDebugger { | ||||
| 	void notify(uint32_t address, uint32_t instruction, Executor &executor) { | ||||
| 		pc_history[pc_history_ptr] = address; | ||||
| 		pc_history_ptr = (pc_history_ptr + 1) % pc_history.size(); | ||||
|  | ||||
| //			if( | ||||
| //				executor_.pc() > 0x038021d0 && | ||||
| //					last_r1 != executor_.registers()[1] | ||||
| //					 || | ||||
| //				( | ||||
| //					last_link != executor_.registers()[14] || | ||||
| //					last_r0 != executor_.registers()[0] || | ||||
| //					last_r10 != executor_.registers()[10] || | ||||
| //					last_r1 != executor_.registers()[1] | ||||
| //				) | ||||
| //			) { | ||||
| //				logger.info().append("%08x modified R14 to %08x; R0 to %08x; R10 to %08x; R1 to %08x", | ||||
| //					last_pc, | ||||
| //					executor_.registers()[14], | ||||
| //					executor_.registers()[0], | ||||
| //					executor_.registers()[10], | ||||
| //					executor_.registers()[1] | ||||
| //				); | ||||
| //				logger.info().append("%08x modified R1 to %08x", | ||||
| //					last_pc, | ||||
| //					executor_.registers()[1] | ||||
| //				); | ||||
| //				last_link = executor_.registers()[14]; | ||||
| //				last_r0 = executor_.registers()[0]; | ||||
| //				last_r10 = executor_.registers()[10]; | ||||
| //				last_r1 = executor_.registers()[1]; | ||||
| //			} | ||||
|  | ||||
| //			if(instruction == 0xe8fd7fff) { | ||||
| //				printf("At %08x [%d]; after last PC %08x and %zu ago was %08x\n", | ||||
| //					address, | ||||
| //					instr_count, | ||||
| //					pc_history[(pc_history_ptr - 2 + pc_history.size()) % pc_history.size()], | ||||
| //					pc_history.size(), | ||||
| //					pc_history[pc_history_ptr]); | ||||
| //			} | ||||
| //			last_r9 = executor_.registers()[9]; | ||||
|  | ||||
| //			log |= address == 0x038031c4; | ||||
| //			log |= instr_count == 53552731 - 30; | ||||
| //			log &= executor_.pc() != 0x000000a0; | ||||
|  | ||||
| //			log = (executor_.pc() == 0x038162afc) || (executor_.pc() == 0x03824b00); | ||||
| //			log |= instruction & ; | ||||
|  | ||||
| 		// The following has the effect of logging all taken SWIs and their return codes. | ||||
| /*		if( | ||||
| 			(instruction & 0x0f00'0000) == 0x0f00'0000 && | ||||
| 			executor.registers().test(InstructionSet::ARM::Condition(instruction >> 28)) | ||||
| 		) { | ||||
| 			if(instruction & 0x2'0000) { | ||||
| 				swis.emplace_back(); | ||||
| 				swis.back().count = swi_count++; | ||||
| 				swis.back().opcode = instruction; | ||||
| 				swis.back().address = executor.pc(); | ||||
| 				swis.back().return_address = executor.registers().pc(4); | ||||
| 				for(int c = 0; c < 10; c++) swis.back().regs[c] = executor.registers()[uint32_t(c)]; | ||||
|  | ||||
| 				// Possibly capture more detail. | ||||
| 				// | ||||
| 				// Cf. http://productsdb.riscos.com/support/developers/prm_index/numswilist.html | ||||
| 				uint32_t pointer = 0; | ||||
| 				switch(instruction & 0xfd'ffff) { | ||||
| 					case 0x41501: | ||||
| 						swis.back().swi_name = "MessageTrans_OpenFile"; | ||||
|  | ||||
| 						// R0: pointer to file descriptor; R1: pointer to filename; R2: pointer to hold file data. | ||||
| 						// (R0 and R1 are in the RMA if R2 = 0) | ||||
| 						pointer = executor.registers()[1]; | ||||
| 					break; | ||||
| 					case 0x41502: | ||||
| 						swis.back().swi_name = "MessageTrans_Lookup"; | ||||
| 					break; | ||||
| 					case 0x41506: | ||||
| 						swis.back().swi_name = "MessageTrans_ErrorLookup"; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x4028a: | ||||
| 						swis.back().swi_name = "Podule_EnumerateChunksWithInfo"; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x4000a: | ||||
| 						swis.back().swi_name = "Econet_ReadLocalStationAndNet"; | ||||
| 					break; | ||||
| 					case 0x4000e: | ||||
| 						swis.back().swi_name = "Econet_SetProtection"; | ||||
| 					break; | ||||
| 					case 0x40015: | ||||
| 						swis.back().swi_name = "Econet_ClaimPort"; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x40541: | ||||
| 						swis.back().swi_name = "FileCore_Create"; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x80156: | ||||
| 					case 0x8015b: | ||||
| 						swis.back().swi_name = "PDriver_MiscOpForDriver"; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x05: | ||||
| 						swis.back().swi_name = "OS_CLI"; | ||||
| 						pointer = executor.registers()[0]; | ||||
| 					break; | ||||
| 					case 0x0d: | ||||
| 						swis.back().swi_name = "OS_Find"; | ||||
| 						if(executor.registers()[0] >= 0x40) { | ||||
| 							pointer = executor.registers()[1]; | ||||
| 						} | ||||
| 					break; | ||||
| 					case 0x1d: | ||||
| 						swis.back().swi_name = "OS_Heap"; | ||||
| 					break; | ||||
| 					case 0x1e: | ||||
| 						swis.back().swi_name = "OS_Module"; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x20: | ||||
| 						swis.back().swi_name = "OS_Release"; | ||||
| 					break; | ||||
| 					case 0x21: | ||||
| 						swis.back().swi_name = "OS_ReadUnsigned"; | ||||
| 					break; | ||||
| 					case 0x23: | ||||
| 						swis.back().swi_name = "OS_ReadVarVal"; | ||||
|  | ||||
| 						// R0: pointer to variable name. | ||||
| 						pointer = executor.registers()[0]; | ||||
| 					break; | ||||
| 					case 0x24: | ||||
| 						swis.back().swi_name = "OS_SetVarVal"; | ||||
|  | ||||
| 						// R0: pointer to variable name. | ||||
| 						pointer = executor.registers()[0]; | ||||
| 					break; | ||||
| 					case 0x26: | ||||
| 						swis.back().swi_name = "OS_GSRead"; | ||||
| 					break; | ||||
| 					case 0x27: | ||||
| 						swis.back().swi_name = "OS_GSTrans"; | ||||
| 						pointer = executor.registers()[0]; | ||||
| 					break; | ||||
| 					case 0x29: | ||||
| 						swis.back().swi_name = "OS_FSControl"; | ||||
| 					break; | ||||
| 					case 0x2a: | ||||
| 						swis.back().swi_name = "OS_ChangeDynamicArea"; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x4c: | ||||
| 						swis.back().swi_name = "OS_ReleaseDeviceVector"; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x43057: | ||||
| 						swis.back().swi_name = "Territory_LowerCaseTable"; | ||||
| 					break; | ||||
| 					case 0x43058: | ||||
| 						swis.back().swi_name = "Territory_UpperCaseTable"; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x42fc0: | ||||
| 						swis.back().swi_name = "Portable_Speed"; | ||||
| 					break; | ||||
| 					case 0x42fc1: | ||||
| 						swis.back().swi_name = "Portable_Control"; | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				if(pointer) { | ||||
| 					while(true) { | ||||
| 						uint8_t next; | ||||
| 						executor.bus.template read<uint8_t>(pointer, next, InstructionSet::ARM::Mode::Supervisor, false); | ||||
| 						++pointer; | ||||
|  | ||||
| 						if(next < 32) break; | ||||
| 						swis.back().value_name.push_back(static_cast<char>(next)); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 			} | ||||
|  | ||||
| 			if(executor.registers().pc_status(0) & InstructionSet::ARM::ConditionCode::Overflow) { | ||||
| 				logger.error().append("SWI called with V set"); | ||||
| 			} | ||||
| 		} | ||||
| 		if(!swis.empty() && executor.pc() == swis.back().return_address) { | ||||
| 			// Overflow set => SWI failure. | ||||
| 			auto &back = swis.back(); | ||||
| 			if(executor.registers().pc_status(0) & InstructionSet::ARM::ConditionCode::Overflow) { | ||||
| 				auto info = logger.info(); | ||||
|  | ||||
| 				info.append("[%d] Failed swi ", back.count); | ||||
| 				if(back.swi_name.empty()) { | ||||
| 					info.append("&%x", back.opcode & 0xfd'ffff); | ||||
| 				} else { | ||||
| 					info.append("%s", back.swi_name.c_str()); | ||||
| 				} | ||||
|  | ||||
| 				if(!back.value_name.empty()) { | ||||
| 					info.append(" %s", back.value_name.c_str()); | ||||
| 				} | ||||
|  | ||||
| 				info.append(" @ %08x ", back.address); | ||||
| 				for(uint32_t c = 0; c < 10; c++) { | ||||
| 					info.append("r%d:%08x ", c, back.regs[c]); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			swis.pop_back(); | ||||
| 		}*/ | ||||
|  | ||||
| 		if(log) { | ||||
| 			InstructionSet::ARM::Disassembler<model> disassembler; | ||||
| 			InstructionSet::ARM::dispatch<model>(instruction, disassembler); | ||||
|  | ||||
| 			auto info = logger.info(); | ||||
| 			info.append("[%d] %08x: %08x\t\t%s\t prior:[", | ||||
| 				instr_count, | ||||
| 				executor.pc(), | ||||
| 				instruction, | ||||
| 				disassembler.last().to_string(executor.pc()).c_str()); | ||||
| 			for(uint32_t c = 0; c < 15; c++) { | ||||
| 				info.append("r%d:%08x ", c, executor.registers()[c]); | ||||
| 			} | ||||
| 			info.append("]"); | ||||
| 		} | ||||
| //		opcodes.insert(instruction); | ||||
| //		if(accumulate) { | ||||
| //			int c = 0; | ||||
| //			for(auto instr : opcodes) { | ||||
| //				printf("0x%08x, ", instr); | ||||
| //				++c; | ||||
| //				if(!(c&15)) printf("\n"); | ||||
| //			} | ||||
| //			accumulate = false; | ||||
| //		} | ||||
|  | ||||
| 		++instr_count; | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	std::array<uint32_t, 75> pc_history; | ||||
| 	std::size_t pc_history_ptr = 0; | ||||
| 	uint32_t instr_count = 0; | ||||
| 	uint32_t swi_count = 0; | ||||
|  | ||||
| 	struct SWICall { | ||||
| 		uint32_t count; | ||||
| 		uint32_t opcode; | ||||
| 		uint32_t address; | ||||
| 		uint32_t regs[10]; | ||||
| 		uint32_t return_address; | ||||
| 		std::string value_name; | ||||
| 		std::string swi_name; | ||||
| 	}; | ||||
| 	std::vector<SWICall> swis; | ||||
| 	uint32_t last_pc = 0; | ||||
| //	uint32_t last_r9 = 0; | ||||
| 	bool log = false; | ||||
| 	bool accumulate = true; | ||||
|  | ||||
| 	std::set<uint32_t> opcodes; | ||||
| }; | ||||
| #else | ||||
| template <InstructionSet::ARM::Model model, typename Executor> | ||||
| struct HackyDebugger { | ||||
| 	void notify(uint32_t, uint32_t, Executor &) {} | ||||
| }; | ||||
| #endif | ||||
|  | ||||
| class ConcreteMachine: | ||||
| 	public Machine, | ||||
| 	public MachineTypes::AudioProducer, | ||||
| 	public MachineTypes::MappedKeyboardMachine, | ||||
| 	public MachineTypes::MediaTarget, | ||||
| 	public MachineTypes::MouseMachine, | ||||
| 	public MachineTypes::TimedMachine, | ||||
| 	public MachineTypes::ScanProducer, | ||||
| 	public Activity::Source | ||||
| { | ||||
| 	private: | ||||
| 		// TODO: pick a sensible clock rate; this is just code for '24 MIPS, please'. | ||||
| 		static constexpr int ClockRate = 24'000'000; | ||||
|  | ||||
| 		// Runs for 24 cycles, distributing calls to the various ticking subsystems | ||||
| 		// 'correctly' (i.e. correctly for the approximation in use). | ||||
| 		// | ||||
| 		// The implementation of this is coupled to the ClockRate above, hence its | ||||
| 		// appearance here. | ||||
| 		template <int video_divider, bool original_speed> | ||||
| 		void macro_tick() { | ||||
| 			macro_counter_ -= 24; | ||||
|  | ||||
| 			// This is a 24-cycle window, so at 24Mhz macro_tick() is called at 1Mhz. | ||||
| 			// Hence, required ticks are: | ||||
| 			// | ||||
| 			// 	* CPU: 24; | ||||
| 			//	* video: 24 / video_divider; | ||||
| 			//	* floppy: 8; | ||||
| 			//	* timers: 2; | ||||
| 			//	* sound: 1. | ||||
|  | ||||
| 			tick_cpu_video<0, video_divider, original_speed>();		tick_cpu_video<1, video_divider, original_speed>(); | ||||
| 			tick_cpu_video<2, video_divider, original_speed>();		tick_floppy(); | ||||
| 			tick_cpu_video<3, video_divider, original_speed>();		tick_cpu_video<4, video_divider, original_speed>(); | ||||
| 			tick_cpu_video<5, video_divider, original_speed>();		tick_floppy(); | ||||
| 			tick_cpu_video<6, video_divider, original_speed>();		tick_cpu_video<7, video_divider, original_speed>(); | ||||
| 			tick_cpu_video<8, video_divider, original_speed>();		tick_floppy(); | ||||
| 			tick_cpu_video<9, video_divider, original_speed>();		tick_cpu_video<10, video_divider, original_speed>(); | ||||
| 			tick_cpu_video<11, video_divider, original_speed>();	tick_floppy(); | ||||
| 			tick_timers(); | ||||
|  | ||||
| 			tick_cpu_video<12, video_divider, original_speed>();	tick_cpu_video<13, video_divider, original_speed>(); | ||||
| 			tick_cpu_video<14, video_divider, original_speed>();	tick_floppy(); | ||||
| 			tick_cpu_video<15, video_divider, original_speed>();	tick_cpu_video<16, video_divider, original_speed>(); | ||||
| 			tick_cpu_video<17, video_divider, original_speed>();	tick_floppy(); | ||||
| 			tick_cpu_video<18, video_divider, original_speed>();	tick_cpu_video<19, video_divider, original_speed>(); | ||||
| 			tick_cpu_video<20, video_divider, original_speed>();	tick_floppy(); | ||||
| 			tick_cpu_video<21, video_divider, original_speed>();	tick_cpu_video<22, video_divider, original_speed>(); | ||||
| 			tick_cpu_video<23, video_divider, original_speed>();	tick_floppy(); | ||||
| 			tick_timers(); | ||||
| 			tick_sound(); | ||||
| 		} | ||||
| 		int macro_counter_ = 0; | ||||
|  | ||||
| 		template <int offset, int video_divider, bool original_speed> | ||||
| 		void tick_cpu_video() { | ||||
| 			if constexpr (!(offset % video_divider)) { | ||||
| 				tick_video(); | ||||
| 			} | ||||
|  | ||||
| 			// Debug mode: run CPU a lot slower. Actually at close to original advertised MIPS speed. | ||||
| 			if constexpr (original_speed && (offset & 7)) return; | ||||
| 			if constexpr (offset & 1) return; | ||||
| 			tick_cpu(); | ||||
| 		} | ||||
|  | ||||
| 	public: | ||||
| 		ConcreteMachine( | ||||
| 			const Analyser::Static::Target &target, | ||||
| 			const ROMMachine::ROMFetcher &rom_fetcher | ||||
| 		) : executor_(*this, *this, *this) { | ||||
| 			set_clock_rate(ClockRate); | ||||
|  | ||||
| 			constexpr ROM::Name risc_os = ROM::Name::AcornRISCOS311; | ||||
| 			ROM::Request request(risc_os); | ||||
| 			auto roms = rom_fetcher(request); | ||||
| 			if(!request.validate(roms)) { | ||||
| 				throw ROMMachine::Error::MissingROMs; | ||||
| 			} | ||||
|  | ||||
| 			executor_.bus.set_rom(roms.find(risc_os)->second); | ||||
| 			insert_media(target.media); | ||||
|  | ||||
| 			fill_pipeline(0); | ||||
| 		} | ||||
|  | ||||
| 		void update_interrupts() { | ||||
| 			using Exception = InstructionSet::ARM::Registers::Exception; | ||||
|  | ||||
| 			const int requests = executor_.bus.interrupt_mask(); | ||||
| 			if((requests & InterruptRequests::FIQ) && executor_.registers().would_interrupt<Exception::FIQ>()) { | ||||
| 				pipeline_.reschedule(Pipeline::SWISubversion::FIQ); | ||||
| 				return; | ||||
| 			} | ||||
| 			if((requests & InterruptRequests::IRQ) && executor_.registers().would_interrupt<Exception::IRQ>()) { | ||||
| 				pipeline_.reschedule(Pipeline::SWISubversion::IRQ); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		void did_set_status() { | ||||
| 			// This might have been a change of mode, so... | ||||
| 			trans_ = executor_.registers().mode() == InstructionSet::ARM::Mode::User; | ||||
| 			fill_pipeline(executor_.pc()); | ||||
| 			update_interrupts(); | ||||
| 		} | ||||
|  | ||||
| 		void did_set_pc() { | ||||
| 			fill_pipeline(executor_.pc()); | ||||
| 		} | ||||
|  | ||||
| 		bool should_swi(uint32_t) { | ||||
| 			using Exception = InstructionSet::ARM::Registers::Exception; | ||||
| 			using SWISubversion = Pipeline::SWISubversion; | ||||
|  | ||||
| 			switch(pipeline_.swi_subversion()) { | ||||
| 				case Pipeline::SWISubversion::None: | ||||
| 				return true; | ||||
|  | ||||
| 				case SWISubversion::DataAbort: | ||||
| //					executor_.set_pc(executor_.pc() - 4); | ||||
| 					executor_.registers().exception<Exception::DataAbort>(); | ||||
| 				break; | ||||
|  | ||||
| 				// FIQ and IRQ decrement the PC because their apperance in the pipeline causes | ||||
| 				// it to look as though they were fetched, but they weren't. | ||||
| 				case SWISubversion::FIQ: | ||||
| 					executor_.set_pc(executor_.pc() - 4); | ||||
| 					executor_.registers().exception<Exception::FIQ>(); | ||||
| 				break; | ||||
| 				case SWISubversion::IRQ: | ||||
| 					executor_.set_pc(executor_.pc() - 4); | ||||
| 					executor_.registers().exception<Exception::IRQ>(); | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 			did_set_pc(); | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		void update_clock_rates() { | ||||
| 			video_divider_ = executor_.bus.video().clock_divider(); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		// MARK: - ScanProducer. | ||||
| 		void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { | ||||
| 			executor_.bus.video().crt().set_scan_target(scan_target); | ||||
| 		} | ||||
| 		Outputs::Display::ScanStatus get_scaled_scan_status() const override { | ||||
| 			return executor_.bus.video().crt().get_scaled_scan_status() * video_divider_; | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - TimedMachine. | ||||
| 		int video_divider_ = 1; | ||||
| 		void run_for(Cycles cycles) override { | ||||
| #ifndef NDEBUG | ||||
| 			// Debug mode: always run 'slowly' because that's less of a burden, and | ||||
| 			// because it allows me to peer at problems with greater leisure. | ||||
| 			const bool use_original_speed = true; | ||||
| #else | ||||
| 			// As a first, blunt implementation: try to model something close | ||||
| 			// to original speed if there have been 10 frame rate overages in total. | ||||
| 			const bool use_original_speed = executor_.bus.video().frame_rate_overages() > 10; | ||||
| #endif | ||||
|  | ||||
| 			if(use_original_speed) run_for<true>(cycles); | ||||
| 			else run_for<false>(cycles); | ||||
| 		} | ||||
|  | ||||
| 		template <bool original_speed> | ||||
| 		void run_for(Cycles cycles) { | ||||
| 			macro_counter_ += cycles.as<int>(); | ||||
|  | ||||
| 			while(macro_counter_ > 0) { | ||||
| 				switch(video_divider_) { | ||||
| 					default:	macro_tick<2, original_speed>();	break; | ||||
| 					case 3:		macro_tick<3, original_speed>();	break; | ||||
| 					case 4:		macro_tick<4, original_speed>();	break; | ||||
| 					case 6:		macro_tick<6, original_speed>();	break; | ||||
| 				} | ||||
|  | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		void tick_cpu() { | ||||
| 			const uint32_t instruction = advance_pipeline(executor_.pc() + 8); | ||||
| 			debugger_.notify(executor_.pc(), instruction, executor_); | ||||
| 			InstructionSet::ARM::execute(instruction, executor_); | ||||
| 		} | ||||
|  | ||||
| 		void tick_timers()	{	executor_.bus.tick_timers();	} | ||||
| 		void tick_sound()	{	executor_.bus.sound().tick();	} | ||||
| 		void tick_video()	{	executor_.bus.video().tick();	} | ||||
| 		void tick_floppy()	{	executor_.bus.tick_floppy();	} | ||||
|  | ||||
| 		// MARK: - MediaTarget | ||||
| 		bool insert_media(const Analyser::Static::Media &media) override { | ||||
| 			size_t c = 0; | ||||
| 			for(auto &disk : media.disks) { | ||||
| 				executor_.bus.set_disk(disk, c); | ||||
| 				c++; | ||||
| 				if(c == 4) break; | ||||
| 			} | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - AudioProducer | ||||
| 		Outputs::Speaker::Speaker *get_speaker() override { | ||||
| 			return executor_.bus.speaker(); | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - Activity::Source. | ||||
| 		void set_activity_observer(Activity::Observer *observer) final { | ||||
| 			executor_.bus.set_activity_observer(observer); | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - MappedKeyboardMachine. | ||||
| 		MappedKeyboardMachine::KeyboardMapper *get_keyboard_mapper() override { | ||||
| 			return &keyboard_mapper_; | ||||
| 		} | ||||
| 		Archimedes::KeyboardMapper keyboard_mapper_; | ||||
|  | ||||
| 		void set_key_state(uint16_t key, bool is_pressed) override { | ||||
| 			const int row = Archimedes::KeyboardMapper::row(key); | ||||
| 			const int column = Archimedes::KeyboardMapper::column(key); | ||||
| 			executor_.bus.keyboard().set_key_state(row, column, is_pressed); | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - MouseMachine. | ||||
| 		Inputs::Mouse &get_mouse() override { | ||||
| 			return executor_.bus.keyboard().mouse(); | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - ARM execution. | ||||
| 		static constexpr auto arm_model = InstructionSet::ARM::Model::ARMv2; | ||||
| 		using Executor = InstructionSet::ARM::Executor<arm_model, MemoryController<ConcreteMachine, ConcreteMachine>, ConcreteMachine>; | ||||
| 		Executor executor_; | ||||
| 		bool trans_ = false; | ||||
|  | ||||
| 		void fill_pipeline(uint32_t pc) { | ||||
| 			if(pipeline_.interrupt_next()) return; | ||||
| 			advance_pipeline(pc); | ||||
| 			advance_pipeline(pc + 4); | ||||
| 		} | ||||
|  | ||||
| 		uint32_t advance_pipeline(uint32_t pc) { | ||||
| 			uint32_t instruction = 0;	// Value should never be used; this avoids a spurious GCC warning. | ||||
| 			const bool did_read = executor_.bus.read(pc, instruction, trans_); | ||||
| 			return pipeline_.exchange( | ||||
| 				did_read ? instruction : Pipeline::SWI, | ||||
| 				did_read ? Pipeline::SWISubversion::None : Pipeline::SWISubversion::DataAbort); | ||||
| 		} | ||||
|  | ||||
| 		struct Pipeline { | ||||
| 			enum SWISubversion: uint8_t { | ||||
| 				None, | ||||
| 				DataAbort, | ||||
| 				IRQ, | ||||
| 				FIQ, | ||||
| 			}; | ||||
|  | ||||
| 			static constexpr uint32_t SWI = 0xef'000000; | ||||
|  | ||||
| 			uint32_t exchange(uint32_t next, SWISubversion subversion) { | ||||
| 				const uint32_t result = upcoming_[active_].opcode; | ||||
| 				latched_subversion_ = upcoming_[active_].subversion; | ||||
|  | ||||
| 				upcoming_[active_].opcode = next; | ||||
| 				upcoming_[active_].subversion = subversion; | ||||
| 				active_ ^= 1; | ||||
|  | ||||
| 				return result; | ||||
| 			} | ||||
|  | ||||
| 			SWISubversion swi_subversion() const { | ||||
| 				return latched_subversion_; | ||||
| 			} | ||||
|  | ||||
| 			// TODO: one day, possibly: schedule the subversion one slot further into the future | ||||
| 			// (i.e. active_ ^ 1) to allow one further instruction to occur as usual before the | ||||
| 			// action paplies. That is, if interrupts take effect one instruction later after a flags | ||||
| 			// change, which I don't yet know. | ||||
| 			// | ||||
| 			// In practice I got into a bit of a race condition between interrupt scheduling and | ||||
| 			// flags changes, so have backed off for now. | ||||
| 			void reschedule(SWISubversion subversion) { | ||||
| 				upcoming_[active_].opcode = SWI; | ||||
| 				upcoming_[active_].subversion = subversion; | ||||
| 			} | ||||
|  | ||||
| 			bool interrupt_next() const { | ||||
| 				return upcoming_[active_].subversion == SWISubversion::IRQ || upcoming_[active_].subversion == SWISubversion::FIQ; | ||||
| 			} | ||||
|  | ||||
| 		private: | ||||
| 			struct Stage { | ||||
| 				uint32_t opcode; | ||||
| 				SWISubversion subversion = SWISubversion::None; | ||||
| 			}; | ||||
| 			Stage upcoming_[2]; | ||||
| 			int active_ = 0; | ||||
|  | ||||
| 			SWISubversion latched_subversion_; | ||||
| 		} pipeline_; | ||||
|  | ||||
| 		// MARK: - Yucky, temporary junk. | ||||
| 		HackyDebugger<arm_model, Executor> debugger_; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| using namespace Archimedes; | ||||
|  | ||||
| std::unique_ptr<Machine> Machine::Archimedes(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { | ||||
| 	return std::make_unique<ConcreteMachine>(*target, rom_fetcher); | ||||
| } | ||||
							
								
								
									
										27
									
								
								Machines/Acorn/Archimedes/Archimedes.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Machines/Acorn/Archimedes/Archimedes.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  Archimedes.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 04/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../../Analyser/Static/StaticAnalyser.hpp" | ||||
| #include "../../ROMMachine.hpp" | ||||
|  | ||||
| #include <memory> | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| class Machine { | ||||
| 	public: | ||||
| 		virtual ~Machine() = default; | ||||
| 		static std::unique_ptr<Machine> Archimedes( | ||||
| 			const Analyser::Static::Target *target, | ||||
| 			const ROMMachine::ROMFetcher &rom_fetcher | ||||
| 		); | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										85
									
								
								Machines/Acorn/Archimedes/CMOSRAM.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								Machines/Acorn/Archimedes/CMOSRAM.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| // | ||||
| // CMOSRAM.hpp | ||||
| // Clock Signal | ||||
| // | ||||
| // Created by Thomas Harte on 20/03/2024. | ||||
| // Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../../Components/I2C/I2C.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| #include <array> | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| struct CMOSRAM: public I2C::Peripheral { | ||||
| 	CMOSRAM() { | ||||
| 		ram_ = default_ram; | ||||
| 	} | ||||
|  | ||||
| 	void start(bool is_read) override { | ||||
| 		expecting_address_ = !is_read; | ||||
| 	} | ||||
|  | ||||
| 	// TODO: first 16 addresses are registers, not RAM. | ||||
|  | ||||
| 	std::optional<uint8_t> read() override { | ||||
| 		if(address_ < 16) { | ||||
| 			logger.error().append("TODO: read at %d", address_); | ||||
| 		} | ||||
|  | ||||
| 		const uint8_t result = ram_[address_]; | ||||
| 		++address_; | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	bool write(uint8_t value) override { | ||||
| 		if(expecting_address_) { | ||||
| 			address_ = value; | ||||
| 			expecting_address_ = false; | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if(address_ < 16) { | ||||
| 			logger.error().append("TODO: write at %d", address_); | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		ram_[address_] = value; | ||||
| 		++address_; | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	bool expecting_address_ = false; | ||||
| 	uint8_t address_; | ||||
|  | ||||
| 	std::array<uint8_t, 256> ram_{}; | ||||
|  | ||||
| 	// This is the default contents of RAM as written by RISC OS 3.11. | ||||
| 	static constexpr std::array<uint8_t, 256> default_ram = { | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x03, 0x14, 0x00, 0x6f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, | ||||
| 		0x00, 0xfe, 0x00, 0xeb, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x10, 0x50, 0x20, 0x08, 0x0a, 0x2c, | ||||
| 		0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x5c, 0x13, 0x00, 0x00, 0x04, 0xfd, 0x08, 0x01, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
| 	}; | ||||
|  | ||||
| 	Log::Logger<Log::Source::CMOSRTC> logger; | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										51
									
								
								Machines/Acorn/Archimedes/FloppyDisc.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								Machines/Acorn/Archimedes/FloppyDisc.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| // | ||||
| //  FloppyDisc.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 07/04/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../../Components/1770/1770.hpp" | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| template <typename InterruptObserverT> | ||||
| class FloppyDisc: public WD::WD1770, public WD::WD1770::Delegate { | ||||
| public: | ||||
| 	FloppyDisc(InterruptObserverT &observer) : WD::WD1770(P1772), observer_(observer) { | ||||
| 		emplace_drives(1, 8000000, 300, 2, Storage::Disk::Drive::ReadyType::ShugartModifiedRDY);	// A guess at RDY type. | ||||
| 		set_delegate(this); | ||||
| 	} | ||||
|  | ||||
| 	void wd1770_did_change_output(WD::WD1770 *) override { | ||||
| 		observer_.update_interrupts(); | ||||
| 	} | ||||
|  | ||||
| 	void set_control(uint8_t value) { | ||||
| 		//	b0, b1, b2, b3 = drive selects; | ||||
| 		//	b4 = side select; | ||||
| 		//	b5 = motor on/off | ||||
| 		//	b6 = floppy in use (i.e. LED?); | ||||
| 		//	b7 = disc eject/change reset. | ||||
| 		set_drive((value & 0x1) ^ 0x1); | ||||
| 		get_drive().set_head(1 ^ ((value >> 4) & 1)); | ||||
| 		get_drive().set_motor_on(!(value & 0x20)); | ||||
| 	} | ||||
| 	void reset() {} | ||||
|  | ||||
| 	void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) { | ||||
| 		get_drive(drive).set_disk(disk); | ||||
| 	} | ||||
|  | ||||
| 	bool ready() const { | ||||
| 		return get_drive().get_is_ready(); | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	InterruptObserverT &observer_; | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										88
									
								
								Machines/Acorn/Archimedes/HalfDuplexSerial.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								Machines/Acorn/Archimedes/HalfDuplexSerial.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| // | ||||
| //  HalfDuplexSerial.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 20/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| /// Models a half-duplex serial link between two parties, framing bytes with one start bit and two stop bits. | ||||
| struct HalfDuplexSerial { | ||||
| 	static constexpr uint16_t ShiftMask = 0b1111'1110'0000'0000; | ||||
|  | ||||
| 	/// Enqueues @c value for output. | ||||
| 	void output(int party, uint8_t value) { | ||||
| 		parties_[party].output_count = 11; | ||||
| 		parties_[party].input = 0x7ff; | ||||
| 		parties_[party].output = uint16_t((value << 1) | ShiftMask); | ||||
| 	} | ||||
|  | ||||
| 	/// @returns The last observed input. | ||||
| 	uint8_t input(int party) const { | ||||
| 		return uint8_t(parties_[party].input >> 1); | ||||
| 	} | ||||
|  | ||||
| 	static constexpr uint8_t Receive = 1 << 0; | ||||
| 	static constexpr uint8_t Transmit = 1 << 1; | ||||
|  | ||||
| 	/// @returns A bitmask of events that occurred during the last shift. | ||||
| 	uint8_t events(int party) { | ||||
| 		const auto result = parties_[party].events; | ||||
| 		parties_[party].events = 0; | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	bool is_outputting(int party) const { | ||||
| 		return parties_[party].output_count != 11; | ||||
| 	} | ||||
|  | ||||
| 	/// Updates the shifters on both sides of the serial link. | ||||
| 	void shift() { | ||||
| 		const uint16_t next = parties_[0].output & parties_[1].output & 1; | ||||
|  | ||||
| 		for(int c = 0; c < 2; c++) { | ||||
| 			if(parties_[c].output_count) { | ||||
| 				--parties_[c].output_count; | ||||
| 				if(!parties_[c].output_count) { | ||||
| 					parties_[c].events |= Transmit; | ||||
| 					parties_[c].input_count = -1; | ||||
| 				} | ||||
| 				parties_[c].output = (parties_[c].output >> 1) | ShiftMask; | ||||
| 			} else { | ||||
| 				// Check for a start bit. | ||||
| 				if(parties_[c].input_count == -1 && !next) { | ||||
| 					parties_[c].input_count = 0; | ||||
| 				} | ||||
|  | ||||
| 				// Shift in if currently observing. | ||||
| 				if(parties_[c].input_count >= 0 && parties_[c].input_count < 11) { | ||||
| 					parties_[c].input = uint16_t((parties_[c].input >> 1) | (next << 10)); | ||||
|  | ||||
| 					++parties_[c].input_count; | ||||
| 					if(parties_[c].input_count == 11) { | ||||
| 						parties_[c].events |= Receive; | ||||
| 						parties_[c].input_count = -1; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	struct Party { | ||||
| 		int output_count = 0; | ||||
| 		int input_count = -1; | ||||
| 		uint16_t output = 0xffff; | ||||
| 		uint16_t input = 0; | ||||
| 		uint8_t events = 0; | ||||
| 	} parties_[2]; | ||||
| }; | ||||
|  | ||||
| static constexpr int IOCParty = 0; | ||||
| static constexpr int KeyboardParty = 1; | ||||
|  | ||||
| } | ||||
							
								
								
									
										565
									
								
								Machines/Acorn/Archimedes/InputOutputController.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										565
									
								
								Machines/Acorn/Archimedes/InputOutputController.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,565 @@ | ||||
| // | ||||
| //  InputOutputController.h | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 20/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "CMOSRAM.hpp" | ||||
| #include "FloppyDisc.hpp" | ||||
| #include "Keyboard.hpp" | ||||
| #include "Sound.hpp" | ||||
| #include "Video.hpp" | ||||
|  | ||||
| #include "../../../Outputs/Log.hpp" | ||||
| #include "../../../Activity/Observer.hpp" | ||||
| #include "../../../ClockReceiver/ClockingHintSource.hpp" | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| // IRQ A flags | ||||
| namespace IRQA { | ||||
| 	static constexpr uint8_t PrinterBusy		= 0x01; | ||||
| 	static constexpr uint8_t SerialRinging		= 0x02; | ||||
| 	static constexpr uint8_t PrinterAcknowledge	= 0x04; | ||||
| 	static constexpr uint8_t VerticalFlyback	= 0x08; | ||||
| 	static constexpr uint8_t PowerOnReset		= 0x10; | ||||
| 	static constexpr uint8_t Timer0				= 0x20; | ||||
| 	static constexpr uint8_t Timer1				= 0x40; | ||||
| 	static constexpr uint8_t Force				= 0x80; | ||||
| } | ||||
|  | ||||
| // IRQ B flags | ||||
| namespace IRQB { | ||||
| 	static constexpr uint8_t PoduleFIQRequest		= 0x01; | ||||
| 	static constexpr uint8_t SoundBufferPointerUsed	= 0x02; | ||||
| 	static constexpr uint8_t SerialLine				= 0x04; | ||||
| 	static constexpr uint8_t IDE					= 0x08; | ||||
| 	static constexpr uint8_t FloppyDiscChanged		= 0x10; | ||||
| 	static constexpr uint8_t PoduleIRQRequest		= 0x20; | ||||
| 	static constexpr uint8_t KeyboardTransmitEmpty	= 0x40; | ||||
| 	static constexpr uint8_t KeyboardReceiveFull	= 0x80; | ||||
| } | ||||
|  | ||||
| // FIQ flags | ||||
| namespace FIQ { | ||||
| 	static constexpr uint8_t FloppyDiscData			= 0x01; | ||||
| 	static constexpr uint8_t FloppyDiscInterrupt	= 0x02; | ||||
| 	static constexpr uint8_t Econet					= 0x04; | ||||
| 	static constexpr uint8_t PoduleFIQRequest		= 0x40; | ||||
| 	static constexpr uint8_t Force					= 0x80; | ||||
| } | ||||
|  | ||||
| namespace InterruptRequests { | ||||
| 	static constexpr int IRQ = 0x01; | ||||
| 	static constexpr int FIQ = 0x02; | ||||
| }; | ||||
|  | ||||
| template <typename InterruptObserverT, typename ClockRateObserverT> | ||||
| struct InputOutputController: public ClockingHint::Observer { | ||||
| 	InputOutputController(InterruptObserverT &observer, ClockRateObserverT &clock_observer, const uint8_t *ram) : | ||||
| 		observer_(observer), | ||||
| 		keyboard_(serial_), | ||||
| 		floppy_(*this), | ||||
| 		sound_(*this, ram), | ||||
| 		video_(*this, clock_observer, sound_, ram) | ||||
| 	{ | ||||
| 		irq_a_.status = IRQA::Force | IRQA::PowerOnReset; | ||||
| 		irq_b_.status = 0x00; | ||||
| 		fiq_.status = FIQ::Force; | ||||
|  | ||||
| 		floppy_.set_clocking_hint_observer(this); | ||||
|  | ||||
| 		i2c_.add_peripheral(&cmos_, 0xa0); | ||||
| 		update_interrupts(); | ||||
| 	} | ||||
|  | ||||
| 	int interrupt_mask() const { | ||||
| 		return | ||||
| 			((irq_a_.request() | irq_b_.request()) ? InterruptRequests::IRQ : 0) | | ||||
| 			(fiq_.request() ? InterruptRequests::FIQ : 0); | ||||
| 	} | ||||
|  | ||||
| 	template <int c> | ||||
| 	bool tick_timer() { | ||||
| 		if(!counters_[c].value && !counters_[c].reload) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		--counters_[c].value; | ||||
| 		if(!counters_[c].value) { | ||||
| 			counters_[c].value = counters_[c].reload; | ||||
|  | ||||
| 			switch(c) { | ||||
| 				case 0:	return irq_a_.set(IRQA::Timer0); | ||||
| 				case 1:	return irq_a_.set(IRQA::Timer1); | ||||
| 				case 3: { | ||||
| 					serial_.shift(); | ||||
| 					keyboard_.update(); | ||||
|  | ||||
| 					const uint8_t events = serial_.events(IOCParty); | ||||
| 					bool did_interrupt = false; | ||||
| 					if(events & HalfDuplexSerial::Receive) { | ||||
| 						did_interrupt |= irq_b_.set(IRQB::KeyboardReceiveFull); | ||||
| 					} | ||||
| 					if(events & HalfDuplexSerial::Transmit) { | ||||
| 						did_interrupt |= irq_b_.set(IRQB::KeyboardTransmitEmpty); | ||||
| 					} | ||||
|  | ||||
| 					return did_interrupt; | ||||
| 				} | ||||
| 				default: break; | ||||
| 			} | ||||
| 			// TODO: events for timers 2 (baud). | ||||
| 		} | ||||
|  | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	void tick_timers() { | ||||
| 		bool did_change_interrupts = false; | ||||
| 		did_change_interrupts |= tick_timer<0>(); | ||||
| 		did_change_interrupts |= tick_timer<1>(); | ||||
| 		did_change_interrupts |= tick_timer<2>(); | ||||
| 		did_change_interrupts |= tick_timer<3>(); | ||||
| 		if(did_change_interrupts) { | ||||
| 			observer_.update_interrupts(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	void tick_floppy() { | ||||
| 		if(floppy_clocking_ != ClockingHint::Preference::None) { | ||||
| 			floppy_.run_for(Cycles(1)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) { | ||||
| 		floppy_.set_disk(disk, drive); | ||||
| 	} | ||||
|  | ||||
| 	/// Decomposes an Archimedes bus address into bank, offset and type. | ||||
| 	struct Address { | ||||
| 		constexpr Address(uint32_t bus_address) noexcept { | ||||
| 			bank = (bus_address >> 16) & 0b111; | ||||
| 			type = Type((bus_address >> 19) & 0b11); | ||||
| 			offset = bus_address & 0b1111100; | ||||
| 		} | ||||
|  | ||||
| 		/// A value from 0 to 7 indicating the device being addressed. | ||||
| 		uint32_t bank; | ||||
| 		/// A seven-bit value which is a multiple of 4, indicating the address within the bank. | ||||
| 		uint32_t offset; | ||||
| 		/// Access type. | ||||
| 		enum class Type { | ||||
| 			Sync = 0b00, | ||||
| 			Fast = 0b10, | ||||
| 			Medium = 0b01, | ||||
| 			Slow = 0b11 | ||||
| 		} type; | ||||
| 	}; | ||||
|  | ||||
| 	// Peripheral addresses on the A500: | ||||
| 	// | ||||
| 	//	fast/1	=	FDC | ||||
| 	//	sync/2	=	econet | ||||
| 	//	sync/3	= 	serial line | ||||
| 	// | ||||
| 	//	bank 4	=	podules | ||||
| 	// | ||||
| 	//	fast/5 | ||||
|  | ||||
| 	template <typename IntT> | ||||
| 	bool read(uint32_t address, IntT &destination) { | ||||
| 		const Address target(address); | ||||
|  | ||||
| 		const auto set_byte = [&](uint8_t value) { | ||||
| 			if constexpr (std::is_same_v<IntT, uint32_t>) { | ||||
| 				destination = static_cast<uint32_t>(value << 16) | 0xff'00'ff'ff; | ||||
| 			} else { | ||||
| 				destination = value; | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		// TODO: flatten the switch below, and the equivalent in `write`. | ||||
|  | ||||
| 		switch(target.bank) { | ||||
| 			default: | ||||
| 				logger.error().append("Unrecognised IOC read from %08x i.e. bank %d / type %d", address, target.bank, target.type); | ||||
| 				destination = IntT(~0); | ||||
| 			break; | ||||
|  | ||||
| 			// Bank 0: internal registers. | ||||
| 			case 0: | ||||
| 				switch(target.offset) { | ||||
| 					default: | ||||
| 						logger.error().append("Unrecognised IOC bank 0 read; offset %02x", target.offset); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x00: { | ||||
| 						uint8_t value = control_ | 0xc0; | ||||
| 						value &= ~(i2c_.data() ? 0x01 : 0x00); | ||||
| 						value &= ~(i2c_.clock() ? 0x02 : 0x00); | ||||
| 						value &= ~(floppy_.ready() ? 0x00 : 0x04); | ||||
| 						value &= ~(video_.flyback_active() ? 0x00 : 0x80);	// i.e. high during flyback. | ||||
| 						set_byte(value); | ||||
| //						logger.error().append("IOC control read: C:%d D:%d", !(value & 2), !(value & 1)); | ||||
| 					} break; | ||||
|  | ||||
| 					case 0x04: | ||||
| 						set_byte(serial_.input(IOCParty)); | ||||
| 						irq_b_.clear(IRQB::KeyboardReceiveFull); | ||||
| 						observer_.update_interrupts(); | ||||
| //						logger.error().append("IOC keyboard receive: %02x", value); | ||||
| 					break; | ||||
|  | ||||
| 					// IRQ A. | ||||
| 					case 0x10: | ||||
| 						set_byte(irq_a_.status); | ||||
| //						logger.error().append("IRQ A status is %02x", value); | ||||
| 					break; | ||||
| 					case 0x14: | ||||
| 						set_byte(irq_a_.request()); | ||||
| //						logger.error().append("IRQ A request is %02x", value); | ||||
| 					break; | ||||
| 					case 0x18: | ||||
| 						set_byte(irq_a_.mask); | ||||
| //						logger.error().append("IRQ A mask is %02x", value); | ||||
| 					break; | ||||
|  | ||||
| 					// IRQ B. | ||||
| 					case 0x20: | ||||
| 						set_byte(irq_b_.status); | ||||
| //						logger.error().append("IRQ B status is %02x", value); | ||||
| 					break; | ||||
| 					case 0x24: | ||||
| 						set_byte(irq_b_.request()); | ||||
| //						logger.error().append("IRQ B request is %02x", value); | ||||
| 					break; | ||||
| 					case 0x28: | ||||
| 						set_byte(irq_b_.mask); | ||||
| //						logger.error().append("IRQ B mask is %02x", value); | ||||
| 					break; | ||||
|  | ||||
| 					// FIQ. | ||||
| 					case 0x30: | ||||
| 						set_byte(fiq_.status); | ||||
| //						logger.error().append("FIQ status is %02x", fiq_.status); | ||||
| 					break; | ||||
| 					case 0x34: | ||||
| 						set_byte(fiq_.request()); | ||||
| //						logger.error().append("FIQ request is %02x", fiq_.request()); | ||||
| 					break; | ||||
| 					case 0x38: | ||||
| 						set_byte(fiq_.mask); | ||||
| //						logger.error().append("FIQ mask is %02x", fiq_.mask); | ||||
| 					break; | ||||
|  | ||||
| 					// Counters. | ||||
| 					case 0x40:	case 0x50:	case 0x60:	case 0x70: | ||||
| 						set_byte(counters_[(target.offset >> 4) - 0x4].output & 0xff); | ||||
| //						logger.error().append("%02x: Counter %d low is %02x", target, (target >> 4) - 0x4, value); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x44:	case 0x54:	case 0x64:	case 0x74: | ||||
| 						set_byte(counters_[(target.offset >> 4) - 0x4].output >> 8); | ||||
| //						logger.error().append("%02x: Counter %d high is %02x", target, (target >> 4) - 0x4, value); | ||||
| 					break; | ||||
| 				} | ||||
| 			break; | ||||
|  | ||||
| 			// Bank 1: the floppy disc controller. | ||||
| 			case 1: | ||||
| 				set_byte(floppy_.read(target.offset >> 2)); | ||||
| //				logger.error().append("Floppy read; offset %02x", target.offset); | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	template <typename IntT> | ||||
| 	bool write(uint32_t address, IntT bus_value) { | ||||
| 		const Address target(address); | ||||
|  | ||||
| 		// Empirically, RISC OS 3.19: | ||||
| 		//	* at 03801e88 and 03801e8c loads R8 and R9 with 0xbe0000 and 0xff0000 respectively; and | ||||
| 		//	* subsequently uses 32-bit strs (e.g. at 03801eac) to write those values to latch A. | ||||
| 		// | ||||
| 		// Given that 8-bit ARM writes duplicate the 8-bit value four times across the data bus, | ||||
| 		// my conclusion is that the IOC is probably connected to data lines 15–23. | ||||
| 		// | ||||
| 		// Hence: use @c byte to get a current 8-bit value. | ||||
| 		const auto byte = [](IntT original) -> uint8_t { | ||||
| 			if constexpr (std::is_same_v<IntT, uint32_t>) { | ||||
| 				return static_cast<uint8_t>(original >> 16); | ||||
| 			} else { | ||||
| 				return original; | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		switch(target.bank) { | ||||
| 			default: | ||||
| 				logger.error().append("Unrecognised IOC write of %02x to %08x i.e. bank %d / type %d", bus_value, address, target.bank, target.type); | ||||
| 			break; | ||||
|  | ||||
| 			// Bank 0: internal registers. | ||||
| 			case 0: | ||||
| 				switch(target.offset) { | ||||
| 					default: | ||||
| 						logger.error().append("Unrecognised IOC bank 0 write; %02x to offset %02x", bus_value, target.offset); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x00: | ||||
| 						control_ = byte(bus_value); | ||||
| 						i2c_.set_clock_data(!(bus_value & 2), !(bus_value & 1)); | ||||
|  | ||||
| 						// Per the A500 documentation: | ||||
| 						// b7: vertical sync/test input bit, so should be programmed high; | ||||
| 						// b6: input for printer acknowledgement, so should be programmed high; | ||||
| 						// b5: speaker mute; 1 = muted; | ||||
| 						// b4: "Available on the auxiliary I/O connector" | ||||
| 						// b3: "Programmed HIGH, unless Reset Mask is required." | ||||
| 						// b2: Used as the floppy disk (READY) input and must be programmed high; | ||||
| 						// b1 and b0: I2C connections as above. | ||||
| 					break; | ||||
|  | ||||
| 					case 0x04: | ||||
| 						serial_.output(IOCParty, byte(bus_value)); | ||||
| 						irq_b_.clear(IRQB::KeyboardTransmitEmpty); | ||||
| 						observer_.update_interrupts(); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x14: | ||||
| 						// b2: clear IF. | ||||
| 						// b3: clear IR. | ||||
| 						// b4: clear POR. | ||||
| 						// b5: clear TM[0]. | ||||
| 						// b6: clear TM[1]. | ||||
| 						irq_a_.clear(byte(bus_value) & 0x7c); | ||||
| 						observer_.update_interrupts(); | ||||
| 					break; | ||||
|  | ||||
| 					// Interrupts. | ||||
| 					case 0x18: | ||||
| 						irq_a_.mask = byte(bus_value); | ||||
| //						logger.error().append("IRQ A mask set to %02x", byte(bus_value)); | ||||
| 					break; | ||||
| 					case 0x28: | ||||
| 						irq_b_.mask = byte(bus_value); | ||||
| //						logger.error().append("IRQ B mask set to %02x", byte(bus_value)); | ||||
| 					break; | ||||
| 					case 0x38: | ||||
| 						fiq_.mask = byte(bus_value); | ||||
| //						logger.error().append("FIQ mask set to %02x", byte(bus_value)); | ||||
| 					break; | ||||
|  | ||||
| 					// Counters. | ||||
| 					case 0x40:	case 0x50:	case 0x60:	case 0x70: | ||||
| 						counters_[(target.offset >> 4) - 0x4].reload = uint16_t( | ||||
| 							(counters_[(target.offset >> 4) - 0x4].reload & 0xff00) | byte(bus_value) | ||||
| 						); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x44:	case 0x54:	case 0x64:	case 0x74: | ||||
| 						counters_[(target.offset >> 4) - 0x4].reload = uint16_t( | ||||
| 							(counters_[(target.offset >> 4) - 0x4].reload & 0x00ff) | (byte(bus_value) << 8) | ||||
| 						); | ||||
| 					break; | ||||
|  | ||||
| 					case 0x48:	case 0x58:	case 0x68:	case 0x78: | ||||
| 						counters_[(target.offset >> 4) - 0x4].value = counters_[(target.offset >> 4) - 0x4].reload; | ||||
| 					break; | ||||
|  | ||||
| 					case 0x4c:	case 0x5c:	case 0x6c:	case 0x7c: | ||||
| 						counters_[(target.offset >> 4) - 0x4].output = counters_[(target.offset >> 4) - 0x4].value; | ||||
| 					break; | ||||
| 				} | ||||
| 			break; | ||||
|  | ||||
| 			// Bank 1: the floppy disc controller. | ||||
| 			case 1: | ||||
| //				logger.error().append("Floppy write; %02x to offset %02x", bus_value, target.offset); | ||||
| 				floppy_.write(target.offset >> 2, byte(bus_value)); | ||||
| //				set_byte(floppy_.read(target.offset >> 2)); | ||||
| 			break; | ||||
|  | ||||
| 			// Bank 5: both the hard disk and the latches, depending on type. | ||||
| 			case 5: | ||||
| 				switch(target.type) { | ||||
| 					default: | ||||
| 						logger.error().append("Unrecognised IOC bank 5 type %d write; %02x to offset %02x", target.type, bus_value, target.offset); | ||||
| 					break; | ||||
|  | ||||
| 					case Address::Type::Fast: | ||||
| 						switch(target.offset) { | ||||
| 							default: | ||||
| 								logger.error().append("Unrecognised IOC fast bank 5 write; %02x to offset %02x", bus_value, target.offset); | ||||
| 							break; | ||||
|  | ||||
| 							case 0x00: | ||||
| 								logger.error().append("TODO: printer data write; %02x", byte(bus_value)); | ||||
| 							break; | ||||
|  | ||||
| 							case 0x18: { | ||||
| 								// TODO, per the A500 documentation: | ||||
| 								// | ||||
| 								// Latch B: | ||||
| 								//	b0: ? | ||||
| 								//	b1: double/single density; 0 = double. | ||||
| 								//	b2: ? | ||||
| 								// 	b3: floppy drive reset; 0 = reset. | ||||
| 								//	b4: printer strobe | ||||
| 								//	b5: ? | ||||
| 								//	b6: ? | ||||
| 								//	b7: Head select 3? | ||||
| 								const uint8_t value = byte(bus_value); | ||||
|  | ||||
| 								floppy_.set_is_double_density(!(value & 0x2)); | ||||
| 								if(value & 0x08) floppy_.reset(); | ||||
| //								logger.error().append("TODO: latch B write; %02x", byte(bus_value)); | ||||
| 							} break; | ||||
|  | ||||
| 							case 0x40: { | ||||
| 								const uint8_t value = byte(bus_value); | ||||
| 								floppy_.set_control(value); | ||||
|  | ||||
| 								// Set the floppy indicator on if any drive is selected, | ||||
| 								// because this emulator is compressing them all into a | ||||
| 								// single LED, and the machine has indicated 'in use'. | ||||
| 								if(activity_observer_) { | ||||
| 									activity_observer_->set_led_status(FloppyActivityLED, | ||||
| 										!(value & 0x40) && ((value & 0xf) != 0xf) | ||||
| 									); | ||||
| 								} | ||||
| 							} break; | ||||
|  | ||||
| 							case 0x48: | ||||
| 								// TODO, per the A500 documentation: | ||||
| 								// | ||||
| 								// Latch C: | ||||
| 								//		(probably not present on earlier machines?) | ||||
| 								//	b2/b3: sync polarity [b3 = V polarity, b2 = H?] | ||||
| 								//	b0/b1: VIDC master clock; 00 = 24Mhz, 01 = 25.175Mhz; 10 = 36Mhz; 11 = reserved. | ||||
|  | ||||
| 								logger.error().append("TODO: latch C write; %02x", byte(bus_value)); | ||||
| 							break; | ||||
| 						} | ||||
| 					break; | ||||
| 				} | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| //			case 0x327'0000 & AddressMask:	// Bank 7 | ||||
| //				logger.error().append("TODO: exteded external podule space"); | ||||
| //			return true; | ||||
| // | ||||
| //			case 0x336'0000 & AddressMask: | ||||
| //				logger.error().append("TODO: podule interrupt request"); | ||||
| //			return true; | ||||
| // | ||||
| //			case 0x336'0004 & AddressMask: | ||||
| //				logger.error().append("TODO: podule interrupt mask"); | ||||
| //			return true; | ||||
| // | ||||
| //			case 0x33a'0000 & AddressMask: | ||||
| //				logger.error().append("TODO: 6854 / econet write"); | ||||
| //			return true; | ||||
| // | ||||
| //			case 0x33b'0000 & AddressMask: | ||||
| //				logger.error().append("TODO: 6551 / serial line write"); | ||||
| //			return true; | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	auto &sound() 					{	return sound_;	} | ||||
| 	const auto &sound() const		{	return sound_;	} | ||||
| 	auto &video()			 		{	return video_;	} | ||||
| 	const auto &video() const 		{	return video_;	} | ||||
| 	auto &keyboard()			 	{	return keyboard_;	} | ||||
| 	const auto &keyboard() const 	{	return keyboard_;	} | ||||
|  | ||||
| 	void update_interrupts() { | ||||
| 		const auto set = [&](Interrupt &target, uint8_t flag, bool set) { | ||||
| 			if(set) { | ||||
| 				target.set(flag); | ||||
| 			} else { | ||||
| 				target.clear(flag); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		set(irq_b_, IRQB::SoundBufferPointerUsed, sound_.interrupt()); | ||||
| 		set(fiq_, FIQ::FloppyDiscInterrupt, floppy_.get_interrupt_request_line()); | ||||
| 		set(fiq_, FIQ::FloppyDiscData, floppy_.get_data_request_line()); | ||||
|  | ||||
| 		if(video_.interrupt()) { | ||||
| 			irq_a_.set(IRQA::VerticalFlyback); | ||||
| 		} | ||||
|  | ||||
| 		observer_.update_interrupts(); | ||||
| 	} | ||||
|  | ||||
| 	void set_activity_observer(Activity::Observer *observer) { | ||||
| 		activity_observer_ = observer; | ||||
| 		if(activity_observer_) { | ||||
| 			activity_observer_->register_led(FloppyActivityLED); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	Log::Logger<Log::Source::ARMIOC> logger; | ||||
| 	InterruptObserverT &observer_; | ||||
| 	Activity::Observer *activity_observer_ = nullptr; | ||||
| 	static inline const std::string FloppyActivityLED = "Drive"; | ||||
|  | ||||
| 	// IRQA, IRQB and FIQ states. | ||||
| 	struct Interrupt { | ||||
| 		uint8_t status = 0x00, mask = 0x00; | ||||
| 		uint8_t request() const { | ||||
| 			return status & mask; | ||||
| 		} | ||||
| 		bool set(uint8_t value) { | ||||
| 			status |= value; | ||||
| 			return status & mask; | ||||
| 		} | ||||
| 		void clear(uint8_t bits) { | ||||
| 			status &= ~bits; | ||||
| 		} | ||||
| 	}; | ||||
| 	Interrupt irq_a_, irq_b_, fiq_; | ||||
|  | ||||
| 	// The IOCs four counters. | ||||
| 	struct Counter { | ||||
| 		uint16_t value = 0; | ||||
| 		uint16_t reload = 0; | ||||
| 		uint16_t output = 0; | ||||
| 	}; | ||||
| 	Counter counters_[4]; | ||||
|  | ||||
| 	// The KART and keyboard beyond it. | ||||
| 	HalfDuplexSerial serial_; | ||||
| 	Keyboard keyboard_; | ||||
|  | ||||
| 	// The control register. | ||||
| 	uint8_t control_ = 0xff; | ||||
|  | ||||
| 	// The floppy disc interface. | ||||
| 	FloppyDisc<InputOutputController> floppy_; | ||||
| 	ClockingHint::Preference floppy_clocking_ = ClockingHint::Preference::None; | ||||
| 	void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) override { | ||||
| 		floppy_clocking_ = clocking; | ||||
| 	} | ||||
|  | ||||
| 	// The I2C bus. | ||||
| 	I2C::Bus i2c_; | ||||
| 	CMOSRAM cmos_; | ||||
|  | ||||
| 	// Audio and video. | ||||
| 	Sound<InputOutputController> sound_; | ||||
| 	Video<InputOutputController, ClockRateObserverT, Sound<InputOutputController>> video_; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
							
								
								
									
										240
									
								
								Machines/Acorn/Archimedes/Keyboard.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								Machines/Acorn/Archimedes/Keyboard.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,240 @@ | ||||
| // | ||||
| //  Keyboard.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 20/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "HalfDuplexSerial.hpp" | ||||
|  | ||||
| #include "../../../Outputs/Log.hpp" | ||||
| #include "../../../Inputs/Mouse.hpp" | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| // Resource for the keyboard protocol: https://github.com/tmk/tmk_keyboard/wiki/ACORN-ARCHIMEDES-Keyboard | ||||
| struct Keyboard { | ||||
| 	Keyboard(HalfDuplexSerial &serial) : serial_(serial), mouse_(*this) {} | ||||
|  | ||||
| 	void set_key_state(int row, int column, bool is_pressed) { | ||||
| 		if(!scan_keyboard_) { | ||||
| 			logger_.info().append("Ignored key event as key scanning disabled"); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Don't waste bandwidth on repeating facts. | ||||
| 		if(states_[row][column] == is_pressed) return; | ||||
| 		states_[row][column] = is_pressed; | ||||
|  | ||||
| 		// Post new key event. | ||||
| 		logger_.info().append("Posting row %d, column %d is now %s", row, column, is_pressed ? "pressed" : "released"); | ||||
| 		const uint8_t prefix = is_pressed ? 0b1100'0000 : 0b1101'0000; | ||||
| 		enqueue(static_cast<uint8_t>(prefix | row), static_cast<uint8_t>(prefix | column)); | ||||
| 		consider_dequeue(); | ||||
| 	} | ||||
|  | ||||
| 	void update() { | ||||
| 		if(serial_.events(KeyboardParty) & HalfDuplexSerial::Receive) { | ||||
| 			const auto reset = [&]() { | ||||
| 				serial_.output(KeyboardParty, HRST); | ||||
| 				state_ = State::Idle; | ||||
| 			}; | ||||
|  | ||||
| 			const uint8_t input = serial_.input(KeyboardParty); | ||||
|  | ||||
| 			// A reset command is always accepted, usurping any other state. | ||||
| 			if(input == HRST) { | ||||
| 				logger_.info().append("HRST; resetting"); | ||||
| 				state_ = State::ExpectingRAK1; | ||||
| 				event_queue_.clear(); | ||||
| 				serial_.output(KeyboardParty, HRST); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			switch(state_) { | ||||
| 				case State::ExpectingACK: | ||||
| 					if(input != NACK && input != SMAK && input != MACK && input != SACK) { | ||||
| 						logger_.error().append("No ack; requesting reset"); | ||||
| 						reset(); | ||||
| 						break; | ||||
| 					} | ||||
| 					state_ = State::Idle; | ||||
| 					[[fallthrough]]; | ||||
|  | ||||
| 				case State::Idle: | ||||
| 					switch(input) { | ||||
| 						case RQID:	// Post keyboard ID. | ||||
| 							serial_.output(KeyboardParty, 0x81);	// Declare this to be a UK keyboard. | ||||
| 							logger_.info().append("RQID; responded with 0x81"); | ||||
| 						break; | ||||
|  | ||||
| 						case PRST:	// "1-byte command, does nothing." | ||||
| 							logger_.info().append("PRST; ignored"); | ||||
| 						break; | ||||
|  | ||||
| 						case RQMP: | ||||
| 							logger_.error().append("RQMP; TODO: respond something other than 0, 0"); | ||||
| 							enqueue(0, 0); | ||||
| 						break; | ||||
|  | ||||
| 						case NACK:	case SMAK:	case MACK:	case SACK: | ||||
| 							scan_keyboard_ = input & 1; | ||||
| 							scan_mouse_ = input & 2; | ||||
| 							logger_.info().append("ACK; keyboard:%d mouse:%d", scan_keyboard_, scan_mouse_); | ||||
| 						break; | ||||
|  | ||||
| 						default: | ||||
| 							if((input & 0b1111'0000) == 0b0100'0000) { | ||||
| 								// RQPD; request to echo the low nibble. | ||||
| 								serial_.output(KeyboardParty, 0b1110'0000 | (input & 0b1111)); | ||||
| 								logger_.info().append("RQPD; echoing %x", input & 0b1111); | ||||
| 							} else if(!(input & 0b1111'1000)) { | ||||
| 								// LEDS: should set LED outputs. | ||||
| 								logger_.error().append("TODO: set LEDs %d%d%d", static_cast<bool>(input&4), static_cast<bool>(input&2), static_cast<bool>(input&1)); | ||||
| 							} else { | ||||
| 								logger_.info().append("Ignoring unrecognised command %02x received in idle state", input); | ||||
| 							} | ||||
| 						break; | ||||
| 					} | ||||
| 				break; | ||||
|  | ||||
| 				case State::ExpectingRAK1: | ||||
| 					if(input != RAK1) { | ||||
| 						logger_.info().append("Didn't get RAK1; resetting"); | ||||
| 						reset(); | ||||
| 						break; | ||||
| 					} | ||||
| 					logger_.info().append("Got RAK1; echoing"); | ||||
| 					serial_.output(KeyboardParty, input); | ||||
| 					state_ = State::ExpectingRAK2; | ||||
| 				break; | ||||
|  | ||||
| 				case State::ExpectingRAK2: | ||||
| 					if(input != RAK2) { | ||||
| 						logger_.info().append("Didn't get RAK2; resetting"); | ||||
| 						reset(); | ||||
| 						break; | ||||
| 					} | ||||
| 					logger_.info().append("Got RAK2; echoing"); | ||||
| 					serial_.output(KeyboardParty, input); | ||||
| 					state_ = State::ExpectingACK; | ||||
| 				break; | ||||
|  | ||||
| 				case State::ExpectingBACK: | ||||
| 					if(input != BACK) { | ||||
| 						logger_.info().append("Didn't get BACK; resetting"); | ||||
| 						reset(); | ||||
| 						break; | ||||
| 					} | ||||
| 					logger_.info().append("Got BACK; posting next byte"); | ||||
| 					dequeue_next(); | ||||
| 					state_ = State::ExpectingACK; | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		consider_dequeue(); | ||||
| 	} | ||||
|  | ||||
| 	void consider_dequeue() { | ||||
| 		if(state_ == State::Idle) { | ||||
| 			// If the key event queue is empty, grab as much mouse motion | ||||
| 			// as available. | ||||
| 			if(event_queue_.empty()) { | ||||
| 				const int x = std::clamp(mouse_x_, -0x3f, 0x3f); | ||||
| 				const int y = std::clamp(mouse_y_, -0x3f, 0x3f); | ||||
| 				mouse_x_ -= x; | ||||
| 				mouse_y_ -= y; | ||||
|  | ||||
| 				if(x || y) { | ||||
| 					enqueue(static_cast<uint8_t>(x) & 0x7f, static_cast<uint8_t>(-y) & 0x7f); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if(dequeue_next()) { | ||||
| 				state_ = State::ExpectingBACK; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	Inputs::Mouse &mouse() { | ||||
| 		return mouse_; | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	HalfDuplexSerial &serial_; | ||||
| 	Log::Logger<Log::Source::Keyboard> logger_; | ||||
|  | ||||
| 	bool states_[16][16]{}; | ||||
|  | ||||
| 	bool scan_keyboard_ = false; | ||||
| 	bool scan_mouse_ = false; | ||||
| 	enum class State { | ||||
| 		ExpectingRAK1,	// Post a RAK1 and proceed to ExpectingRAK2 if RAK1 is received; otherwise request a reset. | ||||
| 		ExpectingRAK2,	// Post a RAK2 and proceed to ExpectingACK if RAK2 is received; otherwise request a reset. | ||||
| 		ExpectingACK,	// Process NACK, SACK, MACK or SMAK if received; otherwise request a reset. | ||||
|  | ||||
| 		Idle,			// Process any of: NACK, SACK, MACK, SMAK, RQID, RQMP, RQPD or LEDS if received; also | ||||
| 						// unilaterally begin post a byte pair enqueued but not yet sent if any are waiting. | ||||
|  | ||||
| 		ExpectingBACK,	// Dequeue and post one further byte if BACK is received; otherwise request a reset. | ||||
| 	} state_ = State::Idle; | ||||
|  | ||||
| 	std::vector<uint8_t> event_queue_; | ||||
| 	void enqueue(uint8_t first, uint8_t second) { | ||||
| 		event_queue_.push_back(first); | ||||
| 		event_queue_.push_back(second); | ||||
| 	} | ||||
| 	bool dequeue_next() { | ||||
| 		// To consider: a cheaper approach to the queue than this; in practice events | ||||
| 		// are 'rare' so it's not high priority. | ||||
| 		if(event_queue_.empty()) return false; | ||||
| 		serial_.output(KeyboardParty, event_queue_[0]); | ||||
| 		event_queue_.erase(event_queue_.begin()); | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	static constexpr uint8_t HRST	= 0b1111'1111;	// Keyboard reset. | ||||
| 	static constexpr uint8_t RAK1	= 0b1111'1110;	// Reset response #1. | ||||
| 	static constexpr uint8_t RAK2	= 0b1111'1101;	// Reset response #2. | ||||
|  | ||||
| 	static constexpr uint8_t RQID	= 0b0010'0000;	// Request for keyboard ID. | ||||
| 	static constexpr uint8_t RQMP	= 0b0010'0010;	// Request for mouse data. | ||||
|  | ||||
| 	static constexpr uint8_t BACK	= 0b0011'1111;	// Acknowledge for first keyboard data byte pair. | ||||
| 	static constexpr uint8_t NACK	= 0b0011'0000;	// Acknowledge for last keyboard data byte pair, disables both scanning and mouse. | ||||
| 	static constexpr uint8_t SACK	= 0b0011'0001;	// Last data byte acknowledge, enabling scanning but disabling mouse. | ||||
| 	static constexpr uint8_t MACK	= 0b0011'0010;	// Last data byte acknowledge, disabling scanning but enabling mouse. | ||||
| 	static constexpr uint8_t SMAK	= 0b0011'0011;	// Last data byte acknowledge, enabling scanning and mouse. | ||||
| 	static constexpr uint8_t PRST	= 0b0010'0001;	// Does nothing. | ||||
|  | ||||
|  | ||||
| 	struct Mouse: public Inputs::Mouse { | ||||
| 		Mouse(Keyboard &keyboard): keyboard_(keyboard) {} | ||||
|  | ||||
| 		void move(int x, int y) override { | ||||
| 			keyboard_.mouse_x_ += x; | ||||
| 			keyboard_.mouse_y_ += y; | ||||
| 		} | ||||
|  | ||||
| 		int get_number_of_buttons() override { | ||||
| 			return 3; | ||||
| 		} | ||||
|  | ||||
| 		virtual void set_button_pressed(int index, bool is_pressed) override { | ||||
| 			keyboard_.set_key_state(7, index, is_pressed); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		Keyboard &keyboard_; | ||||
| 	}; | ||||
| 	Mouse mouse_; | ||||
|  | ||||
| 	int mouse_x_ = 0; | ||||
| 	int mouse_y_ = 0; | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										148
									
								
								Machines/Acorn/Archimedes/KeyboardMapper.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								Machines/Acorn/Archimedes/KeyboardMapper.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| // | ||||
| //  KeyboardMapper.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 23/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../KeyboardMachine.hpp" | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper { | ||||
| 	public: | ||||
| 		static constexpr uint16_t map(int row, int column) { | ||||
| 			return static_cast<uint16_t>((row << 4) | column); | ||||
| 		} | ||||
|  | ||||
| 		static constexpr int row(uint16_t key) { | ||||
| 			return key >> 4; | ||||
| 		} | ||||
|  | ||||
| 		static constexpr int column(uint16_t key) { | ||||
| 			return key & 0xf; | ||||
| 		} | ||||
|  | ||||
| 		// Adapted from the A500 Series Technical Reference Manual. | ||||
| 		uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const override { | ||||
| 			using k = Inputs::Keyboard::Key; | ||||
| 			switch(key) { | ||||
| 				case k::Escape:			return map(0, 0); | ||||
| 				case k::F1:				return map(0, 1); | ||||
| 				case k::F2:				return map(0, 2); | ||||
| 				case k::F3:				return map(0, 3); | ||||
| 				case k::F4:				return map(0, 4); | ||||
| 				case k::F5:				return map(0, 5); | ||||
| 				case k::F6:				return map(0, 6); | ||||
| 				case k::F7:				return map(0, 7); | ||||
| 				case k::F8:				return map(0, 8); | ||||
| 				case k::F9:				return map(0, 9); | ||||
| 				case k::F10:			return map(0, 10); | ||||
| 				case k::F11:			return map(0, 11); | ||||
| 				case k::F12:			return map(0, 12); | ||||
| 				case k::PrintScreen:	return map(0, 13); | ||||
| 				case k::ScrollLock:		return map(0, 14); | ||||
| 				case k::Pause:			return map(0, 15); | ||||
|  | ||||
| 				case k::BackTick:	return map(1, 0); | ||||
| 				case k::k1:			return map(1, 1); | ||||
| 				case k::k2:			return map(1, 2); | ||||
| 				case k::k3:			return map(1, 3); | ||||
| 				case k::k4:			return map(1, 4); | ||||
| 				case k::k5:			return map(1, 5); | ||||
| 				case k::k6:			return map(1, 6); | ||||
| 				case k::k7:			return map(1, 7); | ||||
| 				case k::k8:			return map(1, 8); | ||||
| 				case k::k9:			return map(1, 9); | ||||
| 				case k::k0:			return map(1, 10); | ||||
| 				case k::Hyphen:		return map(1, 11); | ||||
| 				case k::Equals:		return map(1, 12); | ||||
| 				// TODO: pound key. | ||||
| 				case k::Backspace:	return map(1, 14); | ||||
| 				case k::Insert:		return map(1, 15); | ||||
|  | ||||
| 				case k::Home:			return map(2, 0); | ||||
| 				case k::PageUp:			return map(2, 1); | ||||
| 				case k::NumLock:		return map(2, 2); | ||||
| 				case k::KeypadSlash:	return map(2, 3); | ||||
| 				case k::KeypadAsterisk:	return map(2, 4); | ||||
| 				// TODO: keypad hash key | ||||
| 				case k::Tab:			return map(2, 6); | ||||
| 				case k::Q:				return map(2, 7); | ||||
| 				case k::W:				return map(2, 8); | ||||
| 				case k::E:				return map(2, 9); | ||||
| 				case k::R:				return map(2, 10); | ||||
| 				case k::T:				return map(2, 11); | ||||
| 				case k::Y:				return map(2, 12); | ||||
| 				case k::U:				return map(2, 13); | ||||
| 				case k::I:				return map(2, 14); | ||||
| 				case k::O:				return map(2, 15); | ||||
|  | ||||
| 				case k::P:					return map(3, 0); | ||||
| 				case k::OpenSquareBracket:	return map(3, 1); | ||||
| 				case k::CloseSquareBracket:	return map(3, 2); | ||||
| 				case k::Backslash:			return map(3, 3); | ||||
| 				case k::Delete:				return map(3, 4); | ||||
| 				case k::End:				return map(3, 5); | ||||
| 				case k::PageDown:			return map(3, 6); | ||||
| 				case k::Keypad7:			return map(3, 7); | ||||
| 				case k::Keypad8:			return map(3, 8); | ||||
| 				case k::Keypad9:			return map(3, 9); | ||||
| 				case k::KeypadMinus:		return map(3, 10); | ||||
| 				case k::LeftControl:		return map(3, 11); | ||||
| 				case k::A:					return map(3, 12); | ||||
| 				case k::S:					return map(3, 13); | ||||
| 				case k::D:					return map(3, 14); | ||||
| 				case k::F:					return map(3, 15); | ||||
|  | ||||
| 				case k::G:			return map(4, 0); | ||||
| 				case k::H:			return map(4, 1); | ||||
| 				case k::J:			return map(4, 2); | ||||
| 				case k::K:			return map(4, 3); | ||||
| 				case k::L:			return map(4, 4); | ||||
| 				case k::Semicolon:	return map(4, 5); | ||||
| 				case k::Quote:		return map(4, 6); | ||||
| 				case k::Enter:		return map(4, 7); | ||||
| 				case k::Keypad4:	return map(4, 8); | ||||
| 				case k::Keypad5:	return map(4, 9); | ||||
| 				case k::Keypad6:	return map(4, 10); | ||||
| 				case k::KeypadPlus:	return map(4, 11); | ||||
| 				case k::LeftShift:	return map(4, 12); | ||||
| 				case k::Z:			return map(4, 14); | ||||
| 				case k::X:			return map(4, 15); | ||||
|  | ||||
| 				case k::C:				return map(5, 0); | ||||
| 				case k::V:				return map(5, 1); | ||||
| 				case k::B:				return map(5, 2); | ||||
| 				case k::N:				return map(5, 3); | ||||
| 				case k::M:				return map(5, 4); | ||||
| 				case k::Comma:			return map(5, 5); | ||||
| 				case k::FullStop:		return map(5, 6); | ||||
| 				case k::ForwardSlash:	return map(5, 7); | ||||
| 				case k::RightShift:		return map(5, 8); | ||||
| 				case k::Up:				return map(5, 9); | ||||
| 				case k::Keypad1:		return map(5, 10); | ||||
| 				case k::Keypad2:		return map(5, 11); | ||||
| 				case k::Keypad3:		return map(5, 12); | ||||
| 				case k::CapsLock:		return map(5, 13); | ||||
| 				case k::LeftOption:		return map(5, 14); | ||||
| 				case k::Space:			return map(5, 15); | ||||
|  | ||||
| 				case k::RightOption:		return map(6, 0); | ||||
| 				case k::RightControl:		return map(6, 1); | ||||
| 				case k::Left:				return map(6, 2); | ||||
| 				case k::Down:				return map(6, 3); | ||||
| 				case k::Right:				return map(6, 4); | ||||
| 				case k::Keypad0:			return map(6, 5); | ||||
| 				case k::KeypadDecimalPoint:	return map(6, 6); | ||||
| 				case k::KeypadEnter:		return map(6, 7); | ||||
|  | ||||
| 				default:	return MachineTypes::MappedKeyboardMachine::KeyNotMapped; | ||||
| 			} | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										507
									
								
								Machines/Acorn/Archimedes/MemoryController.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										507
									
								
								Machines/Acorn/Archimedes/MemoryController.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,507 @@ | ||||
| // | ||||
| //  MemoryController.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 20/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "InputOutputController.hpp" | ||||
| #include "Video.hpp" | ||||
| #include "Sound.hpp" | ||||
|  | ||||
| #include "../../../InstructionSets/ARM/Registers.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
| #include "../../../Activity/Observer.hpp" | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| /// Provides the mask with all bits set in the range [start, end], where start must  be >= end. | ||||
| template <int start, int end> struct BitMask { | ||||
| 	static_assert(start >= end); | ||||
| 	static constexpr uint32_t value = ((1 << (start + 1)) - 1) - ((1 << end) - 1); | ||||
| }; | ||||
| static_assert(BitMask<0, 0>::value == 1); | ||||
| static_assert(BitMask<1, 1>::value == 2); | ||||
| static_assert(BitMask<15, 15>::value == 32768); | ||||
| static_assert(BitMask<15, 0>::value == 0xffff); | ||||
| static_assert(BitMask<15, 14>::value == 49152); | ||||
|  | ||||
|  | ||||
| /// Models the MEMC, making this the Archimedes bus. Owns various other chips on the bus as a result. | ||||
| template <typename InterruptObserverT, typename ClockRateObserverT> | ||||
| struct MemoryController { | ||||
| 	MemoryController(InterruptObserverT &observer, ClockRateObserverT &clock_rate_observer) : | ||||
| 		ioc_(observer, clock_rate_observer, ram_.data()) { | ||||
| 		read_zones_[0] = ReadZone::HighROM;	// Temporarily put high ROM at address 0. | ||||
| 											// TODO: could I just copy it in? Or, at least, | ||||
| 											// could I detect at ROM loading time whether I can? | ||||
| 	} | ||||
|  | ||||
| 	int interrupt_mask() const { | ||||
| 		return ioc_.interrupt_mask(); | ||||
| 	} | ||||
|  | ||||
| 	void set_rom(const std::vector<uint8_t> &rom) { | ||||
| 		if(rom_.size() % rom.size() || rom.size() > rom_.size()) { | ||||
| 			// TODO: throw. | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Copy in as many times as it'll fit. | ||||
| 		std::size_t base = 0; | ||||
| 		while(base < rom_.size()) { | ||||
| 			std::copy( | ||||
| 				rom.begin(), | ||||
| 				rom.end(), | ||||
| 				rom_.begin() + base); | ||||
| 			base += rom.size(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	template <typename IntT> | ||||
| 	uint32_t aligned(uint32_t address) { | ||||
| 		if constexpr (std::is_same_v<IntT, uint32_t>) { | ||||
| 			return address & static_cast<uint32_t>(~3); | ||||
| 		} | ||||
| 		return address; | ||||
| 	} | ||||
|  | ||||
| 	template <typename IntT> | ||||
| 	bool write(uint32_t address, IntT source, InstructionSet::ARM::Mode, bool trans) { | ||||
| 		switch(write_zones_[(address >> 21) & 31]) { | ||||
| 			case WriteZone::LogicallyMappedRAM: { | ||||
| 				const auto item = logical_ram<IntT, false>(address, trans); | ||||
| 				if(item < reinterpret_cast<IntT *>(ram_.data())) { | ||||
| 					return false; | ||||
| 				} | ||||
| 				*item = source; | ||||
| 			} break; | ||||
|  | ||||
| 			case WriteZone::PhysicallyMappedRAM: | ||||
| 				if(trans) return false; | ||||
| 				physical_ram<IntT>(address) = source; | ||||
| 			break; | ||||
|  | ||||
| 			case WriteZone::DMAAndMEMC: { | ||||
| 				if(trans) return false; | ||||
|  | ||||
| 				const auto buffer_address = [](uint32_t source) -> uint32_t { | ||||
| 					return (source & 0x1'fffc) << 2; | ||||
| 				}; | ||||
|  | ||||
| 				// The MEMC itself isn't on the data bus; all values below should be taken from `address`. | ||||
| 				switch((address >> 17) & 0b111) { | ||||
| 					case 0b000:	ioc_.video().set_frame_start(buffer_address(address));	break; | ||||
| 					case 0b001:	ioc_.video().set_buffer_start(buffer_address(address));	break; | ||||
| 					case 0b010:	ioc_.video().set_buffer_end(buffer_address(address));	break; | ||||
| 					case 0b011: ioc_.video().set_cursor_start(buffer_address(address));	break; | ||||
|  | ||||
| 					case 0b100:	ioc_.sound().set_next_start(buffer_address(address));	break; | ||||
| 					case 0b101:	ioc_.sound().set_next_end(buffer_address(address));		break; | ||||
| 					case 0b110:	ioc_.sound().swap();									break; | ||||
|  | ||||
| 					case 0b111: | ||||
| 						os_mode_ = address & (1 << 12); | ||||
| 						sound_dma_enable_ = address & (1 << 11); | ||||
| 						ioc_.sound().set_dma_enabled(sound_dma_enable_); | ||||
| 						video_dma_enable_ = address & (1 << 10); | ||||
| 						switch((address >> 8) & 3) { | ||||
| 							default: | ||||
| 								dynamic_ram_refresh_ = DynamicRAMRefresh::None; | ||||
| 							break; | ||||
| 							case 0b01: | ||||
| 							case 0b11: | ||||
| 								dynamic_ram_refresh_ = DynamicRAMRefresh((address >> 8) & 3); | ||||
| 							break; | ||||
| 						} | ||||
| 						high_rom_access_time_ = ROMAccessTime((address >> 6) & 3); | ||||
| 						low_rom_access_time_ = ROMAccessTime((address >> 4) & 3); | ||||
| 						page_size_ = PageSize((address >> 2) & 3); | ||||
| 						switch(page_size_) { | ||||
| 							default: | ||||
| 							case PageSize::kb4: | ||||
| 								page_address_shift_ = 12; | ||||
| 								page_adddress_mask_ = 0x0fff; | ||||
| 							break; | ||||
| 							case PageSize::kb8: | ||||
| 								page_address_shift_ = 13; | ||||
| 								page_adddress_mask_ = 0x1fff; | ||||
| 							break; | ||||
| 							case PageSize::kb16: | ||||
| 								page_address_shift_ = 14; | ||||
| 								page_adddress_mask_ = 0x3fff; | ||||
| 							break; | ||||
| 							case PageSize::kb32: | ||||
| 								page_address_shift_ = 15; | ||||
| 								page_adddress_mask_ = 0x7fff; | ||||
| 							break; | ||||
| 						} | ||||
|  | ||||
| 						logger.info().append("MEMC Control: %08x -> OS:%d sound:%d video:%d refresh:%d high:%d low:%d size:%d", address, os_mode_, sound_dma_enable_, video_dma_enable_, dynamic_ram_refresh_, high_rom_access_time_, low_rom_access_time_, page_size_); | ||||
| 						map_dirty_ = true; | ||||
| 					break; | ||||
| 				} | ||||
| 			} break; | ||||
|  | ||||
| 			case WriteZone::IOControllers: | ||||
| 				if(trans) return false; | ||||
| 				ioc_.template write<IntT>(address, source); | ||||
| 			break; | ||||
|  | ||||
| 			case WriteZone::VideoController: | ||||
| 				if(trans) return false; | ||||
| 				// TODO: handle byte writes correctly. | ||||
| 				ioc_.video().write(source); | ||||
| 			break; | ||||
|  | ||||
| 			case WriteZone::AddressTranslator: | ||||
| 				if(trans) return false; | ||||
| //				printf("Translator write at %08x; replaces %08x\n", address, pages_[address & 0x7f]); | ||||
| 				pages_[address & 0x7f] = address; | ||||
| 				map_dirty_ = true; | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	template <typename IntT> | ||||
| 	bool read(uint32_t address, IntT &source, bool trans) { | ||||
| 		switch(read_zones_[(address >> 21) & 31]) { | ||||
| 			case ReadZone::LogicallyMappedRAM: { | ||||
| 				const auto item = logical_ram<IntT, true>(address, trans); | ||||
| 				if(item < reinterpret_cast<IntT *>(ram_.data())) { | ||||
| 					return false; | ||||
| 				} | ||||
| 				source = *item; | ||||
| 			} break; | ||||
|  | ||||
| 			case ReadZone::HighROM: | ||||
| 				// Real test is: require A24=A25=0, then A25=1. | ||||
| 				read_zones_[0] = ReadZone::LogicallyMappedRAM; | ||||
| 				source = high_rom<IntT>(address); | ||||
| 			break; | ||||
|  | ||||
| 			case ReadZone::PhysicallyMappedRAM: | ||||
| 				if(trans) return false; | ||||
| 				source = physical_ram<IntT>(address); | ||||
| 			break; | ||||
|  | ||||
| 			case ReadZone::LowROM: | ||||
| //				logger.error().append("TODO: Low ROM read from %08x", address); | ||||
| 				source = IntT(~0); | ||||
| 			break; | ||||
|  | ||||
| 			case ReadZone::IOControllers: | ||||
| 				if(trans) return false; | ||||
| 				ioc_.template read<IntT>(address, source); | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	template <typename IntT> | ||||
| 	bool read(uint32_t address, IntT &source, InstructionSet::ARM::Mode, bool trans) { | ||||
| 		return read(address, source, trans); | ||||
| 	} | ||||
|  | ||||
| 	// | ||||
| 	// Expose various IOC-owned things. | ||||
| 	// | ||||
| 	void tick_timers()				{	ioc_.tick_timers();		} | ||||
| 	void tick_floppy() 				{	ioc_.tick_floppy();		} | ||||
| 	void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) { | ||||
| 		ioc_.set_disk(disk, drive); | ||||
| 	} | ||||
|  | ||||
| 	Outputs::Speaker::Speaker *speaker() { | ||||
| 		return ioc_.sound().speaker(); | ||||
| 	} | ||||
|  | ||||
| 	auto &sound() 					{	return ioc_.sound();	} | ||||
| 	const auto &sound() const	 	{	return ioc_.sound();	} | ||||
| 	auto &video()					{	return ioc_.video();	} | ||||
| 	const auto &video() const		{	return ioc_.video();	} | ||||
| 	auto &keyboard()				{	return ioc_.keyboard();	} | ||||
| 	const auto &keyboard() const	{	return ioc_.keyboard();	} | ||||
|  | ||||
| 	void set_activity_observer(Activity::Observer *observer) { | ||||
| 		ioc_.set_activity_observer(observer); | ||||
| 	} | ||||
|  | ||||
| 	private: | ||||
| 		Log::Logger<Log::Source::ARMIOC> logger; | ||||
|  | ||||
| 		enum class ReadZone { | ||||
| 			LogicallyMappedRAM, | ||||
| 			PhysicallyMappedRAM, | ||||
| 			IOControllers, | ||||
| 			LowROM, | ||||
| 			HighROM, | ||||
| 		}; | ||||
| 		enum class WriteZone { | ||||
| 			LogicallyMappedRAM, | ||||
| 			PhysicallyMappedRAM, | ||||
| 			IOControllers, | ||||
| 			VideoController, | ||||
| 			DMAAndMEMC, | ||||
| 			AddressTranslator, | ||||
| 		}; | ||||
| 		template <bool is_read> | ||||
| 		using Zone = std::conditional_t<is_read, ReadZone, WriteZone>; | ||||
|  | ||||
| 		template <bool is_read> | ||||
| 		static std::array<Zone<is_read>, 0x20> zones() { | ||||
| 			std::array<Zone<is_read>, 0x20> zones{}; | ||||
| 			for(size_t c = 0; c < zones.size(); c++) { | ||||
| 				const auto address = c << 21; | ||||
| 				if(address < 0x200'0000) { | ||||
| 					zones[c] = Zone<is_read>::LogicallyMappedRAM; | ||||
| 				} else if(address < 0x300'0000) { | ||||
| 					zones[c] = Zone<is_read>::PhysicallyMappedRAM; | ||||
| 				} else if(address < 0x340'0000) { | ||||
| 					zones[c] = Zone<is_read>::IOControllers; | ||||
| 				} else if(address < 0x360'0000) { | ||||
| 					if constexpr (is_read) { | ||||
| 						zones[c] = Zone<is_read>::LowROM; | ||||
| 					} else { | ||||
| 						zones[c] = Zone<is_read>::VideoController; | ||||
| 					} | ||||
| 				} else if(address < 0x380'0000) { | ||||
| 					if constexpr (is_read) { | ||||
| 						zones[c] = Zone<is_read>::LowROM; | ||||
| 					} else { | ||||
| 						zones[c] = Zone<is_read>::DMAAndMEMC; | ||||
| 					} | ||||
| 				} else { | ||||
| 					if constexpr (is_read) { | ||||
| 						zones[c] = Zone<is_read>::HighROM; | ||||
| 					} else { | ||||
| 						zones[c] = Zone<is_read>::AddressTranslator; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			return zones; | ||||
| 		} | ||||
|  | ||||
| 		bool has_moved_rom_ = false; | ||||
| 		std::array<uint8_t, 2*1024*1024> rom_; | ||||
| 		std::array<uint8_t, 4*1024*1024> ram_{}; | ||||
| 		InputOutputController<InterruptObserverT, ClockRateObserverT> ioc_; | ||||
|  | ||||
| 		template <typename IntT> | ||||
| 		IntT &physical_ram(uint32_t address) { | ||||
| 			address = aligned<IntT>(address); | ||||
| 			address &= (ram_.size() - 1); | ||||
| 			return *reinterpret_cast<IntT *>(&ram_[address]); | ||||
| 		} | ||||
|  | ||||
| 		template <typename IntT> | ||||
| 		IntT &high_rom(uint32_t address) { | ||||
| 			address = aligned<IntT>(address); | ||||
| 			return *reinterpret_cast<IntT *>(&rom_[address & (rom_.size() - 1)]); | ||||
| 		} | ||||
|  | ||||
| 		std::array<ReadZone, 0x20> read_zones_ = zones<true>(); | ||||
| 		const std::array<WriteZone, 0x20> write_zones_ = zones<false>(); | ||||
|  | ||||
| 		// Control register values. | ||||
| 		bool os_mode_ = false; | ||||
| 		bool sound_dma_enable_ = false; | ||||
| 		bool video_dma_enable_ = false;	// "Unaffected" by reset, so here picked arbitrarily. | ||||
|  | ||||
| 		enum class DynamicRAMRefresh { | ||||
| 			None = 0b00, | ||||
| 			DuringFlyback = 0b01, | ||||
| 			Continuous = 0b11, | ||||
| 		} dynamic_ram_refresh_ = DynamicRAMRefresh::None;	// State at reset is undefined; constrain to a valid enum value. | ||||
|  | ||||
| 		enum class ROMAccessTime { | ||||
| 			ns450 = 0b00, | ||||
| 			ns325 = 0b01, | ||||
| 			ns200 = 0b10, | ||||
| 			ns200with60nsNibble = 0b11, | ||||
| 		} high_rom_access_time_ = ROMAccessTime::ns450, low_rom_access_time_ = ROMAccessTime::ns450; | ||||
|  | ||||
| 		enum class PageSize { | ||||
| 			kb4 = 0b00, | ||||
| 			kb8 = 0b01, | ||||
| 			kb16 = 0b10, | ||||
| 			kb32 = 0b11, | ||||
| 		} page_size_ = PageSize::kb4; | ||||
| 		int page_address_shift_ = 12; | ||||
| 		uint32_t page_adddress_mask_ = 0xffff; | ||||
|  | ||||
| 		// Address translator. | ||||
| 		// | ||||
| 		// MEMC contains one entry per a physical page number, indicating where it goes logically. | ||||
| 		// Any logical access is tested against all 128 mappings. So that's backwards compared to | ||||
| 		// the ideal for an emulator, which would map from logical to physical, even if a lot more | ||||
| 		// compact — there are always 128 physical pages; there are up to 8192 logical pages. | ||||
| 		// | ||||
| 		// So captured here are both the physical -> logical map as representative of the real | ||||
| 		// hardware, and the reverse logical -> physical map, which is built (and rebuilt, and rebuilt) | ||||
| 		// from the other. | ||||
|  | ||||
| 		// Physical to logical mapping. | ||||
| 		std::array<uint32_t, 128> pages_{}; | ||||
|  | ||||
| 		// Logical to physical mapping; this is divided by 'access mode' | ||||
| 		// (i.e. the combination of read/write, trans and OS mode flags, | ||||
| 		// as multipliexed by the @c mapping() function) because mapping | ||||
| 		// varies by mode — not just in terms of restricting access, but | ||||
| 		// actually presenting different memory. | ||||
| 		using MapTarget = std::array<uint8_t *, 8192>; | ||||
| 		std::array<MapTarget, 6> mapping_; | ||||
|  | ||||
| 		template <bool is_read> | ||||
| 		MapTarget &mapping(bool trans, bool os_mode) { | ||||
| 			const size_t index = (is_read ? 1 : 0) | (os_mode ? 2 : 0) | ((trans && !os_mode) ? 4 : 0); | ||||
| 			return mapping_[index]; | ||||
| 		} | ||||
|  | ||||
| 		bool map_dirty_ = true; | ||||
|  | ||||
| 		/// @returns A pointer to somewhere in @c ram_ if RAM is mapped to this area, or a pointer to somewhere lower than @c ram_.data() otherwise. | ||||
| 		template <typename IntT, bool is_read> | ||||
| 		IntT *logical_ram(uint32_t address, bool trans) { | ||||
| 			// Possibly TODO: this recompute-if-dirty flag is supposed to ameliorate for an expensive | ||||
| 			// mapping process. It can be eliminated when the process is improved. | ||||
| 			if(map_dirty_) { | ||||
| 				update_mapping(); | ||||
| 				map_dirty_ = false; | ||||
| 			} | ||||
| 			address = aligned<IntT>(address); | ||||
| 			address &= 0x1ff'ffff; | ||||
| 			const size_t page = address >> page_address_shift_; | ||||
|  | ||||
| 			const auto &map = mapping<is_read>(trans, os_mode_); | ||||
| 			address &= page_adddress_mask_; | ||||
| 			return reinterpret_cast<IntT *>(&map[page][address]); | ||||
| 		} | ||||
|  | ||||
| 		void update_mapping() { | ||||
| 			// For each physical page, project it into logical space. | ||||
| 			switch(page_size_) { | ||||
| 				default: | ||||
| 				case PageSize::kb4:		update_mapping<PageSize::kb4>();	break; | ||||
| 				case PageSize::kb8:		update_mapping<PageSize::kb8>();	break; | ||||
| 				case PageSize::kb16:	update_mapping<PageSize::kb16>();	break; | ||||
| 				case PageSize::kb32:	update_mapping<PageSize::kb32>();	break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		template <PageSize size> | ||||
| 		void update_mapping() { | ||||
| 			// Clear all logical mappings. | ||||
| 			for(auto &map: mapping_) { | ||||
| 				// Seed all pointers to an address sufficiently far lower than the beginning of RAM as to mark | ||||
| 				// the entire page as unmapped no matter what offset is added. | ||||
| 				std::fill(map.begin(), map.end(), ram_.data() - 32768); | ||||
| 			} | ||||
|  | ||||
| 			// For each physical page, project it into logical space | ||||
| 			// and store it. | ||||
| 			for(const auto page: pages_) { | ||||
| 				uint32_t physical, logical; | ||||
|  | ||||
| 				switch(size) { | ||||
| 					case PageSize::kb4: | ||||
| 						// 4kb: | ||||
| 						//		A[6:0] -> PPN[6:0] | ||||
| 						//		A[11:10] -> LPN[12:11];	A[22:12] -> LPN[10:0]	i.e. 8192 logical pages | ||||
| 						physical = page & BitMask<6, 0>::value; | ||||
|  | ||||
| 						physical <<= 12; | ||||
|  | ||||
| 						logical = (page & BitMask<11, 10>::value) << 1; | ||||
| 						logical |= (page & BitMask<22, 12>::value) >> 12; | ||||
| 					break; | ||||
|  | ||||
| 					case PageSize::kb8: | ||||
| 						// 8kb: | ||||
| 						//		A[0] -> PPN[6]; A[6:1] -> PPN[5:0] | ||||
| 						//		A[11:10] -> LPN[11:10];	A[22:13] -> LPN[9:0]	i.e. 4096 logical pages | ||||
| 						physical = (page & BitMask<0, 0>::value) << 6; | ||||
| 						physical |= (page & BitMask<6, 1>::value) >> 1; | ||||
|  | ||||
| 						physical <<= 13; | ||||
|  | ||||
| 						logical = page & BitMask<11, 10>::value; | ||||
| 						logical |= (page & BitMask<22, 13>::value) >> 13; | ||||
| 					break; | ||||
|  | ||||
| 					case PageSize::kb16: | ||||
| 						// 16kb: | ||||
| 						//		A[1:0] -> PPN[6:5]; A[6:2] -> PPN[4:0] | ||||
| 						//		A[11:10] -> LPN[10:9];	A[22:14] -> LPN[8:0]	i.e. 2048 logical pages | ||||
| 						physical = (page & BitMask<1, 0>::value) << 5; | ||||
| 						physical |= (page & BitMask<6, 2>::value) >> 2; | ||||
|  | ||||
| 						physical <<= 14; | ||||
|  | ||||
| 						logical = (page & BitMask<11, 10>::value) >> 1; | ||||
| 						logical |= (page & BitMask<22, 14>::value) >> 14; | ||||
| 					break; | ||||
|  | ||||
| 					case PageSize::kb32: | ||||
| 						// 32kb: | ||||
| 						//		A[1] -> PPN[6]; A[2] -> PPN[5]; A[0] -> PPN[4]; A[6:3] -> PPN[3:0] | ||||
| 						//		A[11:10] -> LPN[9:8];	A[22:15] -> LPN[7:0]	i.e. 1024 logical pages | ||||
| 						physical = (page & BitMask<1, 1>::value) << 5; | ||||
| 						physical |= (page & BitMask<2, 2>::value) << 3; | ||||
| 						physical |= (page & BitMask<0, 0>::value) << 4; | ||||
| 						physical |= (page & BitMask<6, 3>::value) >> 3; | ||||
|  | ||||
| 						physical <<= 15; | ||||
|  | ||||
| 						logical = (page & BitMask<11, 10>::value) >> 2; | ||||
| 						logical |= (page & BitMask<22, 15>::value) >> 15; | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| //				printf("%08x => physical %d -> logical %d\n", page, (physical >> 15), logical); | ||||
|  | ||||
| 				// TODO: consider clashes. | ||||
| 				// TODO: what if there's less than 4mb present? | ||||
| 				const auto target = &ram_[physical]; | ||||
|  | ||||
| 				const auto set_supervisor = [&](bool read, bool write) { | ||||
| 					if(read) mapping<true>(false, false)[logical] = target; | ||||
| 					if(write) mapping<false>(false, false)[logical] = target; | ||||
| 				}; | ||||
|  | ||||
| 				const auto set_os = [&](bool read, bool write) { | ||||
| 					if(read) mapping<true>(true, true)[logical] = target; | ||||
| 					if(write) mapping<false>(true, true)[logical] = target; | ||||
| 				}; | ||||
|  | ||||
| 				const auto set_user = [&](bool read, bool write) { | ||||
| 					if(read) mapping<true>(true, false)[logical] = target; | ||||
| 					if(write) mapping<false>(true, false)[logical] = target; | ||||
| 				}; | ||||
|  | ||||
| 				set_supervisor(true, true); | ||||
| 				switch((page >> 8) & 3) { | ||||
| 					case 0b00: | ||||
| 						set_os(true, true); | ||||
| 						set_user(true, true); | ||||
| 					break; | ||||
| 					case 0b01: | ||||
| 						set_os(true, true); | ||||
| 						set_user(true, false); | ||||
| 					break; | ||||
| 					default: | ||||
| 						set_os(true, false); | ||||
| 						set_user(false, false); | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										211
									
								
								Machines/Acorn/Archimedes/Sound.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								Machines/Acorn/Archimedes/Sound.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,211 @@ | ||||
| // | ||||
| //  Audio.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 20/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../../Concurrency/AsyncTaskQueue.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
|  | ||||
| #include <array> | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| // Generate lookup table for sound output levels, and hold it only once regardless | ||||
| // of how many template instantiations there are of @c Sound. | ||||
| static constexpr std::array<int16_t, 256> generate_levels() { | ||||
| 	std::array<int16_t, 256> result{}; | ||||
|  | ||||
| 	// There are 8 segments of 16 steps; each segment is a linear | ||||
| 	// interpolation from its start level to its end level and | ||||
| 	// each level is double the previous. | ||||
| 	// | ||||
| 	// Bit 7 provides a sign. | ||||
|  | ||||
| 	for(size_t c = 0; c < 256; c++) { | ||||
| 		// This is the VIDC1 rule. | ||||
| //		const bool is_negative = c & 128; | ||||
| //		const auto point = static_cast<int>(c & 0xf); | ||||
| //		const auto chord = static_cast<int>((c >> 4) & 7); | ||||
|  | ||||
| 		// VIDC2 rule, which seems to be effective. I've yet to spot the rule by which | ||||
| 		// VIDC1/2 is detected. | ||||
| 		const bool is_negative = c & 1; | ||||
| 		const auto point = static_cast<int>((c >> 1) & 0xf); | ||||
| 		const auto chord = static_cast<int>((c >> 5) & 7); | ||||
|  | ||||
| 		const int start = (1 << chord) - 1; | ||||
| 		const int end = (chord == 7) ? 247 : ((start << 1) + 1); | ||||
|  | ||||
| 		const int level = start * (16 - point) + end * point; | ||||
| 		result[c] = static_cast<int16_t>((level * 32767) / 3832); | ||||
| 		if(is_negative) result[c] = -result[c]; | ||||
| 	} | ||||
|  | ||||
| 	return result; | ||||
| } | ||||
| struct SoundLevels { | ||||
| 	static constexpr auto levels = generate_levels(); | ||||
| }; | ||||
|  | ||||
| /// Models the Archimedes sound output; in a real machine this is a joint efort between the VIDC and the MEMC. | ||||
| template <typename InterruptObserverT> | ||||
| struct Sound: private SoundLevels { | ||||
| 	Sound(InterruptObserverT &observer, const uint8_t *ram) : ram_(ram), observer_(observer) { | ||||
| 		speaker_.set_input_rate(1'000'000); | ||||
| 		speaker_.set_high_frequency_cutoff(2'200.0f); | ||||
| 	} | ||||
|  | ||||
| 	void set_next_end(uint32_t value) { | ||||
| 		next_.end = value; | ||||
| 	} | ||||
|  | ||||
| 	void set_next_start(uint32_t value) { | ||||
| 		next_.start = value; | ||||
| 		set_buffer_valid(true);	// My guess: this is triggered on next buffer start write. | ||||
|  | ||||
| 		// Definitely wrong; testing. | ||||
| //		set_halted(false); | ||||
| 	} | ||||
|  | ||||
| 	bool interrupt() const { | ||||
| 		return !next_buffer_valid_; | ||||
| 	} | ||||
|  | ||||
| 	void swap() { | ||||
| 		current_.start = next_.start; | ||||
| 		std::swap(current_.end, next_.end); | ||||
| 		set_buffer_valid(false); | ||||
| 		set_halted(false); | ||||
| 	} | ||||
|  | ||||
| 	void set_frequency(uint8_t frequency) { | ||||
| 		divider_ = reload_ = frequency; | ||||
| 	} | ||||
|  | ||||
| 	void set_stereo_image(uint8_t channel, uint8_t value) { | ||||
| 		if(!value) { | ||||
| 			positions_[channel].left = | ||||
| 			positions_[channel].right = 0; | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		positions_[channel].right = value - 1; | ||||
| 		positions_[channel].left = 6 - positions_[channel].right; | ||||
| 	} | ||||
|  | ||||
| 	void set_dma_enabled(bool enabled) { | ||||
| 		dma_enabled_ = enabled; | ||||
| 	} | ||||
|  | ||||
| 	void tick() { | ||||
| 		// Write silence if not currently outputting. | ||||
| 		if(halted_ || !dma_enabled_) { | ||||
| 			post_sample(Outputs::Speaker::StereoSample()); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Apply user-programmed clock divider. | ||||
| 		--divider_; | ||||
| 		if(!divider_) { | ||||
| 			divider_ = reload_ + 2; | ||||
|  | ||||
| 			// Grab a single byte from the FIFO. | ||||
| 			const uint8_t raw = ram_[static_cast<std::size_t>(current_.start) + static_cast<std::size_t>(byte_)]; | ||||
| 			sample_ = Outputs::Speaker::StereoSample(	// TODO: pan, volume. | ||||
| 				static_cast<int16_t>((levels[raw] * positions_[byte_ & 7].left) / 6), | ||||
| 				static_cast<int16_t>((levels[raw] * positions_[byte_ & 7].right) / 6) | ||||
| 			); | ||||
| 			++byte_; | ||||
|  | ||||
| 			// If the FIFO is exhausted, consider triggering a DMA request. | ||||
| 			if(byte_ == 16) { | ||||
| 				byte_ = 0; | ||||
|  | ||||
| 				current_.start += 16; | ||||
| 				if(current_.start == current_.end) { | ||||
| 					if(next_buffer_valid_) { | ||||
| 						swap(); | ||||
| 					} else { | ||||
| 						set_halted(true); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		post_sample(sample_); | ||||
| 	} | ||||
|  | ||||
| 	Outputs::Speaker::Speaker *speaker() { | ||||
| 		return &speaker_; | ||||
| 	} | ||||
|  | ||||
| 	~Sound() { | ||||
| 		while(is_posting_.test_and_set()); | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	const uint8_t *ram_ = nullptr; | ||||
|  | ||||
| 	uint8_t divider_ = 0, reload_ = 0; | ||||
| 	int byte_ = 0; | ||||
|  | ||||
| 	void set_buffer_valid(bool valid) { | ||||
| 		next_buffer_valid_ = valid; | ||||
| 		observer_.update_interrupts(); | ||||
| 	} | ||||
|  | ||||
| 	void set_halted(bool halted) { | ||||
| 		if(halted_ != halted && !halted) { | ||||
| 			byte_ = 0; | ||||
| 			divider_ = reload_; | ||||
| 		} | ||||
| 		halted_ = halted; | ||||
| 	} | ||||
|  | ||||
| 	bool next_buffer_valid_ = false; | ||||
| 	bool halted_ = true;				// This is a bit of a guess. | ||||
| 	bool dma_enabled_ = false; | ||||
|  | ||||
| 	struct Buffer { | ||||
| 		uint32_t start = 0, end = 0; | ||||
| 	}; | ||||
| 	Buffer current_, next_; | ||||
|  | ||||
| 	struct StereoPosition { | ||||
| 		// These are maintained as sixths, i.e. a value of 6 means 100%. | ||||
| 		int left, right; | ||||
| 	} positions_[8]; | ||||
|  | ||||
| 	InterruptObserverT &observer_; | ||||
| 	Outputs::Speaker::PushLowpass<true> speaker_; | ||||
| 	Concurrency::AsyncTaskQueue<true> queue_; | ||||
|  | ||||
| 	void post_sample(Outputs::Speaker::StereoSample sample) { | ||||
| 		samples_[sample_target_][sample_pointer_++] = sample; | ||||
| 		if(sample_pointer_ == samples_[0].size()) { | ||||
| 			while(is_posting_.test_and_set()); | ||||
|  | ||||
| 			const auto post_source = sample_target_; | ||||
| 			sample_target_ ^= 1; | ||||
| 			sample_pointer_ = 0; | ||||
| 			queue_.enqueue([this, post_source]() { | ||||
| 				speaker_.push(reinterpret_cast<int16_t *>(samples_[post_source].data()), samples_[post_source].size()); | ||||
| 				is_posting_.clear(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 	std::size_t sample_pointer_ = 0; | ||||
| 	std::size_t sample_target_ = 0; | ||||
| 	Outputs::Speaker::StereoSample sample_; | ||||
|  | ||||
| 	using SampleBuffer = std::array<Outputs::Speaker::StereoSample, 4096>; | ||||
| 	std::array<SampleBuffer, 2> samples_; | ||||
| 	std::atomic_flag is_posting_ = ATOMIC_FLAG_INIT; | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										625
									
								
								Machines/Acorn/Archimedes/Video.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										625
									
								
								Machines/Acorn/Archimedes/Video.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,625 @@ | ||||
| // | ||||
| //  Video.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 20/03/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "../../../Outputs/Log.hpp" | ||||
| #include "../../../Outputs/CRT/CRT.hpp" | ||||
|  | ||||
| #include <array> | ||||
| #include <cassert> | ||||
| #include <cstdint> | ||||
| #include <cstring> | ||||
|  | ||||
| namespace Archimedes { | ||||
|  | ||||
| template <typename InterruptObserverT, typename ClockRateObserverT, typename SoundT> | ||||
| struct Video { | ||||
| 	Video(InterruptObserverT &interrupt_observer, ClockRateObserverT &clock_rate_observer, SoundT &sound, const uint8_t *ram) : | ||||
| 		interrupt_observer_(interrupt_observer), | ||||
| 		clock_rate_observer_(clock_rate_observer), | ||||
| 		sound_(sound), | ||||
| 		ram_(ram), | ||||
| 		crt_(Outputs::Display::InputDataType::Red4Green4Blue4) { | ||||
| 		set_clock_divider(3); | ||||
| 		crt_.set_visible_area(Outputs::Display::Rect(0.041f, 0.04f, 0.95f, 0.95f)); | ||||
| 		crt_.set_display_type(Outputs::Display::DisplayType::RGB); | ||||
| 	} | ||||
|  | ||||
| 	static constexpr uint16_t colour(uint32_t value) { | ||||
| 		uint8_t packed[2]{}; | ||||
| 		packed[0] = value & 0xf; | ||||
| 		packed[1] = (value & 0xf0) | ((value & 0xf00) >> 8); | ||||
|  | ||||
| #if TARGET_RT_BIG_ENDIAN | ||||
| 		return static_cast<uint16_t>(packed[1] | (packed[0] << 8)); | ||||
| #else | ||||
| 		return static_cast<uint16_t>(packed[0] | (packed[1] << 8)); | ||||
| #endif | ||||
| 	}; | ||||
| 	static constexpr uint16_t high_spread[] = { | ||||
| 		colour(0b0000'0000'0000),	colour(0b0000'0000'1000),	colour(0b0000'0100'0000),	colour(0b0000'0100'1000), | ||||
| 		colour(0b0000'1000'0000),	colour(0b0000'1000'1000),	colour(0b0000'1100'0000),	colour(0b0000'1100'1000), | ||||
| 		colour(0b1000'0000'0000),	colour(0b1000'0000'1000),	colour(0b1000'0100'0000),	colour(0b1000'0100'1000), | ||||
| 		colour(0b1000'1000'0000),	colour(0b1000'1000'1000),	colour(0b1000'1100'0000),	colour(0b1000'1100'1000), | ||||
| 	}; | ||||
|  | ||||
| 	void write(uint32_t value) { | ||||
| 		const auto target = (value >> 24) & 0xfc; | ||||
| 		const auto timing_value = [](uint32_t value) -> uint32_t { | ||||
| 			return (value >> 14) & 0x3ff; | ||||
| 		}; | ||||
|  | ||||
| 		switch(target) { | ||||
| 			case 0x00:	case 0x04:	case 0x08:	case 0x0c: | ||||
| 			case 0x10:	case 0x14:	case 0x18:	case 0x1c: | ||||
| 			case 0x20:	case 0x24:	case 0x28:	case 0x2c: | ||||
| 			case 0x30:	case 0x34:	case 0x38:	case 0x3c: | ||||
| 				colours_[target >> 2] = colour(value); | ||||
| 			break; | ||||
|  | ||||
| 			case 0x40:	border_colour_ = colour(value);	break; | ||||
|  | ||||
| 			case 0x44:	case 0x48:	case 0x4c: | ||||
| 				cursor_colours_[(target - 0x40) >> 2] = colour(value); | ||||
| 			break; | ||||
|  | ||||
| 			case 0x80:	horizontal_timing_.period = timing_value(value);		break; | ||||
| 			case 0x84:	horizontal_timing_.sync_width = timing_value(value);	break; | ||||
| 			case 0x88:	horizontal_timing_.border_start = timing_value(value);	break; | ||||
| 			case 0x8c:	horizontal_timing_.display_start = timing_value(value);	break; | ||||
| 			case 0x90:	horizontal_timing_.display_end = timing_value(value);	break; | ||||
| 			case 0x94:	horizontal_timing_.border_end = timing_value(value);	break; | ||||
| 			case 0x98: | ||||
| 				horizontal_timing_.cursor_start = (value >> 13) & 0x7ff; | ||||
| 				cursor_shift_ = (value >> 11) & 3; | ||||
| 			break; | ||||
| 			case 0x9c:	horizontal_timing_.interlace_sync_position = timing_value(value);	break; | ||||
|  | ||||
| 			case 0xa0:	vertical_timing_.period = timing_value(value);			break; | ||||
| 			case 0xa4:	vertical_timing_.sync_width = timing_value(value);		break; | ||||
| 			case 0xa8:	vertical_timing_.border_start = timing_value(value);	break; | ||||
| 			case 0xac:	vertical_timing_.display_start = timing_value(value);	break; | ||||
| 			case 0xb0:	vertical_timing_.display_end = timing_value(value);		break; | ||||
| 			case 0xb4:	vertical_timing_.border_end = timing_value(value);		break; | ||||
| 			case 0xb8:	vertical_timing_.cursor_start = timing_value(value);	break; | ||||
| 			case 0xbc:	vertical_timing_.cursor_end = timing_value(value);		break; | ||||
|  | ||||
| 			case 0xe0: | ||||
| 				// Set pixel rate. This is the value that a 24Mhz clock should be divided | ||||
| 				// by to get half the pixel rate. | ||||
| 				switch(value & 0b11) { | ||||
| 					case 0b00:	set_clock_divider(6);	break;	// i.e. pixel clock = 8Mhz. | ||||
| 					case 0b01:	set_clock_divider(4);	break;	// 12Mhz. | ||||
| 					case 0b10:	set_clock_divider(3);	break;	// 16Mhz. | ||||
| 					case 0b11:	set_clock_divider(2);	break;	// 24Mhz. | ||||
| 				} | ||||
|  | ||||
| 				// Set colour depth. | ||||
| 				colour_depth_ = Depth((value >> 2) & 0b11); | ||||
|  | ||||
| 				// Crib interlace-enable. | ||||
| 				vertical_timing_.is_interlaced = value & (1 << 6); | ||||
| 			break; | ||||
|  | ||||
| 			// | ||||
| 			// Sound parameters. | ||||
| 			// | ||||
| 			case 0x60:	case 0x64:	case 0x68:	case 0x6c: | ||||
| 			case 0x70:	case 0x74:	case 0x78:	case 0x7c: { | ||||
| 				const uint8_t channel = ((value >> 26) + 7) & 7; | ||||
| 				sound_.set_stereo_image(channel, value & 7); | ||||
| 			} break; | ||||
|  | ||||
| 			case 0xc0: | ||||
| 				sound_.set_frequency(value & 0xff); | ||||
| 			break; | ||||
|  | ||||
| 			default: | ||||
| 				logger.error().append("TODO: unrecognised VIDC write of %08x", value); | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	void tick() { | ||||
| 		// Pick new horizontal state, possibly rolling over into the vertical. | ||||
| 		horizontal_state_.increment_position(horizontal_timing_); | ||||
|  | ||||
| 		if(horizontal_state_.did_restart()) { | ||||
| 			end_horizontal(); | ||||
|  | ||||
| 			const auto old_phase = vertical_state_.phase(); | ||||
| 			vertical_state_.increment_position(vertical_timing_); | ||||
|  | ||||
| 			const auto phase = vertical_state_.phase(); | ||||
| 			if(phase != old_phase) { | ||||
| 				// Copy frame and cursor start addresses into counters at the | ||||
| 				// start of the first vertical display line. | ||||
| 				if(phase == Phase::Display) { | ||||
| 					address_ = frame_start_; | ||||
| 					cursor_address_ = cursor_start_; | ||||
|  | ||||
| 					// Accumulate a count of how many times the processor has tried | ||||
| 					// to update the visible buffer more than once in a frame; this | ||||
| 					// will usually indicate that the software being run isn't properly | ||||
| 					// synchronised to actual machine speed. | ||||
| 					++frame_starts_; | ||||
| 					if(frame_start_sets_ > 10) { | ||||
| 						overages_ += frame_start_sets_ > frame_starts_; | ||||
| 						frame_start_sets_ = frame_starts_ = 0; | ||||
| 					} | ||||
| 				} | ||||
| 				if(old_phase == Phase::Display) { | ||||
| 					entered_flyback_ = true; | ||||
| 					interrupt_observer_.update_interrupts(); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Determine which next 8 bytes will be the cursor image for this line. | ||||
| 			// Pragmatically, updating cursor_address_ once per line avoids probable | ||||
| 			// errors in getting it to appear appropriately over both pixels and border. | ||||
| 			if(vertical_state_.cursor_active) { | ||||
| 				uint8_t *cursor_pixel = cursor_image_.data(); | ||||
| 				for(int byte = 0; byte < 8; byte ++) { | ||||
| 					cursor_pixel[0] = (ram_[cursor_address_] >> 0) & 3; | ||||
| 					cursor_pixel[1] = (ram_[cursor_address_] >> 2) & 3; | ||||
| 					cursor_pixel[2] = (ram_[cursor_address_] >> 4) & 3; | ||||
| 					cursor_pixel[3] = (ram_[cursor_address_] >> 6) & 3; | ||||
| 					cursor_pixel += 4; | ||||
| 					++cursor_address_; | ||||
| 				} | ||||
| 			} | ||||
| 			cursor_pixel_ = 32; | ||||
| 		} | ||||
|  | ||||
| 		// Fetch if relevant display signals are active. | ||||
| 		if(vertical_state_.display_active() && horizontal_state_.display_active()) { | ||||
| 			const auto next_byte = [&]() { | ||||
| 				const auto next = ram_[address_]; | ||||
| 				++address_; | ||||
|  | ||||
| 				// `buffer_end_` is the final address that a 16-byte block will be fetched from; | ||||
| 				// the +16 here papers over the fact that I'm not accurately implementing DMA. | ||||
| 				if(address_ == buffer_end_ + 16) { | ||||
| 					address_ = buffer_start_; | ||||
| 				} | ||||
| 				bitmap_queue_[bitmap_queue_pointer_ & 7] = next; | ||||
| 				++bitmap_queue_pointer_; | ||||
| 			}; | ||||
|  | ||||
| 			switch(colour_depth_) { | ||||
| 				case Depth::EightBPP:		next_byte();	next_byte();		break; | ||||
| 				case Depth::FourBPP:		next_byte();						break; | ||||
| 				case Depth::TwoBPP:			if(!(pixel_count_&3)) next_byte();	break; | ||||
| 				case Depth::OneBPP:			if(!(pixel_count_&7)) next_byte();	break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Move along line. | ||||
| 		switch(vertical_state_.phase()) { | ||||
| 			case Phase::Sync:					tick_horizontal<Phase::Sync>();						break; | ||||
| 			case Phase::Blank:					tick_horizontal<Phase::Blank>();					break; | ||||
| 			case Phase::Border:					tick_horizontal<Phase::Border>();					break; | ||||
| 			case Phase::Display:				tick_horizontal<Phase::Display>();					break; | ||||
| 			case Phase::StartInterlacedSync:	tick_horizontal<Phase::StartInterlacedSync>();		break; | ||||
| 			case Phase::EndInterlacedSync:		tick_horizontal<Phase::EndInterlacedSync>();		break; | ||||
| 		} | ||||
| 		++time_in_phase_; | ||||
| 	} | ||||
|  | ||||
| 	/// @returns @c true if a vertical retrace interrupt has been signalled since the last call to @c interrupt(); @c false otherwise. | ||||
| 	bool interrupt() { | ||||
| 		// Guess: edge triggered? | ||||
| 		const bool interrupt = entered_flyback_; | ||||
| 		entered_flyback_ = false; | ||||
| 		return interrupt; | ||||
| 	} | ||||
|  | ||||
| 	bool flyback_active() const { | ||||
| 		return vertical_state_.phase() != Phase::Display; | ||||
| 	} | ||||
|  | ||||
| 	void set_frame_start(uint32_t address) 	{ | ||||
| 		frame_start_ = address; | ||||
| 		++frame_start_sets_; | ||||
| 	} | ||||
| 	void set_buffer_start(uint32_t address)	{	buffer_start_ = address;	} | ||||
| 	void set_buffer_end(uint32_t address)	{	buffer_end_ = address;		} | ||||
| 	void set_cursor_start(uint32_t address)	{	cursor_start_ = address;	} | ||||
|  | ||||
| 	Outputs::CRT::CRT &crt() 				{ return crt_; } | ||||
| 	const Outputs::CRT::CRT &crt() const	{ return crt_; } | ||||
|  | ||||
| 	int clock_divider() const { | ||||
| 		return static_cast<int>(clock_divider_); | ||||
| 	} | ||||
|  | ||||
| 	int frame_rate_overages() const { | ||||
| 		return overages_; | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	Log::Logger<Log::Source::ARMIOC> logger; | ||||
| 	InterruptObserverT &interrupt_observer_; | ||||
| 	ClockRateObserverT &clock_rate_observer_; | ||||
| 	SoundT &sound_; | ||||
|  | ||||
| 	// In the current version of this code, video DMA occurrs costlessly, | ||||
| 	// being deferred to the component itself. | ||||
| 	const uint8_t *ram_ = nullptr; | ||||
| 	Outputs::CRT::CRT crt_; | ||||
|  | ||||
| 	// Horizontal and vertical timing. | ||||
| 	struct Timing { | ||||
| 		uint32_t period = 0; | ||||
| 		uint32_t sync_width = 0; | ||||
| 		uint32_t border_start = 0; | ||||
| 		uint32_t border_end = 0; | ||||
| 		uint32_t display_start = 0; | ||||
| 		uint32_t display_end = 0; | ||||
| 		uint32_t cursor_start = 0; | ||||
| 		uint32_t cursor_end = 0; | ||||
|  | ||||
| 		uint32_t interlace_sync_position = 0; | ||||
| 		bool is_interlaced = false; | ||||
| 	}; | ||||
| 	uint32_t cursor_shift_ = 0; | ||||
| 	Timing horizontal_timing_, vertical_timing_; | ||||
|  | ||||
| 	enum class Depth { | ||||
| 		OneBPP = 0b00, | ||||
| 		TwoBPP = 0b01, | ||||
| 		FourBPP = 0b10, | ||||
| 		EightBPP = 0b11, | ||||
| 	}; | ||||
|  | ||||
| 	// Current video state. | ||||
| 	enum class Phase { | ||||
| 		Sync, Blank, Border, Display, StartInterlacedSync, EndInterlacedSync, | ||||
| 	}; | ||||
| 	template <bool is_vertical> | ||||
| 	struct State { | ||||
| 		uint32_t position = 0; | ||||
| 		uint32_t display_start = 0; | ||||
| 		uint32_t display_end = 0; | ||||
|  | ||||
| 		bool is_odd_iteration_ = false; | ||||
|  | ||||
| 		void increment_position(const Timing &timing) { | ||||
| 			const auto previous_override = interlace_override_; | ||||
| 			if constexpr (is_vertical) { | ||||
| 				interlace_override_ = Phase::Sync;	// i.e. no override. | ||||
| 			} | ||||
| 			if(position == timing.sync_width)		{ | ||||
| 				state |= SyncEnded; | ||||
| 				if(is_vertical && timing.is_interlaced && is_odd_iteration_ && previous_override == Phase::Sync) { | ||||
| 					--position; | ||||
| 					interlace_override_ = Phase::EndInterlacedSync; | ||||
| 				} | ||||
| 			} | ||||
| 			if(position == timing.display_start)	{ state |= DisplayStarted; display_start = position; } | ||||
| 			if(position == timing.display_end)		{ state |= DisplayEnded; display_end = position; } | ||||
| 			if(position == timing.border_start)		state |= BorderStarted; | ||||
| 			if(position == timing.border_end)		state |= BorderEnded; | ||||
|  | ||||
| 			cursor_active |= position == timing.cursor_start; | ||||
| 			cursor_active &= position != timing.cursor_end; | ||||
|  | ||||
| 			if(position == timing.period) { | ||||
| 				state = DidRestart; | ||||
| 				position = 0; | ||||
| 				is_odd_iteration_ ^= true; | ||||
|  | ||||
| 				// Both display start and end need to be seeded as bigger than can be reached, | ||||
| 				// while having some overhead for addition. | ||||
| 				display_end = display_start = std::numeric_limits<uint32_t>::max() >> 1; | ||||
|  | ||||
| 				// Possibly label the next as a start-of-interlaced-sync. | ||||
| 				if(is_vertical && timing.is_interlaced && is_odd_iteration_) { | ||||
| 					interlace_override_ = Phase::StartInterlacedSync; | ||||
| 				} | ||||
| 			} else { | ||||
| 				++position; | ||||
| 				if(position == 1024) position = 0; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		bool is_outputting(Depth depth) const { | ||||
| 			return position >= display_start + output_latencies[static_cast<uint32_t>(depth)] && position < display_end + output_latencies[static_cast<uint32_t>(depth)]; | ||||
| 		} | ||||
|  | ||||
| 		uint32_t output_cycle(Depth depth) const { | ||||
| 			return position - display_start - output_latencies[static_cast<uint32_t>(depth)]; | ||||
| 		} | ||||
|  | ||||
| 		static constexpr uint32_t output_latencies[] = { | ||||
| 			19 >> 1,		// 1 bpp. | ||||
| 			11 >> 1,		// 2 bpp. | ||||
| 			7 >> 1,			// 4 bpp. | ||||
| 			5 >> 1			// 8 bpp. | ||||
| 		}; | ||||
|  | ||||
| 		static constexpr uint8_t SyncEnded = 0x1; | ||||
| 		static constexpr uint8_t BorderStarted = 0x2; | ||||
| 		static constexpr uint8_t BorderEnded = 0x4; | ||||
| 		static constexpr uint8_t DisplayStarted = 0x8; | ||||
| 		static constexpr uint8_t DisplayEnded = 0x10; | ||||
| 		static constexpr uint8_t DidRestart = 0x20; | ||||
| 		uint8_t state = 0; | ||||
| 		Phase interlace_override_ = Phase::Sync; | ||||
|  | ||||
| 		bool cursor_active = false; | ||||
|  | ||||
| 		bool did_restart() { | ||||
| 			const bool result = state & DidRestart; | ||||
| 			state &= ~DidRestart; | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		bool display_active() const { | ||||
| 			return (state & DisplayStarted) && !(state & DisplayEnded); | ||||
| 		} | ||||
|  | ||||
| 		Phase phase(Phase horizontal_fallback = Phase::Border) const { | ||||
| 			if(is_vertical && interlace_override_ != Phase::Sync) { | ||||
| 				return interlace_override_; | ||||
| 			} | ||||
| 			// TODO: turn the following logic into a lookup table. | ||||
| 			if(!(state & SyncEnded)) { | ||||
| 				return Phase::Sync; | ||||
| 			} | ||||
| 			if(!(state & BorderStarted) || (state & BorderEnded)) { | ||||
| 				return Phase::Blank; | ||||
| 			} | ||||
| 			if constexpr (!is_vertical) { | ||||
| 				return horizontal_fallback; | ||||
| 			} | ||||
|  | ||||
| 			if(!(state & DisplayStarted) || (state & DisplayEnded)) { | ||||
| 				return Phase::Border; | ||||
| 			} | ||||
| 			return Phase::Display; | ||||
| 		} | ||||
| 	}; | ||||
| 	State<false> horizontal_state_; | ||||
| 	State<true> vertical_state_; | ||||
|  | ||||
| 	int time_in_phase_ = 0; | ||||
| 	Phase phase_; | ||||
| 	uint16_t phased_border_colour_; | ||||
|  | ||||
| 	int pixel_count_ = 0; | ||||
| 	int display_area_start_ = 0; | ||||
| 	uint16_t *pixels_ = nullptr; | ||||
|  | ||||
| 	// It is elsewhere assumed that this size is a multiple of 8. | ||||
| 	static constexpr size_t PixelBufferSize = 256; | ||||
|  | ||||
| 	// Programmer-set addresses. | ||||
| 	uint32_t buffer_start_ = 0; | ||||
| 	uint32_t buffer_end_ = 0; | ||||
| 	uint32_t frame_start_ = 0; | ||||
| 	uint32_t cursor_start_ = 0; | ||||
|  | ||||
| 	int frame_start_sets_ = 0; | ||||
| 	int frame_starts_ = 0; | ||||
| 	int overages_ = 0; | ||||
|  | ||||
| 	// Ephemeral address state. | ||||
| 	uint32_t address_ = 0; | ||||
|  | ||||
| 	// Horizontal cursor output state. | ||||
| 	uint32_t cursor_address_ = 0; | ||||
| 	int cursor_pixel_ = 0; | ||||
| 	std::array<uint8_t, 32> cursor_image_; | ||||
|  | ||||
| 	// Colour palette, converted to internal format. | ||||
| 	uint16_t border_colour_; | ||||
| 	std::array<uint16_t, 16> colours_{}; | ||||
| 	std::array<uint16_t, 4> cursor_colours_{}; | ||||
|  | ||||
| 	// An interrupt flag; more closely related to the interface by which | ||||
| 	// my implementation of the IOC picks up an interrupt request than | ||||
| 	// to hardware. | ||||
| 	bool entered_flyback_ = false; | ||||
|  | ||||
| 	// The divider that would need to be applied to a 24Mhz clock to | ||||
| 	// get half the current pixel clock; counting is in units of half | ||||
| 	// the pixel clock because that's the fidelity at which the programmer | ||||
| 	// places horizontal events — display start, end, sync period, etc. | ||||
| 	uint32_t clock_divider_ = 0; | ||||
| 	Depth colour_depth_; | ||||
|  | ||||
| 	// A temporary buffer that holds video contents during the latency | ||||
| 	// period between their generation and their output. | ||||
| 	uint8_t bitmap_queue_[8]; | ||||
| 	int bitmap_queue_pointer_ = 0; | ||||
|  | ||||
| 	void set_clock_divider(uint32_t divider) { | ||||
| 		if(divider == clock_divider_) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		clock_divider_ = divider; | ||||
| 		const auto cycles_per_line = static_cast<int>(24'000'000 / (divider * 312 * 50)); | ||||
| 		crt_.set_new_timing( | ||||
| 			cycles_per_line, | ||||
| 			312,								/* Height of display. */ | ||||
| 			Outputs::CRT::PAL::ColourSpace, | ||||
| 			Outputs::CRT::PAL::ColourCycleNumerator, | ||||
| 			Outputs::CRT::PAL::ColourCycleDenominator, | ||||
| 			Outputs::CRT::PAL::VerticalSyncLength, | ||||
| 			Outputs::CRT::PAL::AlternatesPhase); | ||||
| 		clock_rate_observer_.update_clock_rates(); | ||||
| 	} | ||||
|  | ||||
| 	void flush_pixels() { | ||||
| 		crt_.output_data(time_in_phase_, static_cast<size_t>(pixel_count_)); | ||||
| 		time_in_phase_ = 0; | ||||
| 		pixel_count_ = 0; | ||||
| 		pixels_ = nullptr; | ||||
| 	} | ||||
|  | ||||
| 	void set_phase(Phase phase) { | ||||
| 		if(time_in_phase_) { | ||||
| 			switch(phase_) { | ||||
| 				default:				crt_.output_sync(time_in_phase_);									break; | ||||
| 				case Phase::Blank:		crt_.output_blank(time_in_phase_);									break; | ||||
| 				case Phase::Border:		crt_.output_level<uint16_t>(time_in_phase_, phased_border_colour_);	break; | ||||
| 				case Phase::Display:	flush_pixels();														break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		phase_ = phase; | ||||
| 		time_in_phase_ = 0; | ||||
| 		phased_border_colour_ = border_colour_; | ||||
| 		pixel_count_ = 0; | ||||
| 	} | ||||
|  | ||||
| 	void end_horizontal() { | ||||
| 		set_phase(Phase::Sync); | ||||
| 		display_area_start_ = -1; | ||||
| 		bitmap_queue_pointer_ = 0; | ||||
| 	} | ||||
|  | ||||
| 	template <Phase vertical_phase> void tick_horizontal() { | ||||
| 		// Sync lines: obey nothing. All sync, all the time. | ||||
| 		if constexpr (vertical_phase == Phase::Sync) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Start interlaced sync lines: do blank from horizontal sync up to the programmed | ||||
| 		// cutoff, then do sync. | ||||
| 		if constexpr (vertical_phase == Phase::StartInterlacedSync) { | ||||
| 			if(phase_ == Phase::Sync && horizontal_state_.phase() != Phase::Sync) { | ||||
| 				set_phase(Phase::Blank); | ||||
| 			} | ||||
| 			if(phase_ == Phase::Blank && horizontal_state_.position == horizontal_timing_.interlace_sync_position) { | ||||
| 				set_phase(Phase::Sync); | ||||
| 			} | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// End interlaced sync lines: do sync up to the programmed cutoff, then do blank. | ||||
| 		if constexpr (vertical_phase == Phase::EndInterlacedSync) { | ||||
| 			if(phase_ == Phase::Sync && horizontal_state_.position == horizontal_timing_.interlace_sync_position) { | ||||
| 				set_phase(Phase::Blank); | ||||
| 			} | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Blank lines: obey only the transition from sync to non-sync. | ||||
| 		if constexpr (vertical_phase == Phase::Blank) { | ||||
| 			if(phase_ == Phase::Sync && horizontal_state_.phase() != Phase::Sync) { | ||||
| 				set_phase(Phase::Blank); | ||||
| 			} | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// Border lines: ignore display phases; also  reset the border phase if the colour changes. | ||||
| 		if constexpr (vertical_phase == Phase::Border) { | ||||
| 			const auto phase = horizontal_state_.phase(Phase::Border); | ||||
| 			if(phase != phase_ || (phase_ == Phase::Border && border_colour_ != phased_border_colour_)) { | ||||
| 				set_phase(phase); | ||||
| 			} | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if constexpr (vertical_phase != Phase::Display) { | ||||
| 			// Should be impossible. | ||||
| 			assert(false); | ||||
| 		} | ||||
|  | ||||
| 		// Some timing facts, to explain what would otherwise be magic constants. | ||||
| 		static constexpr int CursorDelay = 5;	// The cursor will appear six pixels after its programmed trigger point. | ||||
| 												// ... BUT! Border and display are currently a pixel early. So move the | ||||
| 												// cursor for alignment. | ||||
|  | ||||
| 		// Deal with sync and blank via set_phase(); collapse display and border into Phase::Display. | ||||
| 		const auto phase = horizontal_state_.phase(Phase::Display); | ||||
| 		if(phase != phase_) set_phase(phase); | ||||
|  | ||||
| 		// Update cursor pixel counter if applicable; this might mean triggering it | ||||
| 		// and it might just mean advancing it if it has already been triggered. | ||||
| 		cursor_pixel_ += 2; | ||||
| 		if(vertical_state_.cursor_active) { | ||||
| 			const auto pixel_position = horizontal_state_.position << 1; | ||||
| 			if(pixel_position <= horizontal_timing_.cursor_start && (pixel_position + 2) > horizontal_timing_.cursor_start) { | ||||
| 				cursor_pixel_ = int(horizontal_timing_.cursor_start) - int(pixel_position) - CursorDelay; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// If this is not [collapsed] Phase::Display, just stop here. | ||||
| 		if(phase_ != Phase::Display) return; | ||||
|  | ||||
| 		// Display phase: maintain an output buffer (if available). | ||||
| 		if(pixel_count_ == PixelBufferSize)	flush_pixels(); | ||||
| 		if(!pixel_count_)					pixels_ = reinterpret_cast<uint16_t *>(crt_.begin_data(PixelBufferSize)); | ||||
|  | ||||
| 		// Output. | ||||
| 		if(pixels_) { | ||||
| 			// Paint the border colour for potential painting over. | ||||
|  | ||||
| 			if(horizontal_state_.is_outputting(colour_depth_)) { | ||||
| 				const auto source = horizontal_state_.output_cycle(colour_depth_); | ||||
|  | ||||
| 				// TODO: all below should be delayed an extra pixel. As should the border, actually. Fix up externally? | ||||
| 				switch(colour_depth_) { | ||||
| 					case Depth::EightBPP: { | ||||
| 						const uint8_t *bitmap = &bitmap_queue_[(source << 1) & 7]; | ||||
| 						pixels_[0] = (colours_[bitmap[0] & 0xf] & colour(0b0111'0011'0111)) | high_spread[bitmap[0] >> 4]; | ||||
| 						pixels_[1] = (colours_[bitmap[1] & 0xf] & colour(0b0111'0011'0111)) | high_spread[bitmap[1] >> 4]; | ||||
| 					} break; | ||||
|  | ||||
| 					case Depth::FourBPP: | ||||
| 						pixels_[0] = colours_[bitmap_queue_[source & 7] & 0xf]; | ||||
| 						pixels_[1] = colours_[bitmap_queue_[source & 7] >> 4]; | ||||
| 					break; | ||||
|  | ||||
| 					case Depth::TwoBPP: { | ||||
| 						uint8_t &bitmap = bitmap_queue_[(source >> 1) & 7]; | ||||
| 						pixels_[0] = colours_[bitmap & 3]; | ||||
| 						pixels_[1] = colours_[(bitmap >> 2) & 3]; | ||||
| 						bitmap >>= 4; | ||||
| 					} break; | ||||
|  | ||||
| 					case Depth::OneBPP: { | ||||
| 						uint8_t &bitmap = bitmap_queue_[(source >> 2) & 7]; | ||||
| 						pixels_[0] = colours_[bitmap & 1]; | ||||
| 						pixels_[1] = colours_[(bitmap >> 1) & 1]; | ||||
| 						bitmap >>= 2; | ||||
| 					} break; | ||||
| 				} | ||||
| 			} else { | ||||
| 				pixels_[0] = pixels_[1] = border_colour_; | ||||
| 			} | ||||
|  | ||||
| 			// Overlay cursor if applicable. | ||||
| 			if(cursor_pixel_ < 32) { | ||||
| 				if(cursor_pixel_ >= 0) { | ||||
| 					const auto pixel = cursor_image_[static_cast<size_t>(cursor_pixel_)]; | ||||
| 					if(pixel) { | ||||
| 						pixels_[0] = cursor_colours_[pixel]; | ||||
| 					} | ||||
| 				} | ||||
| 				if(cursor_pixel_ >= -1 && cursor_pixel_ < 31) { | ||||
| 					const auto pixel = cursor_image_[static_cast<size_t>(cursor_pixel_ + 1)]; | ||||
| 					if(pixel) { | ||||
| 						pixels_[1] = cursor_colours_[pixel]; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			pixels_ += 2; | ||||
| 		} | ||||
|  | ||||
| 		pixel_count_ += 2; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| } | ||||
| @@ -8,24 +8,24 @@ | ||||
| 
 | ||||
| #include "Electron.hpp" | ||||
| 
 | ||||
| #include "../../Activity/Source.hpp" | ||||
| #include "../MachineTypes.hpp" | ||||
| #include "../../Configurable/Configurable.hpp" | ||||
| #include "../../../Activity/Source.hpp" | ||||
| #include "../../MachineTypes.hpp" | ||||
| #include "../../../Configurable/Configurable.hpp" | ||||
| 
 | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../Configurable/StandardOptions.hpp" | ||||
| #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../Processors/6502/6502.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../ClockReceiver/ForceInline.hpp" | ||||
| #include "../../../Configurable/StandardOptions.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../../Processors/6502/6502.hpp" | ||||
| 
 | ||||
| #include "../../Storage/MassStorage/SCSI/SCSI.hpp" | ||||
| #include "../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp" | ||||
| #include "../../Storage/Tape/Tape.hpp" | ||||
| #include "../../../Storage/MassStorage/SCSI/SCSI.hpp" | ||||
| #include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp" | ||||
| #include "../../../Storage/Tape/Tape.hpp" | ||||
| 
 | ||||
| #include "../Utility/Typer.hpp" | ||||
| #include "../../Analyser/Static/Acorn/Target.hpp" | ||||
| #include "../../Utility/Typer.hpp" | ||||
| #include "../../../Analyser/Static/Acorn/Target.hpp" | ||||
| 
 | ||||
| #include "../../ClockReceiver/JustInTime.hpp" | ||||
| #include "../../../ClockReceiver/JustInTime.hpp" | ||||
| 
 | ||||
| #include "Interrupts.hpp" | ||||
| #include "Keyboard.hpp" | ||||
| @@ -796,5 +796,3 @@ std::unique_ptr<Machine> Machine::Electron(const Analyser::Static::Target *targe | ||||
| 		return std::make_unique<Electron::ConcreteMachine<true>>(*acorn_target, rom_fetcher); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| Machine::~Machine() {} | ||||
| @@ -8,10 +8,10 @@ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "../../Configurable/Configurable.hpp" | ||||
| #include "../../Configurable/StandardOptions.hpp" | ||||
| #include "../../Analyser/Static/StaticAnalyser.hpp" | ||||
| #include "../ROMMachine.hpp" | ||||
| #include "../../../Configurable/Configurable.hpp" | ||||
| #include "../../../Configurable/StandardOptions.hpp" | ||||
| #include "../../../Analyser/Static/StaticAnalyser.hpp" | ||||
| #include "../../ROMMachine.hpp" | ||||
| 
 | ||||
| #include <memory> | ||||
| 
 | ||||
| @@ -25,7 +25,7 @@ namespace Electron { | ||||
| */ | ||||
| class Machine { | ||||
| 	public: | ||||
| 		virtual ~Machine(); | ||||
| 		virtual ~Machine() = default; | ||||
| 
 | ||||
| 		/// Creates and returns an Electron.
 | ||||
| 		static std::unique_ptr<Machine> Electron(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||
| @@ -8,8 +8,8 @@ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "../KeyboardMachine.hpp" | ||||
| #include "../Utility/Typer.hpp" | ||||
| #include "../../KeyboardMachine.hpp" | ||||
| #include "../../Utility/Typer.hpp" | ||||
| 
 | ||||
| namespace Electron { | ||||
| 
 | ||||
| @@ -8,8 +8,8 @@ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "../../Components/1770/1770.hpp" | ||||
| #include "../../Activity/Observer.hpp" | ||||
| #include "../../../Components/1770/1770.hpp" | ||||
| #include "../../../Activity/Observer.hpp" | ||||
| 
 | ||||
| namespace Electron { | ||||
| 
 | ||||
							
								
								
									
										54
									
								
								Machines/Acorn/Electron/SoundGenerator.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								Machines/Acorn/Electron/SoundGenerator.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| // | ||||
| //  Speaker.cpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 03/12/2016. | ||||
| //  Copyright 2016 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #include "SoundGenerator.hpp" | ||||
|  | ||||
| #include <cstring> | ||||
|  | ||||
| using namespace Electron; | ||||
|  | ||||
| SoundGenerator::SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) : | ||||
| 	audio_queue_(audio_queue) {} | ||||
|  | ||||
| void SoundGenerator::set_sample_volume_range(std::int16_t range) { | ||||
| 	volume_ = unsigned(range / 2); | ||||
| } | ||||
|  | ||||
| template <Outputs::Speaker::Action action> | ||||
| void SoundGenerator::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { | ||||
| 	if constexpr (action == Outputs::Speaker::Action::Ignore) { | ||||
| 		counter_ = (counter_ + number_of_samples) % ((divider_+1) * 2); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	if(is_enabled_) { | ||||
| 		while(number_of_samples--) { | ||||
| 			Outputs::Speaker::apply<action>(*target, Outputs::Speaker::MonoSample((counter_ / (divider_+1)) * volume_)); | ||||
| 			target++; | ||||
| 			counter_ = (counter_ + 1) % ((divider_+1) * 2); | ||||
| 		} | ||||
| 	} else { | ||||
| 		Outputs::Speaker::fill<action>(target, target + number_of_samples, Outputs::Speaker::MonoSample(0)); | ||||
| 	} | ||||
| } | ||||
| template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
|  | ||||
| void SoundGenerator::set_divider(uint8_t divider) { | ||||
| 	audio_queue_.enqueue([this, divider]() { | ||||
| 		divider_ = divider * 32 / clock_rate_divider; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| void SoundGenerator::set_is_enabled(bool is_enabled) { | ||||
| 	audio_queue_.enqueue([this, is_enabled]() { | ||||
| 		is_enabled_ = is_enabled; | ||||
| 		counter_ = 0; | ||||
| 	}); | ||||
| } | ||||
| @@ -8,12 +8,12 @@ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" | ||||
| #include "../../Concurrency/AsyncTaskQueue.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/BufferSource.hpp" | ||||
| #include "../../../Concurrency/AsyncTaskQueue.hpp" | ||||
| 
 | ||||
| namespace Electron { | ||||
| 
 | ||||
| class SoundGenerator: public ::Outputs::Speaker::SampleSource { | ||||
| class SoundGenerator: public ::Outputs::Speaker::BufferSource<SoundGenerator, false> { | ||||
| 	public: | ||||
| 		SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue); | ||||
| 
 | ||||
| @@ -23,11 +23,10 @@ class SoundGenerator: public ::Outputs::Speaker::SampleSource { | ||||
| 
 | ||||
| 		static constexpr unsigned int clock_rate_divider = 8; | ||||
| 
 | ||||
| 		// To satisfy ::SampleSource.
 | ||||
| 		void get_samples(std::size_t number_of_samples, int16_t *target); | ||||
| 		void skip_samples(std::size_t number_of_samples); | ||||
| 		// For BufferSource.
 | ||||
| 		template <Outputs::Speaker::Action action> | ||||
| 		void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); | ||||
| 		void set_sample_volume_range(std::int16_t range); | ||||
| 		static constexpr bool get_is_stereo() { return false; } | ||||
| 
 | ||||
| 	private: | ||||
| 		Concurrency::AsyncTaskQueue<false> &audio_queue_; | ||||
| @@ -10,9 +10,9 @@ | ||||
| 
 | ||||
| #include <cstdint> | ||||
| 
 | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../Storage/Tape/Tape.hpp" | ||||
| #include "../../Storage/Tape/Parsers/Acorn.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../Storage/Tape/Tape.hpp" | ||||
| #include "../../../Storage/Tape/Parsers/Acorn.hpp" | ||||
| #include "Interrupts.hpp" | ||||
| 
 | ||||
| namespace Electron { | ||||
| @@ -8,8 +8,8 @@ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "Interrupts.hpp" | ||||
| 
 | ||||
| #include <vector> | ||||
| @@ -221,7 +221,7 @@ class ConcreteMachine: | ||||
| 		// MARK: - MachineTypes::MouseMachine. | ||||
|  | ||||
| 		Inputs::Mouse &get_mouse() final { | ||||
| 			return chipset_.get_mouse();; | ||||
| 			return chipset_.get_mouse(); | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - MachineTypes::JoystickMachine. | ||||
| @@ -256,5 +256,3 @@ std::unique_ptr<Machine> Machine::Amiga(const Analyser::Static::Target *target, | ||||
| 	const Target *const amiga_target = dynamic_cast<const Target *>(target); | ||||
| 	return std::make_unique<Amiga::ConcreteMachine>(*amiga_target, rom_fetcher); | ||||
| } | ||||
|  | ||||
| Machine::~Machine() {} | ||||
|   | ||||
| @@ -17,7 +17,7 @@ namespace Amiga { | ||||
|  | ||||
| class Machine { | ||||
| 	public: | ||||
| 		virtual ~Machine(); | ||||
| 		virtual ~Machine() = default; | ||||
|  | ||||
| 		/// Creates and returns an Amiga. | ||||
| 		static std::unique_ptr<Machine> Amiga(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||
|   | ||||
| @@ -28,9 +28,6 @@ template <bool record_bus = false> class Blitter: public DMADevice<4, 4> { | ||||
| 		using DMADevice::DMADevice; | ||||
|  | ||||
| 		template <int id, int shift> void set_pointer(uint16_t value) { | ||||
| 			if(get_status() & 0x4000) { | ||||
| 				printf(">>>"); | ||||
| 			} | ||||
| 			DMADevice<4, 4>::set_pointer<id, shift>(value); | ||||
| 		} | ||||
|  | ||||
| @@ -65,7 +62,7 @@ template <bool record_bus = false> class Blitter: public DMADevice<4, 4> { | ||||
| 			uint32_t address = 0; | ||||
| 			uint16_t value = 0; | ||||
|  | ||||
| 			Transaction() {} | ||||
| 			Transaction() = default; | ||||
| 			Transaction(Type type) : type(type) {} | ||||
| 			Transaction(Type type, uint32_t address, uint16_t value) : type(type), address(address), value(value) {} | ||||
|  | ||||
|   | ||||
| @@ -1293,5 +1293,3 @@ std::unique_ptr<Machine> Machine::AmstradCPC(const Analyser::Static::Target *tar | ||||
| 		case Target::Model::CPC464:	return std::make_unique<AmstradCPC::ConcreteMachine<false>>(*cpc_target, rom_fetcher); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Machine::~Machine() {} | ||||
|   | ||||
| @@ -22,7 +22,7 @@ namespace AmstradCPC { | ||||
| */ | ||||
| class Machine { | ||||
| 	public: | ||||
| 		virtual ~Machine(); | ||||
| 		virtual ~Machine() = default; | ||||
|  | ||||
| 		/// Creates and returns an Amstrad CPC. | ||||
| 		static std::unique_ptr<Machine> AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||
|   | ||||
| @@ -34,7 +34,7 @@ struct Command { | ||||
| 	uint8_t device = AllDevices; | ||||
| 	uint8_t reg = NoRegister; | ||||
|  | ||||
| 	Command() {} | ||||
| 	Command() = default; | ||||
| 	Command(Type type) : type(type) {} | ||||
| 	Command(Type type, uint8_t device) : type(type), device(device) {} | ||||
| 	Command(Type type, uint8_t device, uint8_t reg) : type(type), device(device), reg(reg) {} | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
| using namespace Apple::ADB; | ||||
|  | ||||
| namespace { | ||||
| Log::Logger<Log::Source::ADBDevice> logger; | ||||
| [[maybe_unused]] Log::Logger<Log::Source::ADBDevice> logger; | ||||
| } | ||||
|  | ||||
| ReactiveDevice::ReactiveDevice(Apple::ADB::Bus &bus, uint8_t adb_device_id) : | ||||
|   | ||||
| @@ -15,7 +15,9 @@ | ||||
|  | ||||
| #include "../../../Processors/6502/6502.hpp" | ||||
| #include "../../../Components/AudioToggle/AudioToggle.hpp" | ||||
| #include "../../../Components/AY38910/AY38910.hpp" | ||||
|  | ||||
| #include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| @@ -24,6 +26,7 @@ | ||||
| #include "DiskIICard.hpp" | ||||
| #include "Joystick.hpp" | ||||
| #include "LanguageCardSwitches.hpp" | ||||
| #include "Mockingboard.hpp" | ||||
| #include "SCSICard.hpp" | ||||
| #include "Video.hpp" | ||||
|  | ||||
| @@ -43,15 +46,69 @@ namespace { | ||||
|  | ||||
| constexpr int DiskIISlot = 6;		// Apple recommended slot 6 for the (first) Disk II. | ||||
| constexpr int SCSISlot = 7;			// Install the SCSI card in slot 7, to one-up any connected Disk II. | ||||
| constexpr int MockingboardSlot = 4;	// Conventional Mockingboard slot. | ||||
|  | ||||
|  | ||||
| // The system's master clock rate. | ||||
| // | ||||
| // Quick note on this: | ||||
| // | ||||
| //	* 64 out of 65 CPU cycles last for 14 cycles of the master clock; | ||||
| //	* every 65th cycle lasts for 16 cycles of the master clock; | ||||
| //	* that keeps CPU cycles in-phase with the colour subcarrier: each line of output is 64*14 + 16 = 912 master cycles long; | ||||
| //	* ... and since hsync is placed to make each line 228 colour clocks long that means 4 master clocks per colour clock; | ||||
| //	* ... which is why all Apple II video modes are expressible as four binary outputs per colour clock; | ||||
| //	* ... and hence seven pixels per memory access window clock in high-res mode, 14 in double high-res, etc. | ||||
| constexpr float master_clock = 14318180.0; | ||||
|  | ||||
| /// Provides an AY that runs at the CPU rate divided by 4 given an input of the master clock divided by 2, | ||||
| /// allowing for stretched CPU clock cycles. | ||||
| struct StretchedAYPair: | ||||
| 	Apple::II::AYPair, | ||||
| 	public Outputs::Speaker::BufferSource<StretchedAYPair, true> { | ||||
|  | ||||
| 		using AYPair::AYPair; | ||||
|  | ||||
| 		template <Outputs::Speaker::Action action> | ||||
| 		void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target) { | ||||
|  | ||||
| 			// (1) take 64 windows of 7 input cycles followed by one window of 8 input cycles; | ||||
| 			// (2) after each four windows, advance the underlying AY. | ||||
| 			// | ||||
| 			// i.e. advance after: | ||||
| 			// | ||||
| 			//	* 28 cycles, {16 times, then 15 times, then 15 times, then 15 times}; | ||||
| 			//	* 29 cycles, once. | ||||
| 			// | ||||
| 			// so: | ||||
| 			//	16, 1;	15, 1;	15, 1;	15, 1 | ||||
| 			// | ||||
| 			// i.e. add an extra one on the 17th, 33rd, 49th and 65th ticks in a 65-tick loop. | ||||
| 			for(std::size_t c = 0; c < number_of_samples; c++) { | ||||
| 				++subdivider_; | ||||
| 				if(subdivider_ == 28) { | ||||
| 					++phase_; | ||||
| 					subdivider_ = (phase_ & 15) ? 0 : -1; | ||||
| 					if(phase_ == 65) phase_ = 0; | ||||
|  | ||||
| 					advance(); | ||||
| 				} | ||||
|  | ||||
| 				target[c] = level(); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		int phase_ = 0; | ||||
| 		int subdivider_ = 0; | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| namespace Apple { | ||||
| namespace II { | ||||
|  | ||||
| #define is_iie() ((model == Analyser::Static::AppleII::Target::Model::IIe) || (model == Analyser::Static::AppleII::Target::Model::EnhancedIIe)) | ||||
|  | ||||
| template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| template <Analyser::Static::AppleII::Target::Model model, bool has_mockingboard> class ConcreteMachine: | ||||
| 	public Apple::II::Machine, | ||||
| 	public MachineTypes::TimedMachine, | ||||
| 	public MachineTypes::ScanProducer, | ||||
| @@ -83,14 +140,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 			false>; | ||||
| 		Processor m6502_; | ||||
| 		VideoBusHandler video_bus_handler_; | ||||
| 		Apple::II::Video::Video<VideoBusHandler, is_iie()> video_; | ||||
| 		Apple::II::Video::Video<VideoBusHandler, is_iie(model)> video_; | ||||
| 		int cycles_into_current_line_ = 0; | ||||
| 		Cycles cycles_since_video_update_; | ||||
|  | ||||
| 		void update_video() { | ||||
| 			video_.run_for(cycles_since_video_update_.flush<Cycles>()); | ||||
| 		} | ||||
| 		static constexpr int audio_divider = 8; | ||||
| 		static constexpr int audio_divider = has_mockingboard ? 1 : 8; | ||||
| 		void update_audio() { | ||||
| 			speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider))); | ||||
| 		} | ||||
| @@ -109,9 +166,23 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
|  | ||||
| 		Concurrency::AsyncTaskQueue<false> audio_queue_; | ||||
| 		Audio::Toggle audio_toggle_; | ||||
| 		Outputs::Speaker::PullLowpass<Audio::Toggle> speaker_; | ||||
| 		StretchedAYPair ays_; | ||||
| 		using SourceT = | ||||
| 			std::conditional_t<has_mockingboard, Outputs::Speaker::CompoundSource<StretchedAYPair, Audio::Toggle>, Audio::Toggle>; | ||||
| 		using LowpassT = Outputs::Speaker::PullLowpass<SourceT>; | ||||
|  | ||||
| 		Outputs::Speaker::CompoundSource<StretchedAYPair, Audio::Toggle> mixer_; | ||||
| 		Outputs::Speaker::PullLowpass<SourceT> speaker_; | ||||
| 		Cycles cycles_since_audio_update_; | ||||
|  | ||||
| 		constexpr SourceT &lowpass_source() { | ||||
| 			if constexpr (has_mockingboard) { | ||||
| 				return mixer_; | ||||
| 			} else { | ||||
| 				return audio_toggle_; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// MARK: - Cards | ||||
| 		static constexpr size_t NoActiveCard = 7;	// There is no 'card 0' in internal numbering. | ||||
| 		size_t active_card_ = NoActiveCard; | ||||
| @@ -155,6 +226,24 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 			pick_card_messaging_group(card); | ||||
| 		} | ||||
|  | ||||
| 		void card_did_change_interrupt_flags(Apple::II::Card *) final { | ||||
| 			bool nmi = false; | ||||
| 			bool irq = false; | ||||
|  | ||||
| 			for(const auto &card: cards_) { | ||||
| 				if(card) { | ||||
| 					nmi |= card->nmi(); | ||||
| 					irq |= card->irq(); | ||||
| 				} | ||||
| 			} | ||||
| 			m6502_.set_nmi_line(nmi); | ||||
| 			m6502_.set_irq_line(irq); | ||||
| 		} | ||||
|  | ||||
| 		Apple::II::Mockingboard *mockingboard() { | ||||
| 			return dynamic_cast<Apple::II::Mockingboard *>(cards_[MockingboardSlot - 1].get()); | ||||
| 		} | ||||
|  | ||||
| 		Apple::II::DiskIICard *diskii_card() { | ||||
| 			return dynamic_cast<Apple::II::DiskIICard *>(cards_[DiskIISlot - 1].get()); | ||||
| 		} | ||||
| @@ -225,7 +314,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 				const auto zero_state = auxiliary_switches_.zero_state(); | ||||
|  | ||||
| 				uint8_t *const ram = zero_state ? aux_ram_ : ram_; | ||||
| 				uint8_t *const rom = is_iie() ? &rom_[3840] : rom_.data(); | ||||
| 				uint8_t *const rom = is_iie(model) ? &rom_[3840] : rom_.data(); | ||||
|  | ||||
| 				// Which way the region here is mapped to be banks 1 and 2 is | ||||
| 				// arbitrary. | ||||
| @@ -273,7 +362,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 		// MARK: - Keyboard and typing. | ||||
|  | ||||
| 		struct Keyboard: public Inputs::Keyboard { | ||||
| 			Keyboard(Processor *m6502) : m6502_(m6502) {} | ||||
| 			Keyboard(Processor &m6502, AuxiliaryMemorySwitches<ConcreteMachine> &switches) : m6502_(m6502), auxiliary_switches_(switches) {} | ||||
|  | ||||
| 			void reset_all_keys() final { | ||||
| 				open_apple_is_pressed = | ||||
| @@ -286,7 +375,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 			} | ||||
|  | ||||
| 			bool set_key_pressed(Key key, char value, bool is_pressed, bool is_repeat) final { | ||||
| 				if constexpr (!is_iie()) { | ||||
| 				if constexpr (!is_iie(model)) { | ||||
| 					if(is_repeat && !repeat_is_pressed_) return true; | ||||
| 				} | ||||
|  | ||||
| @@ -297,7 +386,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					case Key::Down:			value = 0x0a;	break; | ||||
| 					case Key::Up:			value = 0x0b;	break; | ||||
| 					case Key::Backspace: | ||||
| 						if(is_iie()) { | ||||
| 						if(is_iie(model)) { | ||||
| 							value = 0x7f; | ||||
| 							break; | ||||
| 						} else { | ||||
| @@ -305,7 +394,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 						} | ||||
| 					case Key::Enter:		value = 0x0d;	break; | ||||
| 					case Key::Tab: | ||||
| 						if (is_iie()) { | ||||
| 						if (is_iie(model)) { | ||||
| 							value = '\t'; | ||||
| 							break; | ||||
| 						} else { | ||||
| @@ -316,7 +405,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
|  | ||||
| 					case Key::LeftOption: | ||||
| 					case Key::RightMeta: | ||||
| 						if (is_iie()) { | ||||
| 						if (is_iie(model)) { | ||||
| 							open_apple_is_pressed = is_pressed; | ||||
| 							return true; | ||||
| 						} else { | ||||
| @@ -325,7 +414,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
|  | ||||
| 					case Key::RightOption: | ||||
| 					case Key::LeftMeta: | ||||
| 						if (is_iie()) { | ||||
| 						if (is_iie(model)) { | ||||
| 							closed_apple_is_pressed = is_pressed; | ||||
| 							return true; | ||||
| 						} else { | ||||
| @@ -346,7 +435,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					case Key::F9:	case Key::F10:	case Key::F11: | ||||
| 						repeat_is_pressed_ = is_pressed; | ||||
|  | ||||
| 						if constexpr (!is_iie()) { | ||||
| 						if constexpr (!is_iie(model)) { | ||||
| 							if(is_pressed && (!is_repeat || character_is_pressed_)) { | ||||
| 								keyboard_input_ = uint8_t(last_pressed_character_ | 0x80); | ||||
| 							} | ||||
| @@ -365,7 +454,10 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 						// Accept a bunch non-symbolic other keys, as | ||||
| 						// reset, in the hope that the user can find | ||||
| 						// at least one usable key. | ||||
| 						m6502_->set_reset_line(is_pressed); | ||||
| 						m6502_.set_reset_line(is_pressed); | ||||
| 						if(!is_pressed) { | ||||
| 							auxiliary_switches_.reset(); | ||||
| 						} | ||||
| 					return true; | ||||
|  | ||||
| 					default: | ||||
| @@ -374,12 +466,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 						} | ||||
|  | ||||
| 						// Prior to the IIe, the keyboard could produce uppercase only. | ||||
| 						if(!is_iie()) value = char(toupper(value)); | ||||
| 						if(!is_iie(model)) value = char(toupper(value)); | ||||
|  | ||||
| 						if(control_is_pressed_ && isalpha(value)) value &= 0xbf; | ||||
|  | ||||
| 						// TODO: properly map IIe keys | ||||
| 						if(!is_iie() && shift_is_pressed_) { | ||||
| 						if(!is_iie(model) && shift_is_pressed_) { | ||||
| 							switch(value) { | ||||
| 							case 0x27: value = 0x22; break; // ' -> " | ||||
| 							case 0x2c: value = 0x3c; break; // , -> < | ||||
| @@ -467,7 +559,8 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 				std::unique_ptr<Utility::StringSerialiser> string_serialiser_; | ||||
|  | ||||
| 				// 6502 connection, for applying the reset button. | ||||
| 				Processor *const m6502_; | ||||
| 				Processor &m6502_; | ||||
| 				AuxiliaryMemorySwitches<ConcreteMachine> &auxiliary_switches_; | ||||
| 		}; | ||||
| 		Keyboard keyboard_; | ||||
|  | ||||
| @@ -480,12 +573,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 			video_bus_handler_(ram_, aux_ram_), | ||||
| 			video_(video_bus_handler_), | ||||
| 			audio_toggle_(audio_queue_), | ||||
| 			speaker_(audio_toggle_), | ||||
| 			ays_(audio_queue_), | ||||
| 			mixer_(ays_, audio_toggle_), | ||||
| 			speaker_(lowpass_source()), | ||||
| 			language_card_(*this), | ||||
| 			auxiliary_switches_(*this), | ||||
| 			keyboard_(&m6502_) { | ||||
| 			// The system's master clock rate. | ||||
| 			constexpr float master_clock = 14318180.0; | ||||
| 			keyboard_(m6502_, auxiliary_switches_) { | ||||
|  | ||||
| 			// This is where things get slightly convoluted: establish the machine as having a clock rate | ||||
| 			// equal to the number of cycles of work the 6502 will actually achieve. Which is less than | ||||
| @@ -559,6 +652,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 				install_card(SCSISlot, new Apple::II::SCSICard(roms, int(master_clock / 14.0f))); | ||||
| 			} | ||||
|  | ||||
| 			if(target.has_mockingboard) { | ||||
| 				// The Mockingboard has a parasitic relationship with this class due to the way | ||||
| 				// that audio outputs are implemented in this emulator. | ||||
| 				install_card(MockingboardSlot, new Apple::II::Mockingboard(ays_)); | ||||
| 			} | ||||
|  | ||||
| 			rom_ = std::move(roms.find(system)->second); | ||||
| 			// The IIe and Enhanced IIe ROMs often distributed are oversized; trim if necessary. | ||||
| 			if(system == ROM::Name::AppleIIe || system == ROM::Name::AppleIIEnhancedE) { | ||||
| @@ -629,7 +728,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value; | ||||
| 				} | ||||
|  | ||||
| 				if(is_iie()) { | ||||
| 				if(is_iie(model)) { | ||||
| 					auxiliary_switches_.access(address, isReadOperation(operation)); | ||||
| 				} | ||||
| 			} else { | ||||
| @@ -647,6 +746,11 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 				// actor, but this will actually be the result most of the time so it's not | ||||
| 				// too terrible. | ||||
| 				if(isReadOperation(operation) && address != 0xc000) { | ||||
| 					// Ensure any enqueued video changes are applied before grabbing the | ||||
| 					// vapour value. | ||||
| 					if(video_.has_deferred_actions()) { | ||||
| 						update_video(); | ||||
| 					} | ||||
| 					*value = video_.get_last_read_value(cycles_since_video_update_); | ||||
| 				} | ||||
|  | ||||
| @@ -669,7 +773,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 									*value &= 0x7f; | ||||
| 									if( | ||||
| 										joysticks_.button(0) || | ||||
| 										(is_iie() && keyboard_.open_apple_is_pressed) | ||||
| 										(is_iie(model) && keyboard_.open_apple_is_pressed) | ||||
| 									) | ||||
| 										*value |= 0x80; | ||||
| 								break; | ||||
| @@ -677,7 +781,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 									*value &= 0x7f; | ||||
| 									if( | ||||
| 										joysticks_.button(1) || | ||||
| 										(is_iie() && keyboard_.closed_apple_is_pressed) | ||||
| 										(is_iie(model) && keyboard_.closed_apple_is_pressed) | ||||
| 									) | ||||
| 										*value |= 0x80; | ||||
| 								break; | ||||
| @@ -699,7 +803,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 								} break; | ||||
|  | ||||
| 								// The IIe-only state reads follow... | ||||
| #define IIeSwitchRead(s)	*value = keyboard_.get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00); | ||||
| #define IIeSwitchRead(s)	*value = keyboard_.get_keyboard_input(); if(is_iie(model)) *value = (*value & 0x7f) | (s ? 0x80 : 0x00); | ||||
| 								case 0xc011:	IIeSwitchRead(language_card_.state().bank2);								break; | ||||
| 								case 0xc012:	IIeSwitchRead(language_card_.state().read);									break; | ||||
| 								case 0xc013:	IIeSwitchRead(auxiliary_switches_.switches().read_auxiliary_memory);		break; | ||||
| @@ -718,12 +822,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| #undef IIeSwitchRead | ||||
|  | ||||
| 								case 0xc07f: | ||||
| 									if(is_iie()) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00); | ||||
| 									if(is_iie(model)) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00); | ||||
| 								break; | ||||
| 							} | ||||
| 						} else { | ||||
| 							// Write-only switches. All IIe as currently implemented. | ||||
| 							if(is_iie()) { | ||||
| 							if(is_iie(model)) { | ||||
| 								auxiliary_switches_.access(address, false); | ||||
| 								switch(address) { | ||||
| 									default: break; | ||||
| @@ -731,19 +835,19 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 									case 0xc000: | ||||
| 									case 0xc001: | ||||
| 										update_video(); | ||||
| 										video_.set_80_store(!!(address&1)); | ||||
| 										video_.set_80_store(address&1); | ||||
| 									break; | ||||
|  | ||||
| 									case 0xc00c: | ||||
| 									case 0xc00d: | ||||
| 										update_video(); | ||||
| 										video_.set_80_columns(!!(address&1)); | ||||
| 										video_.set_80_columns(address&1); | ||||
| 									break; | ||||
|  | ||||
| 									case 0xc00e: | ||||
| 									case 0xc00f: | ||||
| 										update_video(); | ||||
| 										video_.set_alternative_character_set(!!(address&1)); | ||||
| 										video_.set_alternative_character_set(address&1); | ||||
| 									break; | ||||
| 								} | ||||
| 							} | ||||
| @@ -756,7 +860,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 					case 0xc050: | ||||
| 					case 0xc051: | ||||
| 						update_video(); | ||||
| 						video_.set_text(!!(address&1)); | ||||
| 						video_.set_text(address&1); | ||||
| 					break; | ||||
| 					case 0xc052:	update_video();		video_.set_mixed(false);		break; | ||||
| 					case 0xc053:	update_video();		video_.set_mixed(true);			break; | ||||
| @@ -775,7 +879,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
|  | ||||
| 					case 0xc05e: | ||||
| 					case 0xc05f: | ||||
| 						if(is_iie()) { | ||||
| 						if(is_iie(model)) { | ||||
| 							update_video(); | ||||
| 							video_.set_annunciator_3(!(address&1)); | ||||
| 						} | ||||
| @@ -785,7 +889,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 						keyboard_.clear_keyboard_input(); | ||||
|  | ||||
| 						// On the IIe, reading C010 returns additional key info. | ||||
| 						if(is_iie() && isReadOperation(operation)) { | ||||
| 						if(is_iie(model) && isReadOperation(operation)) { | ||||
| 							*value = (keyboard_.get_key_is_down() ? 0x80 : 0x00) | (keyboard_.get_keyboard_input() & 0x7f); | ||||
| 						} | ||||
| 					break; | ||||
| @@ -834,14 +938,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 							This also sets the active card for the C8 region. | ||||
| 						*/ | ||||
| 						active_card_ = card_number = (address - 0xc100) >> 8; | ||||
| 						select = Apple::II::Card::Device; | ||||
| 						select = Apple::II::Card::IO; | ||||
| 					} else { | ||||
| 						/* | ||||
| 							Decode the area conventionally used by cards for registers: | ||||
| 								C0n0 to C0nF: card n - 8. | ||||
| 						*/ | ||||
| 						card_number = (address - 0xc090) >> 4; | ||||
| 						select = Apple::II::Card::IO; | ||||
| 						select = Apple::II::Card::Device; | ||||
| 					} | ||||
|  | ||||
| 					// If the selected card is a just-in-time card, update the just-in-time cards, | ||||
| @@ -922,7 +1026,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine: | ||||
| 		} | ||||
|  | ||||
| 		bool prefers_logical_input() final { | ||||
| 			return is_iie(); | ||||
| 			return is_iie(model); | ||||
| 		} | ||||
|  | ||||
| 		Inputs::Keyboard &get_keyboard() final { | ||||
| @@ -988,13 +1092,22 @@ using namespace Apple::II; | ||||
| std::unique_ptr<Machine> Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { | ||||
| 	using Target = Analyser::Static::AppleII::Target; | ||||
| 	const Target *const appleii_target = dynamic_cast<const Target *>(target); | ||||
|  | ||||
| 	if(appleii_target->has_mockingboard) { | ||||
| 		switch(appleii_target->model) { | ||||
| 			default: return nullptr; | ||||
| 		case Target::Model::II: return std::make_unique<ConcreteMachine<Target::Model::II>>(*appleii_target, rom_fetcher); | ||||
| 		case Target::Model::IIplus: return std::make_unique<ConcreteMachine<Target::Model::IIplus>>(*appleii_target, rom_fetcher); | ||||
| 		case Target::Model::IIe: return std::make_unique<ConcreteMachine<Target::Model::IIe>>(*appleii_target, rom_fetcher); | ||||
| 		case Target::Model::EnhancedIIe: return std::make_unique<ConcreteMachine<Target::Model::EnhancedIIe>>(*appleii_target, rom_fetcher); | ||||
| 			case Target::Model::II: return std::make_unique<ConcreteMachine<Target::Model::II, true>>(*appleii_target, rom_fetcher); | ||||
| 			case Target::Model::IIplus: return std::make_unique<ConcreteMachine<Target::Model::IIplus, true>>(*appleii_target, rom_fetcher); | ||||
| 			case Target::Model::IIe: return std::make_unique<ConcreteMachine<Target::Model::IIe, true>>(*appleii_target, rom_fetcher); | ||||
| 			case Target::Model::EnhancedIIe: return std::make_unique<ConcreteMachine<Target::Model::EnhancedIIe, true>>(*appleii_target, rom_fetcher); | ||||
| 		} | ||||
| 	} else { | ||||
| 		switch(appleii_target->model) { | ||||
| 			default: return nullptr; | ||||
| 			case Target::Model::II: return std::make_unique<ConcreteMachine<Target::Model::II, false>>(*appleii_target, rom_fetcher); | ||||
| 			case Target::Model::IIplus: return std::make_unique<ConcreteMachine<Target::Model::IIplus, false>>(*appleii_target, rom_fetcher); | ||||
| 			case Target::Model::IIe: return std::make_unique<ConcreteMachine<Target::Model::IIe, false>>(*appleii_target, rom_fetcher); | ||||
| 			case Target::Model::EnhancedIIe: return std::make_unique<ConcreteMachine<Target::Model::EnhancedIIe, false>>(*appleii_target, rom_fetcher); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Machine::~Machine() {} | ||||
|   | ||||
| @@ -19,7 +19,7 @@ namespace Apple::II { | ||||
|  | ||||
| class Machine { | ||||
| 	public: | ||||
| 		virtual ~Machine(); | ||||
| 		virtual ~Machine() = default; | ||||
|  | ||||
| 		/// Creates and returns an AppleII. | ||||
| 		static std::unique_ptr<Machine> AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||
|   | ||||
| @@ -90,6 +90,10 @@ template <typename Machine> class AuxiliaryMemorySwitches { | ||||
| 			bool alternative_zero_page = false; | ||||
| 			bool video_page_2 = false; | ||||
| 			bool high_resolution = false; | ||||
|  | ||||
| 			void reset() { | ||||
| 				*this = SwitchState(); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		AuxiliaryMemorySwitches(Machine &machine) : machine_(machine) {} | ||||
| @@ -208,6 +212,14 @@ template <typename Machine> class AuxiliaryMemorySwitches { | ||||
| 			return switches_; | ||||
| 		} | ||||
|  | ||||
| 		void reset() { | ||||
| 			switches_.reset(); | ||||
|  | ||||
| 			set_main_paging(); | ||||
| 			set_zero_page_paging(); | ||||
| 			set_card_paging(); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		Machine &machine_; | ||||
| 		SwitchState switches_; | ||||
|   | ||||
| @@ -41,12 +41,12 @@ class Card { | ||||
| 		virtual ~Card() {} | ||||
| 		enum Select: int { | ||||
| 			None		= 0,		// No select line is active. | ||||
| 			IO			= 1 << 0,	// IO select is active; i.e. access is in range $C0x0 to $C0xf. | ||||
| 			Device		= 1 << 1,	// Device select is active; i.e. access is in range $Cx00 to $Cxff. | ||||
| 			Device		= 1 << 0,	// Device select ('DEVSEL') is active; i.e. access is in range $C0x0 to $C0xf. | ||||
| 			IO			= 1 << 1,	// IO select ('IOSEL') is active; i.e. access is in range $Cx00 to $Cxff. | ||||
|  | ||||
| 			C8Region	= 1 << 2,	// Access is to the region $c800 to $cfff, was preceded by at least | ||||
| 									// one Device access to this card, and has not yet been followed up | ||||
| 									// by an access to $cfff. | ||||
| 									// by an access to $cfff. IOSTRB on original hardware. | ||||
| 		}; | ||||
|  | ||||
| 		/*! | ||||
| @@ -54,7 +54,7 @@ class Card { | ||||
|  | ||||
| 			This is posted only to cards that announced a select constraint. Cards with | ||||
| 			no constraints, that want to be informed of every machine cycle, will receive | ||||
| 			a call to perform_bus_operation every cycle and should use that for time keeping. | ||||
| 			a call to @c perform_bus_operation every cycle and should use that for time keeping. | ||||
| 		*/ | ||||
| 		virtual void run_for([[maybe_unused]] Cycles cycles, [[maybe_unused]] int stretches) {} | ||||
|  | ||||
| @@ -93,15 +93,22 @@ class Card { | ||||
| 		/*! Cards may supply a target for activity observation if desired. */ | ||||
| 		virtual void set_activity_observer([[maybe_unused]] Activity::Observer *observer) {} | ||||
|  | ||||
| 		/// @returns The current semantic NMI line output of this card. | ||||
| 		virtual bool nmi() { return false; } | ||||
|  | ||||
| 		/// @returns The current semantic IRQ line output of this card. | ||||
| 		virtual bool irq() { return false; } | ||||
|  | ||||
| 		struct Delegate { | ||||
| 			virtual void card_did_change_select_constraints(Card *card) = 0; | ||||
| 			virtual void card_did_change_interrupt_flags(Card *card) = 0; | ||||
| 		}; | ||||
| 		void set_delegate(Delegate *delegate) { | ||||
| 			delegate_ = delegate; | ||||
| 		} | ||||
|  | ||||
| 	protected: | ||||
| 		int select_constraints_ = IO | Device; | ||||
| 		int select_constraints_ = Device | IO; | ||||
| 		Delegate *delegate_ = nullptr; | ||||
| 		void set_select_constraints(int constraints) { | ||||
| 			if(constraints == select_constraints_) return; | ||||
|   | ||||
| @@ -46,14 +46,14 @@ void DiskIICard::perform_bus_operation(Select select, bool is_read, uint16_t add | ||||
| 	diskii_.set_data_input(*value); | ||||
| 	switch(select) { | ||||
| 		default: break; | ||||
| 		case IO: { | ||||
| 		case Device: { | ||||
| 			const int disk_value = diskii_.read_address(address); | ||||
| 			if(is_read) { | ||||
| 				if(disk_value != diskii_.DidNotLoad) | ||||
| 					*value = uint8_t(disk_value); | ||||
| 			} | ||||
| 		} break; | ||||
| 		case Device: | ||||
| 		case IO: | ||||
| 			if(is_read) *value = boot_[address & 0xff]; | ||||
| 		break; | ||||
| 	} | ||||
| @@ -74,7 +74,7 @@ void DiskIICard::set_activity_observer(Activity::Observer *observer) { | ||||
|  | ||||
| void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference preference) { | ||||
| 	diskii_clocking_preference_ = preference; | ||||
| 	set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : None); | ||||
| 	set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (Device | IO) : None); | ||||
| } | ||||
|  | ||||
| Storage::Disk::Drive &DiskIICard::get_drive(int drive) { | ||||
|   | ||||
							
								
								
									
										135
									
								
								Machines/Apple/AppleII/Mockingboard.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								Machines/Apple/AppleII/Mockingboard.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| // | ||||
| //  Mockingboard.hpp | ||||
| //  Clock Signal | ||||
| // | ||||
| //  Created by Thomas Harte on 14/02/2024. | ||||
| //  Copyright © 2024 Thomas Harte. All rights reserved. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "Card.hpp" | ||||
|  | ||||
| #include "../../../Components/6522/6522.hpp" | ||||
|  | ||||
| namespace Apple::II { | ||||
|  | ||||
| class AYPair { | ||||
| 	public: | ||||
| 		AYPair(Concurrency::AsyncTaskQueue<false> &queue) : | ||||
| 			ays_{ | ||||
| 				{GI::AY38910::Personality::AY38910, queue}, | ||||
| 				{GI::AY38910::Personality::AY38910, queue}, | ||||
| 			} {} | ||||
|  | ||||
| 		void advance() { | ||||
| 			ays_[0].advance(); | ||||
| 			ays_[1].advance(); | ||||
| 		} | ||||
|  | ||||
| 		void set_sample_volume_range(std::int16_t range) { | ||||
| 			ays_[0].set_sample_volume_range(range); | ||||
| 			ays_[1].set_sample_volume_range(range); | ||||
| 		} | ||||
|  | ||||
| 		bool is_zero_level() const { | ||||
| 			return ays_[0].is_zero_level() && ays_[1].is_zero_level(); | ||||
| 		} | ||||
|  | ||||
| 		Outputs::Speaker::StereoSample level() const { | ||||
| 			Outputs::Speaker::StereoSample level; | ||||
| 			level.left = ays_[0].level(); | ||||
| 			level.right = ays_[1].level(); | ||||
| 			return level; | ||||
| 		} | ||||
|  | ||||
| 		GI::AY38910::AY38910SampleSource<false> &get(int index) { | ||||
| 			return ays_[index]; | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		GI::AY38910::AY38910SampleSource<false> ays_[2]; | ||||
| }; | ||||
|  | ||||
| class Mockingboard: public Card { | ||||
| 	public: | ||||
| 		Mockingboard(AYPair &ays) : | ||||
| 			vias_{ {handlers_[0]}, {handlers_[1]} }, | ||||
| 			handlers_{ {*this, ays.get(0)}, {*this, ays.get(1)}} { | ||||
| 			set_select_constraints(0); | ||||
| 		} | ||||
|  | ||||
| 		void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final { | ||||
| 			if(!(select & IO)) { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			int index = (address >> 7) & 1; | ||||
| 			if(is_read) { | ||||
| 				*value = vias_[index].read(address); | ||||
| 			} else { | ||||
| 				vias_[index].write(address, *value); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		void run_for(Cycles cycles, int) final { | ||||
| 			vias_[0].run_for(cycles); | ||||
| 			vias_[1].run_for(cycles); | ||||
| 		} | ||||
|  | ||||
| 		bool nmi() final { | ||||
| 			return handlers_[1].interrupt; | ||||
| 		} | ||||
|  | ||||
| 		bool irq() final { | ||||
| 			return handlers_[0].interrupt; | ||||
| 		} | ||||
|  | ||||
| 		void did_change_interrupt_flags() { | ||||
| 			delegate_->card_did_change_interrupt_flags(this); | ||||
| 		} | ||||
|  | ||||
| 	private: | ||||
| 		struct AYVIA: public MOS::MOS6522::PortHandler { | ||||
| 			AYVIA(Mockingboard &card, GI::AY38910::AY38910SampleSource<false> &ay) : | ||||
| 				card(card), ay(ay) {} | ||||
|  | ||||
| 			void set_interrupt_status(bool status) { | ||||
| 				interrupt = status; | ||||
| 				card.did_change_interrupt_flags(); | ||||
| 			} | ||||
|  | ||||
| 			void set_port_output(MOS::MOS6522::Port port, uint8_t value, uint8_t) { | ||||
| 				if(port) { | ||||
| 					using ControlLines = GI::AY38910::ControlLines; | ||||
| 					ay.set_control_lines( | ||||
| 						ControlLines( | ||||
| 							((value & 1) ? ControlLines::BC1 : 0) | | ||||
| 							((value & 2) ? ControlLines::BDIR : 0) | | ||||
| 							ControlLines::BC2 | ||||
| 						) | ||||
| 					); | ||||
|  | ||||
| 					ay.set_reset(!(value & 4)); | ||||
| 				} else { | ||||
| 					ay.set_data_input(value); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			uint8_t get_port_input(MOS::MOS6522::Port port) { | ||||
| 				if(!port) { | ||||
| 					return ay.get_data_output(); | ||||
| 				} | ||||
| 				return 0xff; | ||||
| 			} | ||||
|  | ||||
| 			bool interrupt; | ||||
| 			Mockingboard &card; | ||||
| 			GI::AY38910::AY38910SampleSource<false> &ay; | ||||
| 		}; | ||||
|  | ||||
| 		MOS::MOS6522::MOS6522<AYVIA> vias_[2]; | ||||
| 		AYVIA handlers_[2]; | ||||
| }; | ||||
|  | ||||
| } | ||||
| @@ -79,13 +79,13 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre | ||||
| 	switch(select) { | ||||
| 		default: break; | ||||
|  | ||||
| 		case Select::Device: | ||||
| 		case Select::IO: | ||||
| 			if(is_read) { | ||||
| 				*value = rom_[address & 255]; | ||||
| 			} | ||||
| 		break; | ||||
|  | ||||
| 		case Select::IO: | ||||
| 		case Select::Device: | ||||
| 			address &= 0xf; | ||||
| 			switch(address) { | ||||
| 				case 0x0:	case 0x1:	case 0x2:	case 0x3: | ||||
| @@ -125,13 +125,13 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre | ||||
|  | ||||
| 				case 0xb: | ||||
| 					if(!is_read) { | ||||
| 						printf("TODO: NCR reset\n"); | ||||
| 						logger_.error().append("TODO: NCR reset"); | ||||
| 					} | ||||
| 				break; | ||||
|  | ||||
| 				case 0xd: | ||||
| 					if(!is_read) { | ||||
| 						printf("TODO: Enable PDMA\n"); | ||||
| 						logger_.error().append("TODO: Enable PDMA"); | ||||
| 					} | ||||
| 				break; | ||||
|  | ||||
| @@ -143,7 +143,7 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre | ||||
| 				break; | ||||
|  | ||||
| 				default: | ||||
| 					printf("Unhandled: %04x %c %02x\n", address, is_read ? 'r' : 'w', *value); | ||||
| 					logger_.error().append("Unhandled: %04x %c %02x\n", address, is_read ? 'r' : 'w', *value); | ||||
| 				break; | ||||
| 			} | ||||
| 		break; | ||||
|   | ||||
| @@ -17,6 +17,8 @@ | ||||
| #include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp" | ||||
| #include "../../../Storage/MassStorage/MassStorageDevice.hpp" | ||||
|  | ||||
| #include "../../../Outputs/Log.hpp" | ||||
|  | ||||
| #include <array> | ||||
| #include <memory> | ||||
|  | ||||
| @@ -47,6 +49,7 @@ class SCSICard: public Card { | ||||
| 		SCSI::Bus scsi_bus_; | ||||
| 		NCR::NCR5380::NCR5380 ncr5380_; | ||||
| 		SCSI::Target::Target<SCSI::DirectAccessDevice> storage_; | ||||
| 		Log::Logger<Log::Source::AppleIISCSICard> logger_; | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,6 @@ | ||||
|  | ||||
| #include "../../../Outputs/CRT/CRT.hpp" | ||||
| #include "../../../ClockReceiver/ClockReceiver.hpp" | ||||
| #include "../../../ClockReceiver/DeferredQueue.hpp" | ||||
|  | ||||
| #include "VideoSwitches.hpp" | ||||
|  | ||||
| @@ -124,7 +123,8 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase { | ||||
| 			bus_handler_(bus_handler) {} | ||||
|  | ||||
| 		/*! | ||||
| 			Obtains the last value the video read prior to time now+offset. | ||||
| 			Obtains the last value the video read prior to time now+offset, according to the *current* | ||||
| 			video mode, i.e. not allowing for any changes still enqueued. | ||||
| 		*/ | ||||
| 		uint8_t get_last_read_value(Cycles offset) { | ||||
| 			// Rules of generation: | ||||
|   | ||||
| @@ -247,6 +247,10 @@ template <typename TimeUnit> class VideoSwitches { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		bool has_deferred_actions() const { | ||||
| 			return !deferrer_.empty(); | ||||
| 		} | ||||
|  | ||||
| 	protected: | ||||
| 		GraphicsMode graphics_mode(int row) const { | ||||
| 			if( | ||||
| @@ -306,7 +310,14 @@ template <typename TimeUnit> class VideoSwitches { | ||||
| 			bool annunciator_3 = false; | ||||
| 		} external_, internal_; | ||||
|  | ||||
| 		int flash_length = 8406; | ||||
| 		// 8406 lines covers a complete on-off cycle, i.e. because the Apple II has a line | ||||
| 		// rate of around 15734.26 lines/second, flashing has a frequency of roughly | ||||
| 		// 15734.26 / 8406, or roughly 1.83Hz. | ||||
| 		// | ||||
| 		// Internally that's modelled by a counter that goes to **twice** the value of the | ||||
| 		// constant specified below. Hence the divide by two. | ||||
| 		static constexpr int flash_length = 8406 / 2; | ||||
|  | ||||
| 		int flash_ = 0; | ||||
| 		uint8_t flash_mask() const { | ||||
| 			return uint8_t((flash_ / flash_length) * 0xff); | ||||
|   | ||||
| @@ -8,16 +8,18 @@ | ||||
|  | ||||
| #include "AppleIIgs.hpp" | ||||
|  | ||||
| #include "../../../Activity/Source.hpp" | ||||
| #include "../../MachineTypes.hpp" | ||||
|  | ||||
| #include "../../../Analyser/Static/AppleIIgs/Target.hpp" | ||||
|  | ||||
| #include "ADB.hpp" | ||||
| #include "MemoryMap.hpp" | ||||
| #include "Video.hpp" | ||||
| #include "Sound.hpp" | ||||
|  | ||||
| #include "../AppleII/Joystick.hpp" | ||||
|  | ||||
| #include "../../../Activity/Source.hpp" | ||||
| #include "../../MachineTypes.hpp" | ||||
|  | ||||
| #include "../../../Analyser/Static/AppleIIgs/Target.hpp" | ||||
|  | ||||
| #include "../../../Processors/65816/65816.hpp" | ||||
| #include "../../../Components/8530/z8530.hpp" | ||||
| #include "../../../Components/AppleClock/AppleClock.hpp" | ||||
| @@ -26,8 +28,6 @@ | ||||
| #include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp" | ||||
| #include "../../../Components/DiskII/DiskIIDrive.hpp" | ||||
|  | ||||
| #include "../AppleII/Joystick.hpp" | ||||
|  | ||||
| #include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp" | ||||
| #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||
|  | ||||
| @@ -1013,6 +1013,3 @@ using namespace Apple::IIgs; | ||||
| std::unique_ptr<Machine> Machine::AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { | ||||
| 	return std::make_unique<ConcreteMachine>(*dynamic_cast<const Analyser::Static::AppleIIgs::Target *>(target), rom_fetcher); | ||||
| } | ||||
|  | ||||
| Machine::~Machine() {} | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,7 @@ namespace Apple::IIgs { | ||||
|  | ||||
| class Machine { | ||||
| 	public: | ||||
| 		virtual ~Machine(); | ||||
| 		virtual ~Machine() = default; | ||||
|  | ||||
| 		/// Creates and returns an AppleIIgs. | ||||
| 		static std::unique_ptr<Machine> AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); | ||||
|   | ||||
| @@ -208,7 +208,7 @@ void MemoryMap::access(uint16_t address, bool is_read) { | ||||
| 	if((address & 0xfff0) == 0xc080) language_card_.access(address, is_read); | ||||
| } | ||||
|  | ||||
| void MemoryMap::assert_is_region(uint8_t start, uint8_t end) { | ||||
| void MemoryMap::assert_is_region([[maybe_unused]] uint8_t start, [[maybe_unused]] uint8_t end) { | ||||
| 	assert(region_map_[start] == region_map_[start-1]+1); | ||||
| 	assert(region_map_[end-1] == region_map_[start]); | ||||
| 	assert(region_map_[end] == region_map_[end-1]+1); | ||||
|   | ||||
| @@ -11,6 +11,7 @@ | ||||
| #include <array> | ||||
| #include <bitset> | ||||
| #include <cassert> | ||||
| #include <cstdint> | ||||
| #include <vector> | ||||
|  | ||||
| #include "../AppleII/LanguageCardSwitches.hpp" | ||||
| @@ -76,8 +77,8 @@ class MemoryMap { | ||||
| 			} | ||||
|  | ||||
| 			const auto physical = physical_address(region, address); | ||||
| 			assert(physical >= 0 && physical <= 0xff'ffff); | ||||
| 			return shadow_pages_[(physical >> 10) & 127] & shadow_banks_[physical >> 17]; | ||||
| 			assert(physical <= 0xff'ffff); | ||||
| 			return shadow_pages_[(physical >> 10) & 127] && shadow_banks_[physical >> 17]; | ||||
| 		} | ||||
| 		void write(const Region ®ion, uint32_t address, uint8_t value) { | ||||
| 			if(!region.write) { | ||||
|   | ||||
| @@ -108,7 +108,7 @@ uint8_t GLU::get_data() { | ||||
|  | ||||
| 	switch(address & 0xe0) { | ||||
| 		case 0x00:	return local_.oscillators[address & 0x1f].velocity & 0xff; | ||||
| 		case 0x20:	return local_.oscillators[address & 0x1f].velocity >> 8;; | ||||
| 		case 0x20:	return local_.oscillators[address & 0x1f].velocity >> 8; | ||||
| 		case 0x40:	return local_.oscillators[address & 0x1f].volume; | ||||
| 		case 0x60:	return local_.oscillators[address & 0x1f].sample(local_.ram_);	// i.e. look up what the sample was on demand. | ||||
| 		case 0x80:	return local_.oscillators[address & 0x1f].address; | ||||
| @@ -159,29 +159,34 @@ void GLU::run_for(Cycles cycles) { | ||||
| 	pending_store_write_time_ += cycles.as<uint32_t>(); | ||||
| } | ||||
|  | ||||
| void GLU::get_samples(std::size_t number_of_samples, std::int16_t *target) { | ||||
| template <Outputs::Speaker::Action action> | ||||
| void GLU::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { | ||||
| 	// Update remote state, generating audio. | ||||
| 	generate_audio(number_of_samples, target); | ||||
| 	generate_audio<action>(number_of_samples, target); | ||||
| } | ||||
| template void GLU::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void GLU::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
| template void GLU::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *); | ||||
|  | ||||
| void GLU::skip_samples(const std::size_t number_of_samples) { | ||||
| 	// Update remote state, without generating audio. | ||||
| 	skip_audio(remote_, number_of_samples); | ||||
|  | ||||
| 	// Apply any pending stores. | ||||
| 	std::atomic_thread_fence(std::memory_order::memory_order_acquire); | ||||
| 	const uint32_t final_time = pending_store_read_time_ + uint32_t(number_of_samples); | ||||
| 	while(true) { | ||||
| 		auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire); | ||||
| 		if(!next_store.enabled) break; | ||||
| 		if(next_store.time >= final_time) break; | ||||
| 		remote_.ram_[next_store.address] = next_store.value; | ||||
| 		next_store.enabled = false; | ||||
| 		pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed); | ||||
|  | ||||
| 		pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1); | ||||
| 	} | ||||
| } | ||||
| //void GLU::skip_samples(const std::size_t number_of_samples) { | ||||
| //	// Update remote state, without generating audio. | ||||
| //	skip_audio(remote_, number_of_samples); | ||||
| // | ||||
| //	// Apply any pending stores. | ||||
| //	std::atomic_thread_fence(std::memory_order::memory_order_acquire); | ||||
| //	const uint32_t final_time = pending_store_read_time_ + uint32_t(number_of_samples); | ||||
| //	while(true) { | ||||
| //		auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire); | ||||
| //		if(!next_store.enabled) break; | ||||
| //		if(next_store.time >= final_time) break; | ||||
| //		remote_.ram_[next_store.address] = next_store.value; | ||||
| //		next_store.enabled = false; | ||||
| //		pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed); | ||||
| // | ||||
| //		pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1); | ||||
| //	} | ||||
| //} | ||||
|  | ||||
| void GLU::set_sample_volume_range(std::int16_t range) { | ||||
| 	output_range_ = range; | ||||
| @@ -256,7 +261,8 @@ void GLU::skip_audio(EnsoniqState &state, size_t number_of_samples) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void GLU::generate_audio(size_t number_of_samples, std::int16_t *target) { | ||||
| template <Outputs::Speaker::Action action> | ||||
| void GLU::generate_audio(size_t number_of_samples, Outputs::Speaker::MonoSample *target) { | ||||
| 	auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire); | ||||
| 	uint8_t next_amplitude = 255; | ||||
| 	for(size_t sample = 0; sample < number_of_samples; sample++) { | ||||
| @@ -325,7 +331,12 @@ void GLU::generate_audio(size_t number_of_samples, std::int16_t *target) { | ||||
|  | ||||
| 		// Maximum total output was 32 channels times a 16-bit range. Map that down. | ||||
| 		// TODO: dynamic total volume? | ||||
| 		target[sample] = (output * output_range_) >> 20; | ||||
| 		Outputs::Speaker::apply<action>( | ||||
| 			target[sample], | ||||
| 			Outputs::Speaker::MonoSample( | ||||
| 				(output * output_range_) >> 20 | ||||
| 			) | ||||
| 		); | ||||
|  | ||||
| 		// Apply any RAM writes that interleave here. | ||||
| 		++pending_store_read_time_; | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user