mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
1083 Commits
2020-08-02
...
2021-04-16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06cedb2e50 | ||
|
|
7fdb1d848b | ||
|
|
246fd9442f | ||
|
|
eb99a64b29 | ||
|
|
d7954a4cb1 | ||
|
|
ef636da866 | ||
|
|
fa18b06dbf | ||
|
|
349b9ce502 | ||
|
|
b2cf121410 | ||
|
|
71cf63bd35 | ||
|
|
d1bb3aada4 | ||
|
|
b4214c6e08 | ||
|
|
f5c7746493 | ||
|
|
f10ec80153 | ||
|
|
0af405aa46 | ||
|
|
cf481effa6 | ||
|
|
a1511f9600 | ||
|
|
325e2b3941 | ||
|
|
7017324d60 | ||
|
|
deb5d69ac7 | ||
|
|
68a04f4e6a | ||
|
|
0d61902b10 | ||
|
|
3eec210b30 | ||
|
|
5998f3b35b | ||
|
|
869567fdd9 | ||
|
|
2e70b5eb9f | ||
|
|
8a3bfb8672 | ||
|
|
06f1e64177 | ||
|
|
b42780173a | ||
|
|
36c8821c4c | ||
|
|
947de2d54a | ||
|
|
9347fe5f44 | ||
|
|
e82367def3 | ||
|
|
9cde7c12ba | ||
|
|
015556cc91 | ||
|
|
47c5a243aa | ||
|
|
070e359d82 | ||
|
|
b397059d5e | ||
|
|
400f54e508 | ||
|
|
e0736435f8 | ||
|
|
b09c5538c6 | ||
|
|
ce3d2913bf | ||
|
|
87202a2a27 | ||
|
|
818a4dff25 | ||
|
|
eacffa49f5 | ||
|
|
9e506c3206 | ||
|
|
29cf80339a | ||
|
|
50f53f7d97 | ||
|
|
73fbd89c85 | ||
|
|
f74fa06f2d | ||
|
|
ee989ab762 | ||
|
|
818655a9b6 | ||
|
|
57a7e0834f | ||
|
|
cd787486d2 | ||
|
|
67fd6787a6 | ||
|
|
627b96f73c | ||
|
|
8a6985c2e8 | ||
|
|
60e8273de2 | ||
|
|
aa8ce5c1ac | ||
|
|
dd28246f9f | ||
|
|
dc25a60b9b | ||
|
|
094d623485 | ||
|
|
1266bbb224 | ||
|
|
bd1ea5740a | ||
|
|
3e04b51122 | ||
|
|
76f2aba51a | ||
|
|
fd88071c0a | ||
|
|
16bfe1a55c | ||
|
|
90c3d6a1e8 | ||
|
|
18d6197d6c | ||
|
|
27eddf6dff | ||
|
|
57b32d9537 | ||
|
|
837b9499d5 | ||
|
|
c2fde2b147 | ||
|
|
f26bf4b9e4 | ||
|
|
1da51bee6c | ||
|
|
5a66956221 | ||
|
|
91d973c4a9 | ||
|
|
fa79589db8 | ||
|
|
e52649f74d | ||
|
|
d77ddaf4fa | ||
|
|
9ff392279a | ||
|
|
448d9dc3e1 | ||
|
|
afb4e6d37d | ||
|
|
158122fbf4 | ||
|
|
417ece2386 | ||
|
|
77b241af4f | ||
|
|
25b8c4c062 | ||
|
|
1be88a5308 | ||
|
|
294280a94e | ||
|
|
32aebfebe0 | ||
|
|
14663bd06b | ||
|
|
68abd197aa | ||
|
|
18fd21eae7 | ||
|
|
3296347370 | ||
|
|
28c9463e0d | ||
|
|
044ac949ba | ||
|
|
87317f5673 | ||
|
|
5e21a49841 | ||
|
|
687c05365e | ||
|
|
4f80523828 | ||
|
|
76299a2add | ||
|
|
48f794dc2d | ||
|
|
51b8dcd011 | ||
|
|
acdbd88b9e | ||
|
|
00a3a3c724 | ||
|
|
729edeac6c | ||
|
|
faaa4961ed | ||
|
|
7937cc2d0f | ||
|
|
8a11a5832c | ||
|
|
53ba0e67d1 | ||
|
|
e8825aeada | ||
|
|
e90e30e766 | ||
|
|
9f6bb325e6 | ||
|
|
6e2c65435a | ||
|
|
052ab44f1c | ||
|
|
daa5679241 | ||
|
|
e055668554 | ||
|
|
c96829c29e | ||
|
|
c88abed2dc | ||
|
|
e42b6cb3c8 | ||
|
|
465ecc4a78 | ||
|
|
ae4ccdf5e6 | ||
|
|
6bdaa54aaf | ||
|
|
3543a25168 | ||
|
|
03ef81b07c | ||
|
|
0ac11fc39e | ||
|
|
3d0503a35e | ||
|
|
ad8cb52f11 | ||
|
|
496a294c71 | ||
|
|
465c74ab86 | ||
|
|
4e8f82a39c | ||
|
|
584a5ad7fb | ||
|
|
0ab85cce20 | ||
|
|
44e5caf803 | ||
|
|
04291e9a86 | ||
|
|
d0776b58cf | ||
|
|
6da099d7e1 | ||
|
|
60e77785e8 | ||
|
|
19cd6a55d3 | ||
|
|
08432dd94b | ||
|
|
cc3c3663f6 | ||
|
|
b76c923ff4 | ||
|
|
3c1131a84b | ||
|
|
a3cd953415 | ||
|
|
c0abdf1b86 | ||
|
|
3ef2715eee | ||
|
|
4a12d7086d | ||
|
|
a6b75b8637 | ||
|
|
bdb3bce8d6 | ||
|
|
a26716919c | ||
|
|
8dbc7649aa | ||
|
|
42a9dc7c2b | ||
|
|
7965772745 | ||
|
|
f37f89a7d3 | ||
|
|
d987e5a9d7 | ||
|
|
fcba0cc3d6 | ||
|
|
c097ed348a | ||
|
|
0f9ab53ea0 | ||
|
|
21b1dab4a5 | ||
|
|
dd7419282d | ||
|
|
7562917740 | ||
|
|
3925eee575 | ||
|
|
6482303063 | ||
|
|
388b136980 | ||
|
|
9ce1dbaebb | ||
|
|
064667c0c3 | ||
|
|
58be770eaa | ||
|
|
1b0f45649e | ||
|
|
42bfabbe8c | ||
|
|
986c4006a6 | ||
|
|
07a63d62dd | ||
|
|
26911a16e8 | ||
|
|
cf9a5d595b | ||
|
|
09a6a1905b | ||
|
|
2ad2b4384b | ||
|
|
7729f1f3d0 | ||
|
|
7d59ff6d8f | ||
|
|
2ee478b4c4 | ||
|
|
bb0d35e3d0 | ||
|
|
84774a7910 | ||
|
|
a482ce1546 | ||
|
|
a35e1f4fbe | ||
|
|
2371048ad1 | ||
|
|
93b9ea67e6 | ||
|
|
60a0f8e824 | ||
|
|
b3fc64d4f2 | ||
|
|
650b9a139b | ||
|
|
5758693b7d | ||
|
|
f8c9ef2950 | ||
|
|
69ca2e8803 | ||
|
|
87fac15cc4 | ||
|
|
2d51924a3c | ||
|
|
c3d96b30d7 | ||
|
|
44240773ef | ||
|
|
ed587a4db5 | ||
|
|
020a04006e | ||
|
|
622a8abf7f | ||
|
|
871bac6c8a | ||
|
|
fe3e8f87e7 | ||
|
|
87fc7c02e8 | ||
|
|
f2620e6afb | ||
|
|
ab2ad70885 | ||
|
|
135134acfd | ||
|
|
5664e81d48 | ||
|
|
c353923557 | ||
|
|
b830d62850 | ||
|
|
17f551e89d | ||
|
|
4a4da90d56 | ||
|
|
404c1f06e6 | ||
|
|
730bfcd1fd | ||
|
|
97249b0edd | ||
|
|
5a1bda1d82 | ||
|
|
b7d6b8efcf | ||
|
|
9bec91c2b9 | ||
|
|
3d1775d853 | ||
|
|
814c057570 | ||
|
|
b63ca16ce2 | ||
|
|
0ddf09ac0f | ||
|
|
e53586df1d | ||
|
|
54491b35ef | ||
|
|
b447f5f174 | ||
|
|
39a105b48a | ||
|
|
cdc19c6990 | ||
|
|
397704a1e6 | ||
|
|
1a5dafae00 | ||
|
|
d368dae94a | ||
|
|
54e2eb0948 | ||
|
|
7d778bc328 | ||
|
|
7a8317ad81 | ||
|
|
a32a2f36be | ||
|
|
064fe7658c | ||
|
|
cd215ef521 | ||
|
|
14c5e038e2 | ||
|
|
82717b39bb | ||
|
|
f190a1395a | ||
|
|
4eaf3440bd | ||
|
|
f985248902 | ||
|
|
5c90744f0c | ||
|
|
e9177bbb2a | ||
|
|
ab5e4ca9c7 | ||
|
|
40516c9cec | ||
|
|
d93d380c88 | ||
|
|
8a1c6978de | ||
|
|
6839e9e3b3 | ||
|
|
83cbbe09c6 | ||
|
|
166ddab5e0 | ||
|
|
67408521cd | ||
|
|
f05260b839 | ||
|
|
62949d2f8b | ||
|
|
2f18f40697 | ||
|
|
eea4c1f148 | ||
|
|
63a792f434 | ||
|
|
7b164de6fd | ||
|
|
26ad760904 | ||
|
|
24e68166c6 | ||
|
|
b72474f418 | ||
|
|
38046d49aa | ||
|
|
4601421aa6 | ||
|
|
86fd47545d | ||
|
|
c8471eb993 | ||
|
|
83d0cfc24e | ||
|
|
cbf5a79ee8 | ||
|
|
2f45e07d82 | ||
|
|
496b6b5cfc | ||
|
|
8604b1786e | ||
|
|
267e28e012 | ||
|
|
631a8a7421 | ||
|
|
7dcb0553e4 | ||
|
|
2a7ea9f57c | ||
|
|
e2b20568c6 | ||
|
|
4f5eb4d71b | ||
|
|
a1df8452ce | ||
|
|
9781460c41 | ||
|
|
55c9d152e9 | ||
|
|
71a107fe75 | ||
|
|
6cf9099ce1 | ||
|
|
e6dc39f6f0 | ||
|
|
f6466fd657 | ||
|
|
28ce675c96 | ||
|
|
3d91b0a31b | ||
|
|
5d1970d201 | ||
|
|
72d7901c88 | ||
|
|
60cfec6a65 | ||
|
|
2e9065b34c | ||
|
|
992ee6d631 | ||
|
|
772093c311 | ||
|
|
e42843cca0 | ||
|
|
3336a123f8 | ||
|
|
bd54e30748 | ||
|
|
35be402354 | ||
|
|
28bd620e7f | ||
|
|
96f2d802d9 | ||
|
|
b117df3367 | ||
|
|
fa8236741d | ||
|
|
e16d5f33d1 | ||
|
|
2a45e7a8d4 | ||
|
|
f8f0ff0fae | ||
|
|
f5dcff2f29 | ||
|
|
e773b331cd | ||
|
|
99c21925f4 | ||
|
|
eccf5ca043 | ||
|
|
24af62a3e5 | ||
|
|
52cf15c3e6 | ||
|
|
a791680e6f | ||
|
|
a3e98907ca | ||
|
|
6e53b4c507 | ||
|
|
52c38e72f6 | ||
|
|
a51d143c35 | ||
|
|
17e9305282 | ||
|
|
c284b34003 | ||
|
|
2ab3bba695 | ||
|
|
2c4dcf8843 | ||
|
|
ea40b2c982 | ||
|
|
adfdfa205f | ||
|
|
e83b2120ce | ||
|
|
33abdc95aa | ||
|
|
6ca8aa99fc | ||
|
|
17bac4c8cf | ||
|
|
46bd20b5e0 | ||
|
|
3c7f9a43ad | ||
|
|
82312d3b59 | ||
|
|
93a80a30d3 | ||
|
|
77b1efd176 | ||
|
|
acfab1dfb3 | ||
|
|
819e9039ab | ||
|
|
6526c645a5 | ||
|
|
3d2490b774 | ||
|
|
1e041f1adf | ||
|
|
4fdf01a1a8 | ||
|
|
beb514b231 | ||
|
|
f57e897085 | ||
|
|
2a8e8a4982 | ||
|
|
9f202d4238 | ||
|
|
1a40cc048e | ||
|
|
53514c7fdc | ||
|
|
274b3c7d24 | ||
|
|
07df7572b3 | ||
|
|
906b6ccdb7 | ||
|
|
f1ba040dd8 | ||
|
|
8db289e229 | ||
|
|
8142487d57 | ||
|
|
2860be7068 | ||
|
|
b5ecd5f7ef | ||
|
|
7e720e754b | ||
|
|
41a618c957 | ||
|
|
3d85e6bb97 | ||
|
|
d54085c7fd | ||
|
|
0bb8bdf938 | ||
|
|
865058b8d6 | ||
|
|
b6bc0a21fb | ||
|
|
8311ac4a7c | ||
|
|
4636d8dfb7 | ||
|
|
ac95e4d758 | ||
|
|
b8c6d4b153 | ||
|
|
5eddc92846 | ||
|
|
f50e8b5106 | ||
|
|
dcc2fe0990 | ||
|
|
56111c75ae | ||
|
|
cc90935abd | ||
|
|
413e42e1b6 | ||
|
|
fc4bda0047 | ||
|
|
c8beb59172 | ||
|
|
8789ffda15 | ||
|
|
e8e604dc3c | ||
|
|
57e0fdfadc | ||
|
|
7f62732476 | ||
|
|
36aebe0ff9 | ||
|
|
051d2b83f4 | ||
|
|
17b12120eb | ||
|
|
6e9ce50569 | ||
|
|
adef2e9b4e | ||
|
|
0fafbf5092 | ||
|
|
3c887aff95 | ||
|
|
e5076b295b | ||
|
|
c10c161d39 | ||
|
|
04024ca159 | ||
|
|
64d556f60f | ||
|
|
8564e7406b | ||
|
|
ebdb58d790 | ||
|
|
cf8afc70b2 | ||
|
|
4f02e8fbaf | ||
|
|
6e618a6bb7 | ||
|
|
df1bc18fb3 | ||
|
|
9f12ce2fb8 | ||
|
|
b9672c0669 | ||
|
|
e58608b25a | ||
|
|
e502d76371 | ||
|
|
b0c790f3c6 | ||
|
|
aa478cd222 | ||
|
|
c78c121159 | ||
|
|
e71e506883 | ||
|
|
a601ac0cab | ||
|
|
9b92753e0a | ||
|
|
ec0018df79 | ||
|
|
8b19c523cf | ||
|
|
5ace61f9b9 | ||
|
|
8a74f5911c | ||
|
|
4982430a29 | ||
|
|
dea79c6dea | ||
|
|
ad03858c6e | ||
|
|
54b26c7991 | ||
|
|
17c3a3eb4b | ||
|
|
5f413a38df | ||
|
|
8860d0ff51 | ||
|
|
8bd471fa3c | ||
|
|
cd6ac51aa6 | ||
|
|
10caa1a1fb | ||
|
|
722e0068ca | ||
|
|
8f2eea8819 | ||
|
|
3b2d65fa16 | ||
|
|
3dc36b704a | ||
|
|
37a20e125c | ||
|
|
2910faf963 | ||
|
|
321e10fffb | ||
|
|
1acb8c3c42 | ||
|
|
f667dd223f | ||
|
|
e0d90f69ec | ||
|
|
d82187bee2 | ||
|
|
3c20e1f037 | ||
|
|
15bedc74d4 | ||
|
|
4bd6ffa9e4 | ||
|
|
9c2c918760 | ||
|
|
47d20699d8 | ||
|
|
e8ce70dccb | ||
|
|
fa4938f29c | ||
|
|
ddb4bb1421 | ||
|
|
ca94e9038e | ||
|
|
2c72a77a25 | ||
|
|
8c0e06e645 | ||
|
|
a24ae727a7 | ||
|
|
5058a8b96a | ||
|
|
762ecab3aa | ||
|
|
9ba5b7c1d4 | ||
|
|
5f807b6e47 | ||
|
|
718f950071 | ||
|
|
68fe16a092 | ||
|
|
97a64db5e0 | ||
|
|
86577b772b | ||
|
|
306df7554e | ||
|
|
30c2c0f050 | ||
|
|
205649cac2 | ||
|
|
fd49b72e31 | ||
|
|
995904993d | ||
|
|
17cbba85fc | ||
|
|
9d7d45338f | ||
|
|
3b55d3f158 | ||
|
|
fda2293d6b | ||
|
|
da814c62bc | ||
|
|
d4095b1b3b | ||
|
|
ed41154338 | ||
|
|
38bca5f0f0 | ||
|
|
a8738b533a | ||
|
|
29cf96c703 | ||
|
|
782dc3d046 | ||
|
|
0ae217f51d | ||
|
|
adcb2e03e8 | ||
|
|
11b6c1d4b5 | ||
|
|
367cb1789d | ||
|
|
adf1484ecc | ||
|
|
5401ff6c78 | ||
|
|
eb8d0eefd5 | ||
|
|
c934e22cee | ||
|
|
1a3effc692 | ||
|
|
32c942d154 | ||
|
|
9c5dc0ed29 | ||
|
|
290972cedf | ||
|
|
dc9d370952 | ||
|
|
a41be61f99 | ||
|
|
3d1783ddae | ||
|
|
8151c8e409 | ||
|
|
0ef42f93ff | ||
|
|
d318ab4e70 | ||
|
|
ebfa35c2c7 | ||
|
|
db50b0fe23 | ||
|
|
233a69a1d8 | ||
|
|
3749b7b776 | ||
|
|
ed63e7ea75 | ||
|
|
31d68622c8 | ||
|
|
ee5f45c979 | ||
|
|
3d79b11f92 | ||
|
|
dfe4e49110 | ||
|
|
12784a71e2 | ||
|
|
e0b36c9c3d | ||
|
|
c5c56f9d05 | ||
|
|
9f0129cab8 | ||
|
|
5a48e50355 | ||
|
|
86283b1815 | ||
|
|
a38d964f62 | ||
|
|
114d48b076 | ||
|
|
6e9d517c26 | ||
|
|
3b2e97e77c | ||
|
|
159924dcc0 | ||
|
|
5d8f284757 | ||
|
|
c978a95463 | ||
|
|
fe4caf7a41 | ||
|
|
4bf85abf30 | ||
|
|
49cee90b4d | ||
|
|
394f6b58d8 | ||
|
|
dbdea95241 | ||
|
|
1928c955d9 | ||
|
|
a91a13b46b | ||
|
|
2f86d5ebaf | ||
|
|
b589d6e3ef | ||
|
|
db8b265e80 | ||
|
|
8560b38ffa | ||
|
|
049a78c667 | ||
|
|
574a37814c | ||
|
|
94eb17db0c | ||
|
|
9577c8e27f | ||
|
|
c72bdd776e | ||
|
|
d35def4bbc | ||
|
|
d5f209366a | ||
|
|
9062e80e9d | ||
|
|
fd3760cedc | ||
|
|
9b73331ee9 | ||
|
|
65ca931e83 | ||
|
|
6cb71eb11b | ||
|
|
43251193ee | ||
|
|
55de98fb46 | ||
|
|
1422d43c35 | ||
|
|
6273ef8ba2 | ||
|
|
3c6f09a898 | ||
|
|
24fcb0c24b | ||
|
|
3162873a9c | ||
|
|
03e2b6a265 | ||
|
|
ee22cf7ca1 | ||
|
|
187f507532 | ||
|
|
6000bd3a5e | ||
|
|
87069da3dd | ||
|
|
5cb4077576 | ||
|
|
e9c7e0b9dd | ||
|
|
35aa7612bb | ||
|
|
acaa841822 | ||
|
|
46c1c9b5ee | ||
|
|
4bdbca64b2 | ||
|
|
3da6b4709c | ||
|
|
11fe8ab6db | ||
|
|
a9ce43d244 | ||
|
|
091bce9350 | ||
|
|
32ccce3040 | ||
|
|
ab3fcb3ea0 | ||
|
|
9610672615 | ||
|
|
5ee9630624 | ||
|
|
1b3836eb1c | ||
|
|
1302a046e9 | ||
|
|
33dec3c220 | ||
|
|
7c29c3a944 | ||
|
|
c9ca1fc7a0 | ||
|
|
a965c8de9f | ||
|
|
0b4b271e3d | ||
|
|
5fc6dd1a4d | ||
|
|
79ef026b93 | ||
|
|
a4ab5b0b49 | ||
|
|
310282b7c9 | ||
|
|
af667c718e | ||
|
|
950f5b1691 | ||
|
|
f54a3f8619 | ||
|
|
cbc0d848ad | ||
|
|
f4d13d1f6f | ||
|
|
6808ad6f5d | ||
|
|
7a8920ee38 | ||
|
|
4870506f6e | ||
|
|
6f47f9d67c | ||
|
|
8093f67173 | ||
|
|
72884f37c3 | ||
|
|
8edb3fcd5f | ||
|
|
b0efc647f1 | ||
|
|
fdd102df52 | ||
|
|
73d28838c0 | ||
|
|
03a893dc74 | ||
|
|
56de2512ae | ||
|
|
cdc2311045 | ||
|
|
c6c12209e8 | ||
|
|
eec27c3406 | ||
|
|
2ac6f96806 | ||
|
|
0bd3103949 | ||
|
|
098a22aa95 | ||
|
|
9a819d6ca0 | ||
|
|
b4bf541eec | ||
|
|
7ede3d2b9e | ||
|
|
e7160fe3c3 | ||
|
|
9d61665014 | ||
|
|
d2938ad7c8 | ||
|
|
9e0e063f8a | ||
|
|
46f7ff07f7 | ||
|
|
8ace258fbc | ||
|
|
4359fb1746 | ||
|
|
a34f294ba8 | ||
|
|
cd7d080b7a | ||
|
|
b0936b6ef4 | ||
|
|
8fae74f93e | ||
|
|
fca48e4b66 | ||
|
|
dd816c5a0a | ||
|
|
3b2ea37428 | ||
|
|
8a805b6ba1 | ||
|
|
3cc89cb4d2 | ||
|
|
9b45c5a1cd | ||
|
|
3cba3a5ac0 | ||
|
|
4b024c5787 | ||
|
|
4a42de4f18 | ||
|
|
d00e5d23ef | ||
|
|
2c9ce116a2 | ||
|
|
3512352c32 | ||
|
|
4d9372c52f | ||
|
|
1d288b08b6 | ||
|
|
f3c7c11772 | ||
|
|
4b9fe805e9 | ||
|
|
a7051e4e42 | ||
|
|
34794223b4 | ||
|
|
96cf617ee6 | ||
|
|
69dddf34b9 | ||
|
|
8f4597f742 | ||
|
|
98347cb1c3 | ||
|
|
c7ab3d4075 | ||
|
|
cddd72876f | ||
|
|
62f936128d | ||
|
|
bb80e53021 | ||
|
|
952891d1b6 | ||
|
|
6dfad6a44b | ||
|
|
e4c5bfdd5c | ||
|
|
da8563733b | ||
|
|
e41faeb557 | ||
|
|
9a55eb56ea | ||
|
|
9206ab5dc3 | ||
|
|
7e39550fc0 | ||
|
|
96e79301f3 | ||
|
|
c3f5fbd300 | ||
|
|
1db713fec1 | ||
|
|
68ba73bee0 | ||
|
|
cdacf280e1 | ||
|
|
1538a02e18 | ||
|
|
f9cec9a102 | ||
|
|
adda3d8f42 | ||
|
|
ec3ff0da12 | ||
|
|
73c38b3b0d | ||
|
|
edc8050b36 | ||
|
|
37815a982a | ||
|
|
bd8af25294 | ||
|
|
3207183f05 | ||
|
|
e803f993b7 | ||
|
|
5dbc87caf0 | ||
|
|
4862ccc947 | ||
|
|
e1ecf66485 | ||
|
|
2c71ba0744 | ||
|
|
a7aeb779e9 | ||
|
|
e72cfbf447 | ||
|
|
0c04a376c4 | ||
|
|
3c6dc4c448 | ||
|
|
5d154e3d0c | ||
|
|
86a24cc928 | ||
|
|
e8b52d20e9 | ||
|
|
b0fc2f6ecf | ||
|
|
715a1b9cd6 | ||
|
|
81969bbea9 | ||
|
|
86310849eb | ||
|
|
a2a928e262 | ||
|
|
ffc9e229b6 | ||
|
|
3813e00ca3 | ||
|
|
5698aa6499 | ||
|
|
1f5908dc51 | ||
|
|
72884c3ead | ||
|
|
80358cf5bd | ||
|
|
a15af1df5e | ||
|
|
6d511f01a4 | ||
|
|
da9e378ab1 | ||
|
|
6d3d7c6006 | ||
|
|
8024bbd721 | ||
|
|
ece9382a4e | ||
|
|
6ba517a4c1 | ||
|
|
20fd5adb24 | ||
|
|
abb350ff5b | ||
|
|
dc8d4d49f5 | ||
|
|
54352cb1cb | ||
|
|
7e106c6add | ||
|
|
0ae49b356a | ||
|
|
32374444ba | ||
|
|
287bfeb924 | ||
|
|
b5fa574686 | ||
|
|
7aea3dc124 | ||
|
|
81c38c7200 | ||
|
|
3bb3d8c5c1 | ||
|
|
b57a2bfec9 | ||
|
|
93968d267d | ||
|
|
d27fb5f199 | ||
|
|
a51f4122f0 | ||
|
|
35ba5fc894 | ||
|
|
228d901253 | ||
|
|
d37ba62343 | ||
|
|
699fb0aa4b | ||
|
|
613d4b7c8b | ||
|
|
4f9d06d8c7 | ||
|
|
5149e4364a | ||
|
|
6b29e1f598 | ||
|
|
6c9edbb7a2 | ||
|
|
282d0f1ebb | ||
|
|
f466cbadec | ||
|
|
189a468ad4 | ||
|
|
a3414c2673 | ||
|
|
5126163c5d | ||
|
|
46ee98639e | ||
|
|
cc6c0d535c | ||
|
|
78b57e73d5 | ||
|
|
9e2a6526d1 | ||
|
|
d3c7253981 | ||
|
|
e3147b6b45 | ||
|
|
d50b059a17 | ||
|
|
cc5ec78156 | ||
|
|
ddc44ce0d1 | ||
|
|
5cbb91f352 | ||
|
|
91ea2eff4c | ||
|
|
bf85d71674 | ||
|
|
426e90eebf | ||
|
|
3889646d6b | ||
|
|
0178aaee2b | ||
|
|
53f60f7c87 | ||
|
|
2da71acefd | ||
|
|
45f5896b76 | ||
|
|
531a3bb7e6 | ||
|
|
1b28d929e4 | ||
|
|
e8943618dc | ||
|
|
1ae2f6f449 | ||
|
|
88e26b42f5 | ||
|
|
03d1aff6c0 | ||
|
|
e4459b6256 | ||
|
|
2be817a6a1 | ||
|
|
a833bb892b | ||
|
|
7f3f6c339f | ||
|
|
0d562699a2 | ||
|
|
034056d0cd | ||
|
|
1249fb598b | ||
|
|
5a8b8478d2 | ||
|
|
6c54699c44 | ||
|
|
266022b193 | ||
|
|
94a6da6b7d | ||
|
|
885fae1534 | ||
|
|
1df2ce513a | ||
|
|
1e4679ae14 | ||
|
|
267dd59a59 | ||
|
|
0a91ac5af5 | ||
|
|
ad93ad6018 | ||
|
|
0c700094ea | ||
|
|
20631a157b | ||
|
|
bdda84dfde | ||
|
|
e44f95a882 | ||
|
|
31cd45f8b5 | ||
|
|
74f9f6ad3b | ||
|
|
1dfdb51e61 | ||
|
|
18832dc19d | ||
|
|
3dee0666cb | ||
|
|
f830f6a57a | ||
|
|
82c733c68c | ||
|
|
ed510409c4 | ||
|
|
7614eba4bf | ||
|
|
13c8032465 | ||
|
|
44fc08cd5b | ||
|
|
7631b11c55 | ||
|
|
726b5f62bb | ||
|
|
ddd84db510 | ||
|
|
966241b4cc | ||
|
|
9371a8993f | ||
|
|
410c99de54 | ||
|
|
817f93a490 | ||
|
|
43611792ac | ||
|
|
62231708d7 | ||
|
|
a5dcab4092 | ||
|
|
8bde2e5f4c | ||
|
|
5287c57ee0 | ||
|
|
3aa47f9c68 | ||
|
|
ab07814614 | ||
|
|
1653abdf88 | ||
|
|
b3ab9fff9b | ||
|
|
14718b93a4 | ||
|
|
69450e27ad | ||
|
|
0cd08aa79d | ||
|
|
1fa94e1b08 | ||
|
|
76d9893866 | ||
|
|
c3f8982c62 | ||
|
|
99eba2f8ba | ||
|
|
69509f6502 | ||
|
|
c3187fdbe1 | ||
|
|
42228ea955 | ||
|
|
e5f57ea743 | ||
|
|
3b398f7a9a | ||
|
|
096add7551 | ||
|
|
334e0666b7 | ||
|
|
98c81749c8 | ||
|
|
5dcf720bb5 | ||
|
|
9c0c0255f6 | ||
|
|
68c15bd605 | ||
|
|
9a2f32795f | ||
|
|
7aa6cf4c6b | ||
|
|
dfda2adf0d | ||
|
|
c0a1c34012 | ||
|
|
3c6adc1ff4 | ||
|
|
e511d33a7c | ||
|
|
c35969d677 | ||
|
|
27afb8f0a7 | ||
|
|
327ab81436 | ||
|
|
db7178495f | ||
|
|
979186e71d | ||
|
|
f05e0d956b | ||
|
|
b22aa5d699 | ||
|
|
3e6a2adaaf | ||
|
|
8f5537aaaa | ||
|
|
a15d4a156b | ||
|
|
6a47571d17 | ||
|
|
7479dc74ed | ||
|
|
28da1a724a | ||
|
|
f529eadbec | ||
|
|
5dc3cd3a2f | ||
|
|
3039a445f0 | ||
|
|
82797fd395 | ||
|
|
a0885ab7d0 | ||
|
|
8eaf1303a3 | ||
|
|
20cbe72985 | ||
|
|
071ad6b767 | ||
|
|
0619e49eac | ||
|
|
b8848d8580 | ||
|
|
aface1f8be | ||
|
|
ae87728770 | ||
|
|
28c8ba70c1 | ||
|
|
486324ecab | ||
|
|
6892ac13e8 | ||
|
|
340ad093a6 | ||
|
|
0fe09cd1e4 | ||
|
|
da4702851f | ||
|
|
09fba72d58 | ||
|
|
d17c90edf7 | ||
|
|
7966592fae | ||
|
|
6efe4e1753 | ||
|
|
536c4d45c1 | ||
|
|
a02f88fe7c | ||
|
|
d9be6ab806 | ||
|
|
290598429a | ||
|
|
92e72959c3 | ||
|
|
776f014dbe | ||
|
|
c01bc784b9 | ||
|
|
abcd86a294 | ||
|
|
451f83ba51 | ||
|
|
b439f40fe2 | ||
|
|
968166b06d | ||
|
|
88293909f4 | ||
|
|
9b6c48631d | ||
|
|
0ed98cbfac | ||
|
|
7dde7cc743 | ||
|
|
755627f12d | ||
|
|
f8004d7096 | ||
|
|
0418f51ef2 | ||
|
|
054e0af071 | ||
|
|
907c3374c3 | ||
|
|
b578240993 | ||
|
|
f83ee97439 | ||
|
|
19aea85184 | ||
|
|
1ba0a117e7 | ||
|
|
b510b9d337 | ||
|
|
b608e11965 | ||
|
|
e68b3a2f32 | ||
|
|
f7b119ffe1 | ||
|
|
a4cec95db1 | ||
|
|
84c4fa197b | ||
|
|
eac722cf59 | ||
|
|
7439a326a6 | ||
|
|
5ca1c0747f | ||
|
|
466ca38dfa | ||
|
|
93b0839036 | ||
|
|
e068cbc103 | ||
|
|
5c809e5fbf | ||
|
|
3933bf49cf | ||
|
|
7065ba4857 | ||
|
|
ebff83018e | ||
|
|
9ce9167e3c | ||
|
|
993eff1d3d | ||
|
|
7be983ec00 | ||
|
|
18e8d6ce06 | ||
|
|
b7ba0d4327 | ||
|
|
825201f4f2 | ||
|
|
9a05c68ce7 | ||
|
|
d8dccf2500 | ||
|
|
b416aa640f | ||
|
|
4ebf594b3b | ||
|
|
8a83024962 | ||
|
|
bdc1136b96 | ||
|
|
da78dea98f | ||
|
|
dcf8cb14e2 | ||
|
|
38912859e1 | ||
|
|
b83d93abc2 | ||
|
|
36f843bc6e | ||
|
|
15c87e02e9 | ||
|
|
00923eac7c | ||
|
|
a72ac8294c | ||
|
|
4f03bf754d | ||
|
|
78b3ec4b10 | ||
|
|
ef1a514785 | ||
|
|
6635876e7e | ||
|
|
5645f90abe | ||
|
|
b96cd4d18b | ||
|
|
ad8a2e2cb9 | ||
|
|
fa438e5113 | ||
|
|
8641494809 | ||
|
|
f4a23af5d6 | ||
|
|
5449e90b34 | ||
|
|
1cd664ad85 | ||
|
|
e680022b1f | ||
|
|
67c2ce2174 | ||
|
|
596e700b60 | ||
|
|
4a53b6e538 | ||
|
|
687f4bb3bb | ||
|
|
473799cb62 | ||
|
|
ce0536cdfa | ||
|
|
3dc22a9fd5 | ||
|
|
f54b655606 | ||
|
|
d2e868ea2b | ||
|
|
3fc649359a | ||
|
|
1512ac11da | ||
|
|
5039cc7bb2 | ||
|
|
5360a7b4ce | ||
|
|
2957a31f40 | ||
|
|
8c11df52bf | ||
|
|
2b7ffcd48f | ||
|
|
7980a9033e | ||
|
|
125ddfa513 | ||
|
|
636e929607 | ||
|
|
22c792dc46 | ||
|
|
95af1815c8 | ||
|
|
d707c5ac95 | ||
|
|
5c9192e5e6 | ||
|
|
72b5584042 | ||
|
|
f9045b5352 | ||
|
|
f87fe92bc8 | ||
|
|
669d8e64ab | ||
|
|
9447aa38be | ||
|
|
a781c3eb4d | ||
|
|
c0b1308dfd | ||
|
|
2d9dd6704a | ||
|
|
94dba70bbe | ||
|
|
022ec20e75 | ||
|
|
41f69405d8 | ||
|
|
5741e22e29 | ||
|
|
8e242eea54 | ||
|
|
703065a0a5 | ||
|
|
291aa42fe1 | ||
|
|
8fc3496cc9 | ||
|
|
e807a462a1 | ||
|
|
18790a90ae | ||
|
|
21afc70261 | ||
|
|
7bb74af478 | ||
|
|
894269aa06 | ||
|
|
8b16da9695 | ||
|
|
f783ec6269 | ||
|
|
22c9734874 | ||
|
|
a17d0e428f | ||
|
|
bb57f0bcc7 | ||
|
|
b1aefbfe85 | ||
|
|
061288f5a7 | ||
|
|
5a53474536 | ||
|
|
18d0fff8da | ||
|
|
0ac2145740 | ||
|
|
bc8787ded6 | ||
|
|
69d21daaa3 | ||
|
|
5651ef606d | ||
|
|
b831b31382 | ||
|
|
2fd5cc056c | ||
|
|
82dbdf7dfc | ||
|
|
eb9903cd10 | ||
|
|
227e98d6d7 | ||
|
|
35476063b7 | ||
|
|
8557bb2136 | ||
|
|
c0c7818d5d | ||
|
|
ceeadd6a33 | ||
|
|
1a2545fdea | ||
|
|
c5e9a74c88 | ||
|
|
d7972a7b86 | ||
|
|
7dd4c67304 | ||
|
|
e113780fd1 | ||
|
|
e32ae6c191 | ||
|
|
bcaceff378 | ||
|
|
d7b405c6f8 | ||
|
|
edf8cf4dc6 | ||
|
|
dfcc8e9822 | ||
|
|
016e96e6f8 | ||
|
|
e7ce03c418 | ||
|
|
3d392dd81d | ||
|
|
42d810db7f | ||
|
|
18571e8351 | ||
|
|
dda1649ab7 | ||
|
|
c82e0df071 | ||
|
|
06b7ea5a6e | ||
|
|
c49fcb9ec9 | ||
|
|
0e44d6d214 | ||
|
|
6adad7fbf5 | ||
|
|
de6ed7b615 | ||
|
|
07dcb4dbb1 | ||
|
|
e99896eadc | ||
|
|
489701afcb | ||
|
|
55e576cc57 | ||
|
|
6bd8ec9545 | ||
|
|
5cd8d86eef | ||
|
|
74d0acdaec | ||
|
|
0288a1974b | ||
|
|
6efd8782fe | ||
|
|
8bab9d5d60 | ||
|
|
6ef1dfd8be | ||
|
|
7e58648743 | ||
|
|
0f0c3e616d | ||
|
|
c7ce65ea4c | ||
|
|
c36247b609 | ||
|
|
15296e43a4 | ||
|
|
f2929230a2 | ||
|
|
bf252b8061 | ||
|
|
9e2bf2af7e | ||
|
|
245f2654f0 | ||
|
|
67ca298a72 | ||
|
|
67d4dbf91a | ||
|
|
b344269140 | ||
|
|
bb547610f2 | ||
|
|
1e1f007bb7 | ||
|
|
c40d858f02 | ||
|
|
3d564d85fd | ||
|
|
02cea40ffa | ||
|
|
e502d336db | ||
|
|
807cb99f6d | ||
|
|
8b6879a782 | ||
|
|
7ca0362f23 | ||
|
|
56c7bd242a | ||
|
|
5c6112415a | ||
|
|
bf6a0c9fc4 | ||
|
|
d54b937ab6 | ||
|
|
7c23c32e44 | ||
|
|
4e21d24b5f | ||
|
|
ad6fb85fda | ||
|
|
5dc39a5d24 | ||
|
|
3597f687de | ||
|
|
8811506adf | ||
|
|
11dec6fc0f | ||
|
|
59c4c8233f | ||
|
|
9da79d2d81 | ||
|
|
246b474a25 | ||
|
|
27e8a3a1b5 | ||
|
|
745797b596 | ||
|
|
940e9e037e | ||
|
|
512c0079a9 | ||
|
|
645c29f853 | ||
|
|
e55945674d | ||
|
|
7ac88536dd | ||
|
|
230b9fc9e6 | ||
|
|
27ca782cac | ||
|
|
a136a00a2f | ||
|
|
637ec35d6a | ||
|
|
4b55df1cb4 | ||
|
|
b9309268ba | ||
|
|
8fa89baf54 | ||
|
|
8374a5e579 | ||
|
|
525233e10b | ||
|
|
eadda6a967 | ||
|
|
3d6590af89 | ||
|
|
28d933d5d6 | ||
|
|
c1dc42a094 | ||
|
|
6384ff3ee7 | ||
|
|
a118594c8b | ||
|
|
93c6105442 | ||
|
|
ced4a75a1a | ||
|
|
57fecdc09e | ||
|
|
cd491bb6e0 | ||
|
|
f16ad8f71d | ||
|
|
e340685a99 | ||
|
|
df89a8771c | ||
|
|
bdcf266e45 | ||
|
|
edf41b06fd | ||
|
|
38960a08d6 | ||
|
|
fbda7aab23 | ||
|
|
c575aa0640 | ||
|
|
583f6b1ba2 | ||
|
|
bb55ecc101 | ||
|
|
4421acef34 | ||
|
|
4c9418f59a | ||
|
|
219923bd63 | ||
|
|
7551782a25 | ||
|
|
5c836604c0 | ||
|
|
eff24a8726 | ||
|
|
72df6e52cd | ||
|
|
e235a45abb | ||
|
|
fbe479c43f |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://www.amazon.com/hz/wishlist/ls/8WPVFLQQDPTA']
|
||||
# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -14,6 +14,7 @@ namespace Analyser {
|
||||
enum class Machine {
|
||||
AmstradCPC,
|
||||
AppleII,
|
||||
AppleIIgs,
|
||||
Atari2600,
|
||||
AtariST,
|
||||
ColecoVision,
|
||||
@@ -23,7 +24,8 @@ enum class Machine {
|
||||
MSX,
|
||||
Oric,
|
||||
Vic20,
|
||||
ZX8081
|
||||
ZX8081,
|
||||
ZXSpectrum,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s
|
||||
new_file.name = name;
|
||||
new_file.load_address = uint32_t(details->samples[0][file_offset] | (details->samples[0][file_offset+1] << 8) | ((details->samples[0][file_offset+6]&0x0c) << 14));
|
||||
new_file.execution_address = uint32_t(details->samples[0][file_offset+2] | (details->samples[0][file_offset+3] << 8) | ((details->samples[0][file_offset+6]&0xc0) << 10));
|
||||
new_file.is_protected = names->samples[0][file_offset + 7] & 0x80;
|
||||
if(names->samples[0][file_offset + 7] & 0x80) {
|
||||
// File is locked; it may not be altered or deleted.
|
||||
new_file.flags |= File::Flags::Locked;
|
||||
}
|
||||
|
||||
long data_length = long(details->samples[0][file_offset+4] | (details->samples[0][file_offset+5] << 8) | ((details->samples[0][file_offset+6]&0x30) << 12));
|
||||
int start_sector = details->samples[0][file_offset+7] | ((details->samples[0][file_offset+6]&0x03) << 8);
|
||||
@@ -69,11 +72,16 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s
|
||||
new_file.data.insert(new_file.data.end(), next_sector->samples[0].begin(), next_sector->samples[0].begin() + length_from_sector);
|
||||
data_length -= length_from_sector;
|
||||
}
|
||||
if(!data_length) catalogue->files.push_back(new_file);
|
||||
if(!data_length) catalogue->files.push_back(std::move(new_file));
|
||||
}
|
||||
|
||||
return catalogue;
|
||||
}
|
||||
|
||||
/*
|
||||
Primary resource used: "Acorn 8-Bit ADFS Filesystem Structure";
|
||||
http://mdfs.net/Docs/Comp/Disk/Format/ADFS
|
||||
*/
|
||||
std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
auto catalogue = std::make_unique<Catalogue>();
|
||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||
@@ -101,5 +109,73 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::
|
||||
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) {
|
||||
// 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];
|
||||
std::size_t c = 0;
|
||||
for(; c < 10; c++) {
|
||||
const char next = root_directory[file_offset + c] & 0x7f;
|
||||
name[c] = next;
|
||||
if(next == '\0' || next == '\r') break;
|
||||
}
|
||||
name[c] = '\0';
|
||||
|
||||
// Skip if the name is empty.
|
||||
if(name[0] == '\0') continue;
|
||||
|
||||
// Populate a file then.
|
||||
File new_file;
|
||||
new_file.name = name;
|
||||
new_file.flags =
|
||||
(root_directory[file_offset + 0] & 0x80 ? File::Flags::Readable : 0) |
|
||||
(root_directory[file_offset + 1] & 0x80 ? File::Flags::Writable : 0) |
|
||||
(root_directory[file_offset + 2] & 0x80 ? File::Flags::Locked : 0) |
|
||||
(root_directory[file_offset + 3] & 0x80 ? File::Flags::IsDirectory : 0) |
|
||||
(root_directory[file_offset + 4] & 0x80 ? File::Flags::ExecuteOnly : 0) |
|
||||
(root_directory[file_offset + 5] & 0x80 ? File::Flags::PubliclyReadable : 0) |
|
||||
(root_directory[file_offset + 6] & 0x80 ? File::Flags::PubliclyWritable : 0) |
|
||||
(root_directory[file_offset + 7] & 0x80 ? File::Flags::PubliclyExecuteOnly : 0) |
|
||||
(root_directory[file_offset + 8] & 0x80 ? File::Flags::IsPrivate : 0);
|
||||
|
||||
new_file.load_address =
|
||||
(uint32_t(root_directory[file_offset + 0x0a]) << 0) |
|
||||
(uint32_t(root_directory[file_offset + 0x0b]) << 8) |
|
||||
(uint32_t(root_directory[file_offset + 0x0c]) << 16) |
|
||||
(uint32_t(root_directory[file_offset + 0x0d]) << 24);
|
||||
|
||||
new_file.execution_address =
|
||||
(uint32_t(root_directory[file_offset + 0x0e]) << 0) |
|
||||
(uint32_t(root_directory[file_offset + 0x0f]) << 8) |
|
||||
(uint32_t(root_directory[file_offset + 0x10]) << 16) |
|
||||
(uint32_t(root_directory[file_offset + 0x11]) << 24);
|
||||
|
||||
new_file.sequence_number = root_directory[file_offset + 0x19];
|
||||
|
||||
const uint32_t size =
|
||||
(uint32_t(root_directory[file_offset + 0x12]) << 0) |
|
||||
(uint32_t(root_directory[file_offset + 0x13]) << 8) |
|
||||
(uint32_t(root_directory[file_offset + 0x14]) << 16) |
|
||||
(uint32_t(root_directory[file_offset + 0x15]) << 24);
|
||||
|
||||
uint32_t start_sector =
|
||||
(uint32_t(root_directory[file_offset + 0x16]) << 0) |
|
||||
(uint32_t(root_directory[file_offset + 0x17]) << 8) |
|
||||
(uint32_t(root_directory[file_offset + 0x18]) << 16);
|
||||
|
||||
new_file.data.reserve(size);
|
||||
while(new_file.data.size() < size) {
|
||||
const Storage::Encodings::MFM::Sector *const sector = parser.get_sector(start_sector / (80 * 16), (start_sector / 16) % 80, start_sector % 16);
|
||||
if(!sector) break;
|
||||
|
||||
const auto length_from_sector = std::min(size - new_file.data.size(), sector->samples[0].size());
|
||||
new_file.data.insert(new_file.data.end(), sector->samples[0].begin(), sector->samples[0].begin() + ssize_t(length_from_sector));
|
||||
++start_sector;
|
||||
}
|
||||
|
||||
catalogue->files.push_back(std::move(new_file));
|
||||
}
|
||||
|
||||
return catalogue;
|
||||
}
|
||||
|
||||
@@ -19,19 +19,38 @@ namespace Acorn {
|
||||
|
||||
struct File {
|
||||
std::string name;
|
||||
uint32_t load_address;
|
||||
uint32_t execution_address;
|
||||
bool is_protected;
|
||||
uint32_t load_address = 0;
|
||||
uint32_t execution_address = 0;
|
||||
|
||||
enum Flags: uint16_t {
|
||||
Readable = 1 << 0,
|
||||
Writable = 1 << 1,
|
||||
Locked = 1 << 2,
|
||||
IsDirectory = 1 << 3,
|
||||
ExecuteOnly = 1 << 4,
|
||||
PubliclyReadable = 1 << 5,
|
||||
PubliclyWritable = 1 << 6,
|
||||
PubliclyExecuteOnly = 1 << 7,
|
||||
IsPrivate = 1 << 8,
|
||||
};
|
||||
uint16_t flags = Flags::Readable | Flags::Readable | Flags::PubliclyReadable | Flags::PubliclyWritable;
|
||||
uint8_t sequence_number = 0;
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
|
||||
/// Describes a single chunk of file data; these relate to the tape and ROM filing system.
|
||||
/// The File-level fields contain a 'definitive' version of the load and execution addresses,
|
||||
/// but both of those filing systems also store them per chunk.
|
||||
///
|
||||
/// Similarly, the file-level data will contain the aggregate data of all chunks.
|
||||
struct Chunk {
|
||||
std::string name;
|
||||
uint32_t load_address;
|
||||
uint32_t execution_address;
|
||||
uint16_t block_number;
|
||||
uint16_t block_length;
|
||||
uint8_t block_flag;
|
||||
uint32_t next_address;
|
||||
uint32_t load_address = 0;
|
||||
uint32_t execution_address = 0;
|
||||
uint16_t block_number = 0;
|
||||
uint16_t block_length = 0;
|
||||
uint32_t next_address = 0;
|
||||
uint8_t block_flag = 0;
|
||||
|
||||
bool header_crc_matched;
|
||||
bool data_crc_matched;
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "Tape.hpp"
|
||||
#include "Target.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace Analyser::Static::Acorn;
|
||||
|
||||
static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
@@ -59,10 +61,6 @@ 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>();
|
||||
target->confidence = 0.5; // TODO: a proper estimation
|
||||
target->has_dfs = false;
|
||||
target->has_adfs = false;
|
||||
target->should_shift_restart = false;
|
||||
|
||||
// strip out inappropriate cartridges
|
||||
target->media.cartridges = AcornCartridgesFrom(media.cartridges);
|
||||
@@ -77,8 +75,8 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me
|
||||
if(!files.empty()) {
|
||||
bool is_basic = true;
|
||||
|
||||
// protected files are always for *RUNning only
|
||||
if(files.front().is_protected) is_basic = false;
|
||||
// If a file is execute-only, that means *RUN.
|
||||
if(files.front().flags & File::Flags::ExecuteOnly) is_basic = false;
|
||||
|
||||
// check also for a continuous threading of BASIC lines; if none then this probably isn't BASIC code,
|
||||
// so that's also justification to *RUN
|
||||
@@ -108,15 +106,60 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me
|
||||
dfs_catalogue = GetDFSCatalogue(disk);
|
||||
if(dfs_catalogue == nullptr) adfs_catalogue = GetADFSCatalogue(disk);
|
||||
if(dfs_catalogue || adfs_catalogue) {
|
||||
// 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 = !!dfs_catalogue;
|
||||
target->has_adfs = !!adfs_catalogue;
|
||||
target->has_dfs = bool(dfs_catalogue);
|
||||
target->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)
|
||||
if(bootOption != Catalogue::BootOption::None) {
|
||||
target->should_shift_restart = true;
|
||||
else
|
||||
} else {
|
||||
target->loading_command = "*CAT\n";
|
||||
}
|
||||
|
||||
// Check whether adding the AP6 ROM is justified.
|
||||
// For now this is an incredibly dense text search;
|
||||
// if any of the commands that aren't usually present
|
||||
// on a stock Electron are here, add the AP6 ROM and
|
||||
// some sideways RAM such that the SR commands are useful.
|
||||
for(const auto &file: dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files) {
|
||||
for(const auto &command: {
|
||||
"AQRPAGE", "BUILD", "DUMP", "FORMAT", "INSERT", "LANG", "LIST", "LOADROM",
|
||||
"LOCK", "LROMS", "RLOAD", "ROMS", "RSAVE", "SAVEROM", "SRLOAD", "SRPAGE",
|
||||
"SRUNLOCK", "SRWIPE", "TUBE", "TYPE", "UNLOCK", "UNPLUG", "UROMS",
|
||||
"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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
target->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);
|
||||
if(sector[0xfd]) {
|
||||
target->should_shift_restart = true;
|
||||
} else {
|
||||
target->loading_command = "*CAT\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,12 @@ static std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &chunks) {
|
||||
file->name = file->chunks.front().name;
|
||||
file->load_address = file->chunks.front().load_address;
|
||||
file->execution_address = file->chunks.front().execution_address;
|
||||
file->is_protected = !!(file->chunks.back().block_flag & 0x01); // I think the last flags are the ones that count; TODO: check.
|
||||
// I think the final chunk's flags are the ones that count; TODO: check.
|
||||
if(file->chunks.back().block_flag & 0x01) {
|
||||
// File is locked, which in more generalised terms means it is
|
||||
// for execution only.
|
||||
file->flags |= File::Flags::ExecuteOnly;
|
||||
}
|
||||
|
||||
// copy all data into a single big block
|
||||
for(File::Chunk chunk : file->chunks) {
|
||||
|
||||
@@ -18,15 +18,21 @@ namespace Static {
|
||||
namespace Acorn {
|
||||
|
||||
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
|
||||
bool has_adfs = false;
|
||||
bool has_acorn_adfs = false;
|
||||
bool has_pres_adfs = false;
|
||||
bool has_dfs = false;
|
||||
bool has_ap6_rom = false;
|
||||
bool has_sideways_ram = false;
|
||||
bool should_shift_restart = false;
|
||||
std::string loading_command;
|
||||
|
||||
Target() : Analyser::Static::Target(Machine::Electron) {
|
||||
if(needs_declare()) {
|
||||
DeclareField(has_adfs);
|
||||
DeclareField(has_pres_adfs);
|
||||
DeclareField(has_acorn_adfs);
|
||||
DeclareField(has_dfs);
|
||||
DeclareField(has_ap6_rom);
|
||||
DeclareField(has_sideways_ram);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,12 +11,15 @@
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "Target.hpp"
|
||||
|
||||
#include "../../../Storage/Disk/Parsers/CPM.hpp"
|
||||
#include "../../../Storage/Disk/Encodings/MFM/Parser.hpp"
|
||||
#include "../../../Storage/Tape/Parsers/Spectrum.hpp"
|
||||
|
||||
static bool strcmp_insensitive(const char *a, const char *b) {
|
||||
#include "Target.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
bool strcmp_insensitive(const char *a, const char *b) {
|
||||
if(std::strlen(a) != std::strlen(b)) return false;
|
||||
while(*a) {
|
||||
if(std::tolower(*a) != std::tolower(*b)) return false;
|
||||
@@ -26,20 +29,20 @@ static bool strcmp_insensitive(const char *a, const char *b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_implied_extension(const std::string &extension) {
|
||||
bool is_implied_extension(const std::string &extension) {
|
||||
return
|
||||
extension == " " ||
|
||||
strcmp_insensitive(extension.c_str(), "BAS") ||
|
||||
strcmp_insensitive(extension.c_str(), "BIN");
|
||||
}
|
||||
|
||||
static void right_trim(std::string &string) {
|
||||
void right_trim(std::string &string) {
|
||||
string.erase(std::find_if(string.rbegin(), string.rend(), [](int ch) {
|
||||
return !std::isspace(ch);
|
||||
}).base(), string.end());
|
||||
}
|
||||
|
||||
static std::string RunCommandFor(const Storage::Disk::CPM::File &file) {
|
||||
std::string RunCommandFor(const Storage::Disk::CPM::File &file) {
|
||||
// Trim spaces from the name.
|
||||
std::string name = file.name;
|
||||
right_trim(name);
|
||||
@@ -58,7 +61,7 @@ static std::string RunCommandFor(const Storage::Disk::CPM::File &file) {
|
||||
return command + "\n";
|
||||
}
|
||||
|
||||
static void InspectCatalogue(
|
||||
void InspectCatalogue(
|
||||
const Storage::Disk::CPM::Catalogue &catalogue,
|
||||
const std::unique_ptr<Analyser::Static::AmstradCPC::Target> &target) {
|
||||
|
||||
@@ -155,7 +158,7 @@ static void InspectCatalogue(
|
||||
target->loading_command = "cat\n";
|
||||
}
|
||||
|
||||
static bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, const std::unique_ptr<Analyser::Static::AmstradCPC::Target> &target) {
|
||||
bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, const std::unique_ptr<Analyser::Static::AmstradCPC::Target> &target) {
|
||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||
Storage::Encodings::MFM::Sector *boot_sector = parser.get_sector(0, 0, 0x41);
|
||||
if(boot_sector != nullptr && !boot_sector->samples.empty() && boot_sector->samples[0].size() == 512) {
|
||||
@@ -179,6 +182,28 @@ static bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, co
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsAmstradTape(const std::shared_ptr<Storage::Tape::Tape> &tape) {
|
||||
// Limited sophistication here; look for a CPC-style file header, that is
|
||||
// any Spectrum-esque block with a synchronisation character of 0x2c.
|
||||
//
|
||||
// More could be done here: parse the header, look for 0x16 data records.
|
||||
using Parser = Storage::Tape::ZXSpectrum::Parser;
|
||||
Parser parser(Parser::MachineType::AmstradCPC);
|
||||
|
||||
while(true) {
|
||||
const auto block = parser.find_block(tape);
|
||||
if(!block) break;
|
||||
|
||||
if(block->type == 0x2c) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) {
|
||||
TargetList destination;
|
||||
auto target = std::make_unique<Target>();
|
||||
@@ -187,13 +212,19 @@ Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Medi
|
||||
target->model = Target::Model::CPC6128;
|
||||
|
||||
if(!media.tapes.empty()) {
|
||||
// TODO: which of these are actually potentially CPC tapes?
|
||||
target->media.tapes = media.tapes;
|
||||
bool has_cpc_tape = false;
|
||||
for(auto &tape: media.tapes) {
|
||||
has_cpc_tape |= IsAmstradTape(tape);
|
||||
}
|
||||
|
||||
// Ugliness flows here: assume the CPC isn't smart enough to pause between pressing
|
||||
// enter and responding to the follow-on prompt to press a key, so just type for
|
||||
// a while. Yuck!
|
||||
target->loading_command = "|tape\nrun\"\n1234567890";
|
||||
if(has_cpc_tape) {
|
||||
target->media.tapes = media.tapes;
|
||||
|
||||
// Ugliness flows here: assume the CPC isn't smart enough to pause between pressing
|
||||
// enter and responding to the follow-on prompt to press a key, so just type for
|
||||
// a while. Yuck!
|
||||
target->loading_command = "|tape\nrun\"\n123";
|
||||
}
|
||||
}
|
||||
|
||||
if(!media.disks.empty()) {
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Target_h
|
||||
#define Target_h
|
||||
#ifndef Analyser_Static_AppleII_Target_h
|
||||
#define Analyser_Static_AppleII_Target_h
|
||||
|
||||
#include "../../../Reflection/Enum.hpp"
|
||||
#include "../../../Reflection/Struct.hpp"
|
||||
@@ -47,4 +47,4 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Target_h */
|
||||
#endif /* Analyser_Static_AppleII_Target_h */
|
||||
|
||||
19
Analyser/Static/AppleIIgs/StaticAnalyser.cpp
Normal file
19
Analyser/Static/AppleIIgs/StaticAnalyser.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// StaticAnalyser.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/10/2020.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "StaticAnalyser.hpp"
|
||||
#include "Target.hpp"
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::AppleIIgs::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) {
|
||||
auto target = std::make_unique<Target>();
|
||||
target->media = media;
|
||||
|
||||
TargetList targets;
|
||||
targets.push_back(std::move(target));
|
||||
return targets;
|
||||
}
|
||||
26
Analyser/Static/AppleIIgs/StaticAnalyser.hpp
Normal file
26
Analyser/Static/AppleIIgs/StaticAnalyser.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// StaticAnalyser.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/10/2020.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_AppleIIgs_StaticAnalyser_hpp
|
||||
#define Analyser_Static_AppleIIgs_StaticAnalyser_hpp
|
||||
|
||||
#include "../StaticAnalyser.hpp"
|
||||
#include "../../../Storage/TargetPlatforms.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace AppleIIgs {
|
||||
|
||||
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Analyser_Static_AppleIIgs_StaticAnalyser_hpp */
|
||||
49
Analyser/Static/AppleIIgs/Target.hpp
Normal file
49
Analyser/Static/AppleIIgs/Target.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Target.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/10/2020.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_AppleIIgs_Target_h
|
||||
#define Analyser_Static_AppleIIgs_Target_h
|
||||
|
||||
#include "../../../Reflection/Enum.hpp"
|
||||
#include "../../../Reflection/Struct.hpp"
|
||||
#include "../StaticAnalyser.hpp"
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace AppleIIgs {
|
||||
|
||||
struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> {
|
||||
ReflectableEnum(Model,
|
||||
ROM00,
|
||||
ROM01,
|
||||
ROM03
|
||||
);
|
||||
ReflectableEnum(MemoryModel,
|
||||
TwoHundredAndFiftySixKB,
|
||||
OneMB,
|
||||
EightMB
|
||||
);
|
||||
|
||||
Model model = Model::ROM03;
|
||||
MemoryModel memory_model = MemoryModel::EightMB;
|
||||
|
||||
Target() : Analyser::Static::Target(Machine::AppleIIgs) {
|
||||
if(needs_declare()) {
|
||||
DeclareField(model);
|
||||
DeclareField(memory_model);
|
||||
AnnounceEnum(Model);
|
||||
AnnounceEnum(MemoryModel);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Analyser_Static_AppleIIgs_Target_h */
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "StaticAnalyser.hpp"
|
||||
|
||||
#include "../AppleII/Target.hpp"
|
||||
#include "../AppleIIgs/Target.hpp"
|
||||
#include "../Oric/Target.hpp"
|
||||
#include "../Disassembler/6502.hpp"
|
||||
#include "../Disassembler/AddressMapper.hpp"
|
||||
@@ -18,7 +19,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
Analyser::Static::Target *AppleTarget(const Storage::Encodings::AppleGCR::Sector *sector_zero) {
|
||||
Analyser::Static::Target *AppleIITarget(const Storage::Encodings::AppleGCR::Sector *sector_zero) {
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
auto *const target = new Target;
|
||||
|
||||
@@ -31,6 +32,10 @@ Analyser::Static::Target *AppleTarget(const Storage::Encodings::AppleGCR::Sector
|
||||
return target;
|
||||
}
|
||||
|
||||
Analyser::Static::Target *AppleIIgsTarget() {
|
||||
return new Analyser::Static::AppleIIgs::Target();
|
||||
}
|
||||
|
||||
Analyser::Static::Target *OricTarget(const Storage::Encodings::AppleGCR::Sector *) {
|
||||
using Target = Analyser::Static::Oric::Target;
|
||||
auto *const target = new Target;
|
||||
@@ -46,8 +51,18 @@ Analyser::Static::TargetList Analyser::Static::DiskII::GetTargets(const Media &m
|
||||
// This analyser can comprehend disks only.
|
||||
if(media.disks.empty()) return {};
|
||||
|
||||
auto &disk = media.disks.front();
|
||||
TargetList targets;
|
||||
|
||||
// If the disk image is too large for a 5.25" disk, map this to the IIgs.
|
||||
if(disk->get_maximum_head_position() > Storage::Disk::HeadPosition(40)) {
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleIIgsTarget()));
|
||||
targets.back()->media = media;
|
||||
return targets;
|
||||
}
|
||||
|
||||
// Grab track 0, sector 0: the boot sector.
|
||||
const auto track_zero = media.disks.front()->get_track_at_position(Storage::Disk::Track::Address(0, Storage::Disk::HeadPosition(0)));
|
||||
const auto track_zero = disk->get_track_at_position(Storage::Disk::Track::Address(0, Storage::Disk::HeadPosition(0)));
|
||||
const auto sector_map = Storage::Encodings::AppleGCR::sectors_from_segment(
|
||||
Storage::Disk::track_serialisation(*track_zero, Storage::Time(1, 50000)));
|
||||
|
||||
@@ -61,12 +76,11 @@ Analyser::Static::TargetList Analyser::Static::DiskII::GetTargets(const Media &m
|
||||
|
||||
// If there's no boot sector then if there are also no sectors at all,
|
||||
// decline to nominate a machine. Otherwise go with an Apple as the default.
|
||||
TargetList targets;
|
||||
if(!sector_zero) {
|
||||
if(sector_map.empty()) {
|
||||
return targets;
|
||||
} else {
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleTarget(nullptr)));
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleIITarget(nullptr)));
|
||||
targets.back()->media = media;
|
||||
return targets;
|
||||
}
|
||||
@@ -116,7 +130,7 @@ Analyser::Static::TargetList Analyser::Static::DiskII::GetTargets(const Media &m
|
||||
if(is_oric) {
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(OricTarget(sector_zero)));
|
||||
} else {
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleTarget(sector_zero)));
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleIITarget(sector_zero)));
|
||||
}
|
||||
targets.back()->media = media;
|
||||
return targets;
|
||||
|
||||
@@ -19,6 +19,19 @@ Analyser::Static::TargetList Analyser::Static::Macintosh::GetTargets(const Media
|
||||
using Target = Analyser::Static::Macintosh::Target;
|
||||
auto *const target = new Target;
|
||||
target->media = media;
|
||||
|
||||
// If this is a single-sided floppy disk, guess the Macintosh 512kb.
|
||||
if(media.mass_storage_devices.empty()) {
|
||||
bool has_800kb_disks = false;
|
||||
for(const auto &disk: media.disks) {
|
||||
has_800kb_disks |= disk->get_head_count() > 1;
|
||||
}
|
||||
|
||||
if(!has_800kb_disks) {
|
||||
target->model = Target::Model::Mac512k;
|
||||
}
|
||||
}
|
||||
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
|
||||
|
||||
return targets;
|
||||
|
||||
@@ -33,8 +33,14 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta
|
||||
BD500
|
||||
);
|
||||
|
||||
ReflectableEnum(Processor,
|
||||
MOS6502,
|
||||
WDC65816
|
||||
);
|
||||
|
||||
ROM rom = ROM::BASIC11;
|
||||
DiskInterface disk_interface = DiskInterface::None;
|
||||
Processor processor = Processor::MOS6502;
|
||||
std::string loading_command;
|
||||
bool should_start_jasmin = false;
|
||||
|
||||
@@ -42,8 +48,10 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta
|
||||
if(needs_declare()) {
|
||||
DeclareField(rom);
|
||||
DeclareField(disk_interface);
|
||||
DeclareField(processor);
|
||||
AnnounceEnum(ROM);
|
||||
AnnounceEnum(DiskInterface);
|
||||
AnnounceEnum(Processor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "Acorn/StaticAnalyser.hpp"
|
||||
#include "AmstradCPC/StaticAnalyser.hpp"
|
||||
#include "AppleII/StaticAnalyser.hpp"
|
||||
#include "AppleIIgs/StaticAnalyser.hpp"
|
||||
#include "Atari2600/StaticAnalyser.hpp"
|
||||
#include "AtariST/StaticAnalyser.hpp"
|
||||
#include "Coleco/StaticAnalyser.hpp"
|
||||
@@ -27,12 +28,14 @@
|
||||
#include "Oric/StaticAnalyser.hpp"
|
||||
#include "Sega/StaticAnalyser.hpp"
|
||||
#include "ZX8081/StaticAnalyser.hpp"
|
||||
#include "ZXSpectrum/StaticAnalyser.hpp"
|
||||
|
||||
// Cartridges
|
||||
#include "../../Storage/Cartridge/Formats/BinaryDump.hpp"
|
||||
#include "../../Storage/Cartridge/Formats/PRG.hpp"
|
||||
|
||||
// Disks
|
||||
#include "../../Storage/Disk/DiskImage/Formats/2MG.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/AcornADF.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/AppleDSK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp"
|
||||
@@ -51,6 +54,7 @@
|
||||
#include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp"
|
||||
|
||||
// Mass Storage Devices (i.e. usually, hard disks)
|
||||
#include "../../Storage/MassStorage/Formats/DAT.hpp"
|
||||
#include "../../Storage/MassStorage/Formats/HFV.hpp"
|
||||
|
||||
// Tapes
|
||||
@@ -62,6 +66,7 @@
|
||||
#include "../../Storage/Tape/Formats/TapeUEF.hpp"
|
||||
#include "../../Storage/Tape/Formats/TZX.hpp"
|
||||
#include "../../Storage/Tape/Formats/ZX80O81P.hpp"
|
||||
#include "../../Storage/Tape/Formats/ZXSpectrumTAP.hpp"
|
||||
|
||||
// Target Platform Types
|
||||
#include "../../Storage/TargetPlatforms.hpp"
|
||||
@@ -78,36 +83,52 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
std::string extension = file_name.substr(final_dot + 1);
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
|
||||
|
||||
#define Insert(list, class, platforms) \
|
||||
list.emplace_back(new Storage::class(file_name));\
|
||||
#define InsertInstance(list, instance, platforms) \
|
||||
list.emplace_back(instance);\
|
||||
potential_platforms |= platforms;\
|
||||
TargetPlatform::TypeDistinguisher *distinguisher = dynamic_cast<TargetPlatform::TypeDistinguisher *>(list.back().get());\
|
||||
if(distinguisher) potential_platforms &= distinguisher->target_platform_type();
|
||||
if(distinguisher) potential_platforms &= distinguisher->target_platform_type(); \
|
||||
|
||||
#define TryInsert(list, class, platforms) \
|
||||
#define Insert(list, class, platforms, ...) \
|
||||
InsertInstance(list, new Storage::class(__VA_ARGS__), platforms);
|
||||
|
||||
#define TryInsert(list, class, platforms, ...) \
|
||||
try {\
|
||||
Insert(list, class, platforms) \
|
||||
Insert(list, class, platforms, __VA_ARGS__) \
|
||||
} catch(...) {}
|
||||
|
||||
#define Format(ext, list, class, platforms) \
|
||||
if(extension == ext) { \
|
||||
TryInsert(list, class, platforms) \
|
||||
TryInsert(list, class, platforms, file_name) \
|
||||
}
|
||||
|
||||
// 2MG
|
||||
if(extension == "2mg") {
|
||||
// 2MG uses a factory method; defer to it.
|
||||
try {
|
||||
InsertInstance(result.disks, Storage::Disk::Disk2MG::open(file_name), TargetPlatform::DiskII)
|
||||
} catch(...) {}
|
||||
}
|
||||
|
||||
Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 80
|
||||
Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 81
|
||||
Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // A26
|
||||
Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn) // ADF
|
||||
Format("adl", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn) // ADL
|
||||
Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge) // BIN (cartridge dump)
|
||||
Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX) // CAS
|
||||
Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT
|
||||
Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision) // COL
|
||||
Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Coleco) // COL
|
||||
Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape) // CSW
|
||||
Format("d64", result.disks, Disk::DiskImageHolder<Storage::Disk::D64>, TargetPlatform::Commodore) // D64
|
||||
Format("dat", result.mass_storage_devices, MassStorage::DAT, TargetPlatform::Acorn) // DAT
|
||||
Format("dmk", result.disks, Disk::DiskImageHolder<Storage::Disk::DMK>, TargetPlatform::MSX) // DMK
|
||||
Format("do", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DO
|
||||
Format("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // DSD
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC) // DSK (Amstrad CPC)
|
||||
Format( "dsk",
|
||||
result.disks,
|
||||
Disk::DiskImageHolder<Storage::Disk::CPCDSK>,
|
||||
TargetPlatform::AmstradCPC | TargetPlatform::Oric | TargetPlatform::ZXSpectrum) // DSK (Amstrad CPC, etc)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DSK (Apple II)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // DSK (Macintosh, floppy disk)
|
||||
Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk)
|
||||
@@ -117,7 +138,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
Format( "hfe",
|
||||
result.disks,
|
||||
Disk::DiskImageHolder<Storage::Disk::HFE>,
|
||||
TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric)
|
||||
TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric | TargetPlatform::ZXSpectrum)
|
||||
// HFE (TODO: switch to AllDisk once the MSX stops being so greedy)
|
||||
Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
||||
Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
||||
@@ -125,17 +146,23 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII) // NIB
|
||||
Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O
|
||||
Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P
|
||||
Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // PO
|
||||
Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // PO (original Apple II kind)
|
||||
|
||||
// PO (Apple IIgs kind)
|
||||
if(extension == "po") {
|
||||
TryInsert(result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::AppleIIgs, file_name, Storage::Disk::MacintoshIMG::FixedType::GCR)
|
||||
}
|
||||
|
||||
Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P81
|
||||
|
||||
// PRG
|
||||
if(extension == "prg") {
|
||||
// try instantiating as a ROM; failing that accept as a tape
|
||||
try {
|
||||
Insert(result.cartridges, Cartridge::PRG, TargetPlatform::Commodore)
|
||||
Insert(result.cartridges, Cartridge::PRG, TargetPlatform::Commodore, file_name)
|
||||
} catch(...) {
|
||||
try {
|
||||
Insert(result.tapes, Tape::PRG, TargetPlatform::Commodore)
|
||||
Insert(result.tapes, Tape::PRG, TargetPlatform::Commodore, file_name)
|
||||
} catch(...) {}
|
||||
}
|
||||
}
|
||||
@@ -143,7 +170,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
Format( "rom",
|
||||
result.cartridges,
|
||||
Cartridge::BinaryDump,
|
||||
TargetPlatform::AcornElectron | TargetPlatform::ColecoVision | TargetPlatform::MSX) // ROM
|
||||
TargetPlatform::AcornElectron | TargetPlatform::Coleco | TargetPlatform::MSX) // ROM
|
||||
Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SG
|
||||
Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS
|
||||
Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // SSD
|
||||
@@ -151,14 +178,16 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
Format("stx", result.disks, Disk::DiskImageHolder<Storage::Disk::STX>, TargetPlatform::AtariST) // STX
|
||||
Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore)
|
||||
Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric)
|
||||
Format("tap", result.tapes, Tape::ZXSpectrumTAP, TargetPlatform::ZXSpectrum) // TAP (ZX Spectrum)
|
||||
Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX
|
||||
Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081) // TZX
|
||||
Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081 | TargetPlatform::ZXSpectrum) // TZX
|
||||
Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn) // UEF (tape)
|
||||
Format("woz", result.disks, Disk::DiskImageHolder<Storage::Disk::WOZ>, TargetPlatform::DiskII) // WOZ
|
||||
|
||||
#undef Format
|
||||
#undef Insert
|
||||
#undef TryInsert
|
||||
#undef InsertInstance
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -178,26 +207,28 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) {
|
||||
|
||||
// Hand off to platform-specific determination of whether these things are actually compatible and,
|
||||
// if so, how to load them.
|
||||
#define Append(x) {\
|
||||
auto new_targets = x::GetTargets(media, file_name, potential_platforms);\
|
||||
std::move(new_targets.begin(), new_targets.end(), std::back_inserter(targets));\
|
||||
}
|
||||
if(potential_platforms & TargetPlatform::Acorn) Append(Acorn);
|
||||
if(potential_platforms & TargetPlatform::AmstradCPC) Append(AmstradCPC);
|
||||
if(potential_platforms & TargetPlatform::AppleII) Append(AppleII);
|
||||
if(potential_platforms & TargetPlatform::Atari2600) Append(Atari2600);
|
||||
if(potential_platforms & TargetPlatform::AtariST) Append(AtariST);
|
||||
if(potential_platforms & TargetPlatform::ColecoVision) Append(Coleco);
|
||||
if(potential_platforms & TargetPlatform::Commodore) Append(Commodore);
|
||||
if(potential_platforms & TargetPlatform::DiskII) Append(DiskII);
|
||||
if(potential_platforms & TargetPlatform::Macintosh) Append(Macintosh);
|
||||
if(potential_platforms & TargetPlatform::MSX) Append(MSX);
|
||||
if(potential_platforms & TargetPlatform::Oric) Append(Oric);
|
||||
if(potential_platforms & TargetPlatform::Sega) Append(Sega);
|
||||
if(potential_platforms & TargetPlatform::ZX8081) Append(ZX8081);
|
||||
#undef Append
|
||||
#define Append(x) if(potential_platforms & TargetPlatform::x) {\
|
||||
auto new_targets = x::GetTargets(media, file_name, potential_platforms);\
|
||||
std::move(new_targets.begin(), new_targets.end(), std::back_inserter(targets));\
|
||||
}
|
||||
Append(Acorn);
|
||||
Append(AmstradCPC);
|
||||
Append(AppleII);
|
||||
Append(AppleIIgs);
|
||||
Append(Atari2600);
|
||||
Append(AtariST);
|
||||
Append(Coleco);
|
||||
Append(Commodore);
|
||||
Append(DiskII);
|
||||
Append(Macintosh);
|
||||
Append(MSX);
|
||||
Append(Oric);
|
||||
Append(Sega);
|
||||
Append(ZX8081);
|
||||
Append(ZXSpectrum);
|
||||
#undef Append
|
||||
|
||||
// Reset any tapes to their initial position
|
||||
// Reset any tapes to their initial position.
|
||||
for(const auto &target : targets) {
|
||||
for(auto &tape : target->media.tapes) {
|
||||
tape->reset();
|
||||
|
||||
95
Analyser/Static/ZXSpectrum/StaticAnalyser.cpp
Normal file
95
Analyser/Static/ZXSpectrum/StaticAnalyser.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// StaticAnalyser.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 17/03/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "StaticAnalyser.hpp"
|
||||
|
||||
#include "../../../Storage/Disk/Encodings/MFM/Parser.hpp"
|
||||
#include "../../../Storage/Tape/Parsers/Spectrum.hpp"
|
||||
|
||||
#include "Target.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsSpectrumTape(const std::shared_ptr<Storage::Tape::Tape> &tape) {
|
||||
using Parser = Storage::Tape::ZXSpectrum::Parser;
|
||||
Parser parser(Parser::MachineType::ZXSpectrum);
|
||||
|
||||
while(true) {
|
||||
const auto block = parser.find_block(tape);
|
||||
if(!block) break;
|
||||
|
||||
// Check for a Spectrum header block.
|
||||
if(block->type == 0x00) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsSpectrumDisk(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||
|
||||
// Get logical sector 1; the Spectrum appears to support various physical
|
||||
// sectors as sector 1.
|
||||
Storage::Encodings::MFM::Sector *boot_sector = nullptr;
|
||||
uint8_t sector_mask = 0;
|
||||
while(!boot_sector) {
|
||||
boot_sector = parser.get_sector(0, 0, sector_mask + 1);
|
||||
sector_mask += 0x40;
|
||||
if(!sector_mask) break;
|
||||
}
|
||||
if(!boot_sector) return false;
|
||||
|
||||
// Test that the contents of the boot sector sum to 3, modulo 256.
|
||||
uint8_t byte_sum = 0;
|
||||
for(auto byte: boot_sector->samples[0]) {
|
||||
byte_sum += byte;
|
||||
}
|
||||
return byte_sum == 3;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::ZXSpectrum::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) {
|
||||
TargetList destination;
|
||||
auto target = std::make_unique<Target>();
|
||||
target->confidence = 0.5;
|
||||
|
||||
if(!media.tapes.empty()) {
|
||||
bool has_spectrum_tape = false;
|
||||
for(auto &tape: media.tapes) {
|
||||
has_spectrum_tape |= IsSpectrumTape(tape);
|
||||
}
|
||||
|
||||
if(has_spectrum_tape) {
|
||||
target->media.tapes = media.tapes;
|
||||
}
|
||||
}
|
||||
|
||||
if(!media.disks.empty()) {
|
||||
bool has_spectrum_disk = false;
|
||||
|
||||
for(auto &disk: media.disks) {
|
||||
has_spectrum_disk |= IsSpectrumDisk(disk);
|
||||
}
|
||||
|
||||
if(has_spectrum_disk) {
|
||||
target->media.disks = media.disks;
|
||||
target->model = Target::Model::Plus3;
|
||||
}
|
||||
}
|
||||
|
||||
// If any media survived, add the target.
|
||||
if(!target->media.empty()) {
|
||||
target->should_hold_enter = true; // To force entry into the 'loader' and thereby load the media.
|
||||
destination.push_back(std::move(target));
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
26
Analyser/Static/ZXSpectrum/StaticAnalyser.hpp
Normal file
26
Analyser/Static/ZXSpectrum/StaticAnalyser.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// StaticAnalyser.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 17/03/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_ZXSpectrum_StaticAnalyser_hpp
|
||||
#define Analyser_Static_ZXSpectrum_StaticAnalyser_hpp
|
||||
|
||||
#include "../StaticAnalyser.hpp"
|
||||
#include "../../../Storage/TargetPlatforms.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace ZXSpectrum {
|
||||
|
||||
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* StaticAnalyser_hpp */
|
||||
45
Analyser/Static/ZXSpectrum/Target.hpp
Normal file
45
Analyser/Static/ZXSpectrum/Target.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Target.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 18/03/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_ZXSpectrum_Target_h
|
||||
#define Analyser_Static_ZXSpectrum_Target_h
|
||||
|
||||
#include "../../../Reflection/Enum.hpp"
|
||||
#include "../../../Reflection/Struct.hpp"
|
||||
#include "../StaticAnalyser.hpp"
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace ZXSpectrum {
|
||||
|
||||
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
|
||||
ReflectableEnum(Model,
|
||||
SixteenK,
|
||||
FortyEightK,
|
||||
OneTwoEightK,
|
||||
Plus2,
|
||||
Plus2a,
|
||||
Plus3,
|
||||
);
|
||||
|
||||
Model model = Model::Plus2;
|
||||
bool should_hold_enter = false;
|
||||
|
||||
Target(): Analyser::Static::Target(Machine::ZXSpectrum) {
|
||||
if(needs_declare()) {
|
||||
DeclareField(model);
|
||||
AnnounceEnum(Model);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Target_h */
|
||||
@@ -10,7 +10,9 @@
|
||||
#define ClockReceiver_hpp
|
||||
|
||||
#include "ForceInline.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
/*
|
||||
Informal pattern for all classes that run from a clock cycle:
|
||||
@@ -176,6 +178,9 @@ class Cycles: public WrappedInt<Cycles> {
|
||||
public:
|
||||
forceinline constexpr Cycles(IntType l) noexcept : WrappedInt<Cycles>(l) {}
|
||||
forceinline constexpr Cycles() noexcept : WrappedInt<Cycles>() {}
|
||||
forceinline static constexpr Cycles max() {
|
||||
return Cycles(std::numeric_limits<IntType>::max());
|
||||
}
|
||||
|
||||
private:
|
||||
friend WrappedInt;
|
||||
@@ -195,6 +200,9 @@ class HalfCycles: public WrappedInt<HalfCycles> {
|
||||
public:
|
||||
forceinline constexpr HalfCycles(IntType l) noexcept : WrappedInt<HalfCycles>(l) {}
|
||||
forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {}
|
||||
forceinline static constexpr HalfCycles max() {
|
||||
return HalfCycles(std::numeric_limits<IntType>::max());
|
||||
}
|
||||
|
||||
forceinline constexpr HalfCycles(const Cycles &cycles) noexcept : WrappedInt<HalfCycles>(cycles.as_integral() * 2) {}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#define JustInTime_h
|
||||
|
||||
#include "../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "ClockingHintSource.hpp"
|
||||
#include "ForceInline.hpp"
|
||||
|
||||
/*!
|
||||
@@ -21,44 +22,142 @@
|
||||
|
||||
Machines that accumulate HalfCycle time but supply to a Cycle-counted device may supply a
|
||||
separate @c TargetTimeScale at template declaration.
|
||||
|
||||
If the held object implements get_next_sequence_point() then it'll be used to flush implicitly
|
||||
as and when sequence points are hit. Callers can use will_flush() to predict these.
|
||||
|
||||
If the held object is a subclass of ClockingHint::Source, this template will register as an
|
||||
observer and potentially stop clocking or stop delaying clocking until just-in-time references
|
||||
as directed.
|
||||
|
||||
TODO: incorporate and codify AsyncJustInTimeActor.
|
||||
*/
|
||||
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class JustInTimeActor {
|
||||
template <class T, class LocalTimeScale = HalfCycles, int multiplier = 1, int divider = 1> class JustInTimeActor:
|
||||
public ClockingHint::Observer {
|
||||
private:
|
||||
/*!
|
||||
A std::unique_ptr deleter which causes an update_sequence_point to occur on the actor supplied
|
||||
to it at construction if it implements get_next_sequence_point(). Otherwise destruction is a no-op.
|
||||
|
||||
**Does not delete the object.**
|
||||
|
||||
This is used by the -> operators below, which provide a unique pointer to the enclosed object and
|
||||
update their sequence points upon its destruction — i.e. after the caller has made whatever call
|
||||
or calls as were relevant to the enclosed object.
|
||||
*/
|
||||
class SequencePointAwareDeleter {
|
||||
public:
|
||||
explicit SequencePointAwareDeleter(JustInTimeActor<T, LocalTimeScale, multiplier, divider> *actor) noexcept
|
||||
: actor_(actor) {}
|
||||
|
||||
forceinline void operator ()(const T *const) const {
|
||||
if constexpr (has_sequence_points<T>::value) {
|
||||
actor_->update_sequence_point();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
JustInTimeActor<T, LocalTimeScale, multiplier, divider> *const actor_;
|
||||
};
|
||||
|
||||
// This block of SFINAE determines whether objects of type T accepts Cycles or HalfCycles.
|
||||
using HalfRunFor = void (T::*const)(HalfCycles);
|
||||
static uint8_t half_sig(...);
|
||||
static uint16_t half_sig(HalfRunFor);
|
||||
using TargetTimeScale =
|
||||
std::conditional_t<
|
||||
sizeof(half_sig(&T::run_for)) == sizeof(uint16_t),
|
||||
HalfCycles,
|
||||
Cycles>;
|
||||
|
||||
public:
|
||||
/// Constructs a new JustInTimeActor using the same construction arguments as the included object.
|
||||
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {
|
||||
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
|
||||
object_.set_clocking_hint_observer(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds time to the actor.
|
||||
forceinline void operator += (const LocalTimeScale &rhs) {
|
||||
///
|
||||
/// @returns @c true if adding time caused a flush; @c false otherwise.
|
||||
forceinline bool operator += (LocalTimeScale rhs) {
|
||||
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
|
||||
if(clocking_preference_ == ClockingHint::Preference::None) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (multiplier != 1) {
|
||||
time_since_update_ += rhs * multiplier;
|
||||
} else {
|
||||
time_since_update_ += rhs;
|
||||
}
|
||||
is_flushed_ = false;
|
||||
|
||||
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
|
||||
if (clocking_preference_ == ClockingHint::Preference::RealTime) {
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (has_sequence_points<T>::value) {
|
||||
time_until_event_ -= rhs;
|
||||
if(time_until_event_ <= LocalTimeScale(0)) {
|
||||
time_overrun_ = time_until_event_;
|
||||
flush();
|
||||
update_sequence_point();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time and returns a pointer to the included object.
|
||||
forceinline T *operator->() {
|
||||
///
|
||||
/// If this object provides sequence points, checks for changes to the next
|
||||
/// sequence point upon deletion of the pointer.
|
||||
[[nodiscard]] forceinline auto operator->() {
|
||||
flush();
|
||||
return &object_;
|
||||
return std::unique_ptr<T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(this));
|
||||
}
|
||||
|
||||
/// Acts exactly as per the standard ->, but preserves constness.
|
||||
forceinline const T *operator->() const {
|
||||
auto non_const_this = const_cast<JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *>(this);
|
||||
///
|
||||
/// Despite being const, this will flush the object and, if relevant, update the next sequence point.
|
||||
[[nodiscard]] forceinline auto operator -> () const {
|
||||
auto non_const_this = const_cast<JustInTimeActor<T, LocalTimeScale, multiplier, divider> *>(this);
|
||||
non_const_this->flush();
|
||||
return std::unique_ptr<const T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(non_const_this));
|
||||
}
|
||||
|
||||
/// @returns a pointer to the included object, without flushing time.
|
||||
[[nodiscard]] forceinline T *last_valid() {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Returns a pointer to the included object without flushing time.
|
||||
forceinline T *last_valid() {
|
||||
/// @returns a const pointer to the included object, without flushing time.
|
||||
[[nodiscard]] forceinline const T *last_valid() const {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// @returns the amount of time since the object was last flushed, in the target time scale.
|
||||
[[nodiscard]] forceinline TargetTimeScale time_since_flush() const {
|
||||
// TODO: does this handle conversions properly where TargetTimeScale != LocalTimeScale?
|
||||
if constexpr (divider == 1) {
|
||||
return time_since_update_;
|
||||
}
|
||||
return TargetTimeScale(time_since_update_.as_integral() / divider);
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time.
|
||||
///
|
||||
/// This does not affect this actor's record of when the next sequence point will occur.
|
||||
forceinline void flush() {
|
||||
if(!is_flushed_) {
|
||||
is_flushed_ = true;
|
||||
did_flush_ = is_flushed_ = true;
|
||||
if constexpr (divider == 1) {
|
||||
const auto duration = time_since_update_.template flush<TargetTimeScale>();
|
||||
object_.run_for(duration);
|
||||
@@ -70,56 +169,63 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale time_since_update_;
|
||||
bool is_flushed_ = true;
|
||||
};
|
||||
/// Indicates whether a flush has occurred since the last call to did_flush().
|
||||
[[nodiscard]] forceinline bool did_flush() {
|
||||
const bool did_flush = did_flush_;
|
||||
did_flush_ = false;
|
||||
return did_flush;
|
||||
}
|
||||
|
||||
/*!
|
||||
A RealTimeActor presents the same interface as a JustInTimeActor but doesn't defer work.
|
||||
Time added will be performed immediately.
|
||||
/// @returns a number in the range [-max, 0] indicating the offset of the most recent sequence
|
||||
/// point from the final time at the end of the += that triggered the sequence point.
|
||||
[[nodiscard]] forceinline LocalTimeScale last_sequence_point_overrun() {
|
||||
return time_overrun_;
|
||||
}
|
||||
|
||||
Its primary purpose is to allow consumers to remain flexible in their scheduling.
|
||||
*/
|
||||
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class RealTimeActor {
|
||||
public:
|
||||
template<typename... Args> RealTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
/// @returns the number of cycles until the next sequence-point-based flush, if the embedded object
|
||||
/// supports sequence points; @c LocalTimeScale() otherwise.
|
||||
[[nodiscard]] LocalTimeScale cycles_until_implicit_flush() const {
|
||||
return time_until_event_;
|
||||
}
|
||||
|
||||
forceinline void operator += (const LocalTimeScale &rhs) {
|
||||
if constexpr (multiplier == 1 && divider == 1) {
|
||||
object_.run_for(TargetTimeScale(rhs));
|
||||
return;
|
||||
/// Indicates whether a sequence-point-caused flush will occur if the specified period is added.
|
||||
[[nodiscard]] forceinline bool will_flush(LocalTimeScale rhs) const {
|
||||
if constexpr (!has_sequence_points<T>::value) {
|
||||
return false;
|
||||
}
|
||||
return rhs >= time_until_event_;
|
||||
}
|
||||
|
||||
if constexpr (multiplier == 1) {
|
||||
accumulated_time_ += rhs;
|
||||
} else {
|
||||
accumulated_time_ += rhs * multiplier;
|
||||
}
|
||||
|
||||
if constexpr (divider == 1) {
|
||||
const auto duration = accumulated_time_.template flush<TargetTimeScale>();
|
||||
object_.run_for(duration);
|
||||
} else {
|
||||
const auto duration = accumulated_time_.template divide<TargetTimeScale>(LocalTimeScale(divider));
|
||||
if(duration > TargetTimeScale(0))
|
||||
object_.run_for(duration);
|
||||
/// Updates this template's record of the next sequence point.
|
||||
void update_sequence_point() {
|
||||
if constexpr (has_sequence_points<T>::value) {
|
||||
time_until_event_ = object_.get_next_sequence_point();
|
||||
assert(time_until_event_ > LocalTimeScale(0));
|
||||
}
|
||||
}
|
||||
|
||||
forceinline T *operator->() { return &object_; }
|
||||
forceinline const T *operator->() const { return &object_; }
|
||||
forceinline T *last_valid() { return &object_; }
|
||||
forceinline void flush() {}
|
||||
/// @returns A cached copy of the object's clocking preference.
|
||||
ClockingHint::Preference clocking_preference() const {
|
||||
return clocking_preference_;
|
||||
}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale accumulated_time_;
|
||||
LocalTimeScale time_since_update_, time_until_event_, time_overrun_;
|
||||
bool is_flushed_ = true;
|
||||
bool did_flush_ = false;
|
||||
|
||||
template <typename S, typename = void> struct has_sequence_points : std::false_type {};
|
||||
template <typename S> struct has_sequence_points<S, decltype(void(std::declval<S &>().get_next_sequence_point()))> : std::true_type {};
|
||||
|
||||
ClockingHint::Preference clocking_preference_ = ClockingHint::Preference::JustInTime;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) {
|
||||
clocking_preference_ = clocking;
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
|
||||
An AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
|
||||
Any time the amount of accumulated time crosses a threshold provided at construction time,
|
||||
the object will be updated on the AsyncTaskQueue.
|
||||
*/
|
||||
|
||||
@@ -128,13 +128,14 @@ template <class T> class MOS6522: public MOS6522Storage {
|
||||
|
||||
void access(int address);
|
||||
|
||||
uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output);
|
||||
uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output, uint8_t timer_mask);
|
||||
inline void reevaluate_interrupts();
|
||||
|
||||
/// Sets the current intended output value for the port and line;
|
||||
/// if this affects the visible output, it will be passed to the handler.
|
||||
void set_control_line_output(Port port, Line line, LineState value);
|
||||
void evaluate_cb2_output();
|
||||
void evaluate_port_b_output();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
// As-yet unimplemented (incomplete list):
|
||||
//
|
||||
// PB6 count-down mode for timer 2.
|
||||
|
||||
namespace MOS {
|
||||
namespace MOS6522 {
|
||||
|
||||
@@ -34,18 +38,18 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
access(address);
|
||||
switch(address) {
|
||||
case 0x0: // Write Port B.
|
||||
case 0x0: // Write Port B. ('ORB')
|
||||
// Store locally and communicate outwards.
|
||||
registers_.output[1] = value;
|
||||
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_port_output(Port::B, value, registers_.data_direction[1]);
|
||||
evaluate_port_b_output();
|
||||
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | ((registers_.peripheral_control&0x20) ? 0 : InterruptFlag::CB2ActiveEdge));
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
case 0xf:
|
||||
case 0x1: // Write Port A.
|
||||
case 0x1: // Write Port A. ('ORA')
|
||||
registers_.output[0] = value;
|
||||
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
@@ -59,28 +63,44 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
|
||||
case 0x2: // Port B direction.
|
||||
case 0x2: // Port B direction ('DDRB').
|
||||
registers_.data_direction[1] = value;
|
||||
break;
|
||||
case 0x3: // Port A direction.
|
||||
case 0x3: // Port A direction ('DDRA').
|
||||
registers_.data_direction[0] = value;
|
||||
break;
|
||||
|
||||
// Timer 1
|
||||
case 0x6: case 0x4: registers_.timer_latch[0] = (registers_.timer_latch[0]&0xff00) | value; break;
|
||||
case 0x5: case 0x7:
|
||||
case 0x6: case 0x4: // ('T1L-L' and 'T1C-L')
|
||||
registers_.timer_latch[0] = (registers_.timer_latch[0]&0xff00) | value;
|
||||
break;
|
||||
case 0x7: // Timer 1 latch, high ('T1L-H').
|
||||
registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | uint16_t(value << 8);
|
||||
registers_.interrupt_flags &= ~InterruptFlag::Timer1;
|
||||
if(address == 0x05) {
|
||||
registers_.next_timer[0] = registers_.timer_latch[0];
|
||||
timer_is_running_[0] = true;
|
||||
break;
|
||||
case 0x5: // Timer 1 counter, high ('T1C-H').
|
||||
// Fill latch.
|
||||
registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | uint16_t(value << 8);
|
||||
|
||||
// Restart timer.
|
||||
registers_.next_timer[0] = registers_.timer_latch[0];
|
||||
timer_is_running_[0] = true;
|
||||
|
||||
// If PB7 output mode is active, set it low.
|
||||
if(timer1_is_controlling_pb7()) {
|
||||
registers_.timer_port_b_output &= 0x7f;
|
||||
evaluate_port_b_output();
|
||||
}
|
||||
|
||||
// Clear existing interrupt flag.
|
||||
registers_.interrupt_flags &= ~InterruptFlag::Timer1;
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
|
||||
// Timer 2
|
||||
case 0x8: registers_.timer_latch[1] = value; break;
|
||||
case 0x9:
|
||||
case 0x8: // ('T2C-L')
|
||||
registers_.timer_latch[1] = value;
|
||||
break;
|
||||
case 0x9: // ('T2C-H')
|
||||
registers_.interrupt_flags &= ~InterruptFlag::Timer2;
|
||||
registers_.next_timer[1] = registers_.timer_latch[1] | uint16_t(value << 8);
|
||||
timer_is_running_[1] = true;
|
||||
@@ -88,7 +108,7 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
|
||||
break;
|
||||
|
||||
// Shift
|
||||
case 0xa:
|
||||
case 0xa: // ('SR')
|
||||
registers_.shift = value;
|
||||
shift_bits_remaining_ = 8;
|
||||
registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister;
|
||||
@@ -96,11 +116,18 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
|
||||
break;
|
||||
|
||||
// Control
|
||||
case 0xb:
|
||||
case 0xb: // Auxiliary control ('ACR').
|
||||
registers_.auxiliary_control = value;
|
||||
evaluate_cb2_output();
|
||||
|
||||
// This is a bit of a guess: reset the timer-based PB7 output to its default high level
|
||||
// any timer that timer-linked PB7 output is disabled.
|
||||
if(!timer1_is_controlling_pb7()) {
|
||||
registers_.timer_port_b_output |= 0x80;
|
||||
}
|
||||
evaluate_port_b_output();
|
||||
break;
|
||||
case 0xc: {
|
||||
case 0xc: { // Peripheral control ('PCR').
|
||||
// const auto old_peripheral_control = registers_.peripheral_control;
|
||||
registers_.peripheral_control = value;
|
||||
|
||||
@@ -141,11 +168,11 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
|
||||
} break;
|
||||
|
||||
// Interrupt control
|
||||
case 0xd:
|
||||
case 0xd: // Interrupt flag regiser ('IFR').
|
||||
registers_.interrupt_flags &= ~value;
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
case 0xe:
|
||||
case 0xe: // Interrupt enable register ('IER').
|
||||
if(value&0x80)
|
||||
registers_.interrupt_enable |= value;
|
||||
else
|
||||
@@ -159,54 +186,55 @@ template <typename T> uint8_t MOS6522<T>::read(int address) {
|
||||
address &= 0xf;
|
||||
access(address);
|
||||
switch(address) {
|
||||
case 0x0:
|
||||
case 0x0: // Read Port B ('IRB').
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge);
|
||||
reevaluate_interrupts();
|
||||
return get_port_input(Port::B, registers_.data_direction[1], registers_.output[1]);
|
||||
return get_port_input(Port::B, registers_.data_direction[1], registers_.output[1], registers_.auxiliary_control & 0x80);
|
||||
case 0xf:
|
||||
case 0x1:
|
||||
case 0x1: // Read Port A ('IRA').
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge);
|
||||
reevaluate_interrupts();
|
||||
return get_port_input(Port::A, registers_.data_direction[0], registers_.output[0]);
|
||||
return get_port_input(Port::A, registers_.data_direction[0], registers_.output[0], 0);
|
||||
|
||||
case 0x2: return registers_.data_direction[1];
|
||||
case 0x3: return registers_.data_direction[0];
|
||||
case 0x2: return registers_.data_direction[1]; // Port B direction ('DDRB').
|
||||
case 0x3: return registers_.data_direction[0]; // Port A direction ('DDRA').
|
||||
|
||||
// Timer 1
|
||||
case 0x4:
|
||||
case 0x4: // Timer 1 low-order latches ('T1L-L').
|
||||
registers_.interrupt_flags &= ~InterruptFlag::Timer1;
|
||||
reevaluate_interrupts();
|
||||
return registers_.timer[0] & 0x00ff;
|
||||
case 0x5: return registers_.timer[0] >> 8;
|
||||
case 0x6: return registers_.timer_latch[0] & 0x00ff;
|
||||
case 0x7: return registers_.timer_latch[0] >> 8;
|
||||
case 0x5: return registers_.timer[0] >> 8; // Timer 1 high-order counter ('T1C-H')
|
||||
case 0x6: return registers_.timer_latch[0] & 0x00ff; // Timer 1 low-order latches ('T1L-L').
|
||||
case 0x7: return registers_.timer_latch[0] >> 8; // Timer 1 high-order latches ('T1L-H').
|
||||
|
||||
// Timer 2
|
||||
case 0x8:
|
||||
case 0x8: // Timer 2 low-order counter ('T2C-L').
|
||||
registers_.interrupt_flags &= ~InterruptFlag::Timer2;
|
||||
reevaluate_interrupts();
|
||||
return registers_.timer[1] & 0x00ff;
|
||||
case 0x9: return registers_.timer[1] >> 8;
|
||||
case 0x9: return registers_.timer[1] >> 8; // Timer 2 high-order counter ('T2C-H').
|
||||
|
||||
case 0xa:
|
||||
case 0xa: // Shift register ('SR').
|
||||
shift_bits_remaining_ = 8;
|
||||
registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister;
|
||||
reevaluate_interrupts();
|
||||
return registers_.shift;
|
||||
|
||||
case 0xb: return registers_.auxiliary_control;
|
||||
case 0xc: return registers_.peripheral_control;
|
||||
case 0xb: return registers_.auxiliary_control; // Auxiliary control ('ACR').
|
||||
case 0xc: return registers_.peripheral_control; // Peripheral control ('PCR').
|
||||
|
||||
case 0xd: return registers_.interrupt_flags | (get_interrupt_line() ? 0x80 : 0x00);
|
||||
case 0xe: return registers_.interrupt_enable | 0x80;
|
||||
case 0xd: return registers_.interrupt_flags | (get_interrupt_line() ? 0x80 : 0x00); // Interrupt flag register ('IFR').
|
||||
case 0xe: return registers_.interrupt_enable | 0x80; // Interrupt enable register ('IER').
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
template <typename T> uint8_t MOS6522<T>::get_port_input(Port port, uint8_t output_mask, uint8_t output) {
|
||||
template <typename T> uint8_t MOS6522<T>::get_port_input(Port port, uint8_t output_mask, uint8_t output, uint8_t timer_mask) {
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
const uint8_t input = bus_handler_.get_port_input(port);
|
||||
output = (output & ~timer_mask) | (registers_.timer_port_b_output & timer_mask);
|
||||
return (input & ~output_mask) | (output & output_mask);
|
||||
}
|
||||
|
||||
@@ -276,10 +304,13 @@ template <typename T> void MOS6522<T>::do_phase2() {
|
||||
registers_.timer_needs_reload = false;
|
||||
registers_.timer[0] = registers_.timer_latch[0];
|
||||
} else {
|
||||
registers_.timer[0] --;
|
||||
-- registers_.timer[0];
|
||||
}
|
||||
|
||||
registers_.timer[1] --;
|
||||
// Count down timer 2 if it is in timed interrupt mode (i.e. auxiliary control bit 5 is clear).
|
||||
registers_.timer[1] -= timer2_clock_decrement();
|
||||
|
||||
// TODO: can eliminate conditional branches here.
|
||||
if(registers_.next_timer[0] >= 0) {
|
||||
registers_.timer[0] = uint16_t(registers_.next_timer[0]);
|
||||
registers_.next_timer[0] = -1;
|
||||
@@ -330,20 +361,29 @@ template <typename T> void MOS6522<T>::do_phase1() {
|
||||
reevaluate_interrupts();
|
||||
|
||||
// Determine whether to reload.
|
||||
if(registers_.auxiliary_control&0x40)
|
||||
if(timer1_is_continuous())
|
||||
registers_.timer_needs_reload = true;
|
||||
else
|
||||
timer_is_running_[0] = false;
|
||||
|
||||
// Determine whether to toggle PB7.
|
||||
if(registers_.auxiliary_control&0x80) {
|
||||
registers_.output[1] ^= 0x80;
|
||||
if(timer1_is_controlling_pb7()) {
|
||||
registers_.timer_port_b_output ^= 0x80;
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_port_output(Port::B, registers_.output[1], registers_.data_direction[1]);
|
||||
evaluate_port_b_output();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::evaluate_port_b_output() {
|
||||
// Apply current timer-linked PB7 output if any atop the stated output.
|
||||
const uint8_t timer_control_bit = registers_.auxiliary_control & 0x80;
|
||||
bus_handler_.set_port_output(
|
||||
Port::B,
|
||||
(registers_.output[1] & (0xff ^ timer_control_bit)) | timer_control_bit,
|
||||
registers_.data_direction[1] | timer_control_bit);
|
||||
}
|
||||
|
||||
/*! Runs for a specified number of half cycles. */
|
||||
template <typename T> void MOS6522<T>::run_for(const HalfCycles half_cycles) {
|
||||
auto number_of_half_cycles = half_cycles.as_integral();
|
||||
@@ -438,10 +478,11 @@ template <typename T> void MOS6522<T>::shift_in() {
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::shift_out() {
|
||||
// When shifting out, the shift register rotates rather than strictly shifts.
|
||||
// TODO: is that true for all modes?
|
||||
if(shift_mode() == ShiftMode::OutUnderT2FreeRunning || shift_bits_remaining_) {
|
||||
registers_.shift = uint8_t((registers_.shift << 1) | (registers_.shift >> 7));
|
||||
const bool is_free_running = shift_mode() == ShiftMode::OutUnderT2FreeRunning;
|
||||
if(is_free_running || shift_bits_remaining_) {
|
||||
// Recirculate bits only if in free-running mode (?)
|
||||
const uint8_t incoming_bit = (registers_.shift >> 7) * is_free_running;
|
||||
registers_.shift = uint8_t(registers_.shift << 1) | incoming_bit;
|
||||
evaluate_cb2_output();
|
||||
|
||||
--shift_bits_remaining_;
|
||||
|
||||
@@ -34,7 +34,9 @@ class MOS6522Storage {
|
||||
uint8_t peripheral_control = 0;
|
||||
uint8_t interrupt_flags = 0;
|
||||
uint8_t interrupt_enable = 0;
|
||||
|
||||
bool timer_needs_reload = false;
|
||||
uint8_t timer_port_b_output = 0xff;
|
||||
} registers_;
|
||||
|
||||
// Control state.
|
||||
@@ -79,12 +81,30 @@ class MOS6522Storage {
|
||||
OutUnderPhase2 = 6,
|
||||
OutUnderCB1 = 7
|
||||
};
|
||||
ShiftMode shift_mode() const {
|
||||
return ShiftMode((registers_.auxiliary_control >> 2) & 7);
|
||||
bool timer1_is_controlling_pb7() const {
|
||||
return registers_.auxiliary_control & 0x80;
|
||||
}
|
||||
bool timer1_is_continuous() const {
|
||||
return registers_.auxiliary_control & 0x40;
|
||||
}
|
||||
bool is_shifting_out() const {
|
||||
return registers_.auxiliary_control & 0x10;
|
||||
}
|
||||
int timer2_clock_decrement() const {
|
||||
return 1 ^ ((registers_.auxiliary_control >> 5)&1);
|
||||
}
|
||||
int timer2_pb6_decrement() const {
|
||||
return (registers_.auxiliary_control >> 5)&1;
|
||||
}
|
||||
ShiftMode shift_mode() const {
|
||||
return ShiftMode((registers_.auxiliary_control >> 2) & 7);
|
||||
}
|
||||
bool portb_is_latched() const {
|
||||
return registers_.auxiliary_control & 0x02;
|
||||
}
|
||||
bool port1_is_latched() const {
|
||||
return registers_.auxiliary_control & 0x01;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -445,20 +445,20 @@ template <class BusHandler> class MOS6560 {
|
||||
// register state
|
||||
struct {
|
||||
bool interlaced = false, tall_characters = false;
|
||||
uint8_t first_column_location, first_row_location;
|
||||
uint8_t number_of_columns, number_of_rows;
|
||||
uint16_t character_cell_start_address, video_matrix_start_address;
|
||||
uint16_t backgroundColour, borderColour, auxiliary_colour;
|
||||
uint8_t first_column_location = 0, first_row_location = 0;
|
||||
uint8_t number_of_columns = 0, number_of_rows = 0;
|
||||
uint16_t character_cell_start_address = 0, video_matrix_start_address = 0;
|
||||
uint16_t backgroundColour = 0, borderColour = 0, auxiliary_colour = 0;
|
||||
bool invertedCells = false;
|
||||
|
||||
uint8_t direct_values[16];
|
||||
uint8_t direct_values[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
} registers_;
|
||||
|
||||
// output state
|
||||
enum State {
|
||||
Sync, ColourBurst, Border, Pixels
|
||||
} this_state_, output_state_;
|
||||
int cycles_in_state_;
|
||||
} this_state_ = State::Sync, output_state_ = State::Sync;
|
||||
int cycles_in_state_ = 0;
|
||||
|
||||
// counters that cover an entire field
|
||||
int horizontal_counter_ = 0, vertical_counter_ = 0;
|
||||
@@ -487,23 +487,23 @@ template <class BusHandler> class MOS6560 {
|
||||
|
||||
// latches dictating start and length of drawing
|
||||
bool vertical_drawing_latch_ = false, horizontal_drawing_latch_ = false;
|
||||
int rows_this_field_, columns_this_line_;
|
||||
int rows_this_field_ = 0, columns_this_line_ = 0;
|
||||
|
||||
// current drawing position counter
|
||||
int pixel_line_cycle_, column_counter_;
|
||||
int current_row_;
|
||||
uint16_t current_character_row_;
|
||||
uint16_t video_matrix_address_counter_, base_video_matrix_address_counter_;
|
||||
int pixel_line_cycle_ = 0, column_counter_ = 0;
|
||||
int current_row_ = 0;
|
||||
uint16_t current_character_row_ = 0;
|
||||
uint16_t video_matrix_address_counter_ = 0, base_video_matrix_address_counter_ = 0;
|
||||
|
||||
// data latched from the bus
|
||||
uint8_t character_code_, character_colour_, character_value_;
|
||||
uint8_t character_code_ = 0, character_colour_ = 0, character_value_ = 0;
|
||||
|
||||
bool is_odd_frame_ = false, is_odd_line_ = false;
|
||||
|
||||
// lookup table from 6560 colour index to appropriate PAL/NTSC value
|
||||
uint16_t colours_[16];
|
||||
uint16_t colours_[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
uint16_t *pixel_pointer;
|
||||
uint16_t *pixel_pointer = nullptr;
|
||||
void output_border(int number_of_cycles) {
|
||||
uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = registers_.borderColour;
|
||||
@@ -511,13 +511,13 @@ template <class BusHandler> class MOS6560 {
|
||||
}
|
||||
|
||||
struct {
|
||||
int cycles_per_line;
|
||||
int line_counter_increment_offset;
|
||||
int final_line_increment_position;
|
||||
int lines_per_progressive_field;
|
||||
bool supports_interlacing;
|
||||
int cycles_per_line = 0;
|
||||
int line_counter_increment_offset = 0;
|
||||
int final_line_increment_position = 0;
|
||||
int lines_per_progressive_field = 0;
|
||||
bool supports_interlacing = 0;
|
||||
} timing_;
|
||||
OutputMode output_mode_;
|
||||
OutputMode output_mode_ = OutputMode::NTSC;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
#include "z8530.hpp"
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define NDEBUG
|
||||
#endif
|
||||
|
||||
#define LOG_PREFIX "[SCC] "
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
|
||||
@@ -190,7 +190,9 @@ void TMS9918::run_for(const HalfCycles cycles) {
|
||||
int read_cycles_pool = int_cycles;
|
||||
|
||||
while(write_cycles_pool || read_cycles_pool) {
|
||||
#ifndef NDEBUG
|
||||
LineBufferPointer backup = read_pointer_;
|
||||
#endif
|
||||
|
||||
if(write_cycles_pool) {
|
||||
// Determine how much writing to do.
|
||||
@@ -329,8 +331,10 @@ void TMS9918::run_for(const HalfCycles cycles) {
|
||||
}
|
||||
|
||||
|
||||
#ifndef NDEBUG
|
||||
assert(backup.row == read_pointer_.row && backup.column == read_pointer_.column);
|
||||
backup = write_pointer_;
|
||||
#endif
|
||||
|
||||
|
||||
if(read_cycles_pool) {
|
||||
@@ -704,9 +708,9 @@ HalfCycles Base::half_cycles_before_internal_cycles(int internal_cycles) {
|
||||
return HalfCycles(((internal_cycles << 2) + (2 - cycles_error_)) / 3);
|
||||
}
|
||||
|
||||
HalfCycles TMS9918::get_time_until_interrupt() {
|
||||
if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles(-1);
|
||||
if(get_interrupt_line()) return HalfCycles(0);
|
||||
HalfCycles TMS9918::get_next_sequence_point() {
|
||||
if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles::max();
|
||||
if(get_interrupt_line()) return HalfCycles::max();
|
||||
|
||||
// Calculate the amount of time until the next end-of-frame interrupt.
|
||||
const int frame_length = 342 * mode_timing_.total_lines;
|
||||
|
||||
@@ -75,13 +75,13 @@ class TMS9918: public Base {
|
||||
void latch_horizontal_counter();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until @c get_interrupt_line would next return true if
|
||||
Returns the amount of time until @c get_interrupt_line would next change if
|
||||
there are no interceding calls to @c write or to @c read.
|
||||
|
||||
If get_interrupt_line is true now, returns zero. If get_interrupt_line would
|
||||
never return true, returns -1.
|
||||
If get_interrupt_line is true now of if get_interrupt_line would
|
||||
never return true, returns HalfCycles::max().
|
||||
*/
|
||||
HalfCycles get_time_until_interrupt();
|
||||
HalfCycles get_next_sequence_point();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until the nominated line interrupt position is
|
||||
|
||||
@@ -350,11 +350,14 @@ class Base {
|
||||
|
||||
case MemoryAccess::Write:
|
||||
if(master_system_.cram_is_selected) {
|
||||
// Adjust the palette.
|
||||
// Adjust the palette. In a Master System blue has a slightly different
|
||||
// scale; cf. https://www.retrorgb.com/sega-master-system-non-linear-blue-channel-findings.html
|
||||
constexpr uint8_t rg_scale[] = {0, 85, 170, 255};
|
||||
constexpr uint8_t b_scale[] = {0, 104, 170, 255};
|
||||
master_system_.colour_ram[ram_pointer_ & 0x1f] = palette_pack(
|
||||
uint8_t(((read_ahead_buffer_ >> 0) & 3) * 255 / 3),
|
||||
uint8_t(((read_ahead_buffer_ >> 2) & 3) * 255 / 3),
|
||||
uint8_t(((read_ahead_buffer_ >> 4) & 3) * 255 / 3)
|
||||
rg_scale[(read_ahead_buffer_ >> 0) & 3],
|
||||
rg_scale[(read_ahead_buffer_ >> 2) & 3],
|
||||
b_scale[(read_ahead_buffer_ >> 4) & 3]
|
||||
);
|
||||
|
||||
// Schedule a CRAM dot; this is scheduled for wherever it should appear
|
||||
|
||||
@@ -164,6 +164,33 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
|
||||
uint8_t c_left_ = 255, c_right_ = 255;
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides helper code, to provide something closer to the interface exposed by many
|
||||
AY-deploying machines of the era.
|
||||
*/
|
||||
struct Utility {
|
||||
template <typename AY> static void write(AY &ay, bool is_data_write, uint8_t data) {
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | (is_data_write ? 0 : GI::AY38910::BC1)));
|
||||
ay.set_data_input(data);
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
}
|
||||
|
||||
template <typename AY> static void select_register(AY &ay, uint8_t reg) {
|
||||
write(ay, false, reg);
|
||||
}
|
||||
|
||||
template <typename AY> static void write_data(AY &ay, uint8_t reg) {
|
||||
write(ay, true, reg);
|
||||
}
|
||||
|
||||
template <typename AY> static uint8_t read(AY &ay) {
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
const uint8_t result = ay.get_data_output();
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
290
Components/AppleClock/AppleClock.hpp
Normal file
290
Components/AppleClock/AppleClock.hpp
Normal file
@@ -0,0 +1,290 @@
|
||||
//
|
||||
// RealTimeClock.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Apple_RealTimeClock_hpp
|
||||
#define Apple_RealTimeClock_hpp
|
||||
|
||||
namespace Apple {
|
||||
namespace Clock {
|
||||
|
||||
/*!
|
||||
Models Apple's real-time clocks, as contained in the Macintosh and IIgs.
|
||||
|
||||
Since tracking of time is pushed to this class, it is assumed
|
||||
that whomever is translating real time into emulated time
|
||||
will also signal interrupts — this is just the storage and time counting.
|
||||
*/
|
||||
class ClockStorage {
|
||||
public:
|
||||
ClockStorage() {
|
||||
// TODO: this should persist, if possible, rather than
|
||||
// being default initialised.
|
||||
constexpr uint8_t default_data[] = {
|
||||
0xa8, 0x00, 0x00, 0x00,
|
||||
0xcc, 0x0a, 0xcc, 0x0a,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x02, 0x63, 0x00,
|
||||
0x03, 0x88, 0x00, 0x4c
|
||||
};
|
||||
memcpy(data_, default_data, sizeof(default_data));
|
||||
memset(&data_[sizeof(default_data)], 0xff, sizeof(data_) - sizeof(default_data));
|
||||
}
|
||||
|
||||
/*!
|
||||
Advances the clock by 1 second.
|
||||
|
||||
The caller should also signal an interrupt.
|
||||
*/
|
||||
void update() {
|
||||
for(int c = 0; c < 4; ++c) {
|
||||
++seconds_[c];
|
||||
if(seconds_[c]) break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr uint16_t NoResult = 0x100;
|
||||
static constexpr uint16_t DidComplete = 0x101;
|
||||
uint16_t perform(uint8_t command) {
|
||||
/*
|
||||
Documented commands:
|
||||
|
||||
z0000001 Seconds register 0 (lowest order byte)
|
||||
z0000101 Seconds register 1
|
||||
z0001001 Seconds register 2
|
||||
z0001101 Seconds register 3
|
||||
00110001 Test register (write only)
|
||||
00110101 Write-protect register (write only)
|
||||
z010aa01 RAM addresses 0x10 - 0x13
|
||||
z1aaaa01 RAM addresses 0x00 – 0x0f
|
||||
|
||||
z0111abc, followed by 0defgh00
|
||||
RAM address abcdefgh
|
||||
|
||||
z = 1 => a read; z = 0 => a write.
|
||||
|
||||
The top bit of the write-protect register enables (0) or disables (1)
|
||||
writes to other locations.
|
||||
|
||||
All the documentation says about the test register is to set the top
|
||||
two bits to 0 for normal operation. Abnormal operation is undefined.
|
||||
*/
|
||||
switch(phase_) {
|
||||
case Phase::Command:
|
||||
// Decode an address.
|
||||
switch(command & 0x70) {
|
||||
default:
|
||||
if(command & 0x40) {
|
||||
// RAM addresses 0x00 – 0x0f.
|
||||
address_ = (command >> 2) & 0xf;
|
||||
} else return DidComplete; // Unrecognised.
|
||||
break;
|
||||
|
||||
case 0x00:
|
||||
// A time access.
|
||||
address_ = SecondsBuffer + ((command >> 2)&3);
|
||||
break;
|
||||
case 0x30:
|
||||
// Either a register access or an extended instruction.
|
||||
if(command & 0x08) {
|
||||
address_ = (command & 0x7) << 5;
|
||||
phase_ = (command & 0x80) ? Phase::SecondAddressByteRead : Phase::SecondAddressByteWrite;
|
||||
return NoResult;
|
||||
} else {
|
||||
address_ = (command & 4) ? RegisterWriteProtect : RegisterTest;
|
||||
}
|
||||
break;
|
||||
case 0x20:
|
||||
// RAM addresses 0x10 – 0x13.
|
||||
address_ = 0x10 + ((command >> 2) & 0x3);
|
||||
break;
|
||||
}
|
||||
|
||||
// If this is a read, return a result; otherwise prepare to write.
|
||||
if(command & 0x80) {
|
||||
// The two registers are write-only.
|
||||
if(address_ == RegisterTest || address_ == RegisterWriteProtect) {
|
||||
return DidComplete;
|
||||
}
|
||||
return (address_ >= SecondsBuffer) ? seconds_[address_ & 0xff] : data_[address_];
|
||||
}
|
||||
phase_ = Phase::WriteData;
|
||||
return NoResult;
|
||||
|
||||
case Phase::SecondAddressByteRead:
|
||||
case Phase::SecondAddressByteWrite:
|
||||
if(command & 0x83) {
|
||||
return DidComplete;
|
||||
}
|
||||
address_ |= command >> 2;
|
||||
|
||||
if(phase_ == Phase::SecondAddressByteRead) {
|
||||
phase_ = Phase::Command;
|
||||
return data_[address_]; // Only RAM accesses can get this far.
|
||||
} else {
|
||||
phase_ = Phase::WriteData;
|
||||
}
|
||||
return NoResult;
|
||||
|
||||
case Phase::WriteData:
|
||||
// First test: is this to the write-protect register?
|
||||
if(address_ == RegisterWriteProtect) {
|
||||
write_protect_ = command;
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
if(address_ == RegisterTest) {
|
||||
// No documentation here.
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
// No other writing is permitted if the write protect
|
||||
// register won't allow it.
|
||||
if(!(write_protect_ & 0x80)) {
|
||||
if(address_ >= SecondsBuffer) {
|
||||
seconds_[address_ & 0xff] = command;
|
||||
} else {
|
||||
data_[address_] = command;
|
||||
}
|
||||
}
|
||||
|
||||
phase_ = Phase::Command;
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
return NoResult;
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
uint8_t data_[256];
|
||||
uint8_t seconds_[4];
|
||||
uint8_t write_protect_;
|
||||
int address_;
|
||||
|
||||
static constexpr int SecondsBuffer = 0x100;
|
||||
static constexpr int RegisterTest = 0x200;
|
||||
static constexpr int RegisterWriteProtect = 0x201;
|
||||
|
||||
enum class Phase {
|
||||
Command,
|
||||
SecondAddressByteRead,
|
||||
SecondAddressByteWrite,
|
||||
WriteData
|
||||
};
|
||||
Phase phase_ = Phase::Command;
|
||||
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides the serial interface implemented by the Macintosh.
|
||||
*/
|
||||
class SerialClock: public ClockStorage {
|
||||
public:
|
||||
/*!
|
||||
Sets the current clock and data inputs to the clock.
|
||||
*/
|
||||
void set_input(bool clock, bool data) {
|
||||
// The data line is valid when the clock transitions to level 0.
|
||||
if(clock && !previous_clock_) {
|
||||
// Shift into the command_ register, no matter what.
|
||||
command_ = uint16_t((command_ << 1) | (data ? 1 : 0));
|
||||
result_ <<= 1;
|
||||
|
||||
// Increment phase.
|
||||
++phase_;
|
||||
|
||||
// If a whole byte has been collected, push it onwards.
|
||||
if(!(phase_&7)) {
|
||||
// Begin pessimistically.
|
||||
const auto effect = perform(uint8_t(command_));
|
||||
|
||||
switch(effect) {
|
||||
case ClockStorage::NoResult:
|
||||
break;
|
||||
default:
|
||||
result_ = uint8_t(effect);
|
||||
break;
|
||||
case ClockStorage::DidComplete:
|
||||
abort();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previous_clock_ = clock;
|
||||
}
|
||||
|
||||
/*!
|
||||
Reads the current data output level from the clock.
|
||||
*/
|
||||
bool get_data() {
|
||||
return !!(result_ & 0x80);
|
||||
}
|
||||
|
||||
/*!
|
||||
Announces that a serial command has been aborted.
|
||||
*/
|
||||
void abort() {
|
||||
result_ = 0;
|
||||
phase_ = 0;
|
||||
command_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
int phase_ = 0;
|
||||
uint16_t command_;
|
||||
uint8_t result_ = 0;
|
||||
|
||||
bool previous_clock_ = false;
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides the parallel interface implemented by the IIgs.
|
||||
*/
|
||||
class ParallelClock: public ClockStorage {
|
||||
public:
|
||||
void set_control(uint8_t control) {
|
||||
if(!(control&0x80)) return;
|
||||
|
||||
if(control & 0x40) {
|
||||
// Read from the RTC.
|
||||
// A no-op for now.
|
||||
} else {
|
||||
// Write to the RTC. Which in this implementation also sets up a future read.
|
||||
data_ = uint8_t(perform(data_));
|
||||
}
|
||||
|
||||
// MAGIC! The transaction took 0 seconds.
|
||||
// TODO: no magic.
|
||||
control_ = control & 0x7f;
|
||||
|
||||
// Bit 5 is also meant to be 1 or 0 to indicate the final byte.
|
||||
}
|
||||
|
||||
uint8_t get_control() {
|
||||
return control_;
|
||||
}
|
||||
|
||||
void set_data(uint8_t data) {
|
||||
data_ = data;
|
||||
}
|
||||
|
||||
uint8_t get_data() {
|
||||
return data_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t data_;
|
||||
uint8_t control_;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Apple_RealTimeClock_hpp */
|
||||
@@ -180,29 +180,40 @@ void DiskII::set_state_machine(const std::vector<uint8_t> &state_machine) {
|
||||
if(state_machine[0] != 0x18) {
|
||||
for(size_t source_address = 0; source_address < 256; ++source_address) {
|
||||
// Remap into Beneath Apple Pro-DOS address form.
|
||||
size_t destination_address =
|
||||
((source_address&0x80) ? 0x10 : 0x00) |
|
||||
((source_address&0x01) ? 0x20 : 0x00) |
|
||||
((source_address&0x40) ? 0x40 : 0x00) |
|
||||
const size_t destination_address =
|
||||
((source_address&0x20) ? 0x80 : 0x00) |
|
||||
((source_address&0x10) ? 0x01 : 0x00) |
|
||||
((source_address&0x40) ? 0x40 : 0x00) |
|
||||
((source_address&0x01) ? 0x20 : 0x00) |
|
||||
((source_address&0x80) ? 0x10 : 0x00) |
|
||||
((source_address&0x08) ? 0x08 : 0x00) |
|
||||
((source_address&0x04) ? 0x04 : 0x00) |
|
||||
((source_address&0x02) ? 0x02 : 0x00);
|
||||
uint8_t source_value = state_machine[source_address];
|
||||
((source_address&0x02) ? 0x02 : 0x00) |
|
||||
((source_address&0x10) ? 0x01 : 0x00);
|
||||
|
||||
source_value =
|
||||
// Store.
|
||||
const uint8_t source_value = state_machine[source_address];
|
||||
state_machine_[destination_address] =
|
||||
((source_value & 0x80) ? 0x10 : 0x0) |
|
||||
((source_value & 0x40) ? 0x20 : 0x0) |
|
||||
((source_value & 0x20) ? 0x40 : 0x0) |
|
||||
((source_value & 0x10) ? 0x80 : 0x0) |
|
||||
(source_value & 0x0f);
|
||||
|
||||
// Store.
|
||||
state_machine_[destination_address] = source_value;
|
||||
}
|
||||
} else {
|
||||
memcpy(&state_machine_[0], &state_machine[0], 128);
|
||||
for(size_t source_address = 0; source_address < 256; ++source_address) {
|
||||
// Reshuffle ordering of bytes only, to retain indexing by the high nibble.
|
||||
const size_t destination_address =
|
||||
((source_address&0x80) ? 0x80 : 0x00) |
|
||||
((source_address&0x40) ? 0x40 : 0x00) |
|
||||
((source_address&0x01) ? 0x20 : 0x00) |
|
||||
((source_address&0x20) ? 0x10 : 0x00) |
|
||||
((source_address&0x08) ? 0x08 : 0x00) |
|
||||
((source_address&0x04) ? 0x04 : 0x00) |
|
||||
((source_address&0x02) ? 0x02 : 0x00) |
|
||||
((source_address&0x10) ? 0x01 : 0x00);
|
||||
|
||||
state_machine_[destination_address] = state_machine[source_address];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
Components/DiskII/DiskIIDrive.cpp
Normal file
45
Components/DiskII/DiskIIDrive.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// DiskIIDrive.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/11/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "DiskIIDrive.hpp"
|
||||
|
||||
using namespace Apple::Disk;
|
||||
|
||||
DiskIIDrive::DiskIIDrive(int input_clock_rate) :
|
||||
IWMDrive(input_clock_rate, 1) {
|
||||
Drive::set_rotation_speed(300.0f);
|
||||
}
|
||||
|
||||
void DiskIIDrive::set_enabled(bool enabled) {
|
||||
set_motor_on(enabled);
|
||||
}
|
||||
|
||||
void DiskIIDrive::set_control_lines(int lines) {
|
||||
// If the stepper magnet selections have changed, and any is on, see how
|
||||
// that moves the head.
|
||||
if(lines ^ stepper_mask_ && lines) {
|
||||
// Convert from a representation of bits set to the centre of pull.
|
||||
int direction = 0;
|
||||
if(lines&1) direction += (((stepper_position_ - 0) + 4)&7) - 4;
|
||||
if(lines&2) direction += (((stepper_position_ - 2) + 4)&7) - 4;
|
||||
if(lines&4) direction += (((stepper_position_ - 4) + 4)&7) - 4;
|
||||
if(lines&8) direction += (((stepper_position_ - 6) + 4)&7) - 4;
|
||||
const int bits_set = (lines&1) + ((lines >> 1)&1) + ((lines >> 2)&1) + ((lines >> 3)&1);
|
||||
direction /= bits_set;
|
||||
|
||||
// Compare to the stepper position to decide whether that pulls in the
|
||||
// current cog notch, or grabs a later one.
|
||||
step(Storage::Disk::HeadPosition(-direction, 4));
|
||||
stepper_position_ = (stepper_position_ - direction + 8) & 7;
|
||||
}
|
||||
stepper_mask_ = lines;
|
||||
}
|
||||
|
||||
bool DiskIIDrive::read() {
|
||||
return !!(stepper_mask_ & 2) || get_is_read_only();
|
||||
}
|
||||
33
Components/DiskII/DiskIIDrive.hpp
Normal file
33
Components/DiskII/DiskIIDrive.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// DiskIIDrive.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/11/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef DiskIIDrive_hpp
|
||||
#define DiskIIDrive_hpp
|
||||
|
||||
#include "IWM.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Disk {
|
||||
|
||||
class DiskIIDrive: public IWMDrive {
|
||||
public:
|
||||
DiskIIDrive(int input_clock_rate);
|
||||
|
||||
private:
|
||||
void set_enabled(bool) final;
|
||||
void set_control_lines(int) final;
|
||||
bool read() final;
|
||||
|
||||
int stepper_mask_ = 0;
|
||||
int stepper_position_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* DiskIIDrive_hpp */
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
#include "IWM.hpp"
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define NDEBUG
|
||||
#endif
|
||||
|
||||
#define LOG_PREFIX "[IWM] "
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
using namespace Apple;
|
||||
@@ -50,7 +55,7 @@ uint8_t IWM::read(int address) {
|
||||
|
||||
switch(state_ & (Q6 | Q7 | ENABLE)) {
|
||||
default:
|
||||
LOG("[IWM] Invalid read\n");
|
||||
LOG("Invalid read\n");
|
||||
return 0xff;
|
||||
|
||||
// "Read all 1s".
|
||||
@@ -62,9 +67,8 @@ uint8_t IWM::read(int address) {
|
||||
const auto result = data_register_;
|
||||
|
||||
if(data_register_ & 0x80) {
|
||||
// printf("\n\nIWM:%02x\n\n", data_register_);
|
||||
// printf(".");
|
||||
data_register_ = 0;
|
||||
// LOG("Reading data: " << PADHEX(2) << int(result));
|
||||
}
|
||||
// LOG("Reading data register: " << PADHEX(2) << int(result));
|
||||
|
||||
@@ -99,7 +103,7 @@ uint8_t IWM::read(int address) {
|
||||
bit 6: 1 = write state (0 = underrun has occurred; 1 = no underrun so far).
|
||||
bit 7: 1 = write data buffer ready for data (1 = ready; 0 = busy).
|
||||
*/
|
||||
// LOG("Reading write handshake: " << PADHEX(2) << (0x3f | write_handshake_));
|
||||
// LOG("Reading write handshake: " << PADHEX(2) << int(0x3f | write_handshake_));
|
||||
return 0x3f | write_handshake_;
|
||||
}
|
||||
|
||||
@@ -128,13 +132,21 @@ void IWM::write(int address, uint8_t input) {
|
||||
|
||||
mode_ = input;
|
||||
|
||||
// TEMPORARY. To test for the unimplemented mode.
|
||||
if(input&0x2) {
|
||||
LOG("Switched to asynchronous mode");
|
||||
} else {
|
||||
LOG("Switched to synchronous mode");
|
||||
}
|
||||
|
||||
switch(mode_ & 0x18) {
|
||||
case 0x00: bit_length_ = Cycles(24); break; // slow mode, 7Mhz
|
||||
case 0x08: bit_length_ = Cycles(12); break; // fast mode, 7Mhz
|
||||
case 0x00: bit_length_ = Cycles(28); break; // slow mode, 7Mhz
|
||||
case 0x08: bit_length_ = Cycles(14); break; // fast mode, 7Mhz
|
||||
case 0x10: bit_length_ = Cycles(32); break; // slow mode, 8Mhz
|
||||
case 0x18: bit_length_ = Cycles(16); break; // fast mode, 8Mhz
|
||||
}
|
||||
LOG("IWM mode is now " << PADHEX(2) << int(mode_));
|
||||
LOG("Mode is now " << PADHEX(2) << int(mode_));
|
||||
LOG("New bit length is " << std::dec << bit_length_.as_integral());
|
||||
break;
|
||||
|
||||
case Q7|Q6|ENABLE: // Write data register.
|
||||
@@ -248,6 +260,7 @@ void IWM::run_for(const Cycles cycles) {
|
||||
drives_[active_drive_]->run_for(Cycles(1));
|
||||
++cycles_since_shift_;
|
||||
if(cycles_since_shift_ == bit_length_ + error_margin) {
|
||||
// LOG("Shifting 0 at " << std::dec << cycles_since_shift_.as_integral());
|
||||
propose_shift(0);
|
||||
}
|
||||
}
|
||||
@@ -263,41 +276,45 @@ void IWM::run_for(const Cycles cycles) {
|
||||
} break;
|
||||
|
||||
case ShiftMode::Writing:
|
||||
if(drives_[active_drive_]->is_writing()) {
|
||||
while(cycles_since_shift_ + integer_cycles >= bit_length_) {
|
||||
const auto cycles_until_write = bit_length_ - cycles_since_shift_;
|
||||
while(cycles_since_shift_ + integer_cycles >= bit_length_) {
|
||||
const auto cycles_until_write = bit_length_ - cycles_since_shift_;
|
||||
if(drives_[active_drive_]) {
|
||||
drives_[active_drive_]->run_for(cycles_until_write);
|
||||
|
||||
// Output a flux transition if the top bit is set.
|
||||
drives_[active_drive_]->write_bit(shift_register_ & 0x80);
|
||||
shift_register_ <<= 1;
|
||||
}
|
||||
shift_register_ <<= 1;
|
||||
|
||||
integer_cycles -= cycles_until_write.as_integral();
|
||||
cycles_since_shift_ = Cycles(0);
|
||||
integer_cycles -= cycles_until_write.as_integral();
|
||||
cycles_since_shift_ = Cycles(0);
|
||||
|
||||
--output_bits_remaining_;
|
||||
if(!output_bits_remaining_) {
|
||||
if(!(write_handshake_ & 0x80)) {
|
||||
write_handshake_ |= 0x80;
|
||||
shift_register_ = next_output_;
|
||||
output_bits_remaining_ = 8;
|
||||
// LOG("Next byte: " << PADHEX(2) << int(shift_register_));
|
||||
} else {
|
||||
write_handshake_ &= ~0x40;
|
||||
drives_[active_drive_]->end_writing();
|
||||
// printf("\n");
|
||||
LOG("Overrun; done.");
|
||||
select_shift_mode();
|
||||
}
|
||||
--output_bits_remaining_;
|
||||
if(!output_bits_remaining_) {
|
||||
if(!(write_handshake_ & 0x80)) {
|
||||
shift_register_ = next_output_;
|
||||
output_bits_remaining_ = 8;
|
||||
// LOG("Next byte: " << PADHEX(2) << int(shift_register_));
|
||||
} else {
|
||||
write_handshake_ &= ~0x40;
|
||||
if(drives_[active_drive_]) drives_[active_drive_]->end_writing();
|
||||
LOG("Overrun; done.");
|
||||
output_bits_remaining_ = 1;
|
||||
}
|
||||
}
|
||||
|
||||
cycles_since_shift_ = integer_cycles;
|
||||
if(integer_cycles) {
|
||||
drives_[active_drive_]->run_for(cycles_since_shift_);
|
||||
// Either way, the IWM is ready for more data.
|
||||
write_handshake_ |= 0x80;
|
||||
}
|
||||
} else {
|
||||
drives_[active_drive_]->run_for(cycles);
|
||||
}
|
||||
|
||||
// Either some bits were output, in which case cycles_since_shift_ is no 0 and
|
||||
// integer_cycles is some number less than bit_length_, or none were and
|
||||
// cycles_since_shift_ + integer_cycles is less than bit_length, and the new
|
||||
// part should be accumulated.
|
||||
cycles_since_shift_ += integer_cycles;
|
||||
|
||||
if(drives_[active_drive_] && integer_cycles) {
|
||||
drives_[active_drive_]->run_for(cycles_since_shift_);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -332,12 +349,12 @@ void IWM::select_shift_mode() {
|
||||
}
|
||||
|
||||
// If writing mode just began, set the drive into write mode and cue up the first output byte.
|
||||
if(drives_[active_drive_] && old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) {
|
||||
drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_integral()), false);
|
||||
if(old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) {
|
||||
if(drives_[active_drive_]) drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_integral()), false);
|
||||
shift_register_ = next_output_;
|
||||
write_handshake_ |= 0x80 | 0x40;
|
||||
output_bits_remaining_ = 8;
|
||||
LOG("Seeding output with " << PADHEX(2) << shift_register_);
|
||||
LOG("Seeding output with " << PADHEX(2) << int(shift_register_));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +368,7 @@ void IWM::process_event(const Storage::Disk::Drive::Event &event) {
|
||||
switch(event.type) {
|
||||
case Storage::Disk::Track::Event::IndexHole: return;
|
||||
case Storage::Disk::Track::Event::FluxTransition:
|
||||
// LOG("Shifting 1 at " << std::dec << cycles_since_shift_.as_integral());
|
||||
propose_shift(1);
|
||||
break;
|
||||
}
|
||||
@@ -359,12 +377,13 @@ void IWM::process_event(const Storage::Disk::Drive::Event &event) {
|
||||
void IWM::propose_shift(uint8_t bit) {
|
||||
// TODO: synchronous mode.
|
||||
|
||||
// LOG("Shifting at " << std::dec << cycles_since_shift_.as_integral());
|
||||
// LOG("Shifting input");
|
||||
|
||||
// See above for text from the IWM patent, column 7, around line 35 onwards.
|
||||
// The error_margin here implements the 'before' part of that contract.
|
||||
//
|
||||
// Basic effective logic: if at least 1 is fozund in the bit_length_ cycles centred
|
||||
// Basic effective logic: if at least 1 is found in the bit_length_ cycles centred
|
||||
// on the current expected bit delivery time as implied by cycles_since_shift_,
|
||||
// shift in a 1 and start a new window wherever the first found 1 was.
|
||||
//
|
||||
@@ -374,6 +393,7 @@ void IWM::propose_shift(uint8_t bit) {
|
||||
|
||||
shift_register_ = uint8_t((shift_register_ << 1) | bit);
|
||||
if(shift_register_ & 0x80) {
|
||||
// if(data_register_ & 0x80) LOG("Byte missed");
|
||||
data_register_ = shift_register_;
|
||||
shift_register_ = 0;
|
||||
}
|
||||
@@ -386,16 +406,20 @@ void IWM::propose_shift(uint8_t bit) {
|
||||
|
||||
void IWM::set_drive(int slot, IWMDrive *drive) {
|
||||
drives_[slot] = drive;
|
||||
drive->set_event_delegate(this);
|
||||
drive->set_clocking_hint_observer(this);
|
||||
if(drive) {
|
||||
drive->set_event_delegate(this);
|
||||
drive->set_clocking_hint_observer(this);
|
||||
} else {
|
||||
drive_is_rotating_[slot] = false;
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) {
|
||||
const bool is_rotating = clocking != ClockingHint::Preference::None;
|
||||
|
||||
if(component == static_cast<ClockingHint::Source *>(drives_[0])) {
|
||||
if(drives_[0] && component == static_cast<ClockingHint::Source *>(drives_[0])) {
|
||||
drive_is_rotating_[0] = is_rotating;
|
||||
} else {
|
||||
} else if(drives_[1] && component == static_cast<ClockingHint::Source *>(drives_[1])) {
|
||||
drive_is_rotating_[1] = is_rotating;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,8 +142,11 @@ bool DoubleDensityDrive::read() {
|
||||
return !get_is_track_zero();
|
||||
|
||||
case CA1|CA0: // Disk has been ejected.
|
||||
// (0 = user has ejected disk)
|
||||
return !has_new_disk_;
|
||||
// (1 = user has ejected disk)
|
||||
//
|
||||
// TODO: does this really mean _user_ has ejected disk? If so then I should avoid
|
||||
// changing the flag upon a programmatic eject.
|
||||
return has_new_disk_;
|
||||
|
||||
case CA1|CA0|SEL: // Tachometer.
|
||||
// (arbitrary)
|
||||
@@ -170,12 +173,10 @@ bool DoubleDensityDrive::read() {
|
||||
|
||||
case CA2|CA1|CA0|SEL: // Drive installed.
|
||||
// (0 = present, 1 = missing)
|
||||
//
|
||||
// TODO: why do I need to return this the wrong way around for the Mac Plus?
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void DoubleDensityDrive::did_set_disk() {
|
||||
has_new_disk_ = true;
|
||||
void DoubleDensityDrive::did_set_disk(bool did_replace) {
|
||||
has_new_disk_ = did_replace;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class DoubleDensityDrive: public IWMDrive {
|
||||
/*!
|
||||
@returns @c true if this is an 800kb drive; @c false otherwise.
|
||||
*/
|
||||
bool is_800k() {
|
||||
bool is_800k() const {
|
||||
return is_800k_;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class DoubleDensityDrive: public IWMDrive {
|
||||
|
||||
// To receive the proper notifications from Storage::Disk::Drive.
|
||||
void did_step(Storage::Disk::HeadPosition to_position) final;
|
||||
void did_set_disk() final;
|
||||
void did_set_disk(bool) final;
|
||||
|
||||
const bool is_800k_;
|
||||
bool has_new_disk_ = false;
|
||||
|
||||
@@ -46,6 +46,8 @@ 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() {}
|
||||
|
||||
// Host interface.
|
||||
|
||||
/// @returns @c true if the key press affects the machine; @c false otherwise.
|
||||
|
||||
24
InstructionSets/AccessType.hpp
Normal file
24
InstructionSets/AccessType.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// AccessType.h
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef AccessType_h
|
||||
#define AccessType_h
|
||||
|
||||
namespace InstructionSet {
|
||||
|
||||
enum class AccessType {
|
||||
None,
|
||||
Read,
|
||||
Write,
|
||||
ReadModifyWrite
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
#endif /* AccessType_h */
|
||||
200
InstructionSets/CachingExecutor.hpp
Normal file
200
InstructionSets/CachingExecutor.hpp
Normal file
@@ -0,0 +1,200 @@
|
||||
//
|
||||
// CachingExecutor.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/01/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef CachingExecutor_hpp
|
||||
#define CachingExecutor_hpp
|
||||
|
||||
#include "Sizes.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace InstructionSet {
|
||||
|
||||
/*!
|
||||
A caching executor makes use of an instruction set-specific executor to cache 'performers' (i.e. function pointers)
|
||||
that result from decoding.
|
||||
|
||||
In other words, it's almost a JIT compiler, but producing threaded code (in the Forth sense) and then incurring whatever
|
||||
costs sit behind using the C ABI for calling. Since there'll always be exactly one parameter, being the specific executor,
|
||||
hopefully the calling costs are acceptable.
|
||||
|
||||
Intended usage is for specific executors to subclass from this and declare it a friend.
|
||||
|
||||
TODO: determine promises re: interruption, amongst other things.
|
||||
*/
|
||||
template <
|
||||
/// Indicates the Executor for this platform.
|
||||
typename Executor,
|
||||
/// Indicates the greatest value the program counter might take.
|
||||
uint64_t max_address,
|
||||
/// Indicates the maximum number of potential performers that will be provided.
|
||||
uint64_t max_performer_count,
|
||||
/// Provides the type of Instruction to expect.
|
||||
typename InstructionType,
|
||||
/// Indicates whether instructions should be treated as ephemeral or included in the cache.
|
||||
bool retain_instructions
|
||||
> class CachingExecutor {
|
||||
public:
|
||||
using Performer = void (Executor::*)();
|
||||
using PerformerIndex = typename MinIntTypeValue<max_performer_count>::type;
|
||||
using ProgramCounterType = typename MinIntTypeValue<max_address>::type;
|
||||
|
||||
// MARK: - Parser call-ins.
|
||||
|
||||
void announce_overflow(ProgramCounterType) {
|
||||
/*
|
||||
Should be impossible for now; this is intended to provide information
|
||||
when page caching.
|
||||
*/
|
||||
}
|
||||
void announce_instruction(ProgramCounterType, InstructionType instruction) {
|
||||
// Dutifully map the instruction to a performer and keep it.
|
||||
program_.push_back(static_cast<Executor *>(this)->action_for(instruction));
|
||||
|
||||
if constexpr (retain_instructions) {
|
||||
// TODO.
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
// Storage for the statically-allocated list of performers. It's a bit more
|
||||
// work for executors to fill this array, but subsequently performers can be
|
||||
// indexed by array position, which is a lot more compact than a generic pointer.
|
||||
std::array<Performer, max_performer_count+1> performers_;
|
||||
ProgramCounterType program_counter_;
|
||||
|
||||
/*!
|
||||
Moves the current point of execution to @c address, updating necessary performer caches
|
||||
and doing any translation as is necessary.
|
||||
*/
|
||||
void set_program_counter(ProgramCounterType address) {
|
||||
// Set flag to terminate any inner loop currently running through
|
||||
// previously-parsed content.
|
||||
has_branched_ = true;
|
||||
program_counter_ = address;
|
||||
|
||||
// Temporary implementation: just interpret.
|
||||
program_.clear();
|
||||
program_index_ = 0;
|
||||
static_cast<Executor *>(this)->parse(address, ProgramCounterType(max_address));
|
||||
|
||||
// const auto page = find_page(address);
|
||||
// const auto entry = page->entry_points.find(address);
|
||||
// if(entry == page->entry_points.end()) {
|
||||
// // Requested segment wasn't found; check whether it was
|
||||
// // within the recently translated list and otherwise
|
||||
// // translate it.
|
||||
// }
|
||||
}
|
||||
|
||||
/*!
|
||||
Indicates whether the processor is currently 'stopped', i.e. whether all attempts to run
|
||||
should produce no activity. Some processors have such a state when waiting for
|
||||
interrupts or for a reset.
|
||||
*/
|
||||
void set_is_stopped(bool) {}
|
||||
|
||||
/*!
|
||||
Executes up to the next branch.
|
||||
*/
|
||||
void run_to_branch() {
|
||||
has_branched_ = false;
|
||||
for(auto index: program_) {
|
||||
const auto performer = performers_[index];
|
||||
(static_cast<Executor *>(this)->*performer)();
|
||||
if(has_branched_) break;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Runs for @c duration; the intention is that subclasses provide a method
|
||||
that is clear about units, and call this to count down in whatever units they
|
||||
count down in.
|
||||
*/
|
||||
void run_for(int duration) {
|
||||
remaining_duration_ += duration;
|
||||
|
||||
while(remaining_duration_ > 0) {
|
||||
has_branched_ = false;
|
||||
Executor *const executor = static_cast<Executor *>(this);
|
||||
while(remaining_duration_ > 0 && !has_branched_) {
|
||||
const auto performer = performers_[program_[program_index_]];
|
||||
++program_index_;
|
||||
|
||||
(executor->*performer)();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Should be called by a specific executor to subtract from the remaining
|
||||
running duration.
|
||||
*/
|
||||
inline void subtract_duration(int duration) {
|
||||
remaining_duration_ -= duration;
|
||||
}
|
||||
|
||||
private:
|
||||
bool has_branched_ = false;
|
||||
int remaining_duration_ = 0;
|
||||
std::vector<PerformerIndex> program_;
|
||||
size_t program_index_ = 0;
|
||||
|
||||
/* TODO: almost below here can be shoved off into an LRUCache object, or similar. */
|
||||
|
||||
// static constexpr size_t max_cached_pages = 64;
|
||||
|
||||
// struct Page {
|
||||
// std::map<ProgramCounterType, PerformerIndex> entry_points;
|
||||
|
||||
// TODO: can I statically these two? Should I?
|
||||
// std::vector<PerformerIndex> actions_;
|
||||
// std::vector<typename std::enable_if<!std::is_same<InstructionType, void>::value, InstructionType>::type> instructions_;
|
||||
// };
|
||||
// std::array<Page, max_cached_pages> pages_;
|
||||
|
||||
// Maps from page numbers to pages.
|
||||
// std::unordered_map<ProgramCounterType, Page *> cached_pages_;
|
||||
|
||||
// Maintains an LRU of recently-used pages in case of a need for reuse.
|
||||
// std::list<ProgramCounterType> touched_pages_;
|
||||
|
||||
/*!
|
||||
Finds or creates the page that contains @c address.
|
||||
*/
|
||||
/* Page *find_page(ProgramCounterType address) {
|
||||
// TODO: are 1kb pages always appropriate? Is 64 the correct amount to keep?
|
||||
const auto page_address = ProgramCounterType(address >> 10);
|
||||
|
||||
auto page = cached_pages_.find(page_address);
|
||||
if(page == cached_pages_.end()) {
|
||||
// Page wasn't found; either allocate a new one or
|
||||
// reuse one that already exists.
|
||||
if(cached_pages_.size() == max_cached_pages) {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
} else {
|
||||
// Page was found; LRU shuffle it.
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}*/
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* CachingExecutor_hpp */
|
||||
89
InstructionSets/Disassembler.hpp
Normal file
89
InstructionSets/Disassembler.hpp
Normal file
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Disassembler.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 26/01/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Disassembler_hpp
|
||||
#define Disassembler_hpp
|
||||
|
||||
#include "Sizes.hpp"
|
||||
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace InstructionSet {
|
||||
|
||||
template <
|
||||
/// Indicates the Parser for this platform.
|
||||
template<typename, bool> class ParserType,
|
||||
/// Indicates the greatest value the program counter might take.
|
||||
uint64_t max_address,
|
||||
/// Provides the type of Instruction to expect.
|
||||
typename InstructionType,
|
||||
/// Provides the storage size used for memory.
|
||||
typename MemoryWord,
|
||||
/// Provides the addressing range of memory.
|
||||
typename AddressType
|
||||
> class Disassembler {
|
||||
public:
|
||||
using ProgramCounterType = typename MinIntTypeValue<max_address>::type;
|
||||
|
||||
/*!
|
||||
Adds the result of disassembling @c memory which is @c length @c MemoryWords long from @c start_address
|
||||
to the current net total of instructions and recorded memory accesses.
|
||||
*/
|
||||
void disassemble(const MemoryWord *memory, ProgramCounterType location, ProgramCounterType length, ProgramCounterType start_address) {
|
||||
// TODO: possibly, move some of this stuff to instruction-set specific disassemblers, analogous to
|
||||
// the Executor's ownership of the Parser. That would allow handling of stateful parsing.
|
||||
ParserType<decltype(*this), true> parser;
|
||||
pending_entry_points_.push_back(start_address);
|
||||
entry_points_.insert(start_address);
|
||||
|
||||
while(!pending_entry_points_.empty()) {
|
||||
const auto next_entry_point = pending_entry_points_.front();
|
||||
pending_entry_points_.pop_front();
|
||||
|
||||
if(next_entry_point >= location) {
|
||||
parser.parse(*this, memory - location, next_entry_point & max_address, length + location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::map<ProgramCounterType, InstructionType> &instructions() const {
|
||||
return instructions_;
|
||||
}
|
||||
|
||||
const std::set<ProgramCounterType> &entry_points() const {
|
||||
return entry_points_;
|
||||
}
|
||||
|
||||
void announce_overflow(ProgramCounterType) {}
|
||||
void announce_instruction(ProgramCounterType address, InstructionType instruction) {
|
||||
instructions_[address] = instruction;
|
||||
}
|
||||
void add_entry(ProgramCounterType address) {
|
||||
if(entry_points_.find(address) == entry_points_.end()) {
|
||||
pending_entry_points_.push_back(address);
|
||||
entry_points_.insert(address);
|
||||
}
|
||||
}
|
||||
void add_access(AddressType address, AccessType access_type) {
|
||||
// TODO.
|
||||
(void)address;
|
||||
(void)access_type;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<ProgramCounterType, InstructionType> instructions_;
|
||||
std::set<ProgramCounterType> entry_points_;
|
||||
|
||||
std::list<ProgramCounterType> pending_entry_points_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* Disassembler_h */
|
||||
282
InstructionSets/M50740/Decoder.cpp
Normal file
282
InstructionSets/M50740/Decoder.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
//
|
||||
// Decoder.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 15/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Decoder.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace InstructionSet {
|
||||
namespace M50740 {
|
||||
|
||||
Instruction Decoder::instrucion_for_opcode(uint8_t opcode) {
|
||||
switch(opcode) {
|
||||
default: return Instruction(opcode);
|
||||
|
||||
#define Map(opcode, operation, addressing_mode) case opcode: return Instruction(Operation::operation, AddressingMode::addressing_mode, opcode);
|
||||
|
||||
/* 0x00 – 0x0f */
|
||||
Map(0x00, BRK, Implied); Map(0x01, ORA, XIndirect);
|
||||
Map(0x02, JSR, ZeroPageIndirect); Map(0x03, BBS0, AccumulatorRelative);
|
||||
|
||||
Map(0x05, ORA, ZeroPage);
|
||||
Map(0x06, ASL, ZeroPage); Map(0x07, BBS0, ZeroPageRelative);
|
||||
|
||||
Map(0x08, PHP, Implied); Map(0x09, ORA, Immediate);
|
||||
Map(0x0a, ASL, Accumulator); Map(0x0b, SEB0, Accumulator);
|
||||
|
||||
Map(0x0d, ORA, Absolute);
|
||||
Map(0x0e, ASL, Absolute); Map(0x0f, SEB0, ZeroPage);
|
||||
|
||||
/* 0x10 – 0x1f */
|
||||
Map(0x10, BPL, Relative); Map(0x11, ORA, IndirectY);
|
||||
Map(0x12, CLT, Implied); Map(0x13, BBC0, AccumulatorRelative);
|
||||
|
||||
Map(0x15, ORA, ZeroPageX);
|
||||
Map(0x16, ASL, ZeroPageX); Map(0x17, BBC0, ZeroPageRelative);
|
||||
|
||||
Map(0x18, CLC, Implied); Map(0x19, ORA, AbsoluteY);
|
||||
Map(0x1a, DEC, Accumulator); Map(0x1b, CLB0, Accumulator);
|
||||
|
||||
Map(0x1d, ORA, AbsoluteX);
|
||||
Map(0x1e, ASL, AbsoluteX); Map(0x1f, CLB0, ZeroPage);
|
||||
|
||||
/* 0x20 – 0x2f */
|
||||
Map(0x20, JSR, Absolute); Map(0x21, AND, XIndirect);
|
||||
Map(0x22, JSR, SpecialPage); Map(0x23, BBS1, AccumulatorRelative);
|
||||
|
||||
Map(0x24, BIT, ZeroPage); Map(0x25, AND, ZeroPage);
|
||||
Map(0x26, ROL, ZeroPage); Map(0x27, BBS1, ZeroPageRelative);
|
||||
|
||||
Map(0x28, PLP, Implied); Map(0x29, AND, Immediate);
|
||||
Map(0x2a, ROL, Accumulator); Map(0x2b, SEB1, Accumulator);
|
||||
|
||||
Map(0x2c, BIT, Absolute); Map(0x2d, AND, Absolute);
|
||||
Map(0x2e, ROL, Absolute); Map(0x2f, SEB1, ZeroPage);
|
||||
|
||||
/* 0x30 – 0x3f */
|
||||
Map(0x30, BMI, Relative); Map(0x31, AND, IndirectY);
|
||||
Map(0x32, SET, Implied); Map(0x33, BBC1, AccumulatorRelative);
|
||||
|
||||
Map(0x35, AND, ZeroPageX);
|
||||
Map(0x36, ROL, ZeroPageX); Map(0x37, BBC1, ZeroPageRelative);
|
||||
|
||||
Map(0x38, SEC, Implied); Map(0x39, AND, AbsoluteY);
|
||||
Map(0x3a, INC, Accumulator); Map(0x3b, CLB1, Accumulator);
|
||||
|
||||
Map(0x3c, LDM, ImmediateZeroPage); Map(0x3d, AND, AbsoluteX);
|
||||
Map(0x3e, ROL, AbsoluteX); Map(0x3f, CLB1, ZeroPage);
|
||||
|
||||
/* 0x40 – 0x4f */
|
||||
Map(0x40, RTI, Implied); Map(0x41, EOR, XIndirect);
|
||||
Map(0x42, STP, Implied); Map(0x43, BBS2, AccumulatorRelative);
|
||||
|
||||
Map(0x44, COM, ZeroPage); Map(0x45, EOR, ZeroPage);
|
||||
Map(0x46, LSR, ZeroPage); Map(0x47, BBS2, ZeroPageRelative);
|
||||
|
||||
Map(0x48, PHA, Implied); Map(0x49, EOR, Immediate);
|
||||
Map(0x4a, LSR, Accumulator); Map(0x4b, SEB2, Accumulator);
|
||||
|
||||
Map(0x4c, JMP, Absolute); Map(0x4d, EOR, Absolute);
|
||||
Map(0x4e, LSR, Absolute); Map(0x4f, SEB2, ZeroPage);
|
||||
|
||||
/* 0x50 – 0x5f */
|
||||
Map(0x50, BVC, Relative); Map(0x51, EOR, IndirectY);
|
||||
Map(0x53, BBC2, AccumulatorRelative);
|
||||
|
||||
Map(0x55, EOR, ZeroPageX);
|
||||
Map(0x56, LSR, ZeroPageX); Map(0x57, BBC2, ZeroPageRelative);
|
||||
|
||||
Map(0x58, CLI, Implied); Map(0x59, EOR, AbsoluteY);
|
||||
Map(0x5b, CLB2, Accumulator);
|
||||
|
||||
Map(0x5d, EOR, AbsoluteX);
|
||||
Map(0x5e, LSR, AbsoluteX); Map(0x5f, CLB2, ZeroPage);
|
||||
|
||||
/* 0x60 – 0x6f */
|
||||
Map(0x60, RTS, Implied); Map(0x61, ADC, XIndirect);
|
||||
Map(0x63, BBS3, AccumulatorRelative);
|
||||
|
||||
Map(0x64, TST, ZeroPage); Map(0x65, ADC, ZeroPage);
|
||||
Map(0x66, ROR, ZeroPage); Map(0x67, BBS3, ZeroPageRelative);
|
||||
|
||||
Map(0x68, PLA, Implied); Map(0x69, ADC, Immediate);
|
||||
Map(0x6a, ROR, Accumulator); Map(0x6b, SEB3, Accumulator);
|
||||
|
||||
Map(0x6c, JMP, AbsoluteIndirect); Map(0x6d, ADC, Absolute);
|
||||
Map(0x6e, ROR, Absolute); Map(0x6f, SEB3, ZeroPage);
|
||||
|
||||
/* 0x70 – 0x7f */
|
||||
Map(0x70, BVS, Relative); Map(0x71, ADC, IndirectY);
|
||||
Map(0x73, BBC3, AccumulatorRelative);
|
||||
|
||||
Map(0x75, ADC, ZeroPageX);
|
||||
Map(0x76, ROR, ZeroPageX); Map(0x77, BBC3, ZeroPageRelative);
|
||||
|
||||
Map(0x78, SEI, Implied); Map(0x79, ADC, AbsoluteY);
|
||||
Map(0x7b, CLB3, Accumulator);
|
||||
|
||||
Map(0x7d, ADC, AbsoluteX);
|
||||
Map(0x7e, ROR, AbsoluteX); Map(0x7f, CLB3, ZeroPage);
|
||||
|
||||
/* 0x80 – 0x8f */
|
||||
Map(0x80, BRA, Relative); Map(0x81, STA, XIndirect);
|
||||
Map(0x82, RRF, ZeroPage); Map(0x83, BBS4, AccumulatorRelative);
|
||||
|
||||
Map(0x84, STY, ZeroPage); Map(0x85, STA, ZeroPage);
|
||||
Map(0x86, STX, ZeroPage); Map(0x87, BBS4, ZeroPageRelative);
|
||||
|
||||
Map(0x88, DEY, Implied);
|
||||
Map(0x8a, TXA, Implied); Map(0x8b, SEB4, Accumulator);
|
||||
|
||||
Map(0x8c, STY, Absolute); Map(0x8d, STA, Absolute);
|
||||
Map(0x8e, STX, Absolute); Map(0x8f, SEB4, ZeroPage);
|
||||
|
||||
/* 0x90 – 0x9f */
|
||||
Map(0x90, BCC, Relative); Map(0x91, STA, IndirectY);
|
||||
Map(0x93, BBC4, AccumulatorRelative);
|
||||
|
||||
Map(0x94, STY, ZeroPageX); Map(0x95, STA, ZeroPageX);
|
||||
Map(0x96, STX, ZeroPageX); Map(0x97, BBC4, ZeroPageRelative);
|
||||
|
||||
Map(0x98, TYA, Implied); Map(0x99, STA, AbsoluteY);
|
||||
Map(0x9a, TXS, Implied); Map(0x9b, CLB4, Accumulator);
|
||||
|
||||
Map(0x9d, ADC, AbsoluteX);
|
||||
Map(0x9f, CLB4, ZeroPage);
|
||||
|
||||
/* 0xa0 – 0xaf */
|
||||
Map(0xa0, LDY, Immediate); Map(0xa1, LDA, XIndirect);
|
||||
Map(0xa2, LDX, Immediate); Map(0xa3, BBS5, AccumulatorRelative);
|
||||
|
||||
Map(0xa4, LDY, ZeroPage); Map(0xa5, LDA, ZeroPage);
|
||||
Map(0xa6, LDX, ZeroPage); Map(0xa7, BBS5, ZeroPageRelative);
|
||||
|
||||
Map(0xa8, TAY, Implied); Map(0xa9, LDA, Immediate);
|
||||
Map(0xaa, TAX, Implied); Map(0xab, SEB5, Accumulator);
|
||||
|
||||
Map(0xac, LDY, Absolute); Map(0xad, LDA, Absolute);
|
||||
Map(0xae, LDX, Absolute); Map(0xaf, SEB5, ZeroPage);
|
||||
|
||||
/* 0xb0 – 0xbf */
|
||||
Map(0xb0, BCS, Relative); Map(0xb1, STA, IndirectY);
|
||||
Map(0xb2, JMP, ZeroPageIndirect); Map(0xb3, BBC5, AccumulatorRelative);
|
||||
|
||||
Map(0xb4, LDY, ZeroPageX); Map(0xb5, LDA, ZeroPageX);
|
||||
Map(0xb6, LDX, ZeroPageY); Map(0xb7, BBC5, ZeroPageRelative);
|
||||
|
||||
Map(0xb8, CLV, Implied); Map(0xb9, LDA, AbsoluteY);
|
||||
Map(0xba, TSX, Implied); Map(0xbb, CLB5, Accumulator);
|
||||
|
||||
Map(0xbc, LDY, AbsoluteX); Map(0xbd, LDA, AbsoluteX);
|
||||
Map(0xbe, LDX, AbsoluteY); Map(0xbf, CLB5, ZeroPage);
|
||||
|
||||
/* 0xc0 – 0xcf */
|
||||
Map(0xc0, CPY, Immediate); Map(0xc1, CMP, XIndirect);
|
||||
Map(0xc2, SLW, Implied); Map(0xc3, BBS6, AccumulatorRelative);
|
||||
|
||||
Map(0xc4, CPY, ZeroPage); Map(0xc5, CMP, ZeroPage);
|
||||
Map(0xc6, DEC, ZeroPage); Map(0xc7, BBS6, ZeroPageRelative);
|
||||
|
||||
Map(0xc8, INY, Implied); Map(0xc9, CMP, Immediate);
|
||||
Map(0xca, DEX, Implied); Map(0xcb, SEB6, Accumulator);
|
||||
|
||||
Map(0xcc, CPY, Absolute); Map(0xcd, CMP, Absolute);
|
||||
Map(0xce, DEC, Absolute); Map(0xcf, SEB6, ZeroPage);
|
||||
|
||||
/* 0xd0 – 0xdf */
|
||||
Map(0xd0, BNE, Relative); Map(0xd1, CMP, IndirectY);
|
||||
Map(0xd3, BBC6, AccumulatorRelative);
|
||||
|
||||
Map(0xd5, CMP, ZeroPageX);
|
||||
Map(0xd6, DEC, ZeroPageX); Map(0xd7, BBC6, ZeroPageRelative);
|
||||
|
||||
Map(0xd8, CLD, Implied); Map(0xd9, CMP, AbsoluteY);
|
||||
Map(0xdb, CLB6, Accumulator);
|
||||
|
||||
Map(0xdd, CMP, AbsoluteX);
|
||||
Map(0xde, DEC, AbsoluteX); Map(0xdf, CLB6, ZeroPage);
|
||||
|
||||
/* 0xe0 – 0xef */
|
||||
Map(0xe0, CPX, Immediate); Map(0xe1, SBC, XIndirect);
|
||||
Map(0xe2, FST, Implied); Map(0xe3, BBS7, AccumulatorRelative);
|
||||
|
||||
Map(0xe4, CPX, ZeroPage); Map(0xe5, SBC, ZeroPage);
|
||||
Map(0xe6, INC, ZeroPage); Map(0xe7, BBS7, ZeroPageRelative);
|
||||
|
||||
Map(0xe8, INX, Implied); Map(0xe9, SBC, Immediate);
|
||||
Map(0xea, NOP, Implied); Map(0xeb, SEB7, Accumulator);
|
||||
|
||||
Map(0xec, CPX, Absolute); Map(0xed, SBC, Absolute);
|
||||
Map(0xee, INC, Absolute); Map(0xef, SEB7, ZeroPage);
|
||||
|
||||
/* 0xf0 – 0xff */
|
||||
Map(0xf0, BEQ, Relative); Map(0xf1, SBC, IndirectY);
|
||||
Map(0xf3, BBC7, AccumulatorRelative);
|
||||
|
||||
Map(0xf5, SBC, ZeroPageX);
|
||||
Map(0xf6, INC, ZeroPageX); Map(0xf7, BBC7, ZeroPageRelative);
|
||||
|
||||
Map(0xf8, SED, Implied); Map(0xf9, SBC, AbsoluteY);
|
||||
Map(0xfb, CLB7, Accumulator);
|
||||
|
||||
Map(0xfd, SBC, AbsoluteX);
|
||||
Map(0xfe, INC, AbsoluteX); Map(0xff, CLB7, ZeroPage);
|
||||
|
||||
#undef Map
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<int, InstructionSet::M50740::Instruction> Decoder::decode(const uint8_t *source, size_t length) {
|
||||
const uint8_t *const end = source + length;
|
||||
|
||||
if(phase_ == Phase::Instruction && source != end) {
|
||||
const uint8_t instruction = *source;
|
||||
++source;
|
||||
++consumed_;
|
||||
|
||||
// Determine the instruction in hand, and finish now if its undefined.
|
||||
instr_ = instrucion_for_opcode(instruction);
|
||||
if(instr_.operation == Operation::Invalid) {
|
||||
consumed_ = 0;
|
||||
return std::make_pair(1, instr_);
|
||||
}
|
||||
|
||||
// Obtain an operand size and roll onto the correct phase.
|
||||
operand_size_ = size(instr_.addressing_mode);
|
||||
phase_ = operand_size_ ? Phase::AwaitingOperand : Phase::ReadyToPost;
|
||||
operand_bytes_ = 0;
|
||||
}
|
||||
|
||||
if(phase_ == Phase::AwaitingOperand && source != end) {
|
||||
const int outstanding_bytes = operand_size_ - operand_bytes_;
|
||||
const int bytes_to_consume = std::min(int(end - source), outstanding_bytes);
|
||||
|
||||
consumed_ += bytes_to_consume;
|
||||
source += bytes_to_consume;
|
||||
operand_bytes_ += bytes_to_consume;
|
||||
|
||||
if(operand_size_ == operand_bytes_) {
|
||||
phase_ = Phase::ReadyToPost;
|
||||
} else {
|
||||
return std::make_pair(-(operand_size_ - operand_bytes_), Instruction());
|
||||
}
|
||||
}
|
||||
|
||||
if(phase_ == Phase::ReadyToPost) {
|
||||
const auto result = std::make_pair(consumed_, instr_);
|
||||
consumed_ = 0;
|
||||
phase_ = Phase::Instruction;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Decoding didn't complete, without it being clear how many more bytes are required.
|
||||
return std::make_pair(0, Instruction());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
39
InstructionSets/M50740/Decoder.hpp
Normal file
39
InstructionSets/M50740/Decoder.hpp
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Decoder.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 15/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef InstructionSets_M50740_Decoder_hpp
|
||||
#define InstructionSets_M50740_Decoder_hpp
|
||||
|
||||
#include "Instruction.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
|
||||
namespace InstructionSet {
|
||||
namespace M50740 {
|
||||
|
||||
class Decoder {
|
||||
public:
|
||||
std::pair<int, Instruction> decode(const uint8_t *source, size_t length);
|
||||
Instruction instrucion_for_opcode(uint8_t opcode);
|
||||
|
||||
private:
|
||||
enum class Phase {
|
||||
Instruction,
|
||||
AwaitingOperand,
|
||||
ReadyToPost
|
||||
} phase_ = Phase::Instruction;
|
||||
int operand_size_ = 0, operand_bytes_ = 0;
|
||||
int consumed_ = 0;
|
||||
Instruction instr_;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* InstructionSets_M50740_Decoder_hpp */
|
||||
838
InstructionSets/M50740/Executor.cpp
Normal file
838
InstructionSets/M50740/Executor.cpp
Normal file
@@ -0,0 +1,838 @@
|
||||
//
|
||||
// Executor.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/1/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Executor.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
#include "../../Machines/Utility/MemoryFuzzer.hpp"
|
||||
|
||||
#define LOG_PREFIX "[M50740] "
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
using namespace InstructionSet::M50740;
|
||||
|
||||
namespace {
|
||||
constexpr int port_remap[] = {0, 1, 2, 0, 3};
|
||||
}
|
||||
|
||||
Executor::Executor(PortHandler &port_handler) : port_handler_(port_handler) {
|
||||
// Cut down the list of all generated performers to those the processor actually uses, and install that
|
||||
// for future referencing by action_for.
|
||||
Decoder decoder;
|
||||
for(size_t c = 0; c < 256; c++) {
|
||||
const auto instruction = decoder.instrucion_for_opcode(uint8_t(c));
|
||||
|
||||
// Treat invalid as NOP, because I've got to do _something_.
|
||||
if(instruction.operation == Operation::Invalid) {
|
||||
performers_[c] = performer_lookup_.performer(Operation::NOP, instruction.addressing_mode);
|
||||
} else {
|
||||
performers_[c] = performer_lookup_.performer(instruction.operation, instruction.addressing_mode);
|
||||
}
|
||||
}
|
||||
|
||||
// Fuzz RAM; then set anything that may be replaced by ROM to FF.
|
||||
Memory::Fuzz(memory_);
|
||||
memset(&memory_[0x100], 0xff, memory_.size() - 0x100);
|
||||
}
|
||||
|
||||
void Executor::set_rom(const std::vector<uint8_t> &rom) {
|
||||
// Copy into place, and reset.
|
||||
const auto length = std::min(size_t(0x1000), rom.size());
|
||||
memcpy(&memory_[0x2000 - length], rom.data(), length);
|
||||
reset();
|
||||
}
|
||||
|
||||
void Executor::run_for(Cycles cycles) {
|
||||
// The incoming clock is divided by four; the local cycles_ count
|
||||
// ensures that fractional parts are kept track of.
|
||||
cycles_ += cycles;
|
||||
CachingExecutor::run_for(cycles_.divide(Cycles(4)).as<int>());
|
||||
}
|
||||
|
||||
void Executor::reset() {
|
||||
// Just jump to the reset vector.
|
||||
set_program_counter(uint16_t(memory_[0x1ffe] | (memory_[0x1fff] << 8)));
|
||||
}
|
||||
|
||||
void Executor::set_interrupt_line(bool line) {
|
||||
// Super hack: interrupt now, if permitted. Otherwise do nothing.
|
||||
// So this will fail to catch enabling of interrupts while the line
|
||||
// is active, amongst other things.
|
||||
if(interrupt_line_ != line) {
|
||||
interrupt_line_ = line;
|
||||
|
||||
// TODO: verify interrupt connection. Externally, but stubbed off here.
|
||||
// if(!interrupt_disable_ && line) {
|
||||
// perform_interrupt<false>(0x1ff4);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t Executor::read(uint16_t address) {
|
||||
address &= 0x1fff;
|
||||
|
||||
// Deal with RAM and ROM accesses quickly.
|
||||
if(address < 0x60 || address >= 0x100) return memory_[address];
|
||||
|
||||
port_handler_.run_ports_for(cycles_since_port_handler_.flush<Cycles>());
|
||||
switch(address) {
|
||||
default:
|
||||
LOG("Unrecognised read from " << PADHEX(4) << address);
|
||||
return 0xff;
|
||||
|
||||
// "Port R"; sixteen four-bit ports
|
||||
case 0xd0: case 0xd1: case 0xd2: case 0xd3: case 0xd4: case 0xd5: case 0xd6: case 0xd7:
|
||||
case 0xd8: case 0xd9: case 0xda: case 0xdb: case 0xdc: case 0xdd: case 0xde: case 0xdf:
|
||||
LOG("Unimplemented Port R read from " << PADHEX(4) << address);
|
||||
return 0x00;
|
||||
|
||||
// Ports P0–P3.
|
||||
case 0xe0: case 0xe2:
|
||||
case 0xe4: case 0xe8: {
|
||||
const int port = port_remap[(address - 0xe0) >> 1];
|
||||
const uint8_t input = port_handler_.get_port_input(port);
|
||||
|
||||
// In the direction registers, a 0 indicates input, a 1 indicates output.
|
||||
return (input &~ port_directions_[port]) | (port_outputs_[port] & port_directions_[port]);
|
||||
}
|
||||
|
||||
case 0xe1: case 0xe3:
|
||||
case 0xe5: case 0xe9:
|
||||
return port_directions_[port_remap[(address - 0xe0) >> 1]];
|
||||
|
||||
// Timers.
|
||||
case 0xf9: return prescalers_[0].value;
|
||||
case 0xfa: return timers_[0].value;
|
||||
case 0xfb: return timers_[1].value;
|
||||
case 0xfc: return prescalers_[1].value;
|
||||
case 0xfd: return timers_[2].value;
|
||||
|
||||
case 0xfe: return interrupt_control_;
|
||||
case 0xff: return timer_control_;
|
||||
}
|
||||
}
|
||||
|
||||
void Executor::set_port_output(int port) {
|
||||
// Force 'output' to a 1 anywhere that a bit is set as input.
|
||||
port_handler_.set_port_output(port, port_outputs_[port] | ~port_directions_[port]);
|
||||
}
|
||||
|
||||
void Executor::write(uint16_t address, uint8_t value) {
|
||||
address &= 0x1fff;
|
||||
|
||||
// RAM writes are easy.
|
||||
if(address < 0x60) {
|
||||
memory_[address] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
// ROM 'writes' are almost as easy (albeit unexpected).
|
||||
if(address >= 0x100) {
|
||||
LOG("Attempted ROM write of " << PADHEX(2) << value << " to " << PADHEX(4) << address);
|
||||
return;
|
||||
}
|
||||
|
||||
// Push time to the port handler.
|
||||
port_handler_.run_ports_for(cycles_since_port_handler_.flush<Cycles>());
|
||||
|
||||
switch(address) {
|
||||
default:
|
||||
LOG("Unrecognised write of " << PADHEX(2) << value << " to " << PADHEX(4) << address);
|
||||
break;
|
||||
|
||||
// "Port R"; sixteen four-bit ports
|
||||
case 0xd0: case 0xd1: case 0xd2: case 0xd3: case 0xd4: case 0xd5: case 0xd6: case 0xd7:
|
||||
case 0xd8: case 0xd9: case 0xda: case 0xdb: case 0xdc: case 0xdd: case 0xde: case 0xdf:
|
||||
LOG("Unimplemented Port R write of " << PADHEX(2) << value << " from " << PADHEX(4) << address);
|
||||
break;
|
||||
|
||||
// Ports P0–P3.
|
||||
case 0xe0: case 0xe2:
|
||||
case 0xe4: case 0xe8: {
|
||||
const int port = port_remap[(address - 0xe0) >> 1];
|
||||
port_outputs_[port] = value;
|
||||
set_port_output(port);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xe1: case 0xe3:
|
||||
case 0xe5: case 0xe9: {
|
||||
const int port = port_remap[(address - 0xe0) >> 1];
|
||||
port_directions_[port] = value;
|
||||
set_port_output(port);
|
||||
} break;
|
||||
|
||||
// Timers.
|
||||
//
|
||||
// Reloading of value with the reload value is a guess, based upon what I take
|
||||
// to be the intended usage of timer 2 in handling key repeat on the Apple IIgs.
|
||||
case 0xf9: prescalers_[0].value = prescalers_[0].reload_value = value; break;
|
||||
case 0xfa: timers_[0].value = timers_[0].reload_value = value; break;
|
||||
case 0xfb: timers_[1].value = timers_[1].reload_value = value; break;
|
||||
case 0xfc: prescalers_[1].value = prescalers_[1].reload_value = value; break;
|
||||
case 0xfd: timers_[2].value = timers_[2].reload_value = value; break;
|
||||
|
||||
case 0xfe: interrupt_control_ = value; break;
|
||||
case 0xff: timer_control_ = value; break;
|
||||
}
|
||||
}
|
||||
|
||||
void Executor::push(uint8_t value) {
|
||||
write(s_, value);
|
||||
--s_;
|
||||
}
|
||||
|
||||
uint8_t Executor::pull() {
|
||||
++s_;
|
||||
return read(s_);
|
||||
}
|
||||
|
||||
void Executor::set_flags(uint8_t flags) {
|
||||
negative_result_ = flags;
|
||||
overflow_result_ = uint8_t(flags << 1);
|
||||
index_mode_ = flags & 0x20;
|
||||
decimal_mode_ = flags & 0x08;
|
||||
interrupt_disable_ = flags & 0x04;
|
||||
zero_result_ = !(flags & 0x02);
|
||||
carry_flag_ = flags & 0x01;
|
||||
}
|
||||
|
||||
uint8_t Executor::flags() {
|
||||
return
|
||||
(negative_result_ & 0x80) |
|
||||
((overflow_result_ & 0x80) >> 1) |
|
||||
(index_mode_ ? 0x20 : 0x00) |
|
||||
(decimal_mode_ ? 0x08 : 0x00) |
|
||||
interrupt_disable_ |
|
||||
(zero_result_ ? 0x00 : 0x02) |
|
||||
carry_flag_;
|
||||
}
|
||||
|
||||
template<bool is_brk> inline void Executor::perform_interrupt(uint16_t vector) {
|
||||
// BRK has an unused operand.
|
||||
++program_counter_;
|
||||
push(uint8_t(program_counter_ >> 8));
|
||||
push(uint8_t(program_counter_ & 0xff));
|
||||
push(flags() | (is_brk ? 0x10 : 0x00));
|
||||
set_program_counter(uint16_t(memory_[vector] | (memory_[vector+1] << 8)));
|
||||
}
|
||||
|
||||
template <Operation operation, AddressingMode addressing_mode> void Executor::perform() {
|
||||
// Post cycle cost; this emulation _does not provide accurate timing_.
|
||||
#define TLength(mode, base) case AddressingMode::mode: subtract_duration(base + t_lengths[index_mode_]); break;
|
||||
#define Length(mode, base) case AddressingMode::mode: subtract_duration(base); break;
|
||||
|
||||
switch(operation) {
|
||||
case Operation::ADC: case Operation::AND: case Operation::CMP: case Operation::EOR:
|
||||
case Operation::LDA: case Operation::ORA: case Operation::SBC:
|
||||
{
|
||||
constexpr int t_lengths[] = {
|
||||
0,
|
||||
operation == Operation::LDA ? 2 : (operation == Operation::CMP ? 1 : 3)
|
||||
};
|
||||
switch(addressing_mode) {
|
||||
TLength(XIndirect, 6);
|
||||
TLength(ZeroPage, 3);
|
||||
TLength(Immediate, 2);
|
||||
TLength(Absolute, 4);
|
||||
TLength(IndirectY, 6);
|
||||
TLength(ZeroPageX, 4);
|
||||
TLength(AbsoluteY, 5);
|
||||
TLength(AbsoluteX, 5);
|
||||
default: assert(false);
|
||||
}
|
||||
} break;
|
||||
|
||||
case Operation::ASL: case Operation::DEC: case Operation::INC: case Operation::LSR:
|
||||
case Operation::ROL: case Operation::ROR:
|
||||
switch(addressing_mode) {
|
||||
Length(ZeroPage, 5);
|
||||
Length(Accumulator, 2);
|
||||
Length(Absolute, 6);
|
||||
Length(ZeroPageX, 6);
|
||||
Length(AbsoluteX, 7);
|
||||
default: assert(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case Operation::BBC0: case Operation::BBC1: case Operation::BBC2: case Operation::BBC3:
|
||||
case Operation::BBC4: case Operation::BBC5: case Operation::BBC6: case Operation::BBC7:
|
||||
case Operation::BBS0: case Operation::BBS1: case Operation::BBS2: case Operation::BBS3:
|
||||
case Operation::BBS4: case Operation::BBS5: case Operation::BBS6: case Operation::BBS7:
|
||||
switch(addressing_mode) {
|
||||
Length(AccumulatorRelative, 4);
|
||||
Length(ZeroPageRelative, 5);
|
||||
default: assert(false);
|
||||
}
|
||||
break;
|
||||
case Operation::BPL: case Operation::BMI: case Operation::BEQ: case Operation::BNE:
|
||||
case Operation::BCS: case Operation::BCC: case Operation::BVS: case Operation::BVC:
|
||||
case Operation::INX: case Operation::INY:
|
||||
subtract_duration(2);
|
||||
break;
|
||||
|
||||
case Operation::CPX: case Operation::CPY: case Operation::BIT: case Operation::LDX:
|
||||
case Operation::LDY:
|
||||
switch(addressing_mode) {
|
||||
Length(Immediate, 2);
|
||||
Length(ZeroPage, 3);
|
||||
Length(Absolute, 4);
|
||||
Length(ZeroPageX, 4);
|
||||
Length(ZeroPageY, 4);
|
||||
Length(AbsoluteX, 5);
|
||||
Length(AbsoluteY, 5);
|
||||
default: assert(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case Operation::BRA: subtract_duration(4); break;
|
||||
case Operation::BRK: subtract_duration(7); break;
|
||||
|
||||
case Operation::CLB0: case Operation::CLB1: case Operation::CLB2: case Operation::CLB3:
|
||||
case Operation::CLB4: case Operation::CLB5: case Operation::CLB6: case Operation::CLB7:
|
||||
case Operation::SEB0: case Operation::SEB1: case Operation::SEB2: case Operation::SEB3:
|
||||
case Operation::SEB4: case Operation::SEB5: case Operation::SEB6: case Operation::SEB7:
|
||||
switch(addressing_mode) {
|
||||
Length(Accumulator, 2);
|
||||
Length(ZeroPage, 5);
|
||||
default: assert(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case Operation::CLC: case Operation::CLD: case Operation::CLT: case Operation::CLV:
|
||||
case Operation::CLI:
|
||||
case Operation::DEX: case Operation::DEY: case Operation::FST: case Operation::NOP:
|
||||
case Operation::SEC: case Operation::SED: case Operation::SEI: case Operation::SET:
|
||||
case Operation::SLW: case Operation::STP: case Operation::TAX: case Operation::TAY:
|
||||
case Operation::TSX: case Operation::TXA: case Operation::TXS: case Operation::TYA:
|
||||
subtract_duration(2);
|
||||
break;
|
||||
|
||||
case Operation::COM: subtract_duration(5); break;
|
||||
|
||||
case Operation::JMP:
|
||||
switch(addressing_mode) {
|
||||
Length(Absolute, 3);
|
||||
Length(AbsoluteIndirect, 5);
|
||||
Length(ZeroPageIndirect, 4);
|
||||
default: assert(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case Operation::JSR:
|
||||
switch(addressing_mode) {
|
||||
Length(ZeroPageIndirect, 7);
|
||||
Length(Absolute, 6);
|
||||
Length(SpecialPage, 5);
|
||||
default: assert(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case Operation::LDM: subtract_duration(4); break;
|
||||
|
||||
case Operation::PHA: case Operation::PHP: case Operation::TST:
|
||||
subtract_duration(3);
|
||||
break;
|
||||
|
||||
case Operation::PLA: case Operation::PLP:
|
||||
subtract_duration(4);
|
||||
break;
|
||||
|
||||
case Operation::RRF: subtract_duration(8); break;
|
||||
case Operation::RTI: subtract_duration(6); break;
|
||||
case Operation::RTS: subtract_duration(6); break;
|
||||
|
||||
case Operation::STA: case Operation::STX: case Operation::STY:
|
||||
switch(addressing_mode) {
|
||||
Length(XIndirect, 7);
|
||||
Length(ZeroPage, 4);
|
||||
Length(Absolute, 5);
|
||||
Length(IndirectY, 7);
|
||||
Length(ZeroPageX, 5);
|
||||
Length(ZeroPageY, 5);
|
||||
Length(AbsoluteY, 6);
|
||||
Length(AbsoluteX, 6);
|
||||
default: assert(false);
|
||||
}
|
||||
break;
|
||||
|
||||
default: assert(false);
|
||||
}
|
||||
|
||||
// Deal with all modes that don't access memory up here;
|
||||
// those that access memory will go through a slightly longer
|
||||
// sequence below that wraps the address and checks whether
|
||||
// a write is valid [if required].
|
||||
|
||||
unsigned int address;
|
||||
#define next8() memory_[(program_counter_ + 1) & 0x1fff]
|
||||
#define next16() uint16_t(memory_[(program_counter_ + 1) & 0x1fff] | (memory_[(program_counter_ + 2) & 0x1fff] << 8))
|
||||
|
||||
// Underlying assumption below: the instruction stream will never
|
||||
// overlap with IO ports.
|
||||
switch(addressing_mode) {
|
||||
|
||||
// Addressing modes with no further memory access.
|
||||
|
||||
case AddressingMode::Implied:
|
||||
perform<operation>(nullptr);
|
||||
++program_counter_;
|
||||
return;
|
||||
|
||||
case AddressingMode::Accumulator:
|
||||
perform<operation>(&a_);
|
||||
++program_counter_;
|
||||
return;
|
||||
|
||||
case AddressingMode::Immediate:
|
||||
perform<operation>(&next8());
|
||||
program_counter_ += 2;
|
||||
return;
|
||||
|
||||
// Special-purpose addressing modes.
|
||||
|
||||
case AddressingMode::Relative:
|
||||
address = unsigned(program_counter_ + 1 + size(addressing_mode) + int8_t(next8()));
|
||||
break;
|
||||
|
||||
case AddressingMode::SpecialPage: address = 0x1f00 | next8(); break;
|
||||
|
||||
case AddressingMode::ImmediateZeroPage:
|
||||
// LDM only...
|
||||
write(memory_[(program_counter_+2)&0x1fff], memory_[(program_counter_+1)&0x1fff]);
|
||||
program_counter_ += 1 + size(addressing_mode);
|
||||
return;
|
||||
|
||||
case AddressingMode::AccumulatorRelative:
|
||||
case AddressingMode::ZeroPageRelative: {
|
||||
// Order of bytes is: (i) zero page address; (ii) relative jump.
|
||||
uint8_t value;
|
||||
if constexpr (addressing_mode == AddressingMode::AccumulatorRelative) {
|
||||
value = a_;
|
||||
address = unsigned(program_counter_ + 1 + size(addressing_mode) + int8_t(next8()));
|
||||
} else {
|
||||
value = read(next8());
|
||||
address = unsigned(program_counter_ + 1 + size(addressing_mode) + int8_t(memory_[(program_counter_+2)&0x1fff]));
|
||||
}
|
||||
program_counter_ += 1 + size(addressing_mode);
|
||||
switch(operation) {
|
||||
case Operation::BBS0: case Operation::BBS1: case Operation::BBS2: case Operation::BBS3:
|
||||
case Operation::BBS4: case Operation::BBS5: case Operation::BBS6: case Operation::BBS7: {
|
||||
if constexpr (operation >= Operation::BBS0 && operation <= Operation::BBS7) {
|
||||
constexpr uint8_t mask = 1 << (int(operation) - int(Operation::BBS0));
|
||||
if(value & mask) {
|
||||
set_program_counter(uint16_t(address));
|
||||
subtract_duration(2);
|
||||
}
|
||||
}
|
||||
} return;
|
||||
case Operation::BBC0: case Operation::BBC1: case Operation::BBC2: case Operation::BBC3:
|
||||
case Operation::BBC4: case Operation::BBC5: case Operation::BBC6: case Operation::BBC7: {
|
||||
if constexpr (operation >= Operation::BBC0 && operation <= Operation::BBC7) {
|
||||
constexpr uint8_t mask = 1 << (int(operation) - int(Operation::BBC0));
|
||||
if(!(value & mask)) {
|
||||
set_program_counter(uint16_t(address));
|
||||
subtract_duration(2);
|
||||
}
|
||||
}
|
||||
} return;
|
||||
default: assert(false);
|
||||
}
|
||||
} break;
|
||||
|
||||
// Addressing modes with a memory access.
|
||||
|
||||
case AddressingMode::Absolute: address = next16(); break;
|
||||
case AddressingMode::AbsoluteX: address = next16() + x_; break;
|
||||
case AddressingMode::AbsoluteY: address = next16() + y_; break;
|
||||
case AddressingMode::ZeroPage: address = next8(); break;
|
||||
case AddressingMode::ZeroPageX: address = (next8() + x_) & 0xff; break;
|
||||
case AddressingMode::ZeroPageY: address = (next8() + y_) & 0xff; break;
|
||||
|
||||
case AddressingMode::ZeroPageIndirect:
|
||||
address = next8();
|
||||
address = unsigned(memory_[address] | (memory_[(address + 1) & 0xff] << 8));
|
||||
break;
|
||||
|
||||
case AddressingMode::XIndirect:
|
||||
address = (next8() + x_) & 0xff;
|
||||
address = unsigned(memory_[address] | (memory_[(address + 1)&0xff] << 8));
|
||||
break;
|
||||
|
||||
case AddressingMode::IndirectY:
|
||||
address = unsigned((memory_[next8()] | (memory_[(next8()+1)&0xff] << 8)) + y_);
|
||||
break;
|
||||
|
||||
case AddressingMode::AbsoluteIndirect:
|
||||
address = next16();
|
||||
address = unsigned(memory_[address & 0x1fff] | (memory_[(address + 1) & 0x1fff] << 8));
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
|
||||
#undef next16
|
||||
#undef next8
|
||||
program_counter_ += 1 + size(addressing_mode);
|
||||
|
||||
// Check for a branch; those don't go through the memory accesses below.
|
||||
switch(operation) {
|
||||
case Operation::BRA: case Operation::JMP:
|
||||
set_program_counter(uint16_t(address));
|
||||
return;
|
||||
|
||||
case Operation::JSR: {
|
||||
// Push one less than the actual return address.
|
||||
const auto return_address = program_counter_ - 1;
|
||||
push(uint8_t(return_address >> 8));
|
||||
push(uint8_t(return_address & 0xff));
|
||||
set_program_counter(uint16_t(address));
|
||||
} return;
|
||||
|
||||
#define Bcc(c) if(c) { set_program_counter(uint16_t(address)); subtract_duration(2); } return
|
||||
case Operation::BPL: Bcc(!(negative_result_&0x80));
|
||||
case Operation::BMI: Bcc(negative_result_&0x80);
|
||||
case Operation::BEQ: Bcc(!zero_result_);
|
||||
case Operation::BNE: Bcc(zero_result_);
|
||||
case Operation::BCS: Bcc(carry_flag_);
|
||||
case Operation::BCC: Bcc(!carry_flag_);
|
||||
case Operation::BVS: Bcc(overflow_result_ & 0x80);
|
||||
case Operation::BVC: Bcc(!(overflow_result_ & 0x80));
|
||||
#undef Bcc
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
assert(access_type(operation) != AccessType::None);
|
||||
|
||||
if constexpr(access_type(operation) == AccessType::Read) {
|
||||
uint8_t source = read(uint16_t(address));
|
||||
perform<operation>(&source);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t value;
|
||||
if constexpr(access_type(operation) == AccessType::ReadModifyWrite) {
|
||||
value = read(uint16_t(address));
|
||||
} else {
|
||||
value = 0xff;
|
||||
}
|
||||
perform<operation>(&value);
|
||||
write(uint16_t(address), value);
|
||||
}
|
||||
|
||||
template <Operation operation> void Executor::perform(uint8_t *operand [[maybe_unused]]) {
|
||||
|
||||
#define set_nz(a) negative_result_ = zero_result_ = (a)
|
||||
switch(operation) {
|
||||
case Operation::LDA:
|
||||
if(index_mode_) {
|
||||
write(x_, *operand);
|
||||
set_nz(*operand);
|
||||
} else {
|
||||
set_nz(a_ = *operand);
|
||||
}
|
||||
break;
|
||||
case Operation::LDX: set_nz(x_ = *operand); break;
|
||||
case Operation::LDY: set_nz(y_ = *operand); break;
|
||||
|
||||
case Operation::STA: *operand = a_; break;
|
||||
case Operation::STX: *operand = x_; break;
|
||||
case Operation::STY: *operand = y_; break;
|
||||
|
||||
case Operation::TXA: set_nz(a_ = x_); break;
|
||||
case Operation::TYA: set_nz(a_ = y_); break;
|
||||
case Operation::TXS: s_ = x_; break;
|
||||
case Operation::TAX: set_nz(x_ = a_); break;
|
||||
case Operation::TAY: set_nz(y_ = a_); break;
|
||||
case Operation::TSX: set_nz(x_ = s_); break;
|
||||
|
||||
case Operation::SEB0: case Operation::SEB1: case Operation::SEB2: case Operation::SEB3:
|
||||
case Operation::SEB4: case Operation::SEB5: case Operation::SEB6: case Operation::SEB7:
|
||||
if constexpr(operation >= Operation::SEB0 && operation <= Operation::SEB7) {
|
||||
*operand |= 1 << (int(operation) - int(Operation::SEB0));
|
||||
}
|
||||
break;
|
||||
case Operation::CLB0: case Operation::CLB1: case Operation::CLB2: case Operation::CLB3:
|
||||
case Operation::CLB4: case Operation::CLB5: case Operation::CLB6: case Operation::CLB7:
|
||||
if constexpr(operation >= Operation::CLB0 && operation <= Operation::CLB7) {
|
||||
*operand &= ~(1 << (int(operation) - int(Operation::CLB0)));
|
||||
}
|
||||
break;
|
||||
|
||||
case Operation::CLI: interrupt_disable_ = 0x00; break;
|
||||
case Operation::SEI: interrupt_disable_ = 0x04; break;
|
||||
case Operation::CLT: index_mode_ = false; break;
|
||||
case Operation::SET: index_mode_ = true; break;
|
||||
case Operation::CLD: decimal_mode_ = false; break;
|
||||
case Operation::SED: decimal_mode_ = true; break;
|
||||
case Operation::CLC: carry_flag_ = 0; break;
|
||||
case Operation::SEC: carry_flag_ = 1; break;
|
||||
case Operation::CLV: overflow_result_ = 0; break;
|
||||
|
||||
case Operation::DEX: --x_; set_nz(x_); break;
|
||||
case Operation::INX: ++x_; set_nz(x_); break;
|
||||
case Operation::DEY: --y_; set_nz(y_); break;
|
||||
case Operation::INY: ++y_; set_nz(y_); break;
|
||||
case Operation::DEC: --*operand; set_nz(*operand); break;
|
||||
case Operation::INC: ++*operand; set_nz(*operand); break;
|
||||
|
||||
case Operation::RTS: {
|
||||
uint16_t target = pull();
|
||||
target |= pull() << 8;
|
||||
set_program_counter(target+1);
|
||||
--program_counter_; // To undo the unavoidable increment
|
||||
// after exiting from here.
|
||||
} break;
|
||||
|
||||
case Operation::RTI: {
|
||||
set_flags(pull());
|
||||
uint16_t target = pull();
|
||||
target |= pull() << 8;
|
||||
set_program_counter(target);
|
||||
--program_counter_; // To undo the unavoidable increment
|
||||
// after exiting from here.
|
||||
} break;
|
||||
|
||||
case Operation::BRK:
|
||||
perform_interrupt<true>(0x1ff4);
|
||||
--program_counter_; // To undo the unavoidable increment
|
||||
// after exiting from here.
|
||||
break;
|
||||
|
||||
case Operation::STP: set_is_stopped(true); break;
|
||||
|
||||
case Operation::COM: set_nz(*operand ^= 0xff); break;
|
||||
|
||||
case Operation::FST: case Operation::SLW: case Operation::NOP:
|
||||
// TODO: communicate FST and SLW onwards, I imagine. Find out what they interface with.
|
||||
break;
|
||||
|
||||
case Operation::PHA: push(a_); break;
|
||||
case Operation::PHP: push(flags()); break;
|
||||
case Operation::PLA: set_nz(a_ = pull()); break;
|
||||
case Operation::PLP: set_flags(pull()); break;
|
||||
|
||||
case Operation::ASL:
|
||||
carry_flag_ = *operand >> 7;
|
||||
*operand <<= 1;
|
||||
set_nz(*operand);
|
||||
break;
|
||||
|
||||
case Operation::LSR:
|
||||
carry_flag_ = *operand & 1;
|
||||
*operand >>= 1;
|
||||
set_nz(*operand);
|
||||
break;
|
||||
|
||||
case Operation::ROL: {
|
||||
const uint8_t temp8 = uint8_t((*operand << 1) | carry_flag_);
|
||||
carry_flag_ = *operand >> 7;
|
||||
set_nz(*operand = temp8);
|
||||
} break;
|
||||
|
||||
case Operation::ROR: {
|
||||
const uint8_t temp8 = uint8_t((*operand >> 1) | (carry_flag_ << 7));
|
||||
carry_flag_ = *operand & 1;
|
||||
set_nz(*operand = temp8);
|
||||
} break;
|
||||
|
||||
case Operation::RRF:
|
||||
*operand = uint8_t((*operand >> 4) | (*operand << 4));
|
||||
break;
|
||||
|
||||
case Operation::BIT:
|
||||
zero_result_ = *operand & a_;
|
||||
negative_result_ = *operand;
|
||||
overflow_result_ = uint8_t(*operand << 1);
|
||||
break;
|
||||
|
||||
case Operation::TST:
|
||||
set_nz(*operand);
|
||||
break;
|
||||
|
||||
/*
|
||||
Operations affected by the index mode flag: ADC, AND, CMP, EOR, LDA, ORA, and SBC.
|
||||
*/
|
||||
|
||||
#define index(op) \
|
||||
if(index_mode_) { \
|
||||
uint8_t t = read(x_); \
|
||||
op(t); \
|
||||
write(x_, t); \
|
||||
} else { \
|
||||
op(a_); \
|
||||
}
|
||||
|
||||
#define op_ora(x) set_nz(x |= *operand)
|
||||
#define op_and(x) set_nz(x &= *operand)
|
||||
#define op_eor(x) set_nz(x ^= *operand)
|
||||
case Operation::ORA: index(op_ora); break;
|
||||
case Operation::AND: index(op_and); break;
|
||||
case Operation::EOR: index(op_eor); break;
|
||||
#undef op_eor
|
||||
#undef op_and
|
||||
#undef op_ora
|
||||
#undef index
|
||||
|
||||
#define op_cmp(x) { \
|
||||
const uint16_t temp16 = x - *operand; \
|
||||
set_nz(uint8_t(temp16)); \
|
||||
carry_flag_ = (~temp16 >> 8)&1; \
|
||||
}
|
||||
case Operation::CMP:
|
||||
if(index_mode_) {
|
||||
op_cmp(read(x_));
|
||||
} else {
|
||||
op_cmp(a_);
|
||||
}
|
||||
break;
|
||||
case Operation::CPX: op_cmp(x_); break;
|
||||
case Operation::CPY: op_cmp(y_); break;
|
||||
#undef op_cmp
|
||||
|
||||
case Operation::SBC:
|
||||
case Operation::ADC: {
|
||||
const uint8_t a = index_mode_ ? read(x_) : a_;
|
||||
|
||||
if(decimal_mode_) {
|
||||
if(operation == Operation::ADC) {
|
||||
uint16_t partials = 0;
|
||||
int result = carry_flag_;
|
||||
|
||||
#define nibble(mask, limit, adjustment, carry) \
|
||||
result += (a & mask) + (*operand & mask); \
|
||||
partials += result & mask; \
|
||||
if(result >= limit) result = ((result + (adjustment)) & (carry - 1)) + carry;
|
||||
|
||||
nibble(0x000f, 0x000a, 0x0006, 0x00010);
|
||||
nibble(0x00f0, 0x00a0, 0x0060, 0x00100);
|
||||
|
||||
#undef nibble
|
||||
|
||||
overflow_result_ = uint8_t((partials ^ a) & (partials ^ *operand));
|
||||
set_nz(uint8_t(result));
|
||||
carry_flag_ = (result >> 8) & 1;
|
||||
} else {
|
||||
unsigned int result = 0;
|
||||
unsigned int borrow = carry_flag_ ^ 1;
|
||||
const uint16_t decimal_result = uint16_t(a - *operand - borrow);
|
||||
|
||||
#define nibble(mask, adjustment, carry) \
|
||||
result += (a & mask) - (*operand & mask) - borrow; \
|
||||
if(result > mask) result -= adjustment; \
|
||||
borrow = (result > mask) ? carry : 0; \
|
||||
result &= (carry - 1);
|
||||
|
||||
nibble(0x000f, 0x0006, 0x00010);
|
||||
nibble(0x00f0, 0x0060, 0x00100);
|
||||
|
||||
#undef nibble
|
||||
|
||||
overflow_result_ = uint8_t((decimal_result ^ a) & (~decimal_result ^ *operand));
|
||||
set_nz(uint8_t(result));
|
||||
carry_flag_ = ((borrow >> 8)&1)^1;
|
||||
}
|
||||
} else {
|
||||
int result;
|
||||
if(operation == Operation::ADC) {
|
||||
result = int(a + *operand + carry_flag_);
|
||||
overflow_result_ = uint8_t((result ^ a) & (result ^ *operand));
|
||||
} else {
|
||||
result = int(a + ~*operand + carry_flag_);
|
||||
overflow_result_ = uint8_t((result ^ a) & (result ^ ~*operand));
|
||||
}
|
||||
set_nz(uint8_t(result));
|
||||
carry_flag_ = (result >> 8) & 1;
|
||||
}
|
||||
|
||||
if(index_mode_) {
|
||||
write(x_, a);
|
||||
} else {
|
||||
a_ = a;
|
||||
}
|
||||
} break;
|
||||
|
||||
|
||||
/*
|
||||
Already removed from the instruction stream:
|
||||
|
||||
* all branches and jumps;
|
||||
* LDM.
|
||||
*/
|
||||
|
||||
default:
|
||||
LOG("Unimplemented operation: " << operation);
|
||||
assert(false);
|
||||
}
|
||||
#undef set_nz
|
||||
}
|
||||
|
||||
inline void Executor::subtract_duration(int duration) {
|
||||
// Pass along.
|
||||
CachingExecutor::subtract_duration(duration);
|
||||
|
||||
// Update count for potential port accesses.
|
||||
cycles_since_port_handler_ += Cycles(duration);
|
||||
|
||||
// Update timer 1 and 2 prescaler.
|
||||
constexpr int t12_divider = 4; // A divide by 4 has already been applied before counting instruction lengths; therefore
|
||||
// this additional divide by 4 produces the correct net divide by 16.
|
||||
|
||||
timer_divider_ += duration;
|
||||
const int t12_ticks = update_timer(prescalers_[0], timer_divider_ / t12_divider);
|
||||
timer_divider_ &= (t12_divider-1);
|
||||
|
||||
// Update timers 1 and 2. TODO: interrupts (elsewhere?).
|
||||
if(update_timer(timers_[0], t12_ticks)) interrupt_control_ |= 0x20;
|
||||
if(update_timer(timers_[1], t12_ticks)) interrupt_control_ |= 0x08;
|
||||
|
||||
// If timer X is disabled, stop.
|
||||
if(timer_control_&0x20) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update timer X prescaler.
|
||||
switch(timer_control_ & 0x0c) {
|
||||
default: {
|
||||
const int tx_ticks = update_timer(prescalers_[1], t12_ticks); // TODO: don't hard code this. And is this even right?
|
||||
if(update_timer(timers_[2], tx_ticks))
|
||||
timer_control_ |= 0x80; // TODO: interrupt result of this.
|
||||
} break;
|
||||
case 0x04:
|
||||
LOG("TODO: Timer X; Pulse output mode");
|
||||
break;
|
||||
case 0x08:
|
||||
LOG("TODO: Timer X; Event counter mode");
|
||||
break;
|
||||
case 0x0c:
|
||||
LOG("TODO: Timer X; Pulse width measurement mode");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
inline int Executor::update_timer(Timer &timer, int count) {
|
||||
const int next_value = timer.value - count;
|
||||
if(next_value < 0) {
|
||||
// Determine how many reloads were required to get above zero.
|
||||
const int reload_value = timer.reload_value ? timer.reload_value : 256;
|
||||
const int underflow_count = 1 - next_value / reload_value;
|
||||
timer.value = uint8_t((next_value % reload_value) + timer.reload_value);
|
||||
return underflow_count;
|
||||
}
|
||||
timer.value = uint8_t(next_value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t Executor::get_output_mask(int port) {
|
||||
return port_directions_[port];
|
||||
}
|
||||
180
InstructionSets/M50740/Executor.hpp
Normal file
180
InstructionSets/M50740/Executor.hpp
Normal file
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// Executor.h
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Executor_h
|
||||
#define Executor_h
|
||||
|
||||
#include "Instruction.hpp"
|
||||
#include "Parser.hpp"
|
||||
#include "../CachingExecutor.hpp"
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace InstructionSet {
|
||||
namespace M50740 {
|
||||
|
||||
class Executor;
|
||||
using CachingExecutor = CachingExecutor<Executor, 0x1fff, 255, Instruction, false>;
|
||||
|
||||
struct PortHandler {
|
||||
virtual void run_ports_for(Cycles) = 0;
|
||||
virtual void set_port_output(int port, uint8_t value) = 0;
|
||||
virtual uint8_t get_port_input(int port) = 0;
|
||||
};
|
||||
|
||||
/*!
|
||||
Executes M50740 code subject to heavy limitations:
|
||||
|
||||
* the instruction stream cannot run across any of the specialised IO addresses; and
|
||||
* timing is correct to whole-opcode boundaries only.
|
||||
*/
|
||||
class Executor: public CachingExecutor {
|
||||
public:
|
||||
Executor(PortHandler &);
|
||||
void set_rom(const std::vector<uint8_t> &rom);
|
||||
|
||||
void reset();
|
||||
void set_interrupt_line(bool);
|
||||
|
||||
uint8_t get_output_mask(int port);
|
||||
|
||||
/*!
|
||||
Runs, in discrete steps, the minimum number of instructions as it takes to complete at least @c cycles.
|
||||
*/
|
||||
void run_for(Cycles cycles);
|
||||
|
||||
private:
|
||||
// MARK: - CachingExecutor-facing interface.
|
||||
|
||||
friend CachingExecutor;
|
||||
|
||||
/*!
|
||||
Maps instructions to performers; called by the CachingExecutor and for this instruction set, extremely trivial.
|
||||
*/
|
||||
inline PerformerIndex action_for(Instruction instruction) {
|
||||
// This is a super-simple processor, so the opcode can be used directly to index the performers.
|
||||
return instruction.opcode;
|
||||
}
|
||||
|
||||
/*!
|
||||
Parses from @c start and no later than @c max_address, using the CachingExecutor as a target.
|
||||
*/
|
||||
inline void parse(uint16_t start, uint16_t closing_bound) {
|
||||
Parser<Executor, false> parser;
|
||||
parser.parse(*this, &memory_[0], start & 0x1fff, closing_bound);
|
||||
}
|
||||
|
||||
private:
|
||||
// MARK: - Internal framework for generator performers.
|
||||
|
||||
/*!
|
||||
Provides dynamic lookup of @c perform(Executor*).
|
||||
*/
|
||||
class PerformerLookup {
|
||||
public:
|
||||
PerformerLookup() {
|
||||
fill<int(MinOperation)>(performers_);
|
||||
}
|
||||
|
||||
Performer performer(Operation operation, AddressingMode addressing_mode) {
|
||||
const auto index =
|
||||
(int(operation) - MinOperation) * (1 + MaxAddressingMode - MinAddressingMode) +
|
||||
(int(addressing_mode) - MinAddressingMode);
|
||||
return performers_[index];
|
||||
}
|
||||
|
||||
private:
|
||||
Performer performers_[(1 + MaxAddressingMode - MinAddressingMode) * (1 + MaxOperation - MinOperation)];
|
||||
|
||||
template<int operation, int addressing_mode> void fill_operation(Performer *target) {
|
||||
*target = &Executor::perform<Operation(operation), AddressingMode(addressing_mode)>;
|
||||
|
||||
if constexpr (addressing_mode+1 <= MaxAddressingMode) {
|
||||
fill_operation<operation, addressing_mode+1>(target + 1);
|
||||
}
|
||||
}
|
||||
|
||||
template<int operation> void fill(Performer *target) {
|
||||
fill_operation<operation, int(MinAddressingMode)>(target);
|
||||
target += 1 + MaxAddressingMode - MinAddressingMode;
|
||||
|
||||
if constexpr (operation+1 <= MaxOperation) {
|
||||
fill<operation+1>(target);
|
||||
}
|
||||
}
|
||||
};
|
||||
inline static PerformerLookup performer_lookup_;
|
||||
|
||||
/*!
|
||||
Performs @c operation using @c operand as the value fetched from memory, if any.
|
||||
*/
|
||||
template <Operation operation> void perform(uint8_t *operand);
|
||||
|
||||
/*!
|
||||
Performs @c operation in @c addressing_mode.
|
||||
*/
|
||||
template <Operation operation, AddressingMode addressing_mode> void perform();
|
||||
|
||||
private:
|
||||
// MARK: - Instruction set state.
|
||||
|
||||
// Memory.
|
||||
std::array<uint8_t, 0x2000> memory_;
|
||||
|
||||
// Registers.
|
||||
uint8_t a_ = 0, x_ = 0, y_ = 0, s_ = 0;
|
||||
|
||||
uint8_t negative_result_ = 0;
|
||||
uint8_t zero_result_ = 0;
|
||||
uint8_t interrupt_disable_ = 0x04;
|
||||
uint8_t carry_flag_ = 0;
|
||||
uint8_t overflow_result_ = 0;
|
||||
bool index_mode_ = false;
|
||||
bool decimal_mode_ = false;
|
||||
|
||||
// IO ports.
|
||||
uint8_t port_directions_[4] = {0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t port_outputs_[4] = {0xff, 0xff, 0xff, 0xff};
|
||||
|
||||
// Timers.
|
||||
struct Timer {
|
||||
uint8_t value = 0xff, reload_value = 0xff;
|
||||
};
|
||||
int timer_divider_ = 0;
|
||||
Timer timers_[3], prescalers_[2];
|
||||
inline int update_timer(Timer &timer, int count);
|
||||
|
||||
// Interrupt and timer control.
|
||||
uint8_t interrupt_control_ = 0, timer_control_ = 0;
|
||||
bool interrupt_line_ = false;
|
||||
|
||||
// Access helpers.
|
||||
inline uint8_t read(uint16_t address);
|
||||
inline void write(uint16_t address, uint8_t value);
|
||||
inline void push(uint8_t value);
|
||||
inline uint8_t pull();
|
||||
inline void set_flags(uint8_t);
|
||||
inline uint8_t flags();
|
||||
template<bool is_brk> inline void perform_interrupt(uint16_t vector);
|
||||
inline void set_port_output(int port);
|
||||
|
||||
// MARK: - Execution time
|
||||
|
||||
Cycles cycles_;
|
||||
Cycles cycles_since_port_handler_;
|
||||
PortHandler &port_handler_;
|
||||
inline void subtract_duration(int duration);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Executor_h */
|
||||
242
InstructionSets/M50740/Instruction.hpp
Normal file
242
InstructionSets/M50740/Instruction.hpp
Normal file
@@ -0,0 +1,242 @@
|
||||
//
|
||||
// Instruction.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 15/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef InstructionSets_M50740_Instruction_h
|
||||
#define InstructionSets_M50740_Instruction_h
|
||||
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include "../AccessType.hpp"
|
||||
|
||||
namespace InstructionSet {
|
||||
namespace M50740 {
|
||||
|
||||
enum class AddressingMode {
|
||||
Implied, Accumulator, Immediate,
|
||||
Absolute, AbsoluteX, AbsoluteY,
|
||||
ZeroPage, ZeroPageX, ZeroPageY,
|
||||
XIndirect, IndirectY,
|
||||
Relative,
|
||||
AbsoluteIndirect, ZeroPageIndirect,
|
||||
SpecialPage,
|
||||
ImmediateZeroPage,
|
||||
AccumulatorRelative, ZeroPageRelative
|
||||
};
|
||||
|
||||
static constexpr auto MaxAddressingMode = int(AddressingMode::ZeroPageRelative);
|
||||
static constexpr auto MinAddressingMode = int(AddressingMode::Implied);
|
||||
|
||||
constexpr int size(AddressingMode mode) {
|
||||
// This is coupled to the AddressingMode list above; be careful!
|
||||
constexpr int sizes[] = {
|
||||
0, 0, 1,
|
||||
2, 2, 2,
|
||||
1, 1, 1,
|
||||
1, 1,
|
||||
1,
|
||||
2, 1,
|
||||
1,
|
||||
2,
|
||||
1, 2
|
||||
};
|
||||
static_assert(sizeof(sizes)/sizeof(*sizes) == int(MaxAddressingMode) + 1);
|
||||
return sizes[int(mode)];
|
||||
}
|
||||
|
||||
enum class Operation: uint8_t {
|
||||
Invalid,
|
||||
|
||||
// Operations that don't access memory.
|
||||
BBC0, BBC1, BBC2, BBC3, BBC4, BBC5, BBC6, BBC7,
|
||||
BBS0, BBS1, BBS2, BBS3, BBS4, BBS5, BBS6, BBS7,
|
||||
BCC, BCS,
|
||||
BEQ, BMI, BNE, BPL,
|
||||
BVC, BVS, BRA, BRK,
|
||||
JMP, JSR,
|
||||
RTI, RTS,
|
||||
CLC, CLD, CLI, CLT, CLV,
|
||||
SEC, SED, SEI, SET,
|
||||
INX, INY, DEX, DEY,
|
||||
FST, SLW,
|
||||
NOP,
|
||||
PHA, PHP, PLA, PLP,
|
||||
STP,
|
||||
TAX, TAY, TSX, TXA,
|
||||
TXS, TYA,
|
||||
|
||||
// Read operations.
|
||||
ADC, SBC,
|
||||
AND, ORA, EOR, BIT,
|
||||
CMP, CPX, CPY,
|
||||
LDA, LDX, LDY,
|
||||
TST,
|
||||
|
||||
// Read-modify-write operations.
|
||||
ASL, LSR,
|
||||
CLB0, CLB1, CLB2, CLB3, CLB4, CLB5, CLB6, CLB7,
|
||||
SEB0, SEB1, SEB2, SEB3, SEB4, SEB5, SEB6, SEB7,
|
||||
COM,
|
||||
DEC, INC,
|
||||
ROL, ROR, RRF,
|
||||
|
||||
// Write operations.
|
||||
LDM,
|
||||
STA, STX, STY,
|
||||
};
|
||||
|
||||
static constexpr auto MaxOperation = int(Operation::STY);
|
||||
static constexpr auto MinOperation = int(Operation::BBC0);
|
||||
|
||||
constexpr AccessType access_type(Operation operation) {
|
||||
if(operation < Operation::ADC) return AccessType::None;
|
||||
if(operation < Operation::ASL) return AccessType::Read;
|
||||
if(operation < Operation::LDM) return AccessType::ReadModifyWrite;
|
||||
return AccessType::Write;
|
||||
}
|
||||
|
||||
constexpr bool uses_index_mode(Operation operation) {
|
||||
return
|
||||
operation == Operation::ADC || operation == Operation::AND ||
|
||||
operation == Operation::CMP || operation == Operation::EOR ||
|
||||
operation == Operation::LDA || operation == Operation::ORA ||
|
||||
operation == Operation::SBC;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The name of @c operation.
|
||||
*/
|
||||
inline constexpr const char *operation_name(Operation operation) {
|
||||
#define MAP(x) case Operation::x: return #x;
|
||||
switch(operation) {
|
||||
default: break;
|
||||
MAP(BBC0); MAP(BBC1); MAP(BBC2); MAP(BBC3); MAP(BBC4); MAP(BBC5); MAP(BBC6); MAP(BBC7);
|
||||
MAP(BBS0); MAP(BBS1); MAP(BBS2); MAP(BBS3); MAP(BBS4); MAP(BBS5); MAP(BBS6); MAP(BBS7);
|
||||
MAP(BCC); MAP(BCS); MAP(BEQ); MAP(BMI); MAP(BNE); MAP(BPL); MAP(BVC); MAP(BVS);
|
||||
MAP(BRA); MAP(BRK); MAP(JMP); MAP(JSR); MAP(RTI); MAP(RTS); MAP(CLC); MAP(CLD);
|
||||
MAP(CLI); MAP(CLT); MAP(CLV); MAP(SEC); MAP(SED); MAP(SEI); MAP(SET); MAP(INX);
|
||||
MAP(INY); MAP(DEX); MAP(DEY); MAP(FST); MAP(SLW); MAP(NOP); MAP(PHA); MAP(PHP);
|
||||
MAP(PLA); MAP(PLP); MAP(STP); MAP(TAX); MAP(TAY); MAP(TSX); MAP(TXA); MAP(TXS);
|
||||
MAP(TYA); MAP(ADC); MAP(SBC); MAP(AND); MAP(ORA); MAP(EOR); MAP(BIT); MAP(CMP);
|
||||
MAP(CPX); MAP(CPY); MAP(LDA); MAP(LDX); MAP(LDY); MAP(TST); MAP(ASL); MAP(LSR);
|
||||
MAP(CLB0); MAP(CLB1); MAP(CLB2); MAP(CLB3); MAP(CLB4); MAP(CLB5); MAP(CLB6); MAP(CLB7);
|
||||
MAP(SEB0); MAP(SEB1); MAP(SEB2); MAP(SEB3); MAP(SEB4); MAP(SEB5); MAP(SEB6); MAP(SEB7);
|
||||
MAP(COM); MAP(DEC); MAP(INC); MAP(ROL); MAP(ROR); MAP(RRF); MAP(LDM); MAP(STA);
|
||||
MAP(STX); MAP(STY);
|
||||
}
|
||||
#undef MAP
|
||||
|
||||
return "???";
|
||||
}
|
||||
|
||||
inline std::ostream &operator <<(std::ostream &stream, Operation operation) {
|
||||
stream << operation_name(operation);
|
||||
return stream;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The name of @c addressing_mode.
|
||||
*/
|
||||
inline constexpr const char *addressing_mode_name(AddressingMode addressing_mode) {
|
||||
switch(addressing_mode) {
|
||||
default: break;
|
||||
case AddressingMode::Implied: return "";
|
||||
case AddressingMode::Accumulator: return "A";
|
||||
case AddressingMode::Immediate: return "#";
|
||||
case AddressingMode::Absolute: return "abs";
|
||||
case AddressingMode::AbsoluteX: return "abs, x";
|
||||
case AddressingMode::AbsoluteY: return "abs, y";
|
||||
case AddressingMode::ZeroPage: return "zp";
|
||||
case AddressingMode::ZeroPageX: return "zp, x";
|
||||
case AddressingMode::ZeroPageY: return "zp, y";
|
||||
case AddressingMode::XIndirect: return "((zp, x))";
|
||||
case AddressingMode::IndirectY: return "((zp), y)";
|
||||
case AddressingMode::Relative: return "rel";
|
||||
case AddressingMode::AbsoluteIndirect: return "(abs)";
|
||||
case AddressingMode::ZeroPageIndirect: return "(zp)";
|
||||
case AddressingMode::SpecialPage: return "\\sp";
|
||||
case AddressingMode::ImmediateZeroPage: return "#, zp";
|
||||
case AddressingMode::AccumulatorRelative: return "A, rel";
|
||||
case AddressingMode::ZeroPageRelative: return "zp, rel";
|
||||
}
|
||||
|
||||
return "???";
|
||||
}
|
||||
|
||||
inline std::ostream &operator <<(std::ostream &stream, AddressingMode mode) {
|
||||
stream << addressing_mode_name(mode);
|
||||
return stream;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The way that the address for an operation with @c addressing_mode and encoded starting from @c operation
|
||||
would appear in an assembler. E.g. '$5a' for that zero page address, or '$5a, x' for zero-page indexed from $5a. This function
|
||||
may access up to three bytes from @c operation onwards.
|
||||
*/
|
||||
inline std::string address(AddressingMode addressing_mode, const uint8_t *operation, uint16_t program_counter) {
|
||||
std::stringstream output;
|
||||
output << std::hex;
|
||||
|
||||
#define NUM(x) std::setfill('0') << std::setw(2) << int(x)
|
||||
#define NUM4(x) std::setfill('0') << std::setw(4) << int(x)
|
||||
switch(addressing_mode) {
|
||||
default: return "???";
|
||||
case AddressingMode::Implied: return "";
|
||||
case AddressingMode::Accumulator: return "A ";
|
||||
case AddressingMode::Immediate: output << "#$" << NUM(operation[1]); break;
|
||||
case AddressingMode::Absolute: output << "$" << NUM(operation[2]) << NUM(operation[1]); break;
|
||||
case AddressingMode::AbsoluteX: output << "$" << NUM(operation[2]) << NUM(operation[1]) << ", x"; break;
|
||||
case AddressingMode::AbsoluteY: output << "$" << NUM(operation[2]) << NUM(operation[1]) << ", y"; break;
|
||||
case AddressingMode::ZeroPage: output << "$" << NUM(operation[1]); break;
|
||||
case AddressingMode::ZeroPageX: output << "$" << NUM(operation[1]) << ", x"; break;
|
||||
case AddressingMode::ZeroPageY: output << "$" << NUM(operation[1]) << ", y"; break;
|
||||
case AddressingMode::XIndirect: output << "(($" << NUM(operation[1]) << ", x))"; break;
|
||||
case AddressingMode::IndirectY: output << "(($" << NUM(operation[1]) << "), y)"; break;
|
||||
case AddressingMode::Relative: output << "#$" << NUM4(2 + program_counter + int8_t(operation[1])); break;
|
||||
case AddressingMode::AbsoluteIndirect: output << "($" << NUM(operation[2]) << NUM(operation[1]) << ") "; break;
|
||||
case AddressingMode::ZeroPageIndirect: output << "($" << NUM(operation[1]) << ")"; break;
|
||||
case AddressingMode::SpecialPage: output << "$1f" << NUM(operation[1]); break;
|
||||
case AddressingMode::ImmediateZeroPage: output << "#$" << NUM(operation[1]) << ", $" << NUM(operation[2]); break;
|
||||
case AddressingMode::AccumulatorRelative: output << "A, $" << NUM4(2 + program_counter + int8_t(operation[1])); break;
|
||||
case AddressingMode::ZeroPageRelative:
|
||||
output << "$" << NUM(operation[1]) << ", $" << NUM4(3 + program_counter + int8_t(operation[2]));
|
||||
break;
|
||||
}
|
||||
#undef NUM4
|
||||
#undef NUM
|
||||
|
||||
return output.str();
|
||||
}
|
||||
|
||||
/*!
|
||||
Models a complete M50740-style instruction, including its operation, addressing mode and opcode.
|
||||
*/
|
||||
struct Instruction {
|
||||
Operation operation = Operation::Invalid;
|
||||
AddressingMode addressing_mode = AddressingMode::Implied;
|
||||
uint8_t opcode = 0;
|
||||
|
||||
Instruction(Operation operation, AddressingMode addressing_mode, uint8_t opcode) : operation(operation), addressing_mode(addressing_mode), opcode(opcode) {}
|
||||
Instruction(uint8_t opcode) : opcode(opcode) {}
|
||||
Instruction() {}
|
||||
};
|
||||
|
||||
/*!
|
||||
Outputs a description of @c instruction to @c stream.
|
||||
*/
|
||||
inline std::ostream &operator <<(std::ostream &stream, const Instruction &instruction) {
|
||||
stream << operation_name(instruction.operation) << " " << addressing_mode_name(instruction.addressing_mode);
|
||||
return stream;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif /* InstructionSets_M50740_Instruction_h */
|
||||
125
InstructionSets/M50740/Parser.hpp
Normal file
125
InstructionSets/M50740/Parser.hpp
Normal file
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// Parser.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef InstructionSets_M50740_Parser_hpp
|
||||
#define InstructionSets_M50740_Parser_hpp
|
||||
|
||||
#include <cstdint>
|
||||
#include "Decoder.hpp"
|
||||
#include "../AccessType.hpp"
|
||||
|
||||
namespace InstructionSet {
|
||||
namespace M50740 {
|
||||
|
||||
template<typename Target, bool include_entries_and_accesses> struct Parser {
|
||||
void parse(Target &target, const uint8_t *storage, uint16_t start, uint16_t closing_bound) {
|
||||
Decoder decoder;
|
||||
|
||||
while(start <= closing_bound) {
|
||||
const auto next = decoder.decode(&storage[start], 1 + closing_bound - start);
|
||||
if(next.first <= 0) {
|
||||
// If there weren't enough bytes left before the closing bound to complete
|
||||
// an instruction, but implicitly there were some bytes left, announce overflow
|
||||
// and terminate.
|
||||
target.announce_overflow(start);
|
||||
return;
|
||||
} else {
|
||||
// Pass on the instruction.
|
||||
target.announce_instruction(start, next.second);
|
||||
|
||||
if constexpr(!include_entries_and_accesses) {
|
||||
// Do a simplified test: is this a terminating operation?
|
||||
switch(next.second.operation) {
|
||||
case Operation::RTS: case Operation::RTI: case Operation::BRK:
|
||||
case Operation::JMP: case Operation::BRA:
|
||||
return;
|
||||
|
||||
default: break;
|
||||
}
|
||||
} else {
|
||||
// Check for end of stream and potential new entry points.
|
||||
switch(next.second.operation) {
|
||||
// Terminating instructions.
|
||||
case Operation::RTS: case Operation::RTI: case Operation::BRK:
|
||||
return;
|
||||
|
||||
// Terminating operations, possibly with implied additional entry point.
|
||||
case Operation::JMP:
|
||||
if(next.second.addressing_mode == AddressingMode::Absolute) {
|
||||
target.add_entry(uint16_t(storage[start + 1] | (storage[start + 2] << 8)));
|
||||
}
|
||||
return;
|
||||
case Operation::BRA:
|
||||
target.add_entry(uint16_t(start + 2 + int8_t(storage[start + 1])));
|
||||
return;
|
||||
|
||||
// Instructions that suggest another entry point but don't terminate parsing.
|
||||
case Operation::BCC: case Operation::BCS:
|
||||
case Operation::BVC: case Operation::BVS:
|
||||
case Operation::BMI: case Operation::BPL:
|
||||
case Operation::BNE: case Operation::BEQ:
|
||||
target.add_entry(uint16_t(start + 2 + int8_t(storage[start + 1])));
|
||||
break;
|
||||
case Operation::JSR:
|
||||
switch(next.second.addressing_mode) {
|
||||
default: break;
|
||||
case AddressingMode::Absolute:
|
||||
target.add_entry(uint16_t(storage[start + 1] | (storage[start + 2] << 8)));
|
||||
break;
|
||||
case AddressingMode::SpecialPage:
|
||||
target.add_entry(uint16_t(storage[start + 1] | 0x1f00));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case Operation::BBS0: case Operation::BBS1: case Operation::BBS2: case Operation::BBS3:
|
||||
case Operation::BBS4: case Operation::BBS5: case Operation::BBS6: case Operation::BBS7:
|
||||
case Operation::BBC0: case Operation::BBC1: case Operation::BBC2: case Operation::BBC3:
|
||||
case Operation::BBC4: case Operation::BBC5: case Operation::BBC6: case Operation::BBC7:
|
||||
switch(next.second.addressing_mode) {
|
||||
default: break;
|
||||
case AddressingMode::AccumulatorRelative:
|
||||
target.add_entry(uint16_t(start + 2 + int8_t(storage[start + 1])));
|
||||
break;
|
||||
case AddressingMode::ZeroPageRelative:
|
||||
target.add_entry(uint16_t(start + 3 + int8_t(storage[start + 2])));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Provide any fixed address accesses.
|
||||
switch(next.second.addressing_mode) {
|
||||
case AddressingMode::Absolute:
|
||||
target.add_access(uint16_t(storage[start + 1] | (storage[start + 2] << 8)), access_type(next.second.operation));
|
||||
break;
|
||||
case AddressingMode::ZeroPage: case AddressingMode::ZeroPageRelative:
|
||||
target.add_access(storage[start + 1], access_type(next.second.operation));
|
||||
break;
|
||||
case AddressingMode::ImmediateZeroPage:
|
||||
target.add_access(storage[start + 2], access_type(next.second.operation));
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// Advance.
|
||||
start += next.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* InstructionSets_M50740_Parser_hpp */
|
||||
347
InstructionSets/PowerPC/Decoder.cpp
Normal file
347
InstructionSets/PowerPC/Decoder.cpp
Normal file
@@ -0,0 +1,347 @@
|
||||
//
|
||||
// Decoder.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 30/12/20.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Decoder.hpp"
|
||||
|
||||
using namespace InstructionSet::PowerPC;
|
||||
|
||||
Decoder::Decoder(Model model) : model_(model) {}
|
||||
|
||||
Instruction Decoder::decode(uint32_t opcode) {
|
||||
// Quick bluffer's guide to PowerPC instruction encoding:
|
||||
//
|
||||
// There is a six-bit field at the very top of the instruction.
|
||||
// Sometimes that fully identifies an instruction, but usually
|
||||
// it doesn't.
|
||||
//
|
||||
// There is an addition 9- or 10-bit field starting one bit above
|
||||
// least significant that disambiguates the rest. Strictly speaking
|
||||
// it's a 10-bit field, but the mnemonics for many instructions treat
|
||||
// it as a 9-bit field with a flag at the top.
|
||||
//
|
||||
// I've decided to hew directly to the mnemonics.
|
||||
//
|
||||
// Various opcodes in the 1995 documentation define reserved bits,
|
||||
// which are given the nominal value of 0. It does not give a formal
|
||||
// definition of a reserved bit. As a result this code does not
|
||||
// currently check the value of reserved bits. That may need to change
|
||||
// if/when I add support for extended instruction sets.
|
||||
|
||||
#define Bind(mask, operation) case mask: return Instruction(Operation::operation, opcode);
|
||||
#define BindSupervisor(mask, operation) case mask: return Instruction(Operation::operation, opcode, true);
|
||||
#define BindConditional(condition, mask, operation) \
|
||||
case mask: \
|
||||
if(condition()) return Instruction(Operation::operation, opcode); \
|
||||
return Instruction(opcode);
|
||||
#define BindSupervisorConditional(condition, mask, operation) \
|
||||
case mask: \
|
||||
if(condition()) return Instruction(Operation::operation, opcode, true); \
|
||||
return Instruction(opcode);
|
||||
|
||||
#define Six(x) (unsigned(x) << 26)
|
||||
#define SixTen(x, y) (Six(x) | ((y) << 1))
|
||||
|
||||
// First pass: weed out all those instructions identified entirely by the
|
||||
// top six bits.
|
||||
switch(opcode & Six(0b111111)) {
|
||||
default: break;
|
||||
|
||||
BindConditional(is64bit, Six(0b000010), tdi);
|
||||
|
||||
Bind(Six(0b000011), twi);
|
||||
Bind(Six(0b000111), mulli);
|
||||
Bind(Six(0b001000), subfic);
|
||||
Bind(Six(0b001100), addic); Bind(Six(0b001101), addic_);
|
||||
Bind(Six(0b001110), addi); Bind(Six(0b001111), addis);
|
||||
case Six(0b010000): {
|
||||
// This might be a bcx, but check for a valid bo field.
|
||||
switch((opcode >> 21) & 0x1f) {
|
||||
case 0: case 1: case 2: case 3: case 4: case 5:
|
||||
case 8: case 9: case 10: case 11: case 12: case 13:
|
||||
case 16: case 17: case 18: case 19: case 20:
|
||||
return Instruction(Operation::bcx, opcode);
|
||||
|
||||
default: return Instruction(opcode);
|
||||
}
|
||||
} break;
|
||||
Bind(Six(0b010010), bx);
|
||||
Bind(Six(0b010100), rlwimix);
|
||||
Bind(Six(0b010101), rlwinmx);
|
||||
Bind(Six(0b010111), rlwnmx);
|
||||
|
||||
Bind(Six(0b011000), ori); Bind(Six(0b011001), oris);
|
||||
Bind(Six(0b011010), xori); Bind(Six(0b011011), xoris);
|
||||
Bind(Six(0b011100), andi_); Bind(Six(0b011101), andis_);
|
||||
Bind(Six(0b100000), lwz); Bind(Six(0b100001), lwzu);
|
||||
Bind(Six(0b100010), lbz); Bind(Six(0b100011), lbzu);
|
||||
Bind(Six(0b100100), stw); Bind(Six(0b100101), stwu);
|
||||
Bind(Six(0b100110), stb); Bind(Six(0b100111), stbu);
|
||||
Bind(Six(0b101000), lhz); Bind(Six(0b101001), lhzu);
|
||||
Bind(Six(0b101010), lha); Bind(Six(0b101011), lhau);
|
||||
Bind(Six(0b101100), sth); Bind(Six(0b101101), sthu);
|
||||
Bind(Six(0b101110), lmw); Bind(Six(0b101111), stmw);
|
||||
Bind(Six(0b110000), lfs); Bind(Six(0b110001), lfsu);
|
||||
Bind(Six(0b110010), lfd); Bind(Six(0b110011), lfdu);
|
||||
Bind(Six(0b110100), stfs); Bind(Six(0b110101), stfsu);
|
||||
Bind(Six(0b110110), stfd); Bind(Six(0b110111), stfdu);
|
||||
|
||||
BindConditional(is601, Six(9), dozi);
|
||||
BindConditional(is601, Six(22), rlmix);
|
||||
|
||||
Bind(Six(0b001010), cmpli); Bind(Six(0b001011), cmpi);
|
||||
}
|
||||
|
||||
// Second pass: all those with a top six bits and a bottom nine or ten.
|
||||
switch(opcode & SixTen(0b111111, 0b1111111111)) {
|
||||
default: break;
|
||||
|
||||
// 64-bit instructions.
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0000001001), mulhdux); BindConditional(is64bit, SixTen(0b011111, 0b1000001001), mulhdux);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0000010101), ldx);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0000011011), sldx);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0000110101), ldux);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0000111010), cntlzdx);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0001000100), td);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0001001001), mulhdx); BindConditional(is64bit, SixTen(0b011111, 0b1001001001), mulhdx);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0001010100), ldarx);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0010010101), stdx);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0010110101), stdux);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0011101001), mulld); BindConditional(is64bit, SixTen(0b011111, 0b1011101001), mulld);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0101010101), lwax);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0101110101), lwaux);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b1100111011), sradix); BindConditional(is64bit, SixTen(0b011111, 0b1100111010), sradix);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0110110010), slbie);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0111001001), divdux); BindConditional(is64bit, SixTen(0b011111, 0b1111001001), divdux);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b0111101001), divdx); BindConditional(is64bit, SixTen(0b011111, 0b1111101001), divdx);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b1000011011), srdx);
|
||||
BindConditional(is64bit, SixTen(0b011111, 0b1100011010), sradx);
|
||||
BindConditional(is64bit, SixTen(0b111111, 0b1111011010), extsw);
|
||||
|
||||
// Power instructions; these are all taken from the MPC601 manual rather than
|
||||
// the PowerPC Programmer's Reference Guide, hence the decimal encoding of the
|
||||
// ten-bit field.
|
||||
BindConditional(is601, SixTen(0b011111, 360), absx); BindConditional(is601, SixTen(0b011111, 512 + 360), absx);
|
||||
BindConditional(is601, SixTen(0b011111, 531), clcs);
|
||||
BindConditional(is601, SixTen(0b011111, 331), divx); BindConditional(is601, SixTen(0b011111, 512 + 331), divx);
|
||||
BindConditional(is601, SixTen(0b011111, 363), divsx); BindConditional(is601, SixTen(0b011111, 512 + 363), divsx);
|
||||
BindConditional(is601, SixTen(0b011111, 264), dozx); BindConditional(is601, SixTen(0b011111, 512 + 264), dozx);
|
||||
BindConditional(is601, SixTen(0b011111, 277), lscbxx);
|
||||
BindConditional(is601, SixTen(0b011111, 29), maskgx);
|
||||
BindConditional(is601, SixTen(0b011111, 541), maskirx);
|
||||
BindConditional(is601, SixTen(0b011111, 107), mulx); BindConditional(is601, SixTen(0b011111, 512 + 107), mulx);
|
||||
BindConditional(is601, SixTen(0b011111, 488), nabsx); BindConditional(is601, SixTen(0b011111, 512 + 488), nabsx);
|
||||
BindConditional(is601, SixTen(0b011111, 537), rribx);
|
||||
BindConditional(is601, SixTen(0b011111, 153), slex);
|
||||
BindConditional(is601, SixTen(0b011111, 217), sleqx);
|
||||
BindConditional(is601, SixTen(0b011111, 184), sliqx);
|
||||
BindConditional(is601, SixTen(0b011111, 248), slliqx);
|
||||
BindConditional(is601, SixTen(0b011111, 216), sllqx);
|
||||
BindConditional(is601, SixTen(0b011111, 152), slqx);
|
||||
BindConditional(is601, SixTen(0b011111, 952), sraiqx);
|
||||
BindConditional(is601, SixTen(0b011111, 920), sraqx);
|
||||
BindConditional(is601, SixTen(0b011111, 665), srex);
|
||||
BindConditional(is601, SixTen(0b011111, 921), sreax);
|
||||
BindConditional(is601, SixTen(0b011111, 729), sreqx);
|
||||
BindConditional(is601, SixTen(0b011111, 696), sriqx);
|
||||
BindConditional(is601, SixTen(0b011111, 760), srliqx);
|
||||
BindConditional(is601, SixTen(0b011111, 728), srlqx);
|
||||
BindConditional(is601, SixTen(0b011111, 664), srqx);
|
||||
|
||||
// 32-bit instructions.
|
||||
Bind(SixTen(0b010011, 0b0000000000), mcrf);
|
||||
Bind(SixTen(0b010011, 0b0000010000), bclrx);
|
||||
Bind(SixTen(0b010011, 0b0000100001), crnor);
|
||||
Bind(SixTen(0b010011, 0b0000110010), rfi);
|
||||
Bind(SixTen(0b010011, 0b0010000001), crandc);
|
||||
Bind(SixTen(0b010011, 0b0010010110), isync);
|
||||
Bind(SixTen(0b010011, 0b0011000001), crxor);
|
||||
Bind(SixTen(0b010011, 0b0011100001), crnand);
|
||||
Bind(SixTen(0b010011, 0b0100000001), crand);
|
||||
Bind(SixTen(0b010011, 0b0100100001), creqv);
|
||||
Bind(SixTen(0b010011, 0b0110100001), crorc);
|
||||
Bind(SixTen(0b010011, 0b0111000001), cror);
|
||||
Bind(SixTen(0b010011, 0b1000010000), bcctrx);
|
||||
Bind(SixTen(0b011111, 0b0000000000), cmp);
|
||||
Bind(SixTen(0b011111, 0b0000000100), tw);
|
||||
Bind(SixTen(0b011111, 0b0000001000), subfcx); Bind(SixTen(0b011111, 0b1000001000), subfcx);
|
||||
Bind(SixTen(0b011111, 0b0000001010), addcx); Bind(SixTen(0b011111, 0b1000001010), addcx);
|
||||
Bind(SixTen(0b011111, 0b0000001011), mulhwux); Bind(SixTen(0b011111, 0b1000001011), mulhwux);
|
||||
Bind(SixTen(0b011111, 0b0000010011), mfcr);
|
||||
Bind(SixTen(0b011111, 0b0000010100), lwarx);
|
||||
Bind(SixTen(0b011111, 0b0000010111), lwzx);
|
||||
Bind(SixTen(0b011111, 0b0000011000), slwx);
|
||||
Bind(SixTen(0b011111, 0b0000011010), cntlzwx);
|
||||
Bind(SixTen(0b011111, 0b0000011100), andx);
|
||||
Bind(SixTen(0b011111, 0b0000100000), cmpl);
|
||||
Bind(SixTen(0b011111, 0b0000101000), subfx); Bind(SixTen(0b011111, 0b1000101000), subfx);
|
||||
Bind(SixTen(0b011111, 0b0000110110), dcbst);
|
||||
Bind(SixTen(0b011111, 0b0000110111), lwzux);
|
||||
Bind(SixTen(0b011111, 0b0000111100), andcx);
|
||||
Bind(SixTen(0b011111, 0b0001001011), mulhwx); Bind(SixTen(0b011111, 0b1001001011), mulhwx);
|
||||
Bind(SixTen(0b011111, 0b0001010011), mfmsr);
|
||||
Bind(SixTen(0b011111, 0b0001010110), dcbf);
|
||||
Bind(SixTen(0b011111, 0b0001010111), lbzx);
|
||||
Bind(SixTen(0b011111, 0b0001101000), negx); Bind(SixTen(0b011111, 0b1001101000), negx);
|
||||
Bind(SixTen(0b011111, 0b0001110111), lbzux);
|
||||
Bind(SixTen(0b011111, 0b0001111100), norx);
|
||||
Bind(SixTen(0b011111, 0b0010001000), subfex); Bind(SixTen(0b011111, 0b1010001000), subfex);
|
||||
Bind(SixTen(0b011111, 0b0010001010), addex); Bind(SixTen(0b011111, 0b1010001010), addex);
|
||||
Bind(SixTen(0b011111, 0b0010010000), mtcrf);
|
||||
Bind(SixTen(0b011111, 0b0010010010), mtmsr);
|
||||
Bind(SixTen(0b011111, 0b0010010111), stwx);
|
||||
Bind(SixTen(0b011111, 0b0010110111), stwux);
|
||||
Bind(SixTen(0b011111, 0b0011001000), subfzex); Bind(SixTen(0b011111, 0b1011001000), subfzex);
|
||||
Bind(SixTen(0b011111, 0b0011001010), addzex); Bind(SixTen(0b011111, 0b1011001010), addzex);
|
||||
Bind(SixTen(0b011111, 0b0011010111), stbx);
|
||||
Bind(SixTen(0b011111, 0b0011101000), subfmex); Bind(SixTen(0b011111, 0b1011101000), subfmex);
|
||||
Bind(SixTen(0b011111, 0b0011101010), addmex); Bind(SixTen(0b011111, 0b1011101010), addmex);
|
||||
Bind(SixTen(0b011111, 0b0011101011), mullwx); Bind(SixTen(0b011111, 0b1011101011), mullwx);
|
||||
Bind(SixTen(0b011111, 0b0011110110), dcbtst);
|
||||
Bind(SixTen(0b011111, 0b0011110111), stbux);
|
||||
Bind(SixTen(0b011111, 0b0100001010), addx); Bind(SixTen(0b011111, 0b1100001010), addx);
|
||||
Bind(SixTen(0b011111, 0b0100010110), dcbt);
|
||||
Bind(SixTen(0b011111, 0b0100010111), lhzx);
|
||||
Bind(SixTen(0b011111, 0b0100011100), eqvx);
|
||||
Bind(SixTen(0b011111, 0b0100110110), eciwx);
|
||||
Bind(SixTen(0b011111, 0b0100110111), lhzux);
|
||||
Bind(SixTen(0b011111, 0b0100111100), xorx);
|
||||
Bind(SixTen(0b011111, 0b0101010111), lhax);
|
||||
Bind(SixTen(0b011111, 0b0101110011), mftb);
|
||||
Bind(SixTen(0b011111, 0b0101110111), lhaux);
|
||||
Bind(SixTen(0b011111, 0b0110010111), sthx);
|
||||
Bind(SixTen(0b011111, 0b0110011100), orcx);
|
||||
Bind(SixTen(0b011111, 0b0110110110), ecowx);
|
||||
Bind(SixTen(0b011111, 0b0110110111), sthux);
|
||||
Bind(SixTen(0b011111, 0b0110111100), orx);
|
||||
Bind(SixTen(0b011111, 0b0111001011), divwux); Bind(SixTen(0b011111, 0b1111001011), divwux);
|
||||
Bind(SixTen(0b011111, 0b0111010110), dcbi);
|
||||
Bind(SixTen(0b011111, 0b0111011100), nandx);
|
||||
Bind(SixTen(0b011111, 0b0111101011), divwx); Bind(SixTen(0b011111, 0b1111101011), divwx);
|
||||
Bind(SixTen(0b011111, 0b1000000000), mcrxr);
|
||||
Bind(SixTen(0b011111, 0b1000010101), lswx);
|
||||
Bind(SixTen(0b011111, 0b1000010110), lwbrx);
|
||||
Bind(SixTen(0b011111, 0b1000010111), lfsx);
|
||||
Bind(SixTen(0b011111, 0b1000011000), srwx);
|
||||
Bind(SixTen(0b011111, 0b1000110111), lfsux);
|
||||
Bind(SixTen(0b011111, 0b1001010101), lswi);
|
||||
Bind(SixTen(0b011111, 0b1001010110), sync);
|
||||
Bind(SixTen(0b011111, 0b1001010111), lfdx);
|
||||
Bind(SixTen(0b011111, 0b1001110111), lfdux);
|
||||
Bind(SixTen(0b011111, 0b1010010101), stswx);
|
||||
Bind(SixTen(0b011111, 0b1010010110), stwbrx);
|
||||
Bind(SixTen(0b011111, 0b1010010111), stfsx);
|
||||
Bind(SixTen(0b011111, 0b1010110111), stfsux);
|
||||
Bind(SixTen(0b011111, 0b1011010101), stswi);
|
||||
Bind(SixTen(0b011111, 0b1011010111), stfdx);
|
||||
Bind(SixTen(0b011111, 0b1011110111), stfdux);
|
||||
Bind(SixTen(0b011111, 0b1100010110), lhbrx);
|
||||
Bind(SixTen(0b011111, 0b1100011000), srawx);
|
||||
Bind(SixTen(0b011111, 0b1100111000), srawix);
|
||||
Bind(SixTen(0b011111, 0b1101010110), eieio);
|
||||
Bind(SixTen(0b011111, 0b1110010110), sthbrx);
|
||||
Bind(SixTen(0b011111, 0b1110011010), extshx);
|
||||
Bind(SixTen(0b011111, 0b1110111010), extsbx);
|
||||
Bind(SixTen(0b011111, 0b1111010110), icbi);
|
||||
Bind(SixTen(0b011111, 0b1111010111), stfiwx);
|
||||
Bind(SixTen(0b011111, 0b1111110110), dcbz);
|
||||
Bind(SixTen(0b111111, 0b0000000000), fcmpu);
|
||||
Bind(SixTen(0b111111, 0b0000001100), frspx);
|
||||
Bind(SixTen(0b111111, 0b0000001110), fctiwx);
|
||||
Bind(SixTen(0b111111, 0b0000001111), fctiwzx);
|
||||
Bind(SixTen(0b111111, 0b0000100000), fcmpo);
|
||||
Bind(SixTen(0b111111, 0b0000100110), mtfsb1x);
|
||||
Bind(SixTen(0b111111, 0b0000101000), fnegx);
|
||||
Bind(SixTen(0b111111, 0b0001000000), mcrfs);
|
||||
Bind(SixTen(0b111111, 0b0001000110), mtfsb0x);
|
||||
Bind(SixTen(0b111111, 0b0001001000), fmrx);
|
||||
Bind(SixTen(0b111111, 0b0010000110), mtfsfix);
|
||||
Bind(SixTen(0b111111, 0b0010001000), fnabsx);
|
||||
Bind(SixTen(0b111111, 0b0100001000), fabsx);
|
||||
Bind(SixTen(0b111111, 0b1001000111), mffsx);
|
||||
Bind(SixTen(0b111111, 0b1011000111), mtfsfx);
|
||||
Bind(SixTen(0b111111, 0b1100101110), fctidx);
|
||||
Bind(SixTen(0b111111, 0b1100101111), fctidzx);
|
||||
Bind(SixTen(0b111111, 0b1101001110), fcfidx);
|
||||
|
||||
Bind(SixTen(0b011111, 0b0101010011), mfspr); // Flagged as "supervisor and user"?
|
||||
Bind(SixTen(0b011111, 0b0111010011), mtspr); // Flagged as "supervisor and user"?
|
||||
|
||||
BindSupervisorConditional(is32bit, SixTen(0b011111, 0b0011010010), mtsr);
|
||||
BindSupervisorConditional(is32bit, SixTen(0b011111, 0b0011110010), mtsrin);
|
||||
BindSupervisorConditional(is32bit, SixTen(0b011111, 0b1001010011), mfsr);
|
||||
BindSupervisorConditional(is32bit, SixTen(0b011111, 0b1010010011), mfsrin);
|
||||
|
||||
BindSupervisorConditional(is64bit, SixTen(0b011111, 0b0111110010), slbia); // optional
|
||||
|
||||
// The following are all optional; should I record that?
|
||||
BindSupervisor(SixTen(0b011111, 0b0100110010), tlbie);
|
||||
BindSupervisor(SixTen(0b011111, 0b0101110010), tlbia);
|
||||
BindSupervisor(SixTen(0b011111, 0b1000110110), tlbsync);
|
||||
}
|
||||
|
||||
// Third pass: like six-ten except that the top five of the final ten
|
||||
// are reserved (i.e. ignored here).
|
||||
switch(opcode & SixTen(0b111111, 0b11111)) {
|
||||
default: break;
|
||||
|
||||
Bind(SixTen(0b111011, 0b10010), fdivsx);
|
||||
Bind(SixTen(0b111011, 0b10100), fsubsx);
|
||||
Bind(SixTen(0b111011, 0b10101), faddsx);
|
||||
Bind(SixTen(0b111011, 0b11001), fmulsx);
|
||||
Bind(SixTen(0b111011, 0b11100), fmsubsx);
|
||||
Bind(SixTen(0b111011, 0b11101), fmaddsx);
|
||||
Bind(SixTen(0b111011, 0b11110), fnmsubsx);
|
||||
Bind(SixTen(0b111011, 0b11111), fnmaddsx);
|
||||
|
||||
Bind(SixTen(0b111111, 0b10010), fdivx);
|
||||
Bind(SixTen(0b111111, 0b10100), fsubx);
|
||||
Bind(SixTen(0b111111, 0b10101), faddx);
|
||||
Bind(SixTen(0b111111, 0b11001), fmulx);
|
||||
Bind(SixTen(0b111111, 0b11100), fmsubx);
|
||||
Bind(SixTen(0b111111, 0b11101), fmaddx);
|
||||
Bind(SixTen(0b111111, 0b11110), fnmsubx);
|
||||
Bind(SixTen(0b111111, 0b11111), fnmaddx);
|
||||
|
||||
BindConditional(is64bit, SixTen(0b111011, 0b10110), fsqrtsx);
|
||||
BindConditional(is64bit, SixTen(0b111011, 0b11000), fresx);
|
||||
|
||||
// Optional...
|
||||
Bind(SixTen(0b111111, 0b10110), fsqrtx);
|
||||
Bind(SixTen(0b111111, 0b10111), fselx);
|
||||
Bind(SixTen(0b111111, 0b11010), frsqrtex);
|
||||
}
|
||||
|
||||
// stwcx. and stdcx.
|
||||
switch(opcode & 0b111111'00'00000000'000'111111111'1){
|
||||
case 0b011111'00'00000000'00000'0010010110'1: return Instruction(Operation::stwcx_, opcode);
|
||||
case 0b011111'00'00000000'00000'0011010110'1:
|
||||
if(is64bit()) return Instruction(Operation::stdcx_, opcode);
|
||||
return Instruction(opcode);
|
||||
}
|
||||
|
||||
// std and stdu
|
||||
switch(opcode & 0b111111'00'00000000'00000000'000000'11){
|
||||
case 0b111110'00'00000000'00000000'000000'00: return Instruction(Operation::std, opcode);
|
||||
case 0b111110'00'00000000'00000000'000000'01:
|
||||
if(is64bit()) return Instruction(Operation::stdu, opcode);
|
||||
return Instruction(opcode);
|
||||
}
|
||||
|
||||
// sc
|
||||
if((opcode & 0b111111'00'00000000'00000000'000000'1'0) == 0b010001'00'00000000'00000000'000000'1'0) {
|
||||
return Instruction(Operation::sc, opcode);
|
||||
}
|
||||
|
||||
#undef Six
|
||||
#undef SixTen
|
||||
|
||||
#undef Bind
|
||||
#undef BindConditional
|
||||
|
||||
return Instruction(opcode);
|
||||
}
|
||||
56
InstructionSets/PowerPC/Decoder.hpp
Normal file
56
InstructionSets/PowerPC/Decoder.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Decoder.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 30/12/20.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef InstructionSets_PowerPC_Decoder_hpp
|
||||
#define InstructionSets_PowerPC_Decoder_hpp
|
||||
|
||||
#include "Instruction.hpp"
|
||||
|
||||
namespace InstructionSet {
|
||||
namespace PowerPC {
|
||||
|
||||
enum class Model {
|
||||
/// i.e. 32-bit, with POWER carry-over instructions.
|
||||
MPC601,
|
||||
/// i.e. 32-bit, no POWER instructions.
|
||||
MPC603,
|
||||
/// i.e. 64-bit.
|
||||
MPC620,
|
||||
};
|
||||
|
||||
/*!
|
||||
Implements PowerPC instruction decoding.
|
||||
|
||||
This is an experimental implementation; it has not yet undergone significant testing.
|
||||
*/
|
||||
struct Decoder {
|
||||
public:
|
||||
Decoder(Model model);
|
||||
|
||||
Instruction decode(uint32_t opcode);
|
||||
|
||||
private:
|
||||
Model model_;
|
||||
|
||||
bool is64bit() const {
|
||||
return model_ == Model::MPC620;
|
||||
}
|
||||
|
||||
bool is32bit() const {
|
||||
return !is64bit();
|
||||
}
|
||||
|
||||
bool is601() const {
|
||||
return model_ == Model::MPC601;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* InstructionSets_PowerPC_Decoder_hpp */
|
||||
190
InstructionSets/PowerPC/Instruction.hpp
Normal file
190
InstructionSets/PowerPC/Instruction.hpp
Normal file
@@ -0,0 +1,190 @@
|
||||
//
|
||||
// Instruction.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 15/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef InstructionSets_PowerPC_Instruction_h
|
||||
#define InstructionSets_PowerPC_Instruction_h
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace InstructionSet {
|
||||
namespace PowerPC {
|
||||
|
||||
enum class Operation: uint8_t {
|
||||
Undefined,
|
||||
|
||||
// These 601-exclusive instructions; a lot of them are carry-overs
|
||||
// from POWER.
|
||||
absx, clcs, divx, divsx, dozx, dozi, lscbxx, maskgx, maskirx, mulx,
|
||||
nabsx, rlmix, rribx, slex, sleqx, sliqx, slliqx, sllqx, slqx,
|
||||
sraiqx, sraqx, srex, sreax, sreqx, sriqx, srliqx, srlqx, srqx,
|
||||
|
||||
// 32- and 64-bit PowerPC instructions.
|
||||
addx, addcx, addex, addi, addic, addic_, addis, addmex, addzex, andx,
|
||||
andcx, andi_, andis_, bx, bcx, bcctrx, bclrx, cmp, cmpi, cmpl, cmpli,
|
||||
cntlzwx, crand, crandc, creqv, crnand, crnor, cror, crorc, crxor, dcbf,
|
||||
dcbst, dcbt, dcbtst, dcbz, divwx, divwux, eciwx, ecowx, eieio, eqvx,
|
||||
extsbx, extshx, fabsx, faddx, faddsx, fcmpo, fcmpu, fctiwx, fctiwzx,
|
||||
fdivx, fdivsx, fmaddx, fmaddsx, fmrx, fmsubx, fmsubsx, fmulx, fmulsx,
|
||||
fnabsx, fnegx, fnmaddx, fnmaddsx, fnmsubx, fnmsubsx, frspx, fsubx, fsubsx,
|
||||
icbi, isync, lbz, lbzu, lbzux, lbzx, lfd, lfdu, lfdux, lfdx, lfs, lfsu,
|
||||
lfsux, lfsx, lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx, lmw,
|
||||
lswi, lswx, lwarx, lwbrx, lwz, lwzu, lwzux, lwzx, mcrf, mcrfs, mcrxr,
|
||||
mfcr, mffsx, mfmsr, mfspr, mfsr, mfsrin, mtcrf, mtfsb0x, mtfsb1x, mtfsfx,
|
||||
mtfsfix, mtmsr, mtspr, mtsr, mtsrin, mulhwx, mulhwux, mulli, mullwx,
|
||||
nandx, negx, norx, orx, orcx, ori, oris, rfi, rlwimix, rlwinmx, rlwnmx,
|
||||
sc, slwx, srawx, srawix, srwx, stb, stbu, stbux, stbx, stfd, stfdu,
|
||||
stfdux, stfdx, stfs, stfsu, stfsux, stfsx, sth, sthbrx, sthu, sthux, sthx,
|
||||
stmw, stswi, stswx, stw, stwbrx, stwcx_, stwu, stwux, stwx, subfx, subfcx,
|
||||
subfex, subfic, subfmex, subfzex, sync, tw, twi, xorx, xori, xoris, mftb,
|
||||
|
||||
// 32-bit, supervisor level.
|
||||
dcbi,
|
||||
|
||||
// Supervisor, optional.
|
||||
tlbia, tlbie, tlbsync,
|
||||
|
||||
// Optional.
|
||||
fresx, frsqrtex, fselx, fsqrtx, slbia, slbie, stfiwx,
|
||||
|
||||
// 64-bit only PowerPC instructions.
|
||||
cntlzdx, divdx, divdux, extswx, fcfidx, fctidx, fctidzx, tdi, mulhdux,
|
||||
ldx, sldx, ldux, td, mulhdx, ldarx, stdx, stdux, mulld, lwax, lwaux,
|
||||
sradix, srdx, sradx, extsw, fsqrtsx, std, stdu, stdcx_,
|
||||
};
|
||||
|
||||
/*!
|
||||
Holds a decoded PowerPC instruction.
|
||||
|
||||
Implementation note: because the PowerPC encoding is particularly straightforward,
|
||||
only the operation has been decoded ahead of time; all other fields are decoded on-demand.
|
||||
|
||||
It would be possible to partition the ordering of Operations into user followed by supervisor,
|
||||
eliminating the storage necessary for a flag, but it wouldn't save anything due to alignment.
|
||||
*/
|
||||
struct Instruction {
|
||||
Operation operation = Operation::Undefined;
|
||||
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) {}
|
||||
|
||||
// Instruction fields are decoded below; naming is a compromise between
|
||||
// Motorola's documentation and IBM's.
|
||||
//
|
||||
// I've dutifully implemented various synonyms with unique entry points,
|
||||
// in order to capture that information here rather than thrusting it upon
|
||||
// the reader of whatever implementation may follow.
|
||||
|
||||
// Currently omitted: OPCD and XO, which I think are unnecessary given that
|
||||
// full decoding has already occurred.
|
||||
|
||||
/// Immediate field used to specify an unsigned 16-bit integer.
|
||||
uint16_t uimm() const { return uint16_t(opcode & 0xffff); }
|
||||
/// Immediate field used to specify a signed 16-bit integer.
|
||||
int16_t simm() const { return int16_t(opcode & 0xffff); }
|
||||
/// Immediate field used to specify a signed 16-bit integer.
|
||||
int16_t d() const { return int16_t(opcode & 0xffff); }
|
||||
/// Immediate field used to specify a signed 14-bit integer [64-bit only].
|
||||
int16_t ds() const { return int16_t(opcode & 0xfffc); }
|
||||
/// Immediate field used as data to be placed into a field in the floating point status and condition register.
|
||||
int32_t imm() const { return (opcode >> 12) & 0xf; }
|
||||
|
||||
/// Specifies the conditions on which to trap.
|
||||
int32_t to() const { return (opcode >> 21) & 0x1f; }
|
||||
|
||||
/// Register source A or destination.
|
||||
uint32_t rA() const { return (opcode >> 16) & 0x1f; }
|
||||
/// Register source B.
|
||||
uint32_t rB() const { return (opcode >> 11) & 0x1f; }
|
||||
/// Register destination.
|
||||
uint32_t rD() const { return (opcode >> 21) & 0x1f; }
|
||||
/// Register source.
|
||||
uint32_t rS() const { return (opcode >> 21) & 0x1f; }
|
||||
|
||||
/// Floating point register source A.
|
||||
uint32_t frA() const { return (opcode >> 16) & 0x1f; }
|
||||
/// Floating point register source B.
|
||||
uint32_t frB() const { return (opcode >> 11) & 0x1f; }
|
||||
/// Floating point register source C.
|
||||
uint32_t frC() const { return (opcode >> 6) & 0x1f; }
|
||||
/// Floating point register source.
|
||||
uint32_t frS() const { return (opcode >> 21) & 0x1f; }
|
||||
/// Floating point register destination.
|
||||
uint32_t frD() const { return (opcode >> 21) & 0x1f; }
|
||||
|
||||
/// Branch conditional options.
|
||||
uint32_t bo() const { return (opcode >> 21) & 0x1f; }
|
||||
/// Source condition register bit for branch conditionals.
|
||||
uint32_t bi() const { return (opcode >> 16) & 0x1f; }
|
||||
/// Branch displacement; provided as already sign extended.
|
||||
int16_t bd() const { return int16_t(opcode & 0xfffc); }
|
||||
|
||||
/// Specifies the first 1 bit of a 32/64-bit mask for rotate operations.
|
||||
uint32_t mb() const { return (opcode >> 6) & 0x1f; }
|
||||
/// Specifies the first 1 bit of a 32/64-bit mask for rotate operations.
|
||||
uint32_t me() const { return (opcode >> 1) & 0x1f; }
|
||||
|
||||
/// Condition register source bit A.
|
||||
uint32_t crbA() const { return (opcode >> 16) & 0x1f; }
|
||||
/// Condition register source bit B.
|
||||
uint32_t crbB() const { return (opcode >> 11) & 0x1f; }
|
||||
/// Condition register (or floating point status & condition register) destination bit.
|
||||
uint32_t crbD() const { return (opcode >> 21) & 0x1f; }
|
||||
|
||||
/// Condition register (or floating point status & condition register) destination field.
|
||||
uint32_t crfD() const { return (opcode >> 23) & 0x07; }
|
||||
/// Condition register (or floating point status & condition register) source field.
|
||||
uint32_t crfS() const { return (opcode >> 18) & 0x07; }
|
||||
|
||||
/// Mask identifying fields to be updated by mtcrf.
|
||||
uint32_t crm() const { return (opcode >> 12) & 0xff; }
|
||||
|
||||
/// Mask identifying fields to be updated by mtfsf.
|
||||
uint32_t fm() const { return (opcode >> 17) & 0xff; }
|
||||
|
||||
/// Specifies the number of bytes to move in an immediate string load or store.
|
||||
uint32_t nb() const { return (opcode >> 11) & 0x1f; }
|
||||
|
||||
/// Specifies a shift amount.
|
||||
/// TODO: possibly bit 30 is also used in 64-bit mode, find out.
|
||||
uint32_t sh() const { return (opcode >> 11) & 0x1f; }
|
||||
|
||||
/// Specifies one of the 16 segment registers [32-bit only].
|
||||
uint32_t sr() const { return (opcode >> 16) & 0xf; }
|
||||
|
||||
/// A 24-bit signed number; provided as already sign extended.
|
||||
int32_t li() const {
|
||||
constexpr uint32_t extensions[2] = {
|
||||
0x0000'0000,
|
||||
0xfc00'0000
|
||||
};
|
||||
const uint32_t value = (opcode & 0x03ff'fffc) | extensions[(opcode >> 25) & 1];
|
||||
return int32_t(value);
|
||||
}
|
||||
|
||||
/// Absolute address bit; @c 0 or @c non-0.
|
||||
uint32_t aa() const { return opcode & 0x02; }
|
||||
/// Link bit; @c 0 or @c non-0.
|
||||
uint32_t lk() const { return opcode & 0x01; }
|
||||
/// Record bit; @c 0 or @c non-0.
|
||||
uint32_t rc() const { return opcode & 0x01; }
|
||||
/// Whether to compare 32-bit or 64-bit numbers [for 64-bit implementations only]; @c 0 or @c non-0.
|
||||
uint32_t l() const { return opcode & 0x200000; }
|
||||
/// Enables setting of OV and SO in the XER; @c 0 or @c non-0.
|
||||
uint32_t oe() const { return opcode & 0x800; }
|
||||
};
|
||||
|
||||
// Sanity check on Instruction size.
|
||||
static_assert(sizeof(Instruction) <= 8);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* InstructionSets_PowerPC_Instruction_h */
|
||||
86
InstructionSets/README.md
Normal file
86
InstructionSets/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Instruction Sets
|
||||
|
||||
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.
|
||||
|
||||
This part of CLK is intended primarily to provide disassembly services for static analysis, and processing for machines where timing is not part of the specification — i.e. anything that's an instruction set and a HAL.
|
||||
|
||||
## Decoders
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
It may also be reasonable to make allowances for bus-centric CPU emulators, but those will be tightly coupled to specific decoders so no general rules need apply.
|
||||
|
||||
Disassemblers are likely to decode an instruction, output it, and then immediately forget about it.
|
||||
|
||||
Instruction executors may opt to cache decoded instructions to reduce recurrent costs, but will always be dealing with an actual instruction stream. The chance of caching means that decoded instructions should seek to be small. If helpful then a decoder might prefer to return a `std::pair` or similar of ephemeral information and stuff that it is meaningful to store.
|
||||
|
||||
### Likely Interfaces
|
||||
|
||||
These examples assume that the processor itself doesn't hold any state that affects instruction parsing. Whether processors with such state offer more than one decoder or take state as an argument will be a question of measure and effect.
|
||||
|
||||
#### Fixed-size instruction words
|
||||
|
||||
If the instructions are a fixed size, the decoder can provide what is functionally a simple lookup, whether implemented as such or not:
|
||||
|
||||
Instruction decode(word_type instruction) { ... }
|
||||
|
||||
For now I have preferred not to make this a simple constructor on `Instruction` because I'm reserving the option of switching to an ephemeral/permanent split in what's returned. More consideration needs to be applied here.
|
||||
|
||||
#### Variable-size instruction words
|
||||
|
||||
If instructions are a variable size, the decoder should maintain internal state such that it can be provided with fragments of instructions until a full decoding has occurred — this avoids an assumption that all source bytes will always be laid out linearly in memory.
|
||||
|
||||
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.
|
||||
|
||||
A caller is permitted to react in any way it prefers to negative numbers; they're a hint potentially to reduce calling overhead only. A size of `0` would be taken to have the same meaning as a size of `-1`.
|
||||
|
||||
## Parsers
|
||||
|
||||
A parser sits one level above a decoder; it is handed:
|
||||
* a start address;
|
||||
* a closing bound; and
|
||||
* a target.
|
||||
|
||||
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
|
||||
* if a final instruction exists but runs beyond the closing bound, notification of that fact.
|
||||
|
||||
So a parser has the same two primary potential recipients as a decoder: diassemblers, and executors.
|
||||
|
||||
## 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.
|
||||
|
||||
## Caching Executor
|
||||
|
||||
The caching executor is a generic class templated on a specific executor. It will use an executor to cache the results of parsing.
|
||||
|
||||
Idiomatically, the objects that perform instructions will expect to receive an appropriate executor as an argument. If they require other information, such as a copy of the decoded instruction, it should be built into the classes.
|
||||
36
InstructionSets/Sizes.hpp
Normal file
36
InstructionSets/Sizes.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Sizes.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 26/01/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Sizes_h
|
||||
#define Sizes_h
|
||||
|
||||
#include <limits>
|
||||
|
||||
/*!
|
||||
Maps to the smallest integral type that can contain max_value, from the following options:
|
||||
|
||||
* uint8_t;
|
||||
* uint16_t;
|
||||
* uint32_t; or
|
||||
* uint64_t.
|
||||
*/
|
||||
template <uint64_t max_value> struct MinIntTypeValue {
|
||||
using type =
|
||||
std::conditional_t<
|
||||
max_value <= std::numeric_limits<uint8_t>::max(), uint8_t,
|
||||
std::conditional_t<
|
||||
max_value <= std::numeric_limits<uint16_t>::max(), uint16_t,
|
||||
std::conditional_t<
|
||||
max_value <= std::numeric_limits<uint32_t>::max(), uint32_t,
|
||||
uint64_t
|
||||
>
|
||||
>
|
||||
>;
|
||||
};
|
||||
|
||||
#endif /* Sizes_h */
|
||||
625
InstructionSets/x86/Decoder.cpp
Normal file
625
InstructionSets/x86/Decoder.cpp
Normal file
@@ -0,0 +1,625 @@
|
||||
//
|
||||
// x86.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 01/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Decoder.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <utility>
|
||||
|
||||
using namespace InstructionSet::x86;
|
||||
|
||||
// Only 8086 is suppoted for now.
|
||||
Decoder::Decoder(Model) {}
|
||||
|
||||
std::pair<int, InstructionSet::x86::Instruction> Decoder::decode(const uint8_t *source, size_t length) {
|
||||
const uint8_t *const end = source + length;
|
||||
|
||||
// MARK: - Prefixes (if present) and the opcode.
|
||||
|
||||
/// Helper macro for those that follow.
|
||||
#define SetOpSrcDestSize(op, src, dest, size) \
|
||||
operation_ = Operation::op; \
|
||||
source_ = Source::src; \
|
||||
destination_ = Source::dest; \
|
||||
operation_size_ = size
|
||||
|
||||
/// Covers anything which is complete as soon as the opcode is encountered.
|
||||
#define Complete(op, src, dest, size) \
|
||||
SetOpSrcDestSize(op, src, dest, size); \
|
||||
phase_ = Phase::ReadyToPost
|
||||
|
||||
/// Handles instructions of the form rr, kk and rr, jjkk, i.e. a destination register plus an operand.
|
||||
#define RegData(op, dest, size) \
|
||||
SetOpSrcDestSize(op, DirectAddress, dest, size); \
|
||||
source_ = Source::Immediate; \
|
||||
operand_size_ = size; \
|
||||
phase_ = Phase::AwaitingDisplacementOrOperand
|
||||
|
||||
/// Handles instructions of the form Ax, jjkk where the latter is implicitly an address.
|
||||
#define RegAddr(op, dest, op_size, addr_size) \
|
||||
SetOpSrcDestSize(op, DirectAddress, dest, op_size); \
|
||||
operand_size_ = addr_size; \
|
||||
phase_ = Phase::AwaitingDisplacementOrOperand
|
||||
|
||||
/// Handles instructions of the form jjkk, Ax where the former is implicitly an address.
|
||||
#define AddrReg(op, source, op_size, addr_size) \
|
||||
SetOpSrcDestSize(op, source, DirectAddress, op_size); \
|
||||
operand_size_ = addr_size; \
|
||||
destination_ = Source::DirectAddress; \
|
||||
phase_ = Phase::AwaitingDisplacementOrOperand
|
||||
|
||||
/// Covers both `mem/reg, reg` and `reg, mem/reg`.
|
||||
#define MemRegReg(op, format, size) \
|
||||
operation_ = Operation::op; \
|
||||
phase_ = Phase::ModRegRM; \
|
||||
modregrm_format_ = ModRegRMFormat::format; \
|
||||
operand_size_ = 0; \
|
||||
operation_size_ = size
|
||||
|
||||
/// Handles JO, JNO, JB, etc — jumps with a single byte displacement.
|
||||
#define Jump(op) \
|
||||
operation_ = Operation::op; \
|
||||
phase_ = Phase::AwaitingDisplacementOrOperand; \
|
||||
displacement_size_ = 1
|
||||
|
||||
/// Handles far CALL and far JMP — fixed four byte operand operations.
|
||||
#define Far(op) \
|
||||
operation_ = Operation::op; \
|
||||
phase_ = Phase::AwaitingDisplacementOrOperand; \
|
||||
operand_size_ = 4; \
|
||||
|
||||
while(phase_ == Phase::Instruction && source != end) {
|
||||
// Retain the instruction byte, in case additional decoding is deferred
|
||||
// to the ModRegRM byte.
|
||||
instr_ = *source;
|
||||
++source;
|
||||
++consumed_;
|
||||
|
||||
switch(instr_) {
|
||||
default: {
|
||||
const auto result = std::make_pair(consumed_, Instruction());
|
||||
reset_parsing();
|
||||
return result;
|
||||
}
|
||||
|
||||
#define PartialBlock(start, operation) \
|
||||
case start + 0x00: MemRegReg(operation, MemReg_Reg, 1); break; \
|
||||
case start + 0x01: MemRegReg(operation, MemReg_Reg, 2); break; \
|
||||
case start + 0x02: MemRegReg(operation, Reg_MemReg, 1); break; \
|
||||
case start + 0x03: MemRegReg(operation, Reg_MemReg, 2); break; \
|
||||
case start + 0x04: RegData(operation, AL, 1); break; \
|
||||
case start + 0x05: RegData(operation, AX, 2)
|
||||
|
||||
PartialBlock(0x00, ADD); break;
|
||||
case 0x06: Complete(PUSH, ES, None, 2); break;
|
||||
case 0x07: Complete(POP, None, ES, 2); break;
|
||||
|
||||
PartialBlock(0x08, OR); break;
|
||||
case 0x0e: Complete(PUSH, CS, None, 2); break;
|
||||
|
||||
PartialBlock(0x10, ADC); break;
|
||||
case 0x16: Complete(PUSH, SS, None, 2); break;
|
||||
case 0x17: Complete(POP, None, SS, 2); break;
|
||||
|
||||
PartialBlock(0x18, SBB); break;
|
||||
case 0x1e: Complete(PUSH, DS, None, 2); break;
|
||||
case 0x1f: Complete(POP, None, DS, 2); break;
|
||||
|
||||
PartialBlock(0x20, AND); break;
|
||||
case 0x26: segment_override_ = Source::ES; break;
|
||||
case 0x27: Complete(DAA, AL, AL, 1); break;
|
||||
|
||||
PartialBlock(0x28, SUB); break;
|
||||
case 0x2e: segment_override_ = Source::CS; break;
|
||||
case 0x2f: Complete(DAS, AL, AL, 1); break;
|
||||
|
||||
PartialBlock(0x30, XOR); break;
|
||||
case 0x36: segment_override_ = Source::SS; break;
|
||||
case 0x37: Complete(AAA, AL, AX, 1); break;
|
||||
|
||||
PartialBlock(0x38, CMP); break;
|
||||
case 0x3e: segment_override_ = Source::DS; break;
|
||||
case 0x3f: Complete(AAS, AL, AX, 1); break;
|
||||
|
||||
#undef PartialBlock
|
||||
|
||||
#define RegisterBlock(start, operation) \
|
||||
case start + 0x00: Complete(operation, AX, AX, 2); break; \
|
||||
case start + 0x01: Complete(operation, CX, CX, 2); break; \
|
||||
case start + 0x02: Complete(operation, DX, DX, 2); break; \
|
||||
case start + 0x03: Complete(operation, BX, BX, 2); break; \
|
||||
case start + 0x04: Complete(operation, SP, SP, 2); break; \
|
||||
case start + 0x05: Complete(operation, BP, BP, 2); break; \
|
||||
case start + 0x06: Complete(operation, SI, SI, 2); break; \
|
||||
case start + 0x07: Complete(operation, DI, DI, 2)
|
||||
|
||||
RegisterBlock(0x40, INC); break;
|
||||
RegisterBlock(0x48, DEC); break;
|
||||
RegisterBlock(0x50, PUSH); break;
|
||||
RegisterBlock(0x58, POP); break;
|
||||
|
||||
#undef RegisterBlock
|
||||
|
||||
// 0x60–0x6f: not used.
|
||||
|
||||
case 0x70: Jump(JO); break;
|
||||
case 0x71: Jump(JNO); break;
|
||||
case 0x72: Jump(JB); break;
|
||||
case 0x73: Jump(JNB); break;
|
||||
case 0x74: Jump(JE); break;
|
||||
case 0x75: Jump(JNE); break;
|
||||
case 0x76: Jump(JBE); break;
|
||||
case 0x77: Jump(JNBE); break;
|
||||
case 0x78: Jump(JS); break;
|
||||
case 0x79: Jump(JNS); break;
|
||||
case 0x7a: Jump(JP); break;
|
||||
case 0x7b: Jump(JNP); break;
|
||||
case 0x7c: Jump(JL); break;
|
||||
case 0x7d: Jump(JNL); break;
|
||||
case 0x7e: Jump(JLE); break;
|
||||
case 0x7f: Jump(JNLE); break;
|
||||
|
||||
case 0x80: MemRegReg(Invalid, MemRegADD_to_CMP, 1); break;
|
||||
case 0x81: MemRegReg(Invalid, MemRegADD_to_CMP, 2); break;
|
||||
case 0x82: MemRegReg(Invalid, MemRegADC_to_CMP, 1); break;
|
||||
case 0x83: MemRegReg(Invalid, MemRegADC_to_CMP, 2); break;
|
||||
|
||||
case 0x84: MemRegReg(TEST, MemReg_Reg, 1); break;
|
||||
case 0x85: MemRegReg(TEST, MemReg_Reg, 2); break;
|
||||
case 0x86: MemRegReg(XCHG, Reg_MemReg, 1); break;
|
||||
case 0x87: MemRegReg(XCHG, Reg_MemReg, 2); break;
|
||||
case 0x88: MemRegReg(MOV, MemReg_Reg, 1); break;
|
||||
case 0x89: MemRegReg(MOV, MemReg_Reg, 2); break;
|
||||
case 0x8a: MemRegReg(MOV, Reg_MemReg, 1); break;
|
||||
case 0x8b: MemRegReg(MOV, Reg_MemReg, 2); break;
|
||||
// 0x8c: not used.
|
||||
case 0x8d: MemRegReg(LEA, Reg_MemReg, 2); break;
|
||||
case 0x8e: MemRegReg(MOV, SegReg, 2); break;
|
||||
case 0x8f: MemRegReg(POP, MemRegPOP, 2); break;
|
||||
|
||||
case 0x90: Complete(NOP, None, None, 0); break; // Or XCHG AX, AX?
|
||||
case 0x91: Complete(XCHG, AX, CX, 2); break;
|
||||
case 0x92: Complete(XCHG, AX, DX, 2); break;
|
||||
case 0x93: Complete(XCHG, AX, BX, 2); break;
|
||||
case 0x94: Complete(XCHG, AX, SP, 2); break;
|
||||
case 0x95: Complete(XCHG, AX, BP, 2); break;
|
||||
case 0x96: Complete(XCHG, AX, SI, 2); break;
|
||||
case 0x97: Complete(XCHG, AX, DI, 2); break;
|
||||
|
||||
case 0x98: Complete(CBW, AL, AH, 1); break;
|
||||
case 0x99: Complete(CWD, AX, DX, 2); break;
|
||||
case 0x9a: Far(CALLF); break;
|
||||
case 0x9b: Complete(WAIT, None, None, 0); break;
|
||||
case 0x9c: Complete(PUSHF, None, None, 2); break;
|
||||
case 0x9d: Complete(POPF, None, None, 2); break;
|
||||
case 0x9e: Complete(SAHF, None, None, 1); break;
|
||||
case 0x9f: Complete(LAHF, None, None, 1); break;
|
||||
|
||||
case 0xa0: RegAddr(MOV, AL, 1, 1); break;
|
||||
case 0xa1: RegAddr(MOV, AX, 2, 2); break;
|
||||
case 0xa2: AddrReg(MOV, AL, 1, 1); break;
|
||||
case 0xa3: AddrReg(MOV, AX, 2, 2); break;
|
||||
|
||||
case 0xa4: Complete(MOVS, None, None, 1); break;
|
||||
case 0xa5: Complete(MOVS, None, None, 2); break;
|
||||
case 0xa6: Complete(CMPS, None, None, 1); break;
|
||||
case 0xa7: Complete(CMPS, None, None, 2); break;
|
||||
case 0xa8: RegData(TEST, AL, 1); break;
|
||||
case 0xa9: RegData(TEST, AX, 2); break;
|
||||
case 0xaa: Complete(STOS, None, None, 1); break;
|
||||
case 0xab: Complete(STOS, None, None, 2); break;
|
||||
case 0xac: Complete(LODS, None, None, 1); break;
|
||||
case 0xad: Complete(LODS, None, None, 2); break;
|
||||
case 0xae: Complete(SCAS, None, None, 1); break;
|
||||
case 0xaf: Complete(SCAS, None, None, 2); break;
|
||||
|
||||
case 0xb0: RegData(MOV, AL, 1); break;
|
||||
case 0xb1: RegData(MOV, CL, 1); break;
|
||||
case 0xb2: RegData(MOV, DL, 1); break;
|
||||
case 0xb3: RegData(MOV, BL, 1); break;
|
||||
case 0xb4: RegData(MOV, AH, 1); break;
|
||||
case 0xb5: RegData(MOV, CH, 1); break;
|
||||
case 0xb6: RegData(MOV, DH, 1); break;
|
||||
case 0xb7: RegData(MOV, BH, 1); break;
|
||||
case 0xb8: RegData(MOV, AX, 2); break;
|
||||
case 0xb9: RegData(MOV, CX, 2); break;
|
||||
case 0xba: RegData(MOV, DX, 2); break;
|
||||
case 0xbb: RegData(MOV, BX, 2); break;
|
||||
case 0xbc: RegData(MOV, SP, 2); break;
|
||||
case 0xbd: RegData(MOV, BP, 2); break;
|
||||
case 0xbe: RegData(MOV, SI, 2); break;
|
||||
case 0xbf: RegData(MOV, DI, 2); break;
|
||||
|
||||
case 0xc2: RegData(RETN, None, 2); break;
|
||||
case 0xc3: Complete(RETN, None, None, 2); break;
|
||||
case 0xc4: MemRegReg(LES, Reg_MemReg, 2); break;
|
||||
case 0xc5: MemRegReg(LDS, Reg_MemReg, 2); break;
|
||||
case 0xc6: MemRegReg(MOV, MemRegMOV, 1); break;
|
||||
case 0xc7: MemRegReg(MOV, MemRegMOV, 2); break;
|
||||
|
||||
case 0xca: RegData(RETF, None, 2); break;
|
||||
case 0xcb: Complete(RETF, None, None, 4); break;
|
||||
|
||||
case 0xcc: Complete(INT3, None, None, 0); break;
|
||||
case 0xcd: RegData(INT, None, 1); break;
|
||||
case 0xce: Complete(INTO, None, None, 0); break;
|
||||
case 0xcf: Complete(IRET, None, None, 0); break;
|
||||
|
||||
case 0xd0: case 0xd1:
|
||||
phase_ = Phase::ModRegRM;
|
||||
modregrm_format_ = ModRegRMFormat::MemRegROL_to_SAR;
|
||||
operation_size_ = 1 + (instr_ & 1);
|
||||
source_ = Source::Immediate;
|
||||
operand_ = 1;
|
||||
break;
|
||||
case 0xd2: case 0xd3:
|
||||
phase_ = Phase::ModRegRM;
|
||||
modregrm_format_ = ModRegRMFormat::MemRegROL_to_SAR;
|
||||
operation_size_ = 1 + (instr_ & 1);
|
||||
source_ = Source::CL;
|
||||
break;
|
||||
case 0xd4: RegData(AAM, AX, 1); break;
|
||||
case 0xd5: RegData(AAD, AX, 1); break;
|
||||
|
||||
case 0xd7: Complete(XLAT, None, None, 1); break;
|
||||
|
||||
case 0xd8: MemRegReg(ESC, MemReg_Reg, 0); break;
|
||||
case 0xd9: MemRegReg(ESC, MemReg_Reg, 0); break;
|
||||
case 0xda: MemRegReg(ESC, MemReg_Reg, 0); break;
|
||||
case 0xdb: MemRegReg(ESC, MemReg_Reg, 0); break;
|
||||
case 0xdc: MemRegReg(ESC, MemReg_Reg, 0); break;
|
||||
case 0xdd: MemRegReg(ESC, MemReg_Reg, 0); break;
|
||||
case 0xde: MemRegReg(ESC, MemReg_Reg, 0); break;
|
||||
case 0xdf: MemRegReg(ESC, MemReg_Reg, 0); break;
|
||||
|
||||
case 0xe0: Jump(LOOPNE); break;
|
||||
case 0xe1: Jump(LOOPE); break;
|
||||
case 0xe2: Jump(LOOP); break;
|
||||
case 0xe3: Jump(JPCX); break;
|
||||
|
||||
case 0xe4: RegAddr(IN, AL, 1, 1); break;
|
||||
case 0xe5: RegAddr(IN, AX, 2, 1); break;
|
||||
case 0xe6: AddrReg(OUT, AL, 1, 1); break;
|
||||
case 0xe7: AddrReg(OUT, AX, 2, 1); break;
|
||||
|
||||
case 0xe8: RegData(CALLD, None, 2); break;
|
||||
case 0xe9: RegData(JMPN, None, 2); break;
|
||||
case 0xea: Far(JMPF); break;
|
||||
case 0xeb: Jump(JMPN); break;
|
||||
|
||||
case 0xec: Complete(IN, DX, AL, 1); break;
|
||||
case 0xed: Complete(IN, DX, AX, 1); break;
|
||||
case 0xee: Complete(OUT, AL, DX, 1); break;
|
||||
case 0xef: Complete(OUT, AX, DX, 2); break;
|
||||
|
||||
case 0xf4: Complete(HLT, None, None, 1); break;
|
||||
case 0xf5: Complete(CMC, None, None, 1); break;
|
||||
case 0xf6: MemRegReg(Invalid, MemRegTEST_to_IDIV, 1); break;
|
||||
case 0xf7: MemRegReg(Invalid, MemRegTEST_to_IDIV, 2); break;
|
||||
|
||||
case 0xf8: Complete(CLC, None, None, 1); break;
|
||||
case 0xf9: Complete(STC, None, None, 1); break;
|
||||
case 0xfa: Complete(CLI, None, None, 1); break;
|
||||
case 0xfb: Complete(STI, None, None, 1); break;
|
||||
case 0xfc: Complete(CLD, None, None, 1); break;
|
||||
case 0xfd: Complete(STD, None, None, 1); break;
|
||||
|
||||
case 0xfe: MemRegReg(Invalid, MemRegINC_DEC, 1); break;
|
||||
case 0xff: MemRegReg(Invalid, MemRegINC_to_PUSH, 1); break;
|
||||
|
||||
// Other prefix bytes.
|
||||
case 0xf0: lock_ = true; break;
|
||||
case 0xf2: repetition_ = Repetition::RepNE; break;
|
||||
case 0xf3: repetition_ = Repetition::RepE; break;
|
||||
}
|
||||
}
|
||||
|
||||
#undef Far
|
||||
#undef Jump
|
||||
#undef MemRegReg
|
||||
#undef AddrReg
|
||||
#undef RegAddr
|
||||
#undef RegData
|
||||
#undef Complete
|
||||
#undef SetOpSrcDestSize
|
||||
|
||||
// MARK: - ModRegRM byte, if any.
|
||||
|
||||
if(phase_ == Phase::ModRegRM && source != end) {
|
||||
const uint8_t mod = *source >> 6; // i.e. mode.
|
||||
const uint8_t reg = (*source >> 3) & 7; // i.e. register.
|
||||
const uint8_t rm = *source & 7; // i.e. register/memory.
|
||||
++source;
|
||||
++consumed_;
|
||||
|
||||
Source memreg;
|
||||
constexpr Source reg_table[3][8] = {
|
||||
{},
|
||||
{
|
||||
Source::AL, Source::CL, Source::DL, Source::BL,
|
||||
Source::AH, Source::CH, Source::DH, Source::BH,
|
||||
}, {
|
||||
Source::AX, Source::CX, Source::DX, Source::BX,
|
||||
Source::SP, Source::BP, Source::SI, Source::DI,
|
||||
}
|
||||
};
|
||||
switch(mod) {
|
||||
case 0: {
|
||||
constexpr Source rm_table[8] = {
|
||||
Source::IndBXPlusSI, Source::IndBXPlusDI,
|
||||
Source::IndBPPlusSI, Source::IndBPPlusDI,
|
||||
Source::IndSI, Source::IndDI,
|
||||
Source::DirectAddress, Source::IndBX,
|
||||
};
|
||||
memreg = rm_table[rm];
|
||||
} break;
|
||||
|
||||
default: {
|
||||
constexpr Source rm_table[8] = {
|
||||
Source::IndBXPlusSI, Source::IndBXPlusDI,
|
||||
Source::IndBPPlusSI, Source::IndBPPlusDI,
|
||||
Source::IndSI, Source::IndDI,
|
||||
Source::IndBP, Source::IndBX,
|
||||
};
|
||||
memreg = rm_table[rm];
|
||||
|
||||
displacement_size_ = 1 + (mod == 2);
|
||||
} break;
|
||||
|
||||
// Other operand is just a register.
|
||||
case 3:
|
||||
memreg = reg_table[operation_size_][rm];
|
||||
|
||||
// LES and LDS accept a memory argument only, not a register.
|
||||
if(operation_ == Operation::LES || operation_ == Operation::LDS) {
|
||||
const auto result = std::make_pair(consumed_, Instruction());
|
||||
reset_parsing();
|
||||
return result;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
switch(modregrm_format_) {
|
||||
case ModRegRMFormat::Reg_MemReg:
|
||||
case ModRegRMFormat::MemReg_Reg: {
|
||||
if(modregrm_format_ == ModRegRMFormat::Reg_MemReg) {
|
||||
source_ = memreg;
|
||||
destination_ = reg_table[operation_size_][reg];
|
||||
} else {
|
||||
source_ = reg_table[operation_size_][reg];
|
||||
destination_ = memreg;
|
||||
}
|
||||
} break;
|
||||
|
||||
case ModRegRMFormat::MemRegTEST_to_IDIV:
|
||||
source_ = destination_ = memreg;
|
||||
|
||||
switch(reg) {
|
||||
default: {
|
||||
const auto result = std::make_pair(consumed_, Instruction());
|
||||
reset_parsing();
|
||||
return result;
|
||||
}
|
||||
|
||||
case 0: operation_ = Operation::TEST; break;
|
||||
case 2: operation_ = Operation::NOT; break;
|
||||
case 3: operation_ = Operation::NEG; break;
|
||||
case 4: operation_ = Operation::MUL; break;
|
||||
case 5: operation_ = Operation::IMUL; break;
|
||||
case 6: operation_ = Operation::DIV; break;
|
||||
case 7: operation_ = Operation::IDIV; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case ModRegRMFormat::SegReg: {
|
||||
source_ = memreg;
|
||||
|
||||
constexpr Source seg_table[4] = {
|
||||
Source::ES, Source::CS,
|
||||
Source::SS, Source::DS,
|
||||
};
|
||||
|
||||
if(reg & 4) {
|
||||
const auto result = std::make_pair(consumed_, Instruction());
|
||||
reset_parsing();
|
||||
return result;
|
||||
}
|
||||
|
||||
destination_ = seg_table[reg];
|
||||
} break;
|
||||
|
||||
case ModRegRMFormat::MemRegROL_to_SAR:
|
||||
destination_ = memreg;
|
||||
|
||||
switch(reg) {
|
||||
default: {
|
||||
const auto result = std::make_pair(consumed_, Instruction());
|
||||
reset_parsing();
|
||||
return result;
|
||||
}
|
||||
|
||||
case 0: operation_ = Operation::ROL; break;
|
||||
case 2: operation_ = Operation::ROR; break;
|
||||
case 3: operation_ = Operation::RCL; break;
|
||||
case 4: operation_ = Operation::RCR; break;
|
||||
case 5: operation_ = Operation::SAL; break;
|
||||
case 6: operation_ = Operation::SHR; break;
|
||||
case 7: operation_ = Operation::SAR; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case ModRegRMFormat::MemRegINC_DEC:
|
||||
source_ = destination_ = memreg;
|
||||
|
||||
switch(reg) {
|
||||
default: {
|
||||
const auto result = std::make_pair(consumed_, Instruction());
|
||||
reset_parsing();
|
||||
return result;
|
||||
}
|
||||
|
||||
case 0: operation_ = Operation::INC; break;
|
||||
case 1: operation_ = Operation::DEC; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case ModRegRMFormat::MemRegINC_to_PUSH:
|
||||
source_ = destination_ = memreg;
|
||||
|
||||
switch(reg) {
|
||||
default: {
|
||||
const auto result = std::make_pair(consumed_, Instruction());
|
||||
reset_parsing();
|
||||
return result;
|
||||
}
|
||||
|
||||
case 0: operation_ = Operation::INC; break;
|
||||
case 1: operation_ = Operation::DEC; break;
|
||||
case 2: operation_ = Operation::CALLN; break;
|
||||
case 3:
|
||||
operation_ = Operation::CALLF;
|
||||
operand_size_ = 4;
|
||||
source_ = Source::Immediate;
|
||||
break;
|
||||
case 4: operation_ = Operation::JMPN; break;
|
||||
case 5:
|
||||
operation_ = Operation::JMPF;
|
||||
operand_size_ = 4;
|
||||
source_ = Source::Immediate;
|
||||
break;
|
||||
case 6: operation_ = Operation::PUSH; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case ModRegRMFormat::MemRegPOP:
|
||||
source_ = destination_ = memreg;
|
||||
|
||||
if(reg != 0) {
|
||||
reset_parsing();
|
||||
return std::make_pair(consumed_, Instruction());
|
||||
}
|
||||
break;
|
||||
|
||||
case ModRegRMFormat::MemRegMOV:
|
||||
source_ = Source::Immediate;
|
||||
destination_ = memreg;
|
||||
operand_size_ = operation_size_;
|
||||
break;
|
||||
|
||||
case ModRegRMFormat::MemRegADD_to_CMP:
|
||||
destination_ = memreg;
|
||||
operand_size_ = operation_size_;
|
||||
|
||||
switch(reg) {
|
||||
default: operation_ = Operation::ADD; break;
|
||||
case 1: operation_ = Operation::OR; break;
|
||||
case 2: operation_ = Operation::ADC; break;
|
||||
case 3: operation_ = Operation::SBB; break;
|
||||
case 4: operation_ = Operation::AND; break;
|
||||
case 5: operation_ = Operation::SUB; break;
|
||||
case 6: operation_ = Operation::XOR; break;
|
||||
case 7: operation_ = Operation::CMP; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case ModRegRMFormat::MemRegADC_to_CMP:
|
||||
destination_ = memreg;
|
||||
source_ = Source::Immediate;
|
||||
operand_size_ = 1; // ... and always 1; it'll be sign extended if
|
||||
// the operation requires it.
|
||||
|
||||
switch(reg) {
|
||||
default: {
|
||||
const auto result = std::make_pair(consumed_, Instruction());
|
||||
reset_parsing();
|
||||
return result;
|
||||
}
|
||||
|
||||
case 0: operation_ = Operation::ADD; break;
|
||||
case 2: operation_ = Operation::ADC; break;
|
||||
case 3: operation_ = Operation::SBB; break;
|
||||
case 5: operation_ = Operation::SUB; break;
|
||||
case 7: operation_ = Operation::CMP; break;
|
||||
}
|
||||
break;
|
||||
|
||||
default: assert(false);
|
||||
}
|
||||
|
||||
phase_ = (displacement_size_ + operand_size_) ? Phase::AwaitingDisplacementOrOperand : Phase::ReadyToPost;
|
||||
}
|
||||
|
||||
// MARK: - Displacement and operand.
|
||||
|
||||
if(phase_ == Phase::AwaitingDisplacementOrOperand && source != end) {
|
||||
const int required_bytes = displacement_size_ + operand_size_;
|
||||
|
||||
const int outstanding_bytes = required_bytes - operand_bytes_;
|
||||
const int bytes_to_consume = std::min(int(end - source), outstanding_bytes);
|
||||
|
||||
// TODO: I can surely do better than this?
|
||||
for(int c = 0; c < bytes_to_consume; c++) {
|
||||
inward_data_ = (inward_data_ >> 8) | (uint64_t(source[0]) << 56);
|
||||
++source;
|
||||
}
|
||||
|
||||
consumed_ += bytes_to_consume;
|
||||
operand_bytes_ += bytes_to_consume;
|
||||
|
||||
if(bytes_to_consume == outstanding_bytes) {
|
||||
phase_ = Phase::ReadyToPost;
|
||||
|
||||
switch(operand_size_) {
|
||||
default: operand_ = 0; break;
|
||||
case 1:
|
||||
operand_ = inward_data_ >> 56; inward_data_ <<= 8;
|
||||
|
||||
// Sign extend if a single byte operand is feeding a two-byte instruction.
|
||||
if(operation_size_ == 2 && operation_ != Operation::IN && operation_ != Operation::OUT) {
|
||||
operand_ |= (operand_ & 0x80) ? 0xff00 : 0x0000;
|
||||
}
|
||||
break;
|
||||
case 4: displacement_size_ = 2; [[fallthrough]];
|
||||
case 2: operand_ = inward_data_ >> 48; inward_data_ <<= 16; break;
|
||||
break;
|
||||
}
|
||||
switch(displacement_size_) {
|
||||
default: displacement_ = 0; break;
|
||||
case 1: displacement_ = int8_t(inward_data_ >> 56); break;
|
||||
case 2: displacement_ = int16_t(inward_data_ >> 48); break;
|
||||
}
|
||||
} else {
|
||||
// Provide a genuine measure of further bytes required.
|
||||
return std::make_pair(-(outstanding_bytes - bytes_to_consume), Instruction());
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Check for completion.
|
||||
|
||||
if(phase_ == Phase::ReadyToPost) {
|
||||
const auto result = std::make_pair(
|
||||
consumed_,
|
||||
Instruction(
|
||||
operation_,
|
||||
source_,
|
||||
destination_,
|
||||
lock_,
|
||||
segment_override_,
|
||||
repetition_,
|
||||
Size(operation_size_),
|
||||
displacement_,
|
||||
operand_)
|
||||
);
|
||||
reset_parsing();
|
||||
return result;
|
||||
}
|
||||
|
||||
// i.e. not done yet.
|
||||
return std::make_pair(0, Instruction());
|
||||
}
|
||||
155
InstructionSets/x86/Decoder.hpp
Normal file
155
InstructionSets/x86/Decoder.hpp
Normal file
@@ -0,0 +1,155 @@
|
||||
//
|
||||
// Decoder.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 01/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef InstructionSets_x86_Decoder_hpp
|
||||
#define InstructionSets_x86_Decoder_hpp
|
||||
|
||||
#include "Instruction.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
|
||||
namespace InstructionSet {
|
||||
namespace x86 {
|
||||
|
||||
enum class Model {
|
||||
i8086,
|
||||
};
|
||||
|
||||
/*!
|
||||
Implements Intel x86 instruction decoding.
|
||||
|
||||
This is an experimental implementation; it has not yet undergone significant testing.
|
||||
*/
|
||||
class Decoder {
|
||||
public:
|
||||
Decoder(Model model);
|
||||
|
||||
/*!
|
||||
@returns an @c Instruction plus a size; a positive size to indicate successful decoding; a
|
||||
negative size specifies the [negatived] number of further bytes the caller should ideally
|
||||
collect before calling again. The caller is free to call with fewer, but may not get a decoded
|
||||
instruction in response, and the decoder may still not be able to complete decoding
|
||||
even if given that number of bytes.
|
||||
*/
|
||||
std::pair<int, Instruction> decode(const uint8_t *source, size_t length);
|
||||
|
||||
private:
|
||||
enum class Phase {
|
||||
/// Captures all prefixes and continues until an instruction byte is encountered.
|
||||
Instruction,
|
||||
/// Receives a ModRegRM byte and either populates the source_ and dest_ fields appropriately
|
||||
/// or completes decoding of the instruction, as per the instruction format.
|
||||
ModRegRM,
|
||||
/// Waits for sufficiently many bytes to pass for the required displacement and operand to be captured.
|
||||
/// Cf. displacement_size_ and operand_size_.
|
||||
AwaitingDisplacementOrOperand,
|
||||
/// Forms and returns an Instruction, and resets parsing state.
|
||||
ReadyToPost
|
||||
} phase_ = Phase::Instruction;
|
||||
|
||||
/// During the ModRegRM phase, format dictates interpretation of the ModRegRM byte.
|
||||
///
|
||||
/// During the ReadyToPost phase, format determines how transiently-recorded fields
|
||||
/// are packaged into an Instruction.
|
||||
enum class ModRegRMFormat: uint8_t {
|
||||
// Parse the ModRegRM for mode, register and register/memory fields
|
||||
// and populate the source_ and destination_ fields appropriate.
|
||||
MemReg_Reg,
|
||||
Reg_MemReg,
|
||||
|
||||
// Parse for mode and register/memory fields, populating both
|
||||
// source_ and destination_ fields with the result. Use the 'register'
|
||||
// field to pick an operation from the TEST/NOT/NEG/MUL/IMUL/DIV/IDIV group.
|
||||
MemRegTEST_to_IDIV,
|
||||
|
||||
// Parse for mode and register/memory fields, populating both
|
||||
// source_ and destination_ fields with the result. Use the 'register'
|
||||
// field to check for the POP operation.
|
||||
MemRegPOP,
|
||||
|
||||
// Parse for mode and register/memory fields, populating both
|
||||
// the destination_ field with the result and setting source_ to Immediate.
|
||||
// Use the 'register' field to check for the MOV operation.
|
||||
MemRegMOV,
|
||||
|
||||
// Parse for mode and register/memory fields, populating the
|
||||
// destination_ field with the result. Use the 'register' field
|
||||
// to pick an operation from the ROL/ROR/RCL/RCR/SAL/SHR/SAR group.
|
||||
MemRegROL_to_SAR,
|
||||
|
||||
// Parse for mode and register/memory fields, populating the
|
||||
// destination_ field with the result. Use the 'register' field
|
||||
// to pick an operation from the ADD/OR/ADC/SBB/AND/SUB/XOR/CMP group and
|
||||
// waits for an operand equal to the operation size.
|
||||
MemRegADD_to_CMP,
|
||||
|
||||
// Parse for mode and register/memory fields, populating the
|
||||
// source_ field with the result. Fills destination_ with a segment
|
||||
// register based on the reg field.
|
||||
SegReg,
|
||||
|
||||
// Parse for mode and register/memory fields, populating the
|
||||
// source_ and destination_ fields with the result. Uses the
|
||||
// 'register' field to pick INC or DEC.
|
||||
MemRegINC_DEC,
|
||||
|
||||
// Parse for mode and register/memory fields, populating the
|
||||
// source_ and destination_ fields with the result. Uses the
|
||||
// 'register' field to pick from INC/DEC/CALL/JMP/PUSH, altering
|
||||
// the source to ::Immediate and setting an operand size if necessary.
|
||||
MemRegINC_to_PUSH,
|
||||
|
||||
// Parse for mode and register/memory fields, populating the
|
||||
// source_ and destination_ fields with the result. Uses the
|
||||
// 'register' field to pick from ADD/ADC/SBB/SUB/CMP, altering
|
||||
// the source to ::Immediate and setting an appropriate operand size.
|
||||
MemRegADC_to_CMP,
|
||||
} modregrm_format_ = ModRegRMFormat::MemReg_Reg;
|
||||
|
||||
// Ephemeral decoding state.
|
||||
Operation operation_ = Operation::Invalid;
|
||||
uint8_t instr_ = 0x00; // TODO: is this desired, versus loading more context into ModRegRMFormat?
|
||||
int consumed_ = 0, operand_bytes_ = 0;
|
||||
|
||||
// Source and destination locations.
|
||||
Source source_ = Source::None;
|
||||
Source destination_ = Source::None;
|
||||
|
||||
// Immediate fields.
|
||||
int16_t displacement_ = 0;
|
||||
uint16_t operand_ = 0;
|
||||
uint64_t inward_data_ = 0;
|
||||
|
||||
// Facts about the instruction.
|
||||
int displacement_size_ = 0; // i.e. size of in-stream displacement, if any.
|
||||
int operand_size_ = 0; // i.e. size of in-stream operand, if any.
|
||||
int operation_size_ = 0; // i.e. size of data manipulated by the operation.
|
||||
|
||||
// Prefix capture fields.
|
||||
Repetition repetition_ = Repetition::None;
|
||||
bool lock_ = false;
|
||||
Source segment_override_ = Source::None;
|
||||
|
||||
/// Resets size capture and all fields with default values.
|
||||
void reset_parsing() {
|
||||
consumed_ = operand_bytes_ = 0;
|
||||
displacement_size_ = operand_size_ = 0;
|
||||
displacement_ = operand_ = 0;
|
||||
lock_ = false;
|
||||
segment_override_ = Source::None;
|
||||
repetition_ = Repetition::None;
|
||||
phase_ = Phase::Instruction;
|
||||
source_ = destination_ = Source::None;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* InstructionSets_x86_Decoder_hpp */
|
||||
303
InstructionSets/x86/Instruction.hpp
Normal file
303
InstructionSets/x86/Instruction.hpp
Normal file
@@ -0,0 +1,303 @@
|
||||
//
|
||||
// Instruction.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 15/01/21.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef InstructionSets_x86_Instruction_h
|
||||
#define InstructionSets_x86_Instruction_h
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace InstructionSet {
|
||||
namespace x86 {
|
||||
|
||||
/*
|
||||
Operations are documented below to establish expectations as to which
|
||||
instruction fields will be meaningful for each; this is a work-in-progress
|
||||
and may currently contain errors in the opcode descriptions — especially
|
||||
where implicit register dependencies are afoot.
|
||||
*/
|
||||
enum class Operation: uint8_t {
|
||||
Invalid,
|
||||
|
||||
/// ASCII adjust after addition; source will be AL and destination will be AX.
|
||||
AAA,
|
||||
/// ASCII adjust before division; destination will be AX and source will be a multiplier.
|
||||
AAD,
|
||||
/// ASCII adjust after multiplication; destination will be AX and source will be a divider.
|
||||
AAM,
|
||||
/// ASCII adjust after subtraction; source will be AL and destination will be AX.
|
||||
AAS,
|
||||
/// Decimal adjust after addition; source and destination will be AL.
|
||||
DAA,
|
||||
/// Decimal adjust after subtraction; source and destination will be AL.
|
||||
DAS,
|
||||
|
||||
/// Convert byte into word; source will be AL, destination will be AH.
|
||||
CBW,
|
||||
/// Convert word to double word; source will be AX and destination will be DX.
|
||||
CWD,
|
||||
|
||||
/// Escape, for a coprocessor; perform the bus cycles necessary to read the source and destination and perform a NOP.
|
||||
ESC,
|
||||
|
||||
/// Stops the processor until the next interrupt is fired.
|
||||
HLT,
|
||||
/// Waits until the WAIT input is asserted; if an interrupt occurs then it is serviced but returns to the WAIT.
|
||||
WAIT,
|
||||
|
||||
/// Add with carry; source, destination, operand and displacement will be populated appropriately.
|
||||
ADC,
|
||||
/// Add; source, destination, operand and displacement will be populated appropriately.
|
||||
ADD,
|
||||
/// Subtract with borrow; source, destination, operand and displacement will be populated appropriately.
|
||||
SBB,
|
||||
/// Subtract; source, destination, operand and displacement will be populated appropriately.
|
||||
SUB,
|
||||
/// Unsigned multiply; multiplies the source value by AX or AL, storing the result in DX:AX or AX.
|
||||
MUL,
|
||||
/// Signed multiply; multiplies the source value by AX or AL, storing the result in DX:AX or AX.
|
||||
IMUL,
|
||||
/// Unsigned divide; divide the source value by AX or AL, storing the quotient in AL and the remainder in AH.
|
||||
DIV,
|
||||
/// Signed divide; divide the source value by AX or AL, storing the quotient in AL and the remainder in AH.
|
||||
IDIV,
|
||||
|
||||
/// Increment; source, destination, operand and displacement will be populated appropriately.
|
||||
INC,
|
||||
/// Decrement; source, destination, operand and displacement will be populated appropriately.
|
||||
DEC,
|
||||
|
||||
/// Reads from the port specified by source to the destination.
|
||||
IN,
|
||||
/// Writes from the port specified by destination from the source.
|
||||
OUT,
|
||||
|
||||
// Various jumps; see the displacement to calculate targets.
|
||||
JO, JNO, JB, JNB, JE, JNE, JBE, JNBE,
|
||||
JS, JNS, JP, JNP, JL, JNL, JLE, JNLE,
|
||||
|
||||
/// Far call; see the segment() and offset() fields.
|
||||
CALLF,
|
||||
/// Displacement call; followed by a 16-bit operand providing a call offset.
|
||||
CALLD,
|
||||
/// Near call.
|
||||
CALLN,
|
||||
/// Return from interrupt.
|
||||
IRET,
|
||||
/// Near return; if source is not ::None then it will be an ::Immediate indicating how many additional bytes to remove from the stack.
|
||||
RETF,
|
||||
/// Far return; if source is not ::None then it will be an ::Immediate indicating how many additional bytes to remove from the stack.
|
||||
RETN,
|
||||
/// Near jump; if an operand is not ::None then it gives an absolute destination; otherwise see the displacement.
|
||||
JMPN,
|
||||
/// Far jump to the indicated segment and offset.
|
||||
JMPF,
|
||||
/// Relative jump performed only if CX = 0; see the displacement.
|
||||
JPCX,
|
||||
/// Generates a software interrupt of the level stated in the operand.
|
||||
INT,
|
||||
/// Generates a software interrupt of level 3.
|
||||
INT3,
|
||||
/// Generates a software interrupt of level 4 if overflow is set.
|
||||
INTO,
|
||||
|
||||
/// Load status flags to AH.
|
||||
LAHF,
|
||||
/// Load status flags from AH.
|
||||
SAHF,
|
||||
/// Load a segment and offset from the source into DS and the destination.
|
||||
LDS,
|
||||
/// Load a segment and offset from the source into ES and the destination.
|
||||
LES,
|
||||
/// Computes the effective address of the source and loads it into the destination.
|
||||
LEA,
|
||||
|
||||
/// Compare [bytes or words, per operation size]; source and destination implied to be DS:[SI] and ES:[DI].
|
||||
CMPS,
|
||||
/// Load string; reads from DS:SI into AL or AX, subject to segment override.
|
||||
LODS,
|
||||
/// Move string; moves a byte or word from DS:SI to ES:DI. If a segment override is provided, it overrides the the source.
|
||||
MOVS,
|
||||
/// Scan string; reads a byte or word from DS:SI and compares it to AL or AX.
|
||||
SCAS,
|
||||
/// Store string; store AL or AX to ES:DI.
|
||||
STOS,
|
||||
|
||||
// Perform a possibly-conditional loop, decrementing CX. See the displacement.
|
||||
LOOP, LOOPE, LOOPNE,
|
||||
|
||||
/// Loads the destination with the source.
|
||||
MOV,
|
||||
/// Negatives; source and destination point to the same thing, to negative.
|
||||
NEG,
|
||||
/// Logical NOT; source and destination point to the same thing, to negative.
|
||||
NOT,
|
||||
/// Logical AND; source, destination, operand and displacement will be populated appropriately.
|
||||
AND,
|
||||
/// Logical OR of source onto destination.
|
||||
OR,
|
||||
/// Logical XOR of source onto destination.
|
||||
XOR,
|
||||
/// NOP; no further fields.
|
||||
NOP,
|
||||
/// POP from the stack to destination.
|
||||
POP,
|
||||
/// POP from the stack to the flags register.
|
||||
POPF,
|
||||
/// PUSH the source to the stack.
|
||||
PUSH,
|
||||
/// PUSH the flags register to the stack.
|
||||
PUSHF,
|
||||
/// Rotate the destination left through carry the number of bits indicated by source.
|
||||
RCL,
|
||||
/// Rotate the destination right through carry the number of bits indicated by source.
|
||||
RCR,
|
||||
/// Rotate the destination left the number of bits indicated by source.
|
||||
ROL,
|
||||
/// Rotate the destination right the number of bits indicated by source.
|
||||
ROR,
|
||||
/// Arithmetic shift left the destination by the number of bits indicated by source.
|
||||
SAL,
|
||||
/// Arithmetic shift right the destination by the number of bits indicated by source.
|
||||
SAR,
|
||||
/// Logical shift right the destination by the number of bits indicated by source.
|
||||
SHR,
|
||||
|
||||
/// Clear carry flag; no source or destination provided.
|
||||
CLC,
|
||||
/// Clear direction flag; no source or destination provided.
|
||||
CLD,
|
||||
/// Clear interrupt flag; no source or destination provided.
|
||||
CLI,
|
||||
/// Set carry flag.
|
||||
STC,
|
||||
/// Set decimal flag.
|
||||
STD,
|
||||
/// Set interrupt flag.
|
||||
STI,
|
||||
/// Complement carry flag; no source or destination provided.
|
||||
CMC,
|
||||
|
||||
/// Compare; source, destination, operand and displacement will be populated appropriately.
|
||||
CMP,
|
||||
/// Sets flags based on the result of a logical AND of source and destination.
|
||||
TEST,
|
||||
|
||||
/// Exchanges the contents of the source and destination.
|
||||
XCHG,
|
||||
|
||||
/// Load AL with DS:[AL+BX].
|
||||
XLAT,
|
||||
};
|
||||
|
||||
enum class Size: uint8_t {
|
||||
Implied = 0,
|
||||
Byte = 1,
|
||||
Word = 2,
|
||||
DWord = 4,
|
||||
};
|
||||
|
||||
enum class Source: uint8_t {
|
||||
None,
|
||||
CS, DS, ES, SS,
|
||||
|
||||
AL, AH, AX,
|
||||
BL, BH, BX,
|
||||
CL, CH, CX,
|
||||
DL, DH, DX,
|
||||
|
||||
SI, DI,
|
||||
BP, SP,
|
||||
|
||||
IndBXPlusSI,
|
||||
IndBXPlusDI,
|
||||
IndBPPlusSI,
|
||||
IndBPPlusDI,
|
||||
IndSI,
|
||||
IndDI,
|
||||
DirectAddress,
|
||||
IndBP,
|
||||
IndBX,
|
||||
|
||||
Immediate
|
||||
};
|
||||
|
||||
enum class Repetition: uint8_t {
|
||||
None, RepE, RepNE
|
||||
};
|
||||
|
||||
class Instruction {
|
||||
public:
|
||||
Operation operation = Operation::Invalid;
|
||||
|
||||
bool operator ==(const Instruction &rhs) const {
|
||||
return
|
||||
repetition_size_ == rhs.repetition_size_ &&
|
||||
sources_ == rhs.sources_ &&
|
||||
displacement_ == rhs.displacement_ &&
|
||||
operand_ == rhs.operand_;
|
||||
}
|
||||
|
||||
private:
|
||||
// b0, b1: a Repetition;
|
||||
// b2+: operation size.
|
||||
uint8_t repetition_size_ = 0;
|
||||
|
||||
// b0–b5: source;
|
||||
// b6–b11: destination;
|
||||
// b12–b14: segment override;
|
||||
// b15: lock.
|
||||
uint16_t sources_ = 0;
|
||||
|
||||
// Unpackable fields.
|
||||
int16_t displacement_ = 0;
|
||||
uint16_t operand_ = 0; // ... or used to store a segment for far operations.
|
||||
|
||||
public:
|
||||
Source source() const { return Source(sources_ & 0x3f); }
|
||||
Source destination() const { return Source((sources_ >> 6) & 0x3f); }
|
||||
bool lock() const { return sources_ & 0x8000; }
|
||||
Source segment_override() const { return Source((sources_ >> 12) & 7); }
|
||||
|
||||
Repetition repetition() const { return Repetition(repetition_size_ & 3); }
|
||||
Size operation_size() const { return Size(repetition_size_ >> 2); }
|
||||
|
||||
uint16_t segment() const { return uint16_t(operand_); }
|
||||
uint16_t offset() const { return uint16_t(displacement_); }
|
||||
|
||||
int16_t displacement() const { return displacement_; }
|
||||
uint16_t operand() const { return operand_; }
|
||||
|
||||
Instruction() noexcept {}
|
||||
Instruction(
|
||||
Operation operation,
|
||||
Source source,
|
||||
Source destination,
|
||||
bool lock,
|
||||
Source segment_override,
|
||||
Repetition repetition,
|
||||
Size operation_size,
|
||||
int16_t displacement,
|
||||
uint16_t operand) noexcept :
|
||||
operation(operation),
|
||||
repetition_size_(uint8_t((int(operation_size) << 2) | int(repetition))),
|
||||
sources_(uint16_t(
|
||||
int(source) |
|
||||
(int(destination) << 6) |
|
||||
(int(segment_override) << 12) |
|
||||
(int(lock) << 15)
|
||||
)),
|
||||
displacement_(displacement),
|
||||
operand_(operand) {}
|
||||
};
|
||||
|
||||
static_assert(sizeof(Instruction) <= 8);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* InstructionSets_x86_Instruction_h */
|
||||
@@ -9,12 +9,12 @@
|
||||
#include "AmstradCPC.hpp"
|
||||
|
||||
#include "Keyboard.hpp"
|
||||
#include "FDC.hpp"
|
||||
|
||||
#include "../../Processors/Z80/Z80.hpp"
|
||||
|
||||
#include "../../Components/6845/CRTC6845.hpp"
|
||||
#include "../../Components/8255/i8255.hpp"
|
||||
#include "../../Components/8272/i8272.hpp"
|
||||
#include "../../Components/AY38910/AY38910.hpp"
|
||||
|
||||
#include "../Utility/MemoryFuzzer.hpp"
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "../MachineTypes.hpp"
|
||||
|
||||
#include "../../Storage/Tape/Tape.hpp"
|
||||
#include "../../Storage/Tape/Parsers/Spectrum.hpp"
|
||||
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
@@ -31,6 +32,8 @@
|
||||
|
||||
#include "../../Analyser/Static/AmstradCPC/Target.hpp"
|
||||
|
||||
#include "../../Numeric/CRC.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
@@ -673,37 +676,6 @@ class KeyboardState: public GI::AY38910::PortHandler {
|
||||
};
|
||||
};
|
||||
|
||||
/*!
|
||||
Wraps the 8272 so as to provide proper clocking and RPM counts, and just directly
|
||||
exposes motor control, applying the same value to all drives.
|
||||
*/
|
||||
class FDC: public Intel::i8272::i8272 {
|
||||
private:
|
||||
Intel::i8272::BusHandler bus_handler_;
|
||||
|
||||
public:
|
||||
FDC() : i8272(bus_handler_, Cycles(8000000)) {
|
||||
emplace_drive(8000000, 300, 1);
|
||||
set_drive(1);
|
||||
}
|
||||
|
||||
void set_motor_on(bool on) {
|
||||
get_drive().set_motor_on(on);
|
||||
}
|
||||
|
||||
void select_drive(int) {
|
||||
// TODO: support more than one drive. (and in set_disk)
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int) {
|
||||
get_drive().set_disk(disk);
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) {
|
||||
get_drive().set_activity_observer(observer, "Drive 1", true);
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides the mechanism of receipt for input and output of the 8255's various ports.
|
||||
*/
|
||||
@@ -915,6 +887,59 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||||
switch(cycle.operation) {
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
|
||||
// TODO: just capturing byte reads as below doesn't seem to do that much in terms of acceleration;
|
||||
// I'm not immediately clear whether that's just because the machine still has to sit through
|
||||
// pilot tone in real time, or just that almost no software uses the ROM loader.
|
||||
if(use_fast_tape_hack_ && address == tape_read_byte_address && read_pointers_[0] == roms_[ROMType::OS].data()) {
|
||||
using Parser = Storage::Tape::ZXSpectrum::Parser;
|
||||
Parser parser(Parser::MachineType::AmstradCPC);
|
||||
|
||||
const auto speed = read_pointers_[tape_speed_value_address >> 14][tape_speed_value_address & 16383];
|
||||
parser.set_cpc_read_speed(speed);
|
||||
|
||||
// Seed with the current pulse; the CPC will have finished the
|
||||
// preceding symbol and be a short way into the pulse that should determine the
|
||||
// first bit of this byte.
|
||||
parser.process_pulse(tape_player_.get_current_pulse());
|
||||
const auto byte = parser.get_byte(tape_player_.get_tape());
|
||||
auto flags = z80_.get_value_of_register(CPU::Z80::Register::Flags);
|
||||
|
||||
if(byte) {
|
||||
// In A ROM-esque fashion, begin the first pulse after the final one
|
||||
// that was just consumed.
|
||||
tape_player_.complete_pulse();
|
||||
|
||||
// Update in-memory CRC.
|
||||
auto crc_value =
|
||||
uint16_t(
|
||||
read_pointers_[tape_crc_address >> 14][tape_crc_address & 16383] |
|
||||
(read_pointers_[(tape_crc_address+1) >> 14][(tape_crc_address+1) & 16383] << 8)
|
||||
);
|
||||
|
||||
tape_crc_.set_value(crc_value);
|
||||
tape_crc_.add(*byte);
|
||||
crc_value = tape_crc_.get_value();
|
||||
|
||||
write_pointers_[tape_crc_address >> 14][tape_crc_address & 16383] = uint8_t(crc_value);
|
||||
write_pointers_[(tape_crc_address+1) >> 14][(tape_crc_address+1) & 16383] = uint8_t(crc_value >> 8);
|
||||
|
||||
// Indicate successful byte read.
|
||||
z80_.set_value_of_register(CPU::Z80::Register::A, *byte);
|
||||
flags |= CPU::Z80::Flag::Carry;
|
||||
} else {
|
||||
// TODO: return tape player to previous state and decline to serve.
|
||||
z80_.set_value_of_register(CPU::Z80::Register::A, 0);
|
||||
flags &= ~CPU::Z80::Flag::Carry;
|
||||
}
|
||||
z80_.set_value_of_register(CPU::Z80::Register::Flags, flags);
|
||||
|
||||
// RET.
|
||||
*cycle.value = 0xc9;
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
*cycle.value = read_pointers_[address >> 14][address & 16383];
|
||||
break;
|
||||
@@ -965,6 +990,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
// Default to nothing answering
|
||||
*cycle.value = 0xff;
|
||||
@@ -1062,6 +1088,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
// If there are any tapes supplied, use the first of them.
|
||||
if(!media.tapes.empty()) {
|
||||
tape_player_.set_tape(media.tapes.front());
|
||||
set_use_fast_tape_hack();
|
||||
}
|
||||
|
||||
// Insert up to four disks.
|
||||
@@ -1089,7 +1116,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
||||
}
|
||||
|
||||
HalfCycles get_typer_delay() const final {
|
||||
HalfCycles get_typer_delay(const std::string &) const final {
|
||||
return z80_.get_is_resetting() ? Cycles(3'400'000) : Cycles(0);
|
||||
}
|
||||
|
||||
@@ -1114,18 +1141,22 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer([[maybe_unused]] Activity::Observer *observer) final {
|
||||
if constexpr (has_fdc) fdc_.set_activity_observer(observer);
|
||||
tape_player_.set_activity_observer(observer);
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::unique_ptr<Reflection::Struct> get_options() final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
options->quickload = allow_fast_tape_hack_;
|
||||
return options;
|
||||
}
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
set_video_signal_configurable(options->output);
|
||||
allow_fast_tape_hack_ = options->quickload;
|
||||
set_use_fast_tape_hack();
|
||||
}
|
||||
|
||||
// MARK: - Joysticks
|
||||
@@ -1188,7 +1219,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
i8255PortHandler i8255_port_handler_;
|
||||
Intel::i8255::i8255<i8255PortHandler> i8255_;
|
||||
|
||||
FDC fdc_;
|
||||
Amstrad::FDC fdc_;
|
||||
HalfCycles time_since_fdc_update_;
|
||||
void flush_fdc() {
|
||||
if constexpr (has_fdc) {
|
||||
@@ -1203,6 +1234,18 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
InterruptTimer interrupt_timer_;
|
||||
Storage::Tape::BinaryTapePlayer tape_player_;
|
||||
|
||||
// By luck these values are the same between the 664 and the 6128;
|
||||
// therefore the has_fdc template flag is sufficient to locate them.
|
||||
static constexpr uint16_t tape_read_byte_address = has_fdc ? 0x2b20 : 0x29b0;
|
||||
static constexpr uint16_t tape_speed_value_address = has_fdc ? 0xb1e7 : 0xbc8f;
|
||||
static constexpr uint16_t tape_crc_address = has_fdc ? 0xb1eb : 0xb8d3;
|
||||
CRC::CCITT tape_crc_;
|
||||
bool use_fast_tape_hack_ = false;
|
||||
bool allow_fast_tape_hack_ = false;
|
||||
void set_use_fast_tape_hack() {
|
||||
use_fast_tape_hack_ = allow_fast_tape_hack_ && tape_player_.has_tape();
|
||||
}
|
||||
|
||||
HalfCycles clock_offset_;
|
||||
HalfCycles crtc_counter_;
|
||||
HalfCycles half_cycles_since_ay_update_;
|
||||
@@ -1219,7 +1262,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
ROMType upper_rom_;
|
||||
|
||||
uint8_t *ram_pages_[4];
|
||||
uint8_t *read_pointers_[4];
|
||||
const uint8_t *read_pointers_[4];
|
||||
uint8_t *write_pointers_[4];
|
||||
|
||||
KeyboardState key_state_;
|
||||
|
||||
@@ -29,12 +29,21 @@ class Machine {
|
||||
static Machine *AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
|
||||
/// Defines the runtime options available for an Amstrad CPC.
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||
class Options:
|
||||
public Reflection::StructImpl<Options>,
|
||||
public Configurable::DisplayOption<Options>,
|
||||
public Configurable::QuickloadOption<Options>
|
||||
{
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType) : Configurable::DisplayOption<Options>(Configurable::Display::RGB) {
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(Configurable::Display::RGB),
|
||||
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly)
|
||||
{
|
||||
if(needs_declare()) {
|
||||
declare_display_option();
|
||||
declare_quickload_option();
|
||||
limit_enum(&output, Configurable::Display::RGB, Configurable::Display::CompositeColour, -1);
|
||||
}
|
||||
}
|
||||
|
||||
51
Machines/AmstradCPC/FDC.hpp
Normal file
51
Machines/AmstradCPC/FDC.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// FDC.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/03/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef FDC_h
|
||||
#define FDC_h
|
||||
|
||||
#include "../../Components/8272/i8272.hpp"
|
||||
|
||||
namespace Amstrad {
|
||||
|
||||
/*!
|
||||
Wraps the 8272 so as to provide proper clocking and RPM counts, and just directly
|
||||
exposes motor control, applying the same value to all drives.
|
||||
*/
|
||||
class FDC: public Intel::i8272::i8272 {
|
||||
private:
|
||||
Intel::i8272::BusHandler bus_handler_;
|
||||
|
||||
public:
|
||||
FDC(Cycles clock_rate = Cycles(8000000)) :
|
||||
i8272(bus_handler_, clock_rate)
|
||||
{
|
||||
emplace_drive(clock_rate.as<int>(), 300, 1);
|
||||
set_drive(1);
|
||||
}
|
||||
|
||||
void set_motor_on(bool on) {
|
||||
get_drive().set_motor_on(on);
|
||||
}
|
||||
|
||||
void select_drive(int) {
|
||||
// TODO: support more than one drive. (and in set_disk)
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int) {
|
||||
get_drive().set_disk(disk);
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) {
|
||||
get_drive().set_activity_observer(observer, "Drive 1", true);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* FDC_h */
|
||||
113
Machines/Apple/ADB/Bus.cpp
Normal file
113
Machines/Apple/ADB/Bus.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// Bus.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 10/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Bus.hpp"
|
||||
|
||||
using namespace Apple::ADB;
|
||||
|
||||
Bus::Bus(HalfCycles clock_speed) : half_cycles_to_microseconds_(1'000'000.0 / clock_speed.as<double>()) {}
|
||||
|
||||
void Bus::run_for(HalfCycles duration) {
|
||||
time_in_state_ += duration;
|
||||
time_since_get_state_ += duration;
|
||||
}
|
||||
|
||||
void Bus::set_device_output(size_t device_id, bool output) {
|
||||
// Modify the all-devices bus state.
|
||||
bus_state_[device_id] = output;
|
||||
|
||||
// React to signal edges only; don't use get_state here to avoid
|
||||
// endless recursion should any reactive devices set new output
|
||||
// during the various calls made below.
|
||||
const bool data_level = bus_state_.all();
|
||||
if(data_level_ != data_level) {
|
||||
data_level_ = data_level;
|
||||
|
||||
if(data_level) {
|
||||
// This was a transition to high; classify what just happened according to
|
||||
// the duration of the low period.
|
||||
const double low_microseconds = time_in_state_.as<double>() * half_cycles_to_microseconds_;
|
||||
|
||||
// Low periods:
|
||||
// (partly as adapted from the AN591 data sheet; otherwise from the IIgs reference manual)
|
||||
//
|
||||
// > 1040 µs reset
|
||||
// 560–1040 µs attention
|
||||
// < 50 µs 1
|
||||
// 50–72 µs 0
|
||||
// 300 µs service request
|
||||
if(low_microseconds > 1040.0) {
|
||||
for(auto device: devices_) {
|
||||
device->adb_bus_did_observe_event(Event::Reset);
|
||||
}
|
||||
} else if(low_microseconds >= 560.0) {
|
||||
for(auto device: devices_) {
|
||||
device->adb_bus_did_observe_event(Event::Attention);
|
||||
}
|
||||
shift_register_ = 1;
|
||||
start_target_ = 9; // Consume the stop bit before posting the next byte.
|
||||
phase_ = Phase::AttentionCapture;
|
||||
} else if(low_microseconds < 50.0) {
|
||||
shift(1);
|
||||
} else if(low_microseconds < 72.0) {
|
||||
shift(0);
|
||||
} else if(low_microseconds >= 291.0 && low_microseconds <= 309.0) {
|
||||
for(auto device: devices_) {
|
||||
device->adb_bus_did_observe_event(Event::ServiceRequest);
|
||||
}
|
||||
} else {
|
||||
for(auto device: devices_) {
|
||||
device->adb_bus_did_observe_event(Event::Unrecognised);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time_in_state_ = HalfCycles(0);
|
||||
}
|
||||
}
|
||||
|
||||
void Bus::shift(unsigned int value) {
|
||||
shift_register_ = (shift_register_ << 1) | value;
|
||||
|
||||
// Trigger a byte whenever either:
|
||||
// * a 'start bit' hits bit 8; or
|
||||
// * if this was a command byte, wait for the stop bit (i.e. the start bit hits 9).
|
||||
if(shift_register_ & (1 << start_target_)) {
|
||||
for(auto device: devices_) {
|
||||
device->adb_bus_did_observe_event(Event::Byte, uint8_t(shift_register_ >> (start_target_ - 8)));
|
||||
}
|
||||
|
||||
// Expect a real start bit only if moving from attention capture to packet
|
||||
// capture. Otherwise adopt an implied start bit.
|
||||
shift_register_ = phase_ == Phase::PacketCapture;
|
||||
start_target_ = 8;
|
||||
phase_ = Phase::PacketCapture;
|
||||
}
|
||||
}
|
||||
|
||||
bool Bus::get_state() const {
|
||||
const auto microseconds = time_since_get_state_.as<double>() * half_cycles_to_microseconds_;
|
||||
time_since_get_state_ = HalfCycles(0);
|
||||
|
||||
const bool current_level = bus_state_.all();
|
||||
for(auto device: devices_) {
|
||||
device->advance_state(microseconds, current_level);
|
||||
}
|
||||
return bus_state_.all();
|
||||
}
|
||||
|
||||
size_t Bus::add_device() {
|
||||
const size_t id = next_device_id_;
|
||||
++next_device_id_;
|
||||
return id;
|
||||
}
|
||||
|
||||
size_t Bus::add_device(Device *device) {
|
||||
devices_.push_back(device);
|
||||
return add_device();
|
||||
}
|
||||
172
Machines/Apple/ADB/Bus.hpp
Normal file
172
Machines/Apple/ADB/Bus.hpp
Normal file
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// Bus.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 10/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Bus_hpp
|
||||
#define Bus_hpp
|
||||
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
#include <bitset>
|
||||
#include <cstddef>
|
||||
#include <ostream>
|
||||
#include <vector>
|
||||
|
||||
namespace Apple {
|
||||
namespace ADB {
|
||||
|
||||
struct Command {
|
||||
enum class Type {
|
||||
Reset,
|
||||
Flush,
|
||||
Reserved,
|
||||
/// The host wishes the device to store register contents.
|
||||
Listen,
|
||||
/// The host wishes the device to broadcast register contents.
|
||||
Talk
|
||||
};
|
||||
static constexpr uint8_t AllDevices = 0xff;
|
||||
static constexpr uint8_t NoRegister = 0xff;
|
||||
|
||||
Type type = Type::Reserved;
|
||||
uint8_t device = AllDevices;
|
||||
uint8_t reg = NoRegister;
|
||||
|
||||
Command() {}
|
||||
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) {}
|
||||
};
|
||||
|
||||
inline std::ostream &operator <<(std::ostream &stream, Command::Type type) {
|
||||
switch(type) {
|
||||
case Command::Type::Reset: stream << "reset"; break;
|
||||
case Command::Type::Flush: stream << "flush"; break;
|
||||
case Command::Type::Listen: stream << "listen"; break;
|
||||
case Command::Type::Talk: stream << "talk"; break;
|
||||
default: stream << "reserved"; break;
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
inline std::ostream &operator <<(std::ostream &stream, Command command) {
|
||||
stream << "Command {";
|
||||
if(command.device != 0xff) stream << "device " << int(command.device) << ", ";
|
||||
if(command.reg != 0xff) stream << "register " << int(command.reg) << ", ";
|
||||
stream << command.type;
|
||||
stream << "}";
|
||||
return stream;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The @c Command encoded in @c code.
|
||||
*/
|
||||
inline Command decode_command(uint8_t code) {
|
||||
switch(code & 0x0f) {
|
||||
default: return Command();
|
||||
|
||||
case 0: return Command(Command::Type::Reset);
|
||||
case 1: return Command(Command::Type::Flush, code >> 4);
|
||||
|
||||
case 8: case 9: case 10: case 11:
|
||||
return Command(Command::Type::Listen, code >> 4, code & 3);
|
||||
|
||||
case 12: case 13: case 14: case 15:
|
||||
return Command(Command::Type::Talk, code >> 4, code & 3);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
The ADB bus models the data line of the ADB bus; it allows multiple devices to
|
||||
post their current data level, or read the current level, and also offers a tokenised
|
||||
version of all activity on the bus.
|
||||
|
||||
In implementation terms, two types of device are envisaged:
|
||||
|
||||
* proactive devices, which use @c add_device() and then merely @c set_device_output
|
||||
and @c get_state() as required, according to their own tracking of time; and
|
||||
|
||||
* reactive devices, which use @c add_device(Device*) and then merely react to
|
||||
@c adb_bus_did_observe_event and @c advance_state in order to
|
||||
update @c set_device_output.
|
||||
*/
|
||||
class Bus {
|
||||
public:
|
||||
Bus(HalfCycles clock_speed);
|
||||
|
||||
/*!
|
||||
Advances time; ADB is a clocked serial signal.
|
||||
*/
|
||||
void run_for(HalfCycles);
|
||||
|
||||
/*!
|
||||
Adds a device to the bus, returning the index it should use
|
||||
to refer to itself in subsequent calls to set_device_output.
|
||||
*/
|
||||
size_t add_device();
|
||||
|
||||
/*!
|
||||
Sets the current data line output for @c device.
|
||||
*/
|
||||
void set_device_output(size_t device_id, bool output);
|
||||
|
||||
/*!
|
||||
@returns The current state of the ADB data line.
|
||||
*/
|
||||
bool get_state() const;
|
||||
|
||||
enum class Event {
|
||||
Reset,
|
||||
Attention,
|
||||
Byte,
|
||||
ServiceRequest,
|
||||
|
||||
Unrecognised
|
||||
};
|
||||
|
||||
struct Device {
|
||||
/// Reports to an observer that @c event was observed in the activity
|
||||
/// observed on this bus. If this was a byte event, that byte's value is given as @c value.
|
||||
virtual void adb_bus_did_observe_event(Event event, uint8_t value = 0xff) = 0;
|
||||
|
||||
/// Requests that the device update itself @c microseconds and, if necessary, post a
|
||||
/// new value ot @c set_device_output. This will be called only when the bus needs
|
||||
/// to reevaluate its current level. It cannot reliably be used to track the timing between
|
||||
/// observed events.
|
||||
virtual void advance_state(double microseconds, bool current_level) = 0;
|
||||
};
|
||||
/*!
|
||||
Adds a device.
|
||||
*/
|
||||
size_t add_device(Device *);
|
||||
|
||||
private:
|
||||
HalfCycles time_in_state_;
|
||||
mutable HalfCycles time_since_get_state_;
|
||||
|
||||
double half_cycles_to_microseconds_ = 1.0;
|
||||
std::vector<Device *> devices_;
|
||||
unsigned int shift_register_ = 0;
|
||||
unsigned int start_target_ = 8;
|
||||
bool data_level_ = true;
|
||||
|
||||
// ADB addressing supports at most 16 devices but that doesn't include
|
||||
// the controller. So assume a maximum of 17 connected devices.
|
||||
std::bitset<17> bus_state_{0xffffffff};
|
||||
size_t next_device_id_ = 0;
|
||||
|
||||
inline void shift(unsigned int);
|
||||
enum class Phase {
|
||||
PacketCapture,
|
||||
AttentionCapture
|
||||
} phase_ = Phase::AttentionCapture;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Bus_hpp */
|
||||
242
Machines/Apple/ADB/Keyboard.cpp
Normal file
242
Machines/Apple/ADB/Keyboard.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
//
|
||||
// Keyboard.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 13/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Keyboard.hpp"
|
||||
|
||||
using namespace Apple::ADB;
|
||||
|
||||
Keyboard::Keyboard(Bus &bus) : ReactiveDevice(bus, 2) {}
|
||||
|
||||
void Keyboard::perform_command(const Command &command) {
|
||||
switch(command.type) {
|
||||
case Command::Type::Reset:
|
||||
modifiers_ = 0xffff;
|
||||
case Command::Type::Flush: {
|
||||
std::lock_guard lock_guard(keys_mutex_);
|
||||
pending_events_.clear();
|
||||
} break;
|
||||
|
||||
case Command::Type::Talk:
|
||||
switch(command.reg) {
|
||||
case 0: {
|
||||
// Post up to two key events, or nothing if there are
|
||||
// no events pending.
|
||||
std::lock_guard lock_guard(keys_mutex_);
|
||||
|
||||
if(!pending_events_.empty()) {
|
||||
if(pending_events_.size() > 1) {
|
||||
post_response({pending_events_[0], pending_events_[1]});
|
||||
pending_events_.erase(pending_events_.begin(), pending_events_.begin()+2);
|
||||
} else {
|
||||
// Two bytes are required; provide a key up of the fictional
|
||||
// key zero as the second.
|
||||
// That's arbitrary; verify with real machines.
|
||||
post_response({pending_events_[0], 0x80});
|
||||
pending_events_.clear();
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case 2: {
|
||||
std::lock_guard lock_guard(keys_mutex_);
|
||||
post_response({uint8_t(modifiers_ >> 8), uint8_t(modifiers_)});
|
||||
} break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
|
||||
case Command::Type::Listen:
|
||||
// If a listen is incoming for register 2, prepare to capture LED statuses.
|
||||
if(command.reg == 2) {
|
||||
receive_bytes(2);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void Keyboard::did_receive_data(const Command &, const std::vector<uint8_t> &data) {
|
||||
// This must be a register 2 listen; update the LED statuses.
|
||||
// TODO: and possibly display these.
|
||||
modifiers_ = (modifiers_ & 0xfff8) | (data[1] & 7);
|
||||
}
|
||||
|
||||
|
||||
bool Keyboard::set_key_pressed(Key key, bool is_pressed) {
|
||||
// ADB keyboard events: low 7 bits are a key code; bit 7 is either 0 for pressed or 1 for released.
|
||||
std::lock_guard lock_guard(keys_mutex_);
|
||||
pending_events_.push_back(uint8_t(key) | (is_pressed ? 0x00 : 0x80));
|
||||
pressed_keys_[size_t(key)] = is_pressed;
|
||||
|
||||
// Track modifier state also.
|
||||
|
||||
/*
|
||||
In all cases below: 0 = pressed/on; 1 = released/off.
|
||||
|
||||
b15: None (reserved)
|
||||
b14: Delete
|
||||
b13: Caps lock
|
||||
b12: Reset
|
||||
b11: Control
|
||||
b10: Shift
|
||||
b9: Option
|
||||
b8: Command
|
||||
|
||||
-- From here onwards, available only on the extended keyboard.
|
||||
|
||||
b7: Num lock/clear
|
||||
b6: Scroll lock
|
||||
b5–3: None (reserved)
|
||||
b2: Scroll Lock LED
|
||||
b1: Caps Lock LED
|
||||
b0: Num Lock LED
|
||||
*/
|
||||
|
||||
#define SetModifierBit(x) modifiers_ = (modifiers_ & ~x) | (is_pressed ? 0 : x);
|
||||
#define ToggleModifierBit(x) if(is_pressed) modifiers_ ^= x;
|
||||
switch(key) {
|
||||
default: break;
|
||||
case Key::Delete: SetModifierBit(0x4000); break;
|
||||
case Key::CapsLock: ToggleModifierBit(0x2000); break;
|
||||
case Key::Power: SetModifierBit(0x1000); break;
|
||||
|
||||
case Key::LeftControl:
|
||||
case Key::RightControl:
|
||||
SetModifierBit(0x0800);
|
||||
break;
|
||||
|
||||
case Key::LeftShift:
|
||||
case Key::RightShift:
|
||||
SetModifierBit(0x0400);
|
||||
break;
|
||||
|
||||
case Key::LeftOption:
|
||||
case Key::RightOption:
|
||||
SetModifierBit(0x0200);
|
||||
break;
|
||||
|
||||
case Key::Command:
|
||||
SetModifierBit(0x0100);
|
||||
break;
|
||||
|
||||
case Key::KeypadClear: ToggleModifierBit(0x0080); break;
|
||||
case Key::Help: ToggleModifierBit(0x0040); break;
|
||||
}
|
||||
#undef SetModifierBit
|
||||
|
||||
// Ensure service occurs.
|
||||
post_service_request();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Keyboard::clear_all_keys() {
|
||||
// For all keys currently marked as down, enqueue key-up actions.
|
||||
std::lock_guard lock_guard(keys_mutex_);
|
||||
for(size_t key = 0; key < pressed_keys_.size(); key++) {
|
||||
if(pressed_keys_[key]) {
|
||||
pending_events_.push_back(0x80 | uint8_t(key));
|
||||
pressed_keys_[key] = false;
|
||||
}
|
||||
}
|
||||
if(!pending_events_.empty()) post_service_request();
|
||||
|
||||
// Mark all modifiers as released.
|
||||
modifiers_ |= 0xfff8;
|
||||
}
|
||||
|
||||
// MARK: - KeyboardMapper
|
||||
|
||||
uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const {
|
||||
using Key = Inputs::Keyboard::Key;
|
||||
using ADBKey = Apple::ADB::Key;
|
||||
switch(key) {
|
||||
default: return MachineTypes::MappedKeyboardMachine::KeyNotMapped;
|
||||
|
||||
#define Bind(x, y) case Key::x: return uint16_t(ADBKey::y)
|
||||
#define BindDirect(x) Bind(x, x)
|
||||
|
||||
BindDirect(BackTick);
|
||||
BindDirect(k1); BindDirect(k2); BindDirect(k3); BindDirect(k4); BindDirect(k5);
|
||||
BindDirect(k6); BindDirect(k7); BindDirect(k8); BindDirect(k9); BindDirect(k0);
|
||||
|
||||
BindDirect(Help);
|
||||
BindDirect(Home);
|
||||
BindDirect(PageUp);
|
||||
BindDirect(Delete);
|
||||
BindDirect(End);
|
||||
BindDirect(PageDown);
|
||||
|
||||
BindDirect(Escape);
|
||||
BindDirect(Hyphen);
|
||||
BindDirect(Equals);
|
||||
BindDirect(Backspace);
|
||||
BindDirect(Tab);
|
||||
|
||||
BindDirect(F1); BindDirect(F2); BindDirect(F3); BindDirect(F4);
|
||||
BindDirect(F5); BindDirect(F6); BindDirect(F7); BindDirect(F8);
|
||||
BindDirect(F9); BindDirect(F10); BindDirect(F11); BindDirect(F12);
|
||||
|
||||
BindDirect(Q); BindDirect(W); BindDirect(E); BindDirect(R);
|
||||
BindDirect(T); BindDirect(Y); BindDirect(U); BindDirect(I);
|
||||
BindDirect(O); BindDirect(P); BindDirect(A); BindDirect(S);
|
||||
BindDirect(D); BindDirect(F); BindDirect(G); BindDirect(H);
|
||||
BindDirect(J); BindDirect(K); BindDirect(L); BindDirect(Z);
|
||||
BindDirect(X); BindDirect(C); BindDirect(V); BindDirect(B);
|
||||
BindDirect(N); BindDirect(M);
|
||||
|
||||
BindDirect(OpenSquareBracket);
|
||||
BindDirect(CloseSquareBracket);
|
||||
BindDirect(Semicolon);
|
||||
BindDirect(Quote);
|
||||
BindDirect(Comma);
|
||||
BindDirect(FullStop);
|
||||
BindDirect(ForwardSlash);
|
||||
|
||||
BindDirect(CapsLock);
|
||||
BindDirect(LeftShift); BindDirect(RightShift);
|
||||
BindDirect(LeftControl); BindDirect(RightControl);
|
||||
BindDirect(LeftOption); BindDirect(RightOption);
|
||||
Bind(LeftMeta, Command); Bind(RightMeta, Command);
|
||||
|
||||
BindDirect(Space);
|
||||
BindDirect(Backslash);
|
||||
Bind(Enter, Return);
|
||||
|
||||
BindDirect(Left); BindDirect(Right);
|
||||
BindDirect(Up); BindDirect(Down);
|
||||
|
||||
Bind(KeypadDelete, KeypadClear);
|
||||
BindDirect(KeypadEquals);
|
||||
BindDirect(KeypadSlash);
|
||||
BindDirect(KeypadAsterisk);
|
||||
BindDirect(KeypadMinus);
|
||||
BindDirect(KeypadPlus);
|
||||
BindDirect(KeypadEnter);
|
||||
BindDirect(KeypadDecimalPoint);
|
||||
|
||||
BindDirect(Keypad9);
|
||||
BindDirect(Keypad8);
|
||||
BindDirect(Keypad7);
|
||||
BindDirect(Keypad6);
|
||||
BindDirect(Keypad5);
|
||||
BindDirect(Keypad4);
|
||||
BindDirect(Keypad3);
|
||||
BindDirect(Keypad2);
|
||||
BindDirect(Keypad1);
|
||||
BindDirect(Keypad0);
|
||||
|
||||
// Leaving unmapped:
|
||||
// Power, F13, F14, F15
|
||||
|
||||
#undef BindDirect
|
||||
#undef Bind
|
||||
}
|
||||
}
|
||||
125
Machines/Apple/ADB/Keyboard.hpp
Normal file
125
Machines/Apple/ADB/Keyboard.hpp
Normal file
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// Keyboard.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 13/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Keyboard_hpp
|
||||
#define Keyboard_hpp
|
||||
|
||||
#include "ReactiveDevice.hpp"
|
||||
#include "../../../Inputs/Keyboard.hpp"
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace Apple {
|
||||
namespace ADB {
|
||||
|
||||
/*!
|
||||
Defines the keycodes that could be passed directly via set_key_pressed; these
|
||||
are based on the Apple Extended Keyboard.
|
||||
*/
|
||||
enum class Key: uint16_t {
|
||||
/*
|
||||
These are transcribed from Page 19-11 of
|
||||
the Macintosh Family Hardware Reference.
|
||||
*/
|
||||
BackTick = 0x32,
|
||||
k1 = 0x12, k2 = 0x13, k3 = 0x14, k4 = 0x15, k5 = 0x17,
|
||||
k6 = 0x16, k7 = 0x1a, k8 = 0x1c, k9 = 0x19, k0 = 0x1d,
|
||||
|
||||
Help = 0x72,
|
||||
Home = 0x73,
|
||||
PageUp = 0x74,
|
||||
Delete = 0x75,
|
||||
End = 0x77,
|
||||
PageDown = 0x79,
|
||||
|
||||
Escape = 0x35,
|
||||
Hyphen = 0x1b,
|
||||
Equals = 0x18,
|
||||
Backspace = 0x33,
|
||||
Tab = 0x30,
|
||||
Power = 0x7f,
|
||||
|
||||
F1 = 0x7a, F2 = 0x78, F3 = 0x63, F4 = 0x76,
|
||||
F5 = 0x60, F6 = 0x61, F7 = 0x62, F8 = 0x64,
|
||||
F9 = 0x65, F10 = 0x6d, F11 = 0x67, F12 = 0x6f,
|
||||
F13 = 0x69, F14 = 0x6b, F15 = 0x71,
|
||||
|
||||
Q = 0x0c, W = 0x0d, E = 0x0e, R = 0x0f, T = 0x11, Y = 0x10, U = 0x20, I = 0x22, O = 0x1f, P = 0x23,
|
||||
A = 0x00, S = 0x01, D = 0x02, F = 0x03, G = 0x05, H = 0x04, J = 0x26, K = 0x28, L = 0x25,
|
||||
Z = 0x06, X = 0x07, C = 0x08, V = 0x09, B = 0x0b, N = 0x2d, M = 0x2e,
|
||||
|
||||
OpenSquareBracket = 0x21,
|
||||
CloseSquareBracket = 0x1e,
|
||||
Semicolon = 0x29,
|
||||
Quote = 0x27,
|
||||
Comma = 0x2b,
|
||||
FullStop = 0x2f,
|
||||
ForwardSlash = 0x2c,
|
||||
|
||||
CapsLock = 0x39,
|
||||
LeftShift = 0x38, RightShift = 0x7b,
|
||||
LeftControl = 0x36, RightControl = 0x7d,
|
||||
LeftOption = 0x3a, RightOption = 0x7c,
|
||||
Command = 0x37,
|
||||
|
||||
Space = 0x31,
|
||||
Backslash = 0x2a,
|
||||
Return = 0x24,
|
||||
|
||||
Left = 0x3b,
|
||||
Right = 0x3c,
|
||||
Up = 0x3e,
|
||||
Down = 0x3d,
|
||||
|
||||
KeypadClear = 0x47,
|
||||
KeypadEquals = 0x51,
|
||||
KeypadSlash = 0x4b,
|
||||
KeypadAsterisk = 0x43,
|
||||
KeypadMinus = 0x4e,
|
||||
KeypadPlus = 0x45,
|
||||
KeypadEnter = 0x4c,
|
||||
KeypadDecimalPoint = 0x41,
|
||||
|
||||
Keypad9 = 0x5c, Keypad8 = 0x5b, Keypad7 = 0x59,
|
||||
Keypad6 = 0x58, Keypad5 = 0x57, Keypad4 = 0x56,
|
||||
Keypad3 = 0x55, Keypad2 = 0x54, Keypad1 = 0x53,
|
||||
Keypad0 = 0x52,
|
||||
};
|
||||
|
||||
class Keyboard: public ReactiveDevice {
|
||||
public:
|
||||
Keyboard(Bus &);
|
||||
|
||||
bool set_key_pressed(Key key, bool is_pressed);
|
||||
void clear_all_keys();
|
||||
|
||||
private:
|
||||
void perform_command(const Command &command) override;
|
||||
void did_receive_data(const Command &, const std::vector<uint8_t> &) override;
|
||||
|
||||
std::mutex keys_mutex_;
|
||||
std::array<bool, 128> pressed_keys_;
|
||||
std::vector<uint8_t> pending_events_;
|
||||
uint16_t modifiers_ = 0xffff;
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides a mapping from idiomatic PC keys to ADB keys.
|
||||
*/
|
||||
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const final;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Keyboard_hpp */
|
||||
62
Machines/Apple/ADB/Mouse.cpp
Normal file
62
Machines/Apple/ADB/Mouse.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// Mouse.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 12/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Mouse.hpp"
|
||||
|
||||
using namespace Apple::ADB;
|
||||
|
||||
Mouse::Mouse(Bus &bus) : ReactiveDevice(bus, 3) {}
|
||||
|
||||
void Mouse::perform_command(const Command &command) {
|
||||
if(command.type == Command::Type::Talk && command.reg == 0) {
|
||||
// Read current deltas and buttons, thread safely.
|
||||
auto delta_x = delta_x_.exchange(0);
|
||||
auto delta_y = delta_y_.exchange(0);
|
||||
const int buttons = button_flags_;
|
||||
|
||||
// Clamp deltas.
|
||||
delta_x = std::max(std::min(delta_x, int16_t(127)), int16_t(-128));
|
||||
delta_y = std::max(std::min(delta_y, int16_t(127)), int16_t(-128));
|
||||
|
||||
// Figure out what that would look like, and don't respond if there's
|
||||
// no change to report.
|
||||
const uint16_t reg0 =
|
||||
((buttons & 1) ? 0x0000 : 0x8000) |
|
||||
((buttons & 2) ? 0x0000 : 0x0080) |
|
||||
uint16_t(delta_x & 0x7f) |
|
||||
uint16_t((delta_y & 0x7f) << 8);
|
||||
if(reg0 == last_posted_reg0_) return;
|
||||
|
||||
// Post change.
|
||||
last_posted_reg0_ = reg0;
|
||||
post_response({uint8_t(reg0 >> 8), uint8_t(reg0)});
|
||||
}
|
||||
}
|
||||
|
||||
void Mouse::move(int x, int y) {
|
||||
delta_x_ += int16_t(x);
|
||||
delta_y_ += int16_t(y);
|
||||
post_service_request();
|
||||
}
|
||||
|
||||
int Mouse::get_number_of_buttons() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
void Mouse::set_button_pressed(int index, bool is_pressed) {
|
||||
if(is_pressed)
|
||||
button_flags_ |= (1 << index);
|
||||
else
|
||||
button_flags_ &= ~(1 << index);
|
||||
post_service_request();
|
||||
}
|
||||
|
||||
void Mouse::reset_all_buttons() {
|
||||
button_flags_ = 0;
|
||||
post_service_request();
|
||||
}
|
||||
38
Machines/Apple/ADB/Mouse.hpp
Normal file
38
Machines/Apple/ADB/Mouse.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Mouse.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 12/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Mouse_hpp
|
||||
#define Mouse_hpp
|
||||
|
||||
#include "ReactiveDevice.hpp"
|
||||
#include "../../../Inputs/Mouse.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace ADB {
|
||||
|
||||
class Mouse: public ReactiveDevice, public Inputs::Mouse {
|
||||
public:
|
||||
Mouse(Bus &);
|
||||
|
||||
private:
|
||||
void perform_command(const Command &command) override;
|
||||
|
||||
void move(int x, int y) override;
|
||||
int get_number_of_buttons() override;
|
||||
void set_button_pressed(int index, bool is_pressed) override;
|
||||
void reset_all_buttons() override;
|
||||
|
||||
std::atomic<int16_t> delta_x_, delta_y_;
|
||||
std::atomic<int> button_flags_ = 0;
|
||||
uint16_t last_posted_reg0_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Mouse_hpp */
|
||||
180
Machines/Apple/ADB/ReactiveDevice.cpp
Normal file
180
Machines/Apple/ADB/ReactiveDevice.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// ReactiveDevice.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 12/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "ReactiveDevice.hpp"
|
||||
|
||||
#define LOG_PREFIX "[ADB device] "
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
using namespace Apple::ADB;
|
||||
|
||||
ReactiveDevice::ReactiveDevice(Apple::ADB::Bus &bus, uint8_t adb_device_id) :
|
||||
bus_(bus),
|
||||
device_id_(bus.add_device(this)),
|
||||
default_adb_device_id_(adb_device_id) {
|
||||
reset();
|
||||
}
|
||||
|
||||
void ReactiveDevice::post_response(const std::vector<uint8_t> &&response) {
|
||||
response_ = std::move(response);
|
||||
microseconds_at_bit_ = 0.0;
|
||||
bit_offset_ = -2;
|
||||
}
|
||||
|
||||
void ReactiveDevice::advance_state(double microseconds, bool current_level) {
|
||||
// First test: is a service request desired?
|
||||
if(phase_ == Phase::ServiceRequestPending) {
|
||||
microseconds_at_bit_ += microseconds;
|
||||
if(microseconds_at_bit_ < 240.0) {
|
||||
bus_.set_device_output(device_id_, false);
|
||||
} else {
|
||||
bus_.set_device_output(device_id_, true);
|
||||
phase_ = Phase::AwaitingAttention;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Do nothing if not in the process of posting a response.
|
||||
if(response_.empty()) return;
|
||||
|
||||
/*
|
||||
Total process below:
|
||||
|
||||
(1) assume that the data was enqueued before the stop bit had
|
||||
concluded; wait for the end of that;
|
||||
(2) wait for the stop-to-start time period;
|
||||
(3) output a start bit of '1';
|
||||
(4) output all enqueued bytes, MSB to LSB;
|
||||
(5) output a stop bit of '0'; and
|
||||
(6) return this device's output level to high and top.
|
||||
*/
|
||||
|
||||
// Wait for the bus to be clear if transmission has not yet begun.
|
||||
if(!current_level && bit_offset_ == -2) return;
|
||||
|
||||
// Advance time.
|
||||
microseconds_at_bit_ += microseconds;
|
||||
|
||||
// If this is the start of the packet, wait an appropriate stop-to-start time.
|
||||
if(bit_offset_ == -2) {
|
||||
if(microseconds_at_bit_ < 150.0) {
|
||||
return;
|
||||
}
|
||||
microseconds_at_bit_ -= 150.0;
|
||||
++bit_offset_;
|
||||
}
|
||||
|
||||
// Advance the implied number of bits.
|
||||
const int step = int(microseconds_at_bit_ / 100.0);
|
||||
bit_offset_ += step;
|
||||
microseconds_at_bit_ -= double(step * 100.0);
|
||||
|
||||
// Check for end-of-transmission.
|
||||
const int response_bit_length = int(response_.size() * 8);
|
||||
if(bit_offset_ >= 1 + response_bit_length) {
|
||||
bus_.set_device_output(device_id_, true);
|
||||
response_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise pick the bit to output: it'll either be the start bit of 1,
|
||||
// from the provided data, or a stop bit of 0.
|
||||
int bit = 0;
|
||||
if(bit_offset_ < 0) {
|
||||
bit = 1;
|
||||
} else if(bit_offset_ < response_bit_length) {
|
||||
const int byte = bit_offset_ >> 3;
|
||||
const int packet = int(response_[size_t(byte)]);
|
||||
bit = (packet >> (7 - (bit_offset_ & 7))) & 1;
|
||||
}
|
||||
|
||||
// Convert that into a level.
|
||||
constexpr double low_periods[] = {66, 33};
|
||||
bus_.set_device_output(device_id_, microseconds_at_bit_ > low_periods[bit]);
|
||||
}
|
||||
|
||||
void ReactiveDevice::adb_bus_did_observe_event(Bus::Event event, uint8_t value) {
|
||||
if(phase_ == Phase::AwaitingAttention) {
|
||||
if(event != Bus::Event::Attention) return;
|
||||
phase_ = Phase::AwaitingCommand;
|
||||
return;
|
||||
}
|
||||
|
||||
if(event != Bus::Event::Byte) return;
|
||||
|
||||
if(phase_ == Phase::AwaitingContent) {
|
||||
content_.push_back(value);
|
||||
if(content_.size() == expected_content_size_) {
|
||||
phase_ = Phase::AwaitingAttention;
|
||||
|
||||
if(command_.reg == 3) {
|
||||
register3_ = uint16_t((content_[0] << 8) | content_[1]);
|
||||
} else {
|
||||
did_receive_data(command_, content_);
|
||||
}
|
||||
content_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if(phase_ == Phase::AwaitingCommand) {
|
||||
phase_ = Phase::AwaitingAttention;
|
||||
|
||||
command_ = decode_command(value);
|
||||
// LOG(command_);
|
||||
|
||||
// If this command doesn't apply here, but a service request is requested,
|
||||
// post a service request.
|
||||
if(command_.device != Command::AllDevices && command_.device != ((register3_ >> 8) & 0xf)) {
|
||||
if(service_desired_) {
|
||||
service_desired_ = false;
|
||||
stop_has_begin_ = false;
|
||||
phase_ = Phase::ServiceRequestPending;
|
||||
microseconds_at_bit_ = 0.0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle reset and register 3 here automatically; pass everything else along.
|
||||
switch(command_.type) {
|
||||
case Command::Type::Reset:
|
||||
reset();
|
||||
[[fallthrough]];
|
||||
default:
|
||||
perform_command(command_);
|
||||
break;
|
||||
|
||||
case Command::Type::Listen:
|
||||
case Command::Type::Talk:
|
||||
if(command_.reg == 3) {
|
||||
if(command_.type == Command::Type::Talk) {
|
||||
post_response({uint8_t(register3_ >> 8), uint8_t(register3_ & 0xff)});
|
||||
} else {
|
||||
receive_bytes(2);
|
||||
}
|
||||
} else {
|
||||
service_desired_ = false;
|
||||
perform_command(command_);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReactiveDevice::receive_bytes(size_t count) {
|
||||
content_.clear();
|
||||
expected_content_size_ = count;
|
||||
phase_ = Phase::AwaitingContent;
|
||||
}
|
||||
|
||||
void ReactiveDevice::reset() {
|
||||
register3_ = uint16_t(0x6001 | (default_adb_device_id_ << 8));
|
||||
}
|
||||
|
||||
void ReactiveDevice::post_service_request() {
|
||||
service_desired_ = true;
|
||||
}
|
||||
66
Machines/Apple/ADB/ReactiveDevice.hpp
Normal file
66
Machines/Apple/ADB/ReactiveDevice.hpp
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// ReactiveDevice.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 12/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef ReactiveDevice_hpp
|
||||
#define ReactiveDevice_hpp
|
||||
|
||||
#include "Bus.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace Apple {
|
||||
namespace ADB {
|
||||
|
||||
class ReactiveDevice: public Bus::Device {
|
||||
protected:
|
||||
ReactiveDevice(Bus &bus, uint8_t adb_device_id);
|
||||
|
||||
void post_response(const std::vector<uint8_t> &&response);
|
||||
void post_service_request();
|
||||
void receive_bytes(size_t count);
|
||||
|
||||
virtual void perform_command(const Command &command) = 0;
|
||||
virtual void did_receive_data(const Command &, const std::vector<uint8_t> &) {}
|
||||
|
||||
private:
|
||||
void advance_state(double microseconds, bool current_level) override;
|
||||
void adb_bus_did_observe_event(Bus::Event event, uint8_t value) override;
|
||||
|
||||
private:
|
||||
Bus &bus_;
|
||||
const size_t device_id_;
|
||||
|
||||
std::vector<uint8_t> response_;
|
||||
int bit_offset_ = 0;
|
||||
double microseconds_at_bit_ = 0;
|
||||
|
||||
enum class Phase {
|
||||
AwaitingAttention,
|
||||
AwaitingCommand,
|
||||
AwaitingContent,
|
||||
ServiceRequestPending,
|
||||
} phase_ = Phase::AwaitingAttention;
|
||||
std::vector<uint8_t> content_;
|
||||
size_t expected_content_size_ = 0;
|
||||
Command command_;
|
||||
bool stop_has_begin_ = false;
|
||||
|
||||
uint16_t register3_;
|
||||
const uint8_t default_adb_device_id_;
|
||||
|
||||
std::atomic<bool> service_desired_ = false;
|
||||
|
||||
void reset();
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* ReactiveDevice_hpp */
|
||||
@@ -19,8 +19,11 @@
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include "AuxiliaryMemorySwitches.hpp"
|
||||
#include "Card.hpp"
|
||||
#include "DiskIICard.hpp"
|
||||
#include "Joystick.hpp"
|
||||
#include "LanguageCardSwitches.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#include "../../../Analyser/Static/AppleII/Target.hpp"
|
||||
@@ -37,6 +40,7 @@ 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:
|
||||
public Apple::II::Machine,
|
||||
public MachineTypes::TimedMachine,
|
||||
public MachineTypes::ScanProducer,
|
||||
public MachineTypes::AudioProducer,
|
||||
@@ -46,7 +50,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Inputs::Keyboard,
|
||||
public Configurable::Device,
|
||||
public Apple::II::Machine,
|
||||
public Activity::Source,
|
||||
public Apple::II::Card::Delegate {
|
||||
private:
|
||||
@@ -175,7 +178,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
to paging every 6502 page of memory independently. It makes the paging events more expensive,
|
||||
but hopefully more clear.
|
||||
*/
|
||||
uint8_t *read_pages_[256]; // each is a pointer to the 256-block of memory the CPU should read when accessing that page of memory
|
||||
const uint8_t *read_pages_[256]; // each is a pointer to the 256-block of memory the CPU should read when accessing that page of memory
|
||||
uint8_t *write_pages_[256]; // as per read_pages_, but this is where the CPU should write. If a pointer is nullptr, don't write.
|
||||
void page(int start, int end, uint8_t *read, uint8_t *write) {
|
||||
for(int position = start; position < end; ++position) {
|
||||
@@ -187,128 +190,79 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The language card.
|
||||
struct {
|
||||
bool bank1 = false;
|
||||
bool read = false;
|
||||
bool pre_write = false;
|
||||
bool write = false;
|
||||
} language_card_;
|
||||
bool has_language_card_ = true;
|
||||
// MARK: The language card.
|
||||
LanguageCardSwitches<ConcreteMachine> language_card_;
|
||||
AuxiliaryMemorySwitches<ConcreteMachine> auxiliary_switches_;
|
||||
friend LanguageCardSwitches<ConcreteMachine>;
|
||||
friend AuxiliaryMemorySwitches<ConcreteMachine>;
|
||||
|
||||
void set_language_card_paging() {
|
||||
uint8_t *const ram = alternative_zero_page_ ? aux_ram_ : ram_;
|
||||
const auto language_state = language_card_.state();
|
||||
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();
|
||||
|
||||
// Which way the region here is mapped to be banks 1 and 2 is
|
||||
// arbitrary.
|
||||
page(0xd0, 0xe0,
|
||||
language_card_.read ? &ram[language_card_.bank1 ? 0xd000 : 0xc000] : rom,
|
||||
language_card_.write ? nullptr : &ram[language_card_.bank1 ? 0xd000 : 0xc000]);
|
||||
language_state.read ? &ram[language_state.bank2 ? 0xd000 : 0xc000] : rom,
|
||||
language_state.write ? nullptr : &ram[language_state.bank2 ? 0xd000 : 0xc000]);
|
||||
|
||||
page(0xe0, 0x100,
|
||||
language_card_.read ? &ram[0xe000] : &rom[0x1000],
|
||||
language_card_.write ? nullptr : &ram[0xe000]);
|
||||
language_state.read ? &ram[0xe000] : &rom[0x1000],
|
||||
language_state.write ? nullptr : &ram[0xe000]);
|
||||
}
|
||||
|
||||
// MARK - The IIe's ROM controls.
|
||||
bool internal_CX_rom_ = false;
|
||||
bool slot_C3_rom_ = false;
|
||||
bool internal_c8_rom_ = false;
|
||||
|
||||
// MARK: Auxiliary memory and the other IIe improvements.
|
||||
void set_card_paging() {
|
||||
page(0xc1, 0xc8, internal_CX_rom_ ? rom_.data() : nullptr, nullptr);
|
||||
const auto state = auxiliary_switches_.card_state();
|
||||
|
||||
if(!internal_CX_rom_) {
|
||||
if(!slot_C3_rom_) read_pages_[0xc3] = &rom_[0xc300 - 0xc100];
|
||||
}
|
||||
|
||||
page(0xc8, 0xd0, (internal_CX_rom_ || internal_c8_rom_) ? &rom_[0xc800 - 0xc100] : nullptr, nullptr);
|
||||
page(0xc1, 0xc4, state.region_C1_C3 ? &rom_[0xc100 - 0xc100] : nullptr, nullptr);
|
||||
read_pages_[0xc3] = state.region_C3 ? &rom_[0xc300 - 0xc100] : nullptr;
|
||||
page(0xc4, 0xc8, state.region_C4_C8 ? &rom_[0xc400 - 0xc100] : nullptr, nullptr);
|
||||
page(0xc8, 0xd0, state.region_C8_D0 ? &rom_[0xc800 - 0xc100] : nullptr, nullptr);
|
||||
}
|
||||
|
||||
// MARK - The IIe's auxiliary RAM controls.
|
||||
bool alternative_zero_page_ = false;
|
||||
void set_zero_page_paging() {
|
||||
if(alternative_zero_page_) {
|
||||
read_pages_[0] = aux_ram_;
|
||||
if(auxiliary_switches_.zero_state()) {
|
||||
write_pages_[0] = aux_ram_;
|
||||
} else {
|
||||
read_pages_[0] = ram_;
|
||||
write_pages_[0] = ram_;
|
||||
}
|
||||
read_pages_[1] = read_pages_[0] + 256;
|
||||
write_pages_[0] = read_pages_[0];
|
||||
write_pages_[1] = read_pages_[1];
|
||||
write_pages_[1] = write_pages_[0] + 256;
|
||||
read_pages_[0] = write_pages_[0];
|
||||
read_pages_[1] = write_pages_[1];
|
||||
|
||||
// Zero page banking also affects interpretation of the language card's switches.
|
||||
set_language_card_paging();
|
||||
}
|
||||
|
||||
bool read_auxiliary_memory_ = false;
|
||||
bool write_auxiliary_memory_ = false;
|
||||
void set_main_paging() {
|
||||
page(0x02, 0xc0,
|
||||
read_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200],
|
||||
write_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200]);
|
||||
const auto state = auxiliary_switches_.main_state();
|
||||
|
||||
if(video_.get_80_store()) {
|
||||
bool use_aux_ram = video_.get_page2();
|
||||
page(0x04, 0x08,
|
||||
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400],
|
||||
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400]);
|
||||
page(0x02, 0x04,
|
||||
state.base.read ? &aux_ram_[0x0200] : &ram_[0x0200],
|
||||
state.base.write ? &aux_ram_[0x0200] : &ram_[0x0200]);
|
||||
page(0x08, 0x20,
|
||||
state.base.read ? &aux_ram_[0x0800] : &ram_[0x0800],
|
||||
state.base.write ? &aux_ram_[0x0800] : &ram_[0x0800]);
|
||||
page(0x40, 0xc0,
|
||||
state.base.read ? &aux_ram_[0x4000] : &ram_[0x4000],
|
||||
state.base.write ? &aux_ram_[0x4000] : &ram_[0x4000]);
|
||||
|
||||
if(video_.get_high_resolution()) {
|
||||
page(0x20, 0x40,
|
||||
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000],
|
||||
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000]);
|
||||
}
|
||||
}
|
||||
page(0x04, 0x08,
|
||||
state.region_04_08.read ? &aux_ram_[0x0400] : &ram_[0x0400],
|
||||
state.region_04_08.write ? &aux_ram_[0x0400] : &ram_[0x0400]);
|
||||
|
||||
page(0x20, 0x40,
|
||||
state.region_20_40.read ? &aux_ram_[0x2000] : &ram_[0x2000],
|
||||
state.region_20_40.write ? &aux_ram_[0x2000] : &ram_[0x2000]);
|
||||
}
|
||||
|
||||
// MARK - typing
|
||||
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
|
||||
|
||||
// MARK - joysticks
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Horizontal),
|
||||
Input(Input::Vertical),
|
||||
|
||||
// The Apple II offers three buttons between two joysticks;
|
||||
// this emulator puts three buttons on each joystick and
|
||||
// combines them.
|
||||
Input(Input::Fire, 0),
|
||||
Input(Input::Fire, 1),
|
||||
Input(Input::Fire, 2),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, float value) final {
|
||||
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
|
||||
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
|
||||
}
|
||||
|
||||
void did_set_input(const Input &input, bool value) final {
|
||||
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
|
||||
buttons[input.info.control.index] = value;
|
||||
}
|
||||
}
|
||||
|
||||
bool buttons[3] = {false, false, false};
|
||||
float axes[2] = {0.5f, 0.5f};
|
||||
};
|
||||
|
||||
// On an Apple II, the programmer strobes 0xc070 and that causes each analogue input
|
||||
// to begin a charge and discharge cycle **if they are not already charging**.
|
||||
// The greater the analogue input, the faster they will charge and therefore the sooner
|
||||
// they will discharge.
|
||||
//
|
||||
// This emulator models that with analogue_charge_ being essentially the amount of time,
|
||||
// in charge threshold units, since 0xc070 was last strobed. But if any of the analogue
|
||||
// inputs were already partially charged then they gain a bias in analogue_biases_.
|
||||
//
|
||||
// It's a little indirect, but it means only having to increment the one value in the
|
||||
// main loop.
|
||||
float analogue_charge_ = 0.0f;
|
||||
float analogue_biases_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
bool analogue_channel_is_discharged(size_t channel) {
|
||||
return (1.0f - static_cast<Joystick *>(joysticks_[channel >> 1].get())->axes[channel & 1]) < analogue_charge_ + analogue_biases_[channel];
|
||||
}
|
||||
// MARK - Joysticks.
|
||||
JoystickPair joysticks_;
|
||||
|
||||
// The IIe has three keys that are wired directly to the same input as the joystick buttons.
|
||||
bool open_apple_is_pressed_ = false;
|
||||
@@ -320,7 +274,9 @@ 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_) {
|
||||
speaker_(audio_toggle_),
|
||||
language_card_(*this),
|
||||
auxiliary_switches_(*this) {
|
||||
// The system's master clock rate.
|
||||
constexpr float master_clock = 14318180.0;
|
||||
|
||||
@@ -342,10 +298,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
Memory::Fuzz(ram_, sizeof(ram_));
|
||||
Memory::Fuzz(aux_ram_, sizeof(aux_ram_));
|
||||
|
||||
// Add a couple of joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
|
||||
// Pick the required ROMs.
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
const std::string machine_name = "AppleII";
|
||||
@@ -353,21 +305,21 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
size_t rom_size = 12*1024;
|
||||
switch(target.model) {
|
||||
default:
|
||||
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::II));
|
||||
rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588);
|
||||
break;
|
||||
case Target::Model::IIplus:
|
||||
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::II));
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26);
|
||||
break;
|
||||
case Target::Model::IIe:
|
||||
rom_size += 3840;
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1);
|
||||
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::IIe));
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d);
|
||||
break;
|
||||
case Target::Model::EnhancedIIe:
|
||||
rom_size += 3840;
|
||||
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d);
|
||||
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::EnhancedIIe));
|
||||
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942);
|
||||
break;
|
||||
}
|
||||
@@ -400,9 +352,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
// Set the whole card area to initially backed by nothing.
|
||||
page(0xc0, 0xd0, nullptr, nullptr);
|
||||
|
||||
// Set proper values for the language card/ROM area.
|
||||
set_language_card_paging();
|
||||
|
||||
insert_media(target.media);
|
||||
}
|
||||
|
||||
@@ -457,14 +406,8 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value;
|
||||
}
|
||||
|
||||
if(is_iie() && address >= 0xc300 && address < 0xd000) {
|
||||
bool internal_c8_rom = internal_c8_rom_;
|
||||
internal_c8_rom |= ((address >> 8) == 0xc3) && !slot_C3_rom_;
|
||||
internal_c8_rom &= (address != 0xcfff);
|
||||
if(internal_c8_rom != internal_c8_rom_) {
|
||||
internal_c8_rom_ = internal_c8_rom;
|
||||
set_card_paging();
|
||||
}
|
||||
if(is_iie()) {
|
||||
auxiliary_switches_.access(address, isReadOperation(operation));
|
||||
}
|
||||
} else {
|
||||
// Assume a vapour read unless it turns out otherwise; this is a little
|
||||
@@ -502,7 +445,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case 0xc061: // Switch input 0.
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
static_cast<Joystick *>(joysticks_[0].get())->buttons[0] || static_cast<Joystick *>(joysticks_[1].get())->buttons[2] ||
|
||||
joysticks_.button(0) ||
|
||||
(is_iie() && open_apple_is_pressed_)
|
||||
)
|
||||
*value |= 0x80;
|
||||
@@ -510,14 +453,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case 0xc062: // Switch input 1.
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
static_cast<Joystick *>(joysticks_[0].get())->buttons[1] || static_cast<Joystick *>(joysticks_[1].get())->buttons[1] ||
|
||||
joysticks_.button(1) ||
|
||||
(is_iie() && closed_apple_is_pressed_)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
case 0xc063: // Switch input 2.
|
||||
*value &= 0x7f;
|
||||
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[2] || static_cast<Joystick *>(joysticks_[1].get())->buttons[0])
|
||||
if(joysticks_.button(2))
|
||||
*value |= 0x80;
|
||||
break;
|
||||
|
||||
@@ -527,20 +470,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case 0xc067: { // Analogue input 3.
|
||||
const size_t input = address - 0xc064;
|
||||
*value &= 0x7f;
|
||||
if(!analogue_channel_is_discharged(input)) {
|
||||
if(!joysticks_.analogue_channel_is_discharged(input)) {
|
||||
*value |= 0x80;
|
||||
}
|
||||
} break;
|
||||
|
||||
// The IIe-only state reads follow...
|
||||
#define IIeSwitchRead(s) *value = get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
|
||||
case 0xc011: IIeSwitchRead(language_card_.bank1); break;
|
||||
case 0xc012: IIeSwitchRead(language_card_.read); break;
|
||||
case 0xc013: IIeSwitchRead(read_auxiliary_memory_); break;
|
||||
case 0xc014: IIeSwitchRead(write_auxiliary_memory_); break;
|
||||
case 0xc015: IIeSwitchRead(internal_CX_rom_); break;
|
||||
case 0xc016: IIeSwitchRead(alternative_zero_page_); break;
|
||||
case 0xc017: IIeSwitchRead(slot_C3_rom_); break;
|
||||
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;
|
||||
case 0xc014: IIeSwitchRead(auxiliary_switches_.switches().write_auxiliary_memory); break;
|
||||
case 0xc015: IIeSwitchRead(auxiliary_switches_.switches().internal_CX_rom); break;
|
||||
case 0xc016: IIeSwitchRead(auxiliary_switches_.switches().alternative_zero_page); break;
|
||||
case 0xc017: IIeSwitchRead(auxiliary_switches_.switches().slot_C3_rom); break;
|
||||
case 0xc018: IIeSwitchRead(video_.get_80_store()); break;
|
||||
case 0xc019: IIeSwitchRead(video_.get_is_vertical_blank(cycles_since_video_update_)); break;
|
||||
case 0xc01a: IIeSwitchRead(video_.get_text()); break;
|
||||
@@ -558,6 +501,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
} else {
|
||||
// Write-only switches. All IIe as currently implemented.
|
||||
if(is_iie()) {
|
||||
auxiliary_switches_.access(address, false);
|
||||
switch(address) {
|
||||
default: break;
|
||||
|
||||
@@ -565,40 +509,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case 0xc001:
|
||||
update_video();
|
||||
video_.set_80_store(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc002:
|
||||
case 0xc003:
|
||||
read_auxiliary_memory_ = !!(address&1);
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc004:
|
||||
case 0xc005:
|
||||
write_auxiliary_memory_ = !!(address&1);
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc006:
|
||||
case 0xc007:
|
||||
internal_CX_rom_ = !!(address&1);
|
||||
set_card_paging();
|
||||
break;
|
||||
|
||||
case 0xc008:
|
||||
case 0xc009:
|
||||
// The alternative zero page setting affects both bank 0 and any RAM
|
||||
// that's paged as though it were on a language card.
|
||||
alternative_zero_page_ = !!(address&1);
|
||||
set_zero_page_paging();
|
||||
set_language_card_paging();
|
||||
break;
|
||||
|
||||
case 0xc00a:
|
||||
case 0xc00b:
|
||||
slot_C3_rom_ = !!(address&1);
|
||||
set_card_paging();
|
||||
break;
|
||||
|
||||
case 0xc00c:
|
||||
@@ -617,17 +527,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc070: { // Permit analogue inputs that are currently discharged to begin a charge cycle.
|
||||
// Ensure those that were still charging retain that state.
|
||||
for(size_t c = 0; c < 4; ++c) {
|
||||
if(analogue_channel_is_discharged(c)) {
|
||||
analogue_biases_[c] = 0.0f;
|
||||
} else {
|
||||
analogue_biases_[c] += analogue_charge_;
|
||||
}
|
||||
}
|
||||
analogue_charge_ = 0.0f;
|
||||
} break;
|
||||
case 0xc070: joysticks_.access_c070(); break;
|
||||
|
||||
/* Switches triggered by reading or writing. */
|
||||
case 0xc050:
|
||||
@@ -640,14 +540,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case 0xc054:
|
||||
case 0xc055:
|
||||
update_video();
|
||||
video_.set_page2(!!(address&1));
|
||||
set_main_paging();
|
||||
video_.set_page2(address&1);
|
||||
auxiliary_switches_.access(address, isReadOperation(operation));
|
||||
break;
|
||||
case 0xc056:
|
||||
case 0xc057:
|
||||
update_video();
|
||||
video_.set_high_resolution(!!(address&1));
|
||||
set_main_paging();
|
||||
video_.set_high_resolution(address&1);
|
||||
auxiliary_switches_.access(address, isReadOperation(operation));
|
||||
break;
|
||||
|
||||
case 0xc05e:
|
||||
@@ -681,28 +581,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
case 0xc081: case 0xc085: case 0xc089: case 0xc08d:
|
||||
case 0xc082: case 0xc086: case 0xc08a: case 0xc08e:
|
||||
case 0xc083: case 0xc087: case 0xc08b: case 0xc08f:
|
||||
// Quotes below taken from Understanding the Apple II, p. 5-28 and 5-29.
|
||||
|
||||
// "A3 controls the 4K bank selection"
|
||||
language_card_.bank1 = (address&8);
|
||||
|
||||
// "Access to $C080, $C083, $C084, $0087, $C088, $C08B, $C08C, or $C08F sets the READ ENABLE flip-flop"
|
||||
// (other accesses reset it)
|
||||
language_card_.read = !(((address&2) >> 1) ^ (address&1));
|
||||
|
||||
// "The WRITE ENABLE' flip-flop is reset by an odd read access to the $C08X range when the PRE-WRITE flip-flop is set."
|
||||
if(language_card_.pre_write && isReadOperation(operation) && (address&1)) language_card_.write = false;
|
||||
|
||||
// "[The WRITE ENABLE' flip-flop] is set by an even access in the $C08X range."
|
||||
if(!(address&1)) language_card_.write = true;
|
||||
|
||||
// ("Any other type of access causes the WRITE ENABLE' flip-flop to hold its current state.")
|
||||
|
||||
// "The PRE-WRITE flip-flop is set by an odd read access in the $C08X range. It is reset by an even access or a write access."
|
||||
language_card_.pre_write = isReadOperation(operation) ? (address&1) : false;
|
||||
|
||||
// Apply whatever the net effect of all that is to the memory map.
|
||||
set_language_card_paging();
|
||||
language_card_.access(address, isReadOperation(operation));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -788,7 +667,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Update analogue charge level.
|
||||
analogue_charge_ = std::min(analogue_charge_ + 1.0f / 2820.0f, 1.1f);
|
||||
joysticks_.update_charge();
|
||||
|
||||
return Cycles(1);
|
||||
}
|
||||
@@ -804,38 +683,62 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void reset_all_keys(Inputs::Keyboard *) final {
|
||||
void reset_all_keys() final {
|
||||
open_apple_is_pressed_ = closed_apple_is_pressed_ = key_is_down_ = false;
|
||||
}
|
||||
|
||||
bool prefers_logical_input() final {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_key_pressed(Key key, char value, bool is_pressed) final {
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
switch(key) {
|
||||
default: break;
|
||||
case Key::F12:
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
return true;
|
||||
case Key::Left: value = 0x08; break;
|
||||
case Key::Right: value = 0x15; break;
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::Backspace: value = 0x7f; break;
|
||||
case Key::Enter: value = 0x0d; break;
|
||||
case Key::Tab: value = '\t'; break;
|
||||
case Key::Escape: value = 0x1b; break;
|
||||
|
||||
case Key::LeftOption:
|
||||
case Key::RightMeta:
|
||||
open_apple_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
|
||||
case Key::RightOption:
|
||||
case Key::LeftMeta:
|
||||
closed_apple_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
if(!value) {
|
||||
switch(key) {
|
||||
case Key::Left: value = 0x08; break;
|
||||
case Key::Right: value = 0x15; break;
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::Backspace: value = 0x7f; break;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
case Key::F1: case Key::F2: case Key::F3: case Key::F4:
|
||||
case Key::F5: case Key::F6: case Key::F7: case Key::F8:
|
||||
case Key::F9: case Key::F10: case Key::F11: case Key::F12:
|
||||
case Key::PrintScreen:
|
||||
case Key::ScrollLock:
|
||||
case Key::Pause:
|
||||
case Key::Insert:
|
||||
case Key::Home:
|
||||
case Key::PageUp:
|
||||
case Key::PageDown:
|
||||
case Key::End:
|
||||
// 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);
|
||||
return true;
|
||||
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie()) value = char(toupper(value));
|
||||
default:
|
||||
if(!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie()) value = char(toupper(value));
|
||||
break;
|
||||
}
|
||||
|
||||
if(is_pressed) {
|
||||
keyboard_input_ = uint8_t(value | 0x80);
|
||||
@@ -892,7 +795,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
// MARK: JoystickMachine
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
return joysticks_.get_joysticks();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
275
Machines/Apple/AppleII/AuxiliaryMemorySwitches.hpp
Normal file
275
Machines/Apple/AppleII/AuxiliaryMemorySwitches.hpp
Normal file
@@ -0,0 +1,275 @@
|
||||
//
|
||||
// AuxiliaryMemorySwitches.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/10/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef AuxiliaryMemorySwitches_h
|
||||
#define AuxiliaryMemorySwitches_h
|
||||
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
/*!
|
||||
Models the auxiliary memory soft switches, added as of the Apple IIe, which allow access to the auxiliary 64kb of RAM and to
|
||||
the additional almost-4kb of ROM.
|
||||
|
||||
Relevant memory accesses should be fed to this class; it'll call:
|
||||
* machine.set_main_paging() if anything in the 'main' state changes, i.e. the lower 48kb excluding the zero and stack pages;
|
||||
* machine.set_card_state() if anything changes with where ROM should appear rather than cards in the $Cxxx range; and
|
||||
* machine.set_zero_page_paging() if the selection of the lowest two pages of RAM changes.
|
||||
|
||||
Implementation observation: as implemented on the IIe, the zero page setting also affects what happens in the language card area.
|
||||
*/
|
||||
template <typename Machine> class AuxiliaryMemorySwitches {
|
||||
public:
|
||||
static constexpr bool Auxiliary = true;
|
||||
static constexpr bool Main = false;
|
||||
static constexpr bool ROM = true;
|
||||
static constexpr bool Card = false;
|
||||
|
||||
/// Describes banking state between $0200 and $BFFF.
|
||||
struct MainState {
|
||||
struct Region {
|
||||
/// @c true indicates auxiliary memory should be read from; @c false indicates main.
|
||||
bool read = false;
|
||||
/// @c true indicates auxiliary memory should be written to; @c false indicates main.
|
||||
bool write = false;
|
||||
};
|
||||
|
||||
/// Describes banking state in the ranges $0200–$03FF, $0800–$1FFF and $4000–$BFFF.
|
||||
Region base;
|
||||
/// Describes banking state in the range $0400–$07FF.
|
||||
Region region_04_08;
|
||||
/// Describes banking state in the range $2000–$3FFF.
|
||||
Region region_20_40;
|
||||
|
||||
bool operator != (const MainState &rhs) const {
|
||||
return
|
||||
base.read != rhs.base.read || base.write != rhs.base.write ||
|
||||
region_04_08.read != rhs.region_04_08.read || region_04_08.write != rhs.region_04_08.write ||
|
||||
region_20_40.read != rhs.region_20_40.read || region_20_40.write != rhs.region_20_40.write;
|
||||
}
|
||||
};
|
||||
|
||||
/// Describes banking state between $C100 and $Cfff.
|
||||
struct CardState {
|
||||
/// @c true indicates that the built-in ROM should appear from $C100 to $C2FF @c false indicates that cards should service those accesses.
|
||||
bool region_C1_C3 = false;
|
||||
/// @c true indicates that the built-in ROM should appear from $C300 to $C3FF; @c false indicates that cards should service those accesses.
|
||||
bool region_C3 = false;
|
||||
/// @c true indicates that the built-in ROM should appear from $C400 to $C7FF; @c false indicates that cards should service those accesses.
|
||||
bool region_C4_C8 = false;
|
||||
/// @c true indicates that the built-in ROM should appear from $C800 to $CFFF; @c false indicates that cards should service those accesses.
|
||||
bool region_C8_D0 = false;
|
||||
|
||||
bool operator != (const CardState &rhs) const {
|
||||
return
|
||||
region_C1_C3 != rhs.region_C1_C3 ||
|
||||
region_C3 != rhs.region_C3 ||
|
||||
region_C4_C8 != rhs.region_C4_C8 ||
|
||||
region_C8_D0 != rhs.region_C8_D0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Descibes banking state between $0000 and $01ff; @c true indicates that auxiliary memory should be used; @c false indicates main memory.
|
||||
using ZeroState = bool;
|
||||
|
||||
/// Returns raw switch state for all switches that affect banking, even if they're logically video switches.
|
||||
struct SwitchState {
|
||||
bool read_auxiliary_memory = false;
|
||||
bool write_auxiliary_memory = false;
|
||||
|
||||
bool internal_CX_rom = false;
|
||||
bool slot_C3_rom = false;
|
||||
bool internal_C8_rom = false;
|
||||
|
||||
bool store_80 = false;
|
||||
bool alternative_zero_page = false;
|
||||
bool video_page_2 = false;
|
||||
bool high_resolution = false;
|
||||
};
|
||||
|
||||
AuxiliaryMemorySwitches(Machine &machine) : machine_(machine) {}
|
||||
|
||||
/// Used by an owner to forward, at least, any access in the range $C000 to $C00B,
|
||||
/// in $C054 to $C058, or in the range $C300 to $CFFF. Safe to call for any [16-bit] address.
|
||||
void access(uint16_t address, bool is_read) {
|
||||
if(address >= 0xc300 && address < 0xd000) {
|
||||
switches_.internal_C8_rom |= ((address >> 8) == 0xc3) && !switches_.slot_C3_rom;
|
||||
switches_.internal_C8_rom &= (address != 0xcfff);
|
||||
set_card_paging();
|
||||
return;
|
||||
}
|
||||
|
||||
if(address < 0xc000 || address >= 0xc058) return;
|
||||
|
||||
switch(address) {
|
||||
default: break;
|
||||
|
||||
case 0xc000: case 0xc001:
|
||||
if(!is_read) {
|
||||
switches_.store_80 = address & 1;
|
||||
set_main_paging();
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc002: case 0xc003:
|
||||
if(!is_read) {
|
||||
switches_.read_auxiliary_memory = address & 1;
|
||||
set_main_paging();
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc004: case 0xc005:
|
||||
if(!is_read) {
|
||||
switches_.write_auxiliary_memory = address & 1;
|
||||
set_main_paging();
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc006: case 0xc007:
|
||||
if(!is_read) {
|
||||
switches_.internal_CX_rom = address & 1;
|
||||
set_card_paging();
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc008: case 0xc009: {
|
||||
const bool alternative_zero_page = address & 1;
|
||||
if(!is_read && switches_.alternative_zero_page != alternative_zero_page) {
|
||||
switches_.alternative_zero_page = alternative_zero_page;
|
||||
set_zero_page_paging();
|
||||
}
|
||||
} break;
|
||||
|
||||
case 0xc00a: case 0xc00b:
|
||||
if(!is_read) {
|
||||
switches_.slot_C3_rom = address & 1;
|
||||
set_card_paging();
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc054: case 0xc055:
|
||||
switches_.video_page_2 = address & 1;
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc056: case 0xc057:
|
||||
switches_.high_resolution = address & 1;
|
||||
set_main_paging();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides part of the IIgs interface.
|
||||
void set_state(uint8_t value) {
|
||||
// b7: 1 => use auxiliary memory for zero page; 0 => use main. [I think the Hardware Reference gets this the wrong way around]
|
||||
// b6: 1 => text page 2 is selected; 0 => text page 1.
|
||||
// b5: 1 => auxiliary RAM bank is selected for reads; 0 => main.
|
||||
// b4: 1 => auxiliary RAM bank is selected for writes; 0 => main.
|
||||
// b0: 1 => the internal ROM is selected for C800+; 0 => card ROM.
|
||||
switches_.alternative_zero_page = value & 0x80;
|
||||
switches_.video_page_2 = value & 0x40;
|
||||
switches_.read_auxiliary_memory = value & 0x20;
|
||||
switches_.write_auxiliary_memory = value & 0x10;
|
||||
switches_.internal_CX_rom = value & 0x01;
|
||||
|
||||
set_main_paging();
|
||||
set_zero_page_paging();
|
||||
set_card_paging();
|
||||
}
|
||||
|
||||
uint8_t get_state() const {
|
||||
return
|
||||
(switches_.alternative_zero_page ? 0x80 : 0x00) |
|
||||
(switches_.video_page_2 ? 0x40 : 0x00) |
|
||||
(switches_.read_auxiliary_memory ? 0x20 : 0x00) |
|
||||
(switches_.write_auxiliary_memory ? 0x10 : 0x00) |
|
||||
(switches_.internal_CX_rom ? 0x01 : 0x00);
|
||||
}
|
||||
|
||||
const MainState &main_state() const {
|
||||
return main_state_;
|
||||
}
|
||||
|
||||
const CardState &card_state() const {
|
||||
return card_state_;
|
||||
}
|
||||
|
||||
/// @returns @c true if the alternative zero page should be used; @c false otherwise.
|
||||
ZeroState zero_state() const {
|
||||
return switches_.alternative_zero_page;
|
||||
}
|
||||
|
||||
const SwitchState switches() const {
|
||||
return switches_;
|
||||
}
|
||||
|
||||
private:
|
||||
Machine &machine_;
|
||||
SwitchState switches_;
|
||||
|
||||
MainState main_state_;
|
||||
void set_main_paging() {
|
||||
const auto previous_state = main_state_;
|
||||
|
||||
// The two appropriately named switches provide the base case.
|
||||
main_state_.base.read = switches_.read_auxiliary_memory;
|
||||
main_state_.base.write = switches_.write_auxiliary_memory;
|
||||
|
||||
if(switches_.store_80) {
|
||||
// If store 80 is set, use the page 2 flag for the lower carve out;
|
||||
// if both store 80 and high resolution are set, use the page 2 flag for both carve outs.
|
||||
main_state_.region_04_08.read = main_state_.region_04_08.write = switches_.video_page_2;
|
||||
|
||||
if(switches_.high_resolution) {
|
||||
main_state_.region_20_40.read = main_state_.region_20_40.write = switches_.video_page_2;
|
||||
} else {
|
||||
main_state_.region_20_40 = main_state_.base;
|
||||
}
|
||||
} else {
|
||||
main_state_.region_04_08 = main_state_.region_20_40 = main_state_.base;
|
||||
}
|
||||
|
||||
if(previous_state != main_state_) {
|
||||
machine_.set_main_paging();
|
||||
}
|
||||
}
|
||||
|
||||
CardState card_state_;
|
||||
void set_card_paging() {
|
||||
const auto previous_state = card_state_;
|
||||
|
||||
// By default apply the CX switch through to $C7FF.
|
||||
card_state_.region_C1_C3 = card_state_.region_C4_C8 = switches_.internal_CX_rom;
|
||||
|
||||
// Allow the C3 region to be switched to internal ROM in isolation even if the rest of the
|
||||
// first half of the CX region is diabled, if its specific switch is also disabled.
|
||||
if(!switches_.internal_CX_rom && !switches_.slot_C3_rom) {
|
||||
card_state_.region_C3 = true;
|
||||
} else {
|
||||
card_state_.region_C3 = card_state_.region_C1_C3;
|
||||
}
|
||||
|
||||
// Apply the CX switch to $C800+, but also allow the C8 switch to select that region in isolation.
|
||||
card_state_.region_C8_D0 = switches_.internal_CX_rom || switches_.internal_C8_rom;
|
||||
|
||||
if(previous_state != card_state_) {
|
||||
machine_.set_card_paging();
|
||||
}
|
||||
}
|
||||
|
||||
void set_zero_page_paging() {
|
||||
// Believe it or not, the zero page is just set or cleared by a single flag.
|
||||
// As though life were rational.
|
||||
machine_.set_zero_page_paging();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* AuxiliaryMemorySwitches_h */
|
||||
9
Machines/Apple/AppleII/Joystick.cpp
Normal file
9
Machines/Apple/AppleII/Joystick.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
//
|
||||
// Joystick.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Joystick.hpp"
|
||||
112
Machines/Apple/AppleII/Joystick.hpp
Normal file
112
Machines/Apple/AppleII/Joystick.hpp
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// Joystick.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 16/02/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef AppleII_Joystick_hpp
|
||||
#define AppleII_Joystick_hpp
|
||||
|
||||
#include "../../../Inputs/Joystick.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
class JoystickPair {
|
||||
public:
|
||||
JoystickPair() {
|
||||
// Add a couple of joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
}
|
||||
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Horizontal),
|
||||
Input(Input::Vertical),
|
||||
|
||||
// The Apple II offers three buttons between two joysticks;
|
||||
// this emulator puts three buttons on each joystick and
|
||||
// combines them.
|
||||
Input(Input::Fire, 0),
|
||||
Input(Input::Fire, 1),
|
||||
Input(Input::Fire, 2),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, float value) final {
|
||||
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
|
||||
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
|
||||
}
|
||||
|
||||
void did_set_input(const Input &input, bool value) final {
|
||||
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
|
||||
buttons[input.info.control.index] = value;
|
||||
}
|
||||
}
|
||||
|
||||
bool buttons[3] = {false, false, false};
|
||||
float axes[2] = {0.5f, 0.5f};
|
||||
};
|
||||
|
||||
inline bool button(size_t index) {
|
||||
return joystick(0)->buttons[index] || joystick(1)->buttons[2-index];
|
||||
}
|
||||
|
||||
inline bool analogue_channel_is_discharged(size_t channel) {
|
||||
return (1.0f - static_cast<Joystick *>(joysticks_[channel >> 1].get())->axes[channel & 1]) < analogue_charge_ + analogue_biases_[channel];
|
||||
}
|
||||
|
||||
inline void update_charge(float one_mhz_cycles = 1.0f) {
|
||||
analogue_charge_ = std::min(analogue_charge_ + one_mhz_cycles * (1.0f / 2820.0f), 1.1f);
|
||||
}
|
||||
|
||||
inline void access_c070() {
|
||||
// Permit analogue inputs that are currently discharged to begin a charge cycle.
|
||||
// Ensure those that were still charging retain that state.
|
||||
for(size_t c = 0; c < 4; ++c) {
|
||||
if(analogue_channel_is_discharged(c)) {
|
||||
analogue_biases_[c] = 0.0f;
|
||||
} else {
|
||||
analogue_biases_[c] += analogue_charge_;
|
||||
}
|
||||
}
|
||||
analogue_charge_ = 0.0f;
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
private:
|
||||
// On an Apple II, the programmer strobes 0xc070 and that causes each analogue input
|
||||
// to begin a charge and discharge cycle **if they are not already charging**.
|
||||
// The greater the analogue input, the faster they will charge and therefore the sooner
|
||||
// they will discharge.
|
||||
//
|
||||
// This emulator models that with analogue_charge_ being essentially the amount of time,
|
||||
// in charge threshold units, since 0xc070 was last strobed. But if any of the analogue
|
||||
// inputs were already partially charged then they gain a bias in analogue_biases_.
|
||||
//
|
||||
// It's a little indirect, but it means only having to increment the one value in the
|
||||
// main loop.
|
||||
float analogue_charge_ = 0.0f;
|
||||
float analogue_biases_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
|
||||
inline Joystick *joystick(size_t index) {
|
||||
return static_cast<Joystick *>(joysticks_[index].get());
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* AppleII_Joystick_hpp */
|
||||
116
Machines/Apple/AppleII/LanguageCardSwitches.hpp
Normal file
116
Machines/Apple/AppleII/LanguageCardSwitches.hpp
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// LanguageCardSwitches.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/10/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef LanguageCardSwitches_h
|
||||
#define LanguageCardSwitches_h
|
||||
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
/*!
|
||||
Models the language card soft switches, present on any Apple II with a language card and provided built-in from the IIe onwards.
|
||||
|
||||
Relevant memory accesses should be fed to this class; it'll call:
|
||||
* machine.set_language_card_paging() if the proper mapped state changes.
|
||||
*/
|
||||
template <typename Machine> class LanguageCardSwitches {
|
||||
public:
|
||||
struct State {
|
||||
/// When RAM is visible in the range $D000–$FFFF:
|
||||
/// @c true indicates that bank 2 should be used between $D000 and $DFFF;
|
||||
/// @c false indicates bank 1.
|
||||
bool bank2 = true;
|
||||
|
||||
/// @c true indicates that RAM should be readable in the range $D000–$FFFF;
|
||||
/// @c false indicates ROM should be readable.
|
||||
bool read = false;
|
||||
|
||||
/// @c true indicates that ROM is selected for 'writing' in the range $D000–$FFFF (i.e. writes are a no-op);
|
||||
/// @c false indicates that RAM is selected for writing.
|
||||
bool write = false;
|
||||
|
||||
bool operator != (const State &rhs) const {
|
||||
return
|
||||
bank2 != rhs.bank2 ||
|
||||
read != rhs.read ||
|
||||
write != rhs.write;
|
||||
}
|
||||
};
|
||||
|
||||
LanguageCardSwitches(Machine &machine) : machine_(machine) {}
|
||||
|
||||
/// Used by an owner to forward any access to $c08x.
|
||||
void access(uint16_t address, bool is_read) {
|
||||
const auto previous_state = state_;
|
||||
|
||||
// Quotes below taken from Understanding the Apple II, p. 5-28 and 5-29.
|
||||
|
||||
// "A3 controls the 4K bank selection"; 0 = bank 2, 1 = bank 1.
|
||||
state_.bank2 = !(address & 8);
|
||||
|
||||
// "Access to $C080, $C083, $C084, $0087, $C088, $C08B, $C08C, or $C08F sets the READ ENABLE flip-flop"
|
||||
// (other accesses reset it)
|
||||
state_.read = !(((address&2) >> 1) ^ (address&1));
|
||||
|
||||
// "The WRITE ENABLE' flip-flop is reset by an odd read access to the $C08X range when the PRE-WRITE flip-flop is set."
|
||||
if(pre_write_ && is_read && (address&1)) state_.write = false;
|
||||
|
||||
// "[The WRITE ENABLE' flip-flop] is set by an even access in the $C08X range."
|
||||
if(!(address&1)) state_.write = true;
|
||||
|
||||
// ("Any other type of access causes the WRITE ENABLE' flip-flop to hold its current state.")
|
||||
|
||||
// "The PRE-WRITE flip-flop is set by an odd read access in the $C08X range. It is reset by an even access or a write access."
|
||||
pre_write_ = is_read ? (address&1) : false;
|
||||
|
||||
// Apply whatever the net effect of all that is to the memory map.
|
||||
if(previous_state != state_) {
|
||||
machine_.set_language_card_paging();
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides read-only access to the current language card switch state.
|
||||
const State &state() const {
|
||||
return state_;
|
||||
}
|
||||
|
||||
/// Provides relevant parts of the IIgs interface.
|
||||
void set_state(uint8_t value) {
|
||||
const auto previous_state = state_;
|
||||
|
||||
// Bit 3: 1 => enable ROM, 0 => enable RAM.
|
||||
state_.read = !(value & 0x08);
|
||||
// Bit 2: 1 => select bank 2, 0 => select bank 1. [per errata to the Hardware Reference
|
||||
// correcting the original, which lists them the other way around]
|
||||
state_.bank2 = value & 0x04;
|
||||
|
||||
if(previous_state != state_) {
|
||||
machine_.set_language_card_paging();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t get_state() const {
|
||||
return
|
||||
(state_.read ? 0x00 : 0x08) |
|
||||
(state_.bank2 ? 0x04 : 0x00);
|
||||
}
|
||||
|
||||
private:
|
||||
Machine &machine_;
|
||||
State state_;
|
||||
|
||||
// This is an additional flip flop contained on the language card, but
|
||||
// it is one step removed from current banking state, so I've excluded it
|
||||
// from the State struct.
|
||||
bool pre_write_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* LanguageCard_h */
|
||||
@@ -11,9 +11,9 @@
|
||||
using namespace Apple::II::Video;
|
||||
|
||||
VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
|
||||
VideoSwitches<Cycles>(is_iie, Cycles(2), std::move(target)),
|
||||
crt_(910, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance1),
|
||||
is_iie_(is_iie),
|
||||
deferrer_(std::move(target)) {
|
||||
is_iie_(is_iie) {
|
||||
|
||||
// Show only the centre 75% of the TV frame.
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
|
||||
@@ -24,23 +24,6 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
|
||||
// use default_colour_bursts elsewhere, though it otherwise should be. If/when
|
||||
// it is, start doing so and return to setting the immediate phase up here.
|
||||
// crt_.set_immediate_default_phase(0.5f);
|
||||
|
||||
character_zones[0].xor_mask = 0;
|
||||
character_zones[0].address_mask = 0x3f;
|
||||
character_zones[1].xor_mask = 0;
|
||||
character_zones[1].address_mask = 0x3f;
|
||||
character_zones[2].xor_mask = 0;
|
||||
character_zones[2].address_mask = 0x3f;
|
||||
character_zones[3].xor_mask = 0;
|
||||
character_zones[3].address_mask = 0x3f;
|
||||
|
||||
if(is_iie) {
|
||||
character_zones[0].xor_mask =
|
||||
character_zones[2].xor_mask =
|
||||
character_zones[3].xor_mask = 0xff;
|
||||
character_zones[2].address_mask =
|
||||
character_zones[3].address_mask = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
@@ -59,121 +42,10 @@ Outputs::Display::DisplayType VideoBase::get_display_type() const {
|
||||
return crt_.get_display_type();
|
||||
}
|
||||
|
||||
/*
|
||||
Rote setters and getters.
|
||||
*/
|
||||
void VideoBase::set_alternative_character_set(bool alternative_character_set) {
|
||||
set_alternative_character_set_ = alternative_character_set;
|
||||
deferrer_.defer(Cycles(2), [this, alternative_character_set] {
|
||||
alternative_character_set_ = alternative_character_set;
|
||||
if(alternative_character_set) {
|
||||
character_zones[1].address_mask = 0xff;
|
||||
character_zones[1].xor_mask = 0;
|
||||
} else {
|
||||
character_zones[1].address_mask = 0x3f;
|
||||
character_zones[1].xor_mask = flash_mask();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_alternative_character_set() {
|
||||
return set_alternative_character_set_;
|
||||
}
|
||||
|
||||
void VideoBase::set_80_columns(bool columns_80) {
|
||||
set_columns_80_ = columns_80;
|
||||
deferrer_.defer(Cycles(2), [this, columns_80] {
|
||||
columns_80_ = columns_80;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_80_columns() {
|
||||
return set_columns_80_;
|
||||
}
|
||||
|
||||
void VideoBase::set_80_store(bool store_80) {
|
||||
set_store_80_ = store_80_ = store_80;
|
||||
}
|
||||
|
||||
bool VideoBase::get_80_store() {
|
||||
return set_store_80_;
|
||||
}
|
||||
|
||||
void VideoBase::set_page2(bool page2) {
|
||||
set_page2_ = page2_ = page2;
|
||||
}
|
||||
|
||||
bool VideoBase::get_page2() {
|
||||
return set_page2_;
|
||||
}
|
||||
|
||||
void VideoBase::set_text(bool text) {
|
||||
set_text_ = text;
|
||||
deferrer_.defer(Cycles(2), [this, text] {
|
||||
text_ = text;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_text() {
|
||||
return set_text_;
|
||||
}
|
||||
|
||||
void VideoBase::set_mixed(bool mixed) {
|
||||
set_mixed_ = mixed;
|
||||
deferrer_.defer(Cycles(2), [this, mixed] {
|
||||
mixed_ = mixed;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_mixed() {
|
||||
return set_mixed_;
|
||||
}
|
||||
|
||||
void VideoBase::set_high_resolution(bool high_resolution) {
|
||||
set_high_resolution_ = high_resolution;
|
||||
deferrer_.defer(Cycles(2), [this, high_resolution] {
|
||||
high_resolution_ = high_resolution;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_high_resolution() {
|
||||
return set_high_resolution_;
|
||||
}
|
||||
|
||||
void VideoBase::set_annunciator_3(bool annunciator_3) {
|
||||
set_annunciator_3_ = annunciator_3;
|
||||
deferrer_.defer(Cycles(2), [this, annunciator_3] {
|
||||
annunciator_3_ = annunciator_3;
|
||||
high_resolution_mask_ = annunciator_3_ ? 0x7f : 0xff;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_annunciator_3() {
|
||||
return set_annunciator_3_;
|
||||
}
|
||||
|
||||
void VideoBase::set_character_rom(const std::vector<uint8_t> &character_rom) {
|
||||
character_rom_ = character_rom;
|
||||
|
||||
// Flip all character contents based on the second line of the $ graphic.
|
||||
if(character_rom_[0x121] == 0x3c || character_rom_[0x122] == 0x3c) {
|
||||
for(auto &graphic : character_rom_) {
|
||||
graphic =
|
||||
((graphic & 0x01) ? 0x40 : 0x00) |
|
||||
((graphic & 0x02) ? 0x20 : 0x00) |
|
||||
((graphic & 0x04) ? 0x10 : 0x00) |
|
||||
((graphic & 0x08) ? 0x08 : 0x00) |
|
||||
((graphic & 0x10) ? 0x04 : 0x00) |
|
||||
((graphic & 0x20) ? 0x02 : 0x00) |
|
||||
((graphic & 0x40) ? 0x01 : 0x00);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_text(uint8_t *target, const uint8_t *const source, size_t length, size_t pixel_row) const {
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
const int character = source[c] & character_zones[source[c] >> 6].address_mask;
|
||||
const uint8_t xor_mask = character_zones[source[c] >> 6].xor_mask;
|
||||
const int character = source[c] & character_zones_[source[c] >> 6].address_mask;
|
||||
const uint8_t xor_mask = character_zones_[source[c] >> 6].xor_mask;
|
||||
const std::size_t character_address = size_t(character << 3) + pixel_row;
|
||||
const uint8_t character_pattern = character_rom_[character_address] ^ xor_mask;
|
||||
|
||||
@@ -194,19 +66,19 @@ void VideoBase::output_double_text(uint8_t *target, const uint8_t *const source,
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
const std::size_t character_addresses[2] = {
|
||||
size_t(
|
||||
(auxiliary_source[c] & character_zones[auxiliary_source[c] >> 6].address_mask) << 3
|
||||
(auxiliary_source[c] & character_zones_[auxiliary_source[c] >> 6].address_mask) << 3
|
||||
) + pixel_row,
|
||||
size_t(
|
||||
(source[c] & character_zones[source[c] >> 6].address_mask) << 3
|
||||
(source[c] & character_zones_[source[c] >> 6].address_mask) << 3
|
||||
) + pixel_row
|
||||
};
|
||||
|
||||
const uint8_t character_patterns[2] = {
|
||||
uint8_t(
|
||||
character_rom_[character_addresses[0]] ^ character_zones[auxiliary_source[c] >> 6].xor_mask
|
||||
character_rom_[character_addresses[0]] ^ character_zones_[auxiliary_source[c] >> 6].xor_mask
|
||||
),
|
||||
uint8_t(
|
||||
character_rom_[character_addresses[1]] ^ character_zones[source[c] >> 6].xor_mask
|
||||
character_rom_[character_addresses[1]] ^ character_zones_[source[c] >> 6].xor_mask
|
||||
)
|
||||
};
|
||||
|
||||
@@ -270,26 +142,26 @@ void VideoBase::output_double_low_resolution(uint8_t *target, const uint8_t *con
|
||||
const int row_shift = row&4;
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
if((column + int(c))&1) {
|
||||
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 2;
|
||||
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 4;
|
||||
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 8;
|
||||
target[3] = (auxiliary_source[c] >> row_shift) & 1;
|
||||
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 4;
|
||||
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 8;
|
||||
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 1;
|
||||
target[3] = (auxiliary_source[c] >> row_shift) & 2;
|
||||
|
||||
target[8] = target[12] = (source[c] >> row_shift) & 4;
|
||||
target[9] = target[13] = (source[c] >> row_shift) & 8;
|
||||
target[10] = (source[c] >> row_shift) & 1;
|
||||
target[7] = target[11] = (source[c] >> row_shift) & 2;
|
||||
target[8] = target[12] = (source[c] >> row_shift) & 8;
|
||||
target[9] = target[13] = (source[c] >> row_shift) & 1;
|
||||
target[10] = (source[c] >> row_shift) & 2;
|
||||
target[7] = target[11] = (source[c] >> row_shift) & 4;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 8;
|
||||
} else {
|
||||
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 8;
|
||||
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 1;
|
||||
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 2;
|
||||
target[3] = (auxiliary_source[c] >> row_shift) & 4;
|
||||
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 1;
|
||||
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 2;
|
||||
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 4;
|
||||
target[3] = (auxiliary_source[c] >> row_shift) & 8;
|
||||
|
||||
target[8] = target[12] = (source[c] >> row_shift) & 1;
|
||||
target[9] = target[13] = (source[c] >> row_shift) & 2;
|
||||
target[10] = (source[c] >> row_shift) & 4;
|
||||
target[7] = target[11] = (source[c] >> row_shift) & 8;
|
||||
target[8] = target[12] = (source[c] >> row_shift) & 2;
|
||||
target[9] = target[13] = (source[c] >> row_shift) & 4;
|
||||
target[10] = (source[c] >> row_shift) & 8;
|
||||
target[7] = target[11] = (source[c] >> row_shift) & 1;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 2;
|
||||
}
|
||||
target += 14;
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Video_hpp
|
||||
#define Video_hpp
|
||||
#ifndef Apple_II_Video_hpp
|
||||
#define Apple_II_Video_hpp
|
||||
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../ClockReceiver/DeferredQueue.hpp"
|
||||
|
||||
#include "VideoSwitches.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
@@ -33,7 +35,7 @@ class BusHandler {
|
||||
}
|
||||
};
|
||||
|
||||
class VideoBase {
|
||||
class VideoBase: public VideoSwitches<Cycles> {
|
||||
public:
|
||||
VideoBase(bool is_iie, std::function<void(Cycles)> &&target);
|
||||
|
||||
@@ -49,112 +51,6 @@ class VideoBase {
|
||||
/// Gets the type of output.
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
|
||||
/*
|
||||
Descriptions for the setters below are taken verbatim from
|
||||
the Apple IIe Technical Reference. Addresses are the conventional
|
||||
locations within the Apple II memory map. Only those which affect
|
||||
video output are implemented here.
|
||||
|
||||
Those registers which don't exist on a II/II+ are marked.
|
||||
*/
|
||||
|
||||
/*!
|
||||
Setter for ALTCHAR ($C00E/$C00F; triggers on write only):
|
||||
|
||||
* Off: display text using primary character set.
|
||||
* On: display text using alternate character set.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_alternative_character_set(bool);
|
||||
bool get_alternative_character_set();
|
||||
|
||||
/*!
|
||||
Setter for 80COL ($C00C/$C00D; triggers on write only).
|
||||
|
||||
* Off: display 40 columns.
|
||||
* On: display 80 columns.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_columns(bool);
|
||||
bool get_80_columns();
|
||||
|
||||
/*!
|
||||
Setter for 80STORE ($C000/$C001; triggers on write only).
|
||||
|
||||
* Off: cause PAGE2 to select auxiliary RAM.
|
||||
* On: cause PAGE2 to switch main RAM areas.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_store(bool);
|
||||
bool get_80_store();
|
||||
|
||||
/*!
|
||||
Setter for PAGE2 ($C054/$C055; triggers on read or write).
|
||||
|
||||
* Off: select Page 1.
|
||||
* On: select Page 2 or, if 80STORE on, Page 1 in auxiliary memory.
|
||||
|
||||
80STORE doesn't exist on a II/II+; therefore this always selects
|
||||
either Page 1 or Page 2 on those machines.
|
||||
*/
|
||||
void set_page2(bool);
|
||||
bool get_page2();
|
||||
|
||||
/*!
|
||||
Setter for TEXT ($C050/$C051; triggers on read or write).
|
||||
|
||||
* Off: display graphics or, if MIXED on, mixed.
|
||||
* On: display text.
|
||||
*/
|
||||
void set_text(bool);
|
||||
bool get_text();
|
||||
|
||||
/*!
|
||||
Setter for MIXED ($C052/$C053; triggers on read or write).
|
||||
|
||||
* Off: display only text or only graphics.
|
||||
* On: if TEXT off, display text and graphics.
|
||||
*/
|
||||
void set_mixed(bool);
|
||||
bool get_mixed();
|
||||
|
||||
/*!
|
||||
Setter for HIRES ($C056/$C057; triggers on read or write).
|
||||
|
||||
* Off: if TEXT off, display low-resolution graphics.
|
||||
* On: if TEXT off, display high-resolution or, if DHIRES on, double high-resolution graphics.
|
||||
|
||||
DHIRES doesn't exist on a II/II+; therefore this always selects
|
||||
either high- or low-resolution graphics on those machines.
|
||||
|
||||
Despite Apple's documentation, the IIe also supports double low-resolution
|
||||
graphics, which are the 80-column analogue to ordinary low-resolution 40-column
|
||||
low-resolution graphics.
|
||||
*/
|
||||
void set_high_resolution(bool);
|
||||
bool get_high_resolution();
|
||||
|
||||
/*!
|
||||
Setter for annunciator 3.
|
||||
|
||||
* On: turn on annunciator 3.
|
||||
* Off: turn off annunciator 3.
|
||||
|
||||
This exists on both the II/II+ and the IIe, but has no effect on
|
||||
video on the older machines. It's intended to be used on the IIe
|
||||
to confirm double-high resolution mode but has side effects in
|
||||
selecting mixed mode output and discarding high-resolution
|
||||
delay bits.
|
||||
*/
|
||||
void set_annunciator_3(bool);
|
||||
bool get_annunciator_3();
|
||||
|
||||
// Setup for text mode.
|
||||
void set_character_rom(const std::vector<uint8_t> &);
|
||||
|
||||
protected:
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
@@ -162,45 +58,13 @@ class VideoBase {
|
||||
uint8_t *pixel_pointer_ = nullptr;
|
||||
|
||||
// State affecting logical state.
|
||||
int row_ = 0, column_ = 0, flash_ = 0;
|
||||
uint8_t flash_mask() {
|
||||
return uint8_t((flash_ / flash_length) * 0xff);
|
||||
}
|
||||
|
||||
// Enumerates all Apple II and IIe display modes.
|
||||
enum class GraphicsMode {
|
||||
Text = 0,
|
||||
DoubleText,
|
||||
HighRes,
|
||||
DoubleHighRes,
|
||||
LowRes,
|
||||
DoubleLowRes,
|
||||
FatLowRes
|
||||
};
|
||||
bool is_text_mode(GraphicsMode m) { return m <= GraphicsMode::DoubleText; }
|
||||
bool is_double_mode(GraphicsMode m) { return !!(int(m)&1); }
|
||||
|
||||
// Various soft-switch values.
|
||||
bool alternative_character_set_ = false, set_alternative_character_set_ = false;
|
||||
bool columns_80_ = false, set_columns_80_ = false;
|
||||
bool store_80_ = false, set_store_80_ = false;
|
||||
bool page2_ = false, set_page2_ = false;
|
||||
bool text_ = true, set_text_ = true;
|
||||
bool mixed_ = false, set_mixed_ = false;
|
||||
bool high_resolution_ = false, set_high_resolution_ = false;
|
||||
bool annunciator_3_ = false, set_annunciator_3_ = false;
|
||||
int row_ = 0, column_ = 0;
|
||||
|
||||
// Graphics carry is the final level output in a fetch window;
|
||||
// it carries on into the next if it's high resolution with
|
||||
// the delay bit set.
|
||||
mutable uint8_t graphics_carry_ = 0;
|
||||
bool was_double_ = false;
|
||||
uint8_t high_resolution_mask_ = 0xff;
|
||||
|
||||
// This holds a copy of the character ROM. The regular character
|
||||
// set is assumed to be in the first 64*8 bytes; the alternative
|
||||
// is in the 128*8 bytes after that.
|
||||
std::vector<uint8_t> character_rom_;
|
||||
|
||||
// Memory is fetched ahead of time into this array;
|
||||
// this permits the correct delay between fetching
|
||||
@@ -208,16 +72,7 @@ class VideoBase {
|
||||
std::array<uint8_t, 40> base_stream_;
|
||||
std::array<uint8_t, 40> auxiliary_stream_;
|
||||
|
||||
bool is_iie_ = false;
|
||||
static constexpr int flash_length = 8406;
|
||||
|
||||
// Describes the current text mode mapping from in-memory character index
|
||||
// to output character.
|
||||
struct CharacterMapping {
|
||||
uint8_t address_mask;
|
||||
uint8_t xor_mask;
|
||||
};
|
||||
CharacterMapping character_zones[4];
|
||||
const bool is_iie_ = false;
|
||||
|
||||
/*!
|
||||
Outputs 40-column text to @c target, using @c length bytes from @c source.
|
||||
@@ -256,9 +111,6 @@ class VideoBase {
|
||||
clock rather than the 14M.
|
||||
*/
|
||||
void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
|
||||
|
||||
// Maintain a DeferredQueue for delayed mode switches.
|
||||
DeferredQueuePerformer<Cycles> deferrer_;
|
||||
};
|
||||
|
||||
template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
@@ -268,13 +120,6 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
VideoBase(is_iie, [this] (Cycles cycles) { advance(cycles); }),
|
||||
bus_handler_(bus_handler) {}
|
||||
|
||||
/*!
|
||||
Runs video for @c cycles.
|
||||
*/
|
||||
void run_for(Cycles cycles) {
|
||||
deferrer_.run_for(cycles);
|
||||
}
|
||||
|
||||
/*!
|
||||
Obtains the last value the video read prior to time now+offset.
|
||||
*/
|
||||
@@ -329,7 +174,10 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
|
||||
// Apply carry into the row counter and test it for location.
|
||||
int mapped_row = row_ + (mapped_column / 65);
|
||||
return (mapped_row % 262) >= 192;
|
||||
|
||||
// Per http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.040.html
|
||||
// "on the IIe, the screen is blanked when the bit is low".
|
||||
return (mapped_row % 262) < 192;
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -345,7 +193,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
|
||||
A frame is oriented around 65 cycles across, 262 lines down.
|
||||
*/
|
||||
constexpr int first_sync_line = 220; // A complete guess. Information needed.
|
||||
constexpr int first_sync_line = 220; // A complete guess. Information needed.
|
||||
constexpr int first_sync_column = 49; // Also a guess.
|
||||
constexpr int sync_length = 4; // One of the two likely candidates.
|
||||
|
||||
@@ -422,7 +270,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
const int pixel_end = std::min(40, ending_column);
|
||||
const int pixel_row = row_ & 7;
|
||||
|
||||
const bool is_double = Video::is_double_mode(line_mode);
|
||||
const bool is_double = is_double_mode(line_mode);
|
||||
if(!is_double && was_double_ && pixel_pointer_) {
|
||||
pixel_pointer_[pixel_start*14 + 0] =
|
||||
pixel_pointer_[pixel_start*14 + 1] =
|
||||
@@ -541,7 +389,17 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
const int colour_burst_start = std::max(first_sync_column + sync_length + 1, column_);
|
||||
const int colour_burst_end = std::min(first_sync_column + sync_length + 4, ending_column);
|
||||
if(colour_burst_end > colour_burst_start) {
|
||||
crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, 0);
|
||||
// UGLY HACK AHOY!
|
||||
// The OpenGL scan target introduces a phase error of 1/8th of a wave. The Metal one does not.
|
||||
// Supply the real phase value if this is an Apple build.
|
||||
// TODO: eliminate UGLY HACK.
|
||||
#ifdef __APPLE__
|
||||
constexpr int phase = 224;
|
||||
#else
|
||||
constexpr int phase = 0;
|
||||
#endif
|
||||
|
||||
crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, phase);
|
||||
}
|
||||
|
||||
second_blank_start = std::max(first_sync_column + sync_length + 3, column_);
|
||||
@@ -558,10 +416,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
column_ = (column_ + cycles_this_line) % 65;
|
||||
if(!column_) {
|
||||
row_ = (row_ + 1) % 262;
|
||||
flash_ = (flash_ + 1) % (2 * flash_length);
|
||||
if(!alternative_character_set_) {
|
||||
character_zones[1].xor_mask = flash_mask();
|
||||
}
|
||||
did_end_line();
|
||||
|
||||
// Add an extra half a colour cycle of blank; this isn't counted in the run_for
|
||||
// count explicitly but is promised. If this is a vertical sync line, output sync
|
||||
@@ -575,35 +430,6 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
}
|
||||
}
|
||||
|
||||
GraphicsMode graphics_mode(int row) {
|
||||
if(
|
||||
text_ ||
|
||||
(mixed_ && row >= 160 && row < 192)
|
||||
) return columns_80_ ? GraphicsMode::DoubleText : GraphicsMode::Text;
|
||||
if(high_resolution_) {
|
||||
return (annunciator_3_ && columns_80_) ? GraphicsMode::DoubleHighRes : GraphicsMode::HighRes;
|
||||
} else {
|
||||
if(columns_80_) return GraphicsMode::DoubleLowRes;
|
||||
if(annunciator_3_) return GraphicsMode::FatLowRes;
|
||||
return GraphicsMode::LowRes;
|
||||
}
|
||||
}
|
||||
|
||||
int video_page() {
|
||||
return (store_80_ || !page2_) ? 0 : 1;
|
||||
}
|
||||
|
||||
uint16_t get_row_address(int row) {
|
||||
const int character_row = row >> 3;
|
||||
const int pixel_row = row & 7;
|
||||
const uint16_t row_address = uint16_t((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
|
||||
const GraphicsMode pixel_mode = graphics_mode(row);
|
||||
return ((pixel_mode == GraphicsMode::HighRes) || (pixel_mode == GraphicsMode::DoubleHighRes)) ?
|
||||
uint16_t(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
|
||||
uint16_t(((video_page()+1) * 0x400) + row_address);
|
||||
}
|
||||
|
||||
BusHandler &bus_handler_;
|
||||
};
|
||||
|
||||
@@ -611,4 +437,4 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Video_hpp */
|
||||
#endif /* Apple_II_Video_hpp */
|
||||
|
||||
377
Machines/Apple/AppleII/VideoSwitches.hpp
Normal file
377
Machines/Apple/AppleII/VideoSwitches.hpp
Normal file
@@ -0,0 +1,377 @@
|
||||
//
|
||||
// VideoSwitches.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 31/10/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef VideoSwitches_h
|
||||
#define VideoSwitches_h
|
||||
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../ClockReceiver/DeferredQueue.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
// Enumerates all Apple II and IIe display modes.
|
||||
enum class GraphicsMode {
|
||||
Text = 0,
|
||||
DoubleText,
|
||||
HighRes,
|
||||
DoubleHighRes,
|
||||
LowRes,
|
||||
DoubleLowRes,
|
||||
/// Fat low res mode is regular low res mode, but clocked out at 7Mhz rather than 14, leading to improper colours.
|
||||
FatLowRes
|
||||
};
|
||||
constexpr bool is_text_mode(GraphicsMode m) { return m <= GraphicsMode::DoubleText; }
|
||||
constexpr bool is_double_mode(GraphicsMode m) { return int(m) & 1; }
|
||||
|
||||
template <typename TimeUnit> class VideoSwitches {
|
||||
public:
|
||||
/*!
|
||||
Constructs a new instance of VideoSwitches in which changes to relevant switches
|
||||
affect the video mode only after @c delay cycles.
|
||||
|
||||
If @c is_iie is true, these switches will set up the character zones for an IIe-esque
|
||||
set of potential flashing characters and alternate video modes.
|
||||
*/
|
||||
VideoSwitches(bool is_iie, TimeUnit delay, std::function<void(TimeUnit)> &&target) : delay_(delay), deferrer_(std::move(target)) {
|
||||
character_zones_[0].xor_mask = 0;
|
||||
character_zones_[0].address_mask = 0x3f;
|
||||
character_zones_[1].xor_mask = 0;
|
||||
character_zones_[1].address_mask = 0x3f;
|
||||
character_zones_[2].xor_mask = 0;
|
||||
character_zones_[2].address_mask = 0x3f;
|
||||
character_zones_[3].xor_mask = 0;
|
||||
character_zones_[3].address_mask = 0x3f;
|
||||
|
||||
if(is_iie) {
|
||||
character_zones_[0].xor_mask =
|
||||
character_zones_[2].xor_mask =
|
||||
character_zones_[3].xor_mask = 0xff;
|
||||
character_zones_[2].address_mask =
|
||||
character_zones_[3].address_mask = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Advances @c cycles.
|
||||
*/
|
||||
void run_for(TimeUnit cycles) {
|
||||
deferrer_.run_for(cycles);
|
||||
}
|
||||
|
||||
/*
|
||||
Descriptions for the setters below are taken verbatim from
|
||||
the Apple IIe Technical Reference. Addresses are the conventional
|
||||
locations within the Apple II memory map. Only those which affect
|
||||
video output are implemented here.
|
||||
|
||||
Those registers which don't exist on a II/II+ are marked.
|
||||
*/
|
||||
|
||||
/*!
|
||||
Setter for ALTCHAR ($C00E/$C00F; triggers on write only):
|
||||
|
||||
* Off: display text using primary character set.
|
||||
* On: display text using alternate character set.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_alternative_character_set(bool alternative_character_set) {
|
||||
external_.alternative_character_set = alternative_character_set;
|
||||
deferrer_.defer(delay_, [this, alternative_character_set] {
|
||||
internal_.alternative_character_set = alternative_character_set;
|
||||
|
||||
if(alternative_character_set) {
|
||||
character_zones_[1].address_mask = 0xff;
|
||||
character_zones_[1].xor_mask = 0;
|
||||
} else {
|
||||
character_zones_[1].address_mask = 0x3f;
|
||||
character_zones_[1].xor_mask = flash_mask();
|
||||
// The XOR mask is seeded here; it's dynamic, so updated elsewhere.
|
||||
}
|
||||
});
|
||||
}
|
||||
bool get_alternative_character_set() const {
|
||||
return external_.alternative_character_set;
|
||||
}
|
||||
|
||||
/*!
|
||||
Setter for 80COL ($C00C/$C00D; triggers on write only).
|
||||
|
||||
* Off: display 40 columns.
|
||||
* On: display 80 columns.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_columns(bool columns_80) {
|
||||
external_.columns_80 = columns_80;
|
||||
deferrer_.defer(delay_, [this, columns_80] {
|
||||
internal_.columns_80 = columns_80;
|
||||
});
|
||||
}
|
||||
bool get_80_columns() const {
|
||||
return external_.columns_80;
|
||||
}
|
||||
|
||||
/*!
|
||||
Setter for 80STORE ($C000/$C001; triggers on write only).
|
||||
|
||||
* Off: cause PAGE2 to select auxiliary RAM.
|
||||
* On: cause PAGE2 to switch main RAM areas.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_store(bool store_80) {
|
||||
external_.store_80 = internal_.store_80 = store_80;
|
||||
}
|
||||
bool get_80_store() const {
|
||||
return external_.store_80;
|
||||
}
|
||||
|
||||
/*!
|
||||
Setter for PAGE2 ($C054/$C055; triggers on read or write).
|
||||
|
||||
* Off: select Page 1.
|
||||
* On: select Page 2 or, if 80STORE on, Page 1 in auxiliary memory.
|
||||
|
||||
80STORE doesn't exist on a II/II+; therefore this always selects
|
||||
either Page 1 or Page 2 on those machines.
|
||||
*/
|
||||
void set_page2(bool page2) {
|
||||
external_.page2 = internal_.page2 = page2;
|
||||
}
|
||||
bool get_page2() const {
|
||||
return external_.page2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Setter for TEXT ($C050/$C051; triggers on read or write).
|
||||
|
||||
* Off: display graphics or, if MIXED on, mixed.
|
||||
* On: display text.
|
||||
*/
|
||||
void set_text(bool text) {
|
||||
external_.text = text;
|
||||
deferrer_.defer(delay_, [this, text] {
|
||||
internal_.text = text;
|
||||
});
|
||||
}
|
||||
bool get_text() const {
|
||||
return external_.text;
|
||||
}
|
||||
|
||||
/*!
|
||||
Setter for MIXED ($C052/$C053; triggers on read or write).
|
||||
|
||||
* Off: display only text or only graphics.
|
||||
* On: if TEXT off, display text and graphics.
|
||||
*/
|
||||
void set_mixed(bool mixed) {
|
||||
external_.mixed = mixed;
|
||||
deferrer_.defer(delay_, [this, mixed] {
|
||||
internal_.mixed = mixed;
|
||||
});
|
||||
}
|
||||
bool get_mixed() const {
|
||||
return external_.mixed;
|
||||
}
|
||||
|
||||
/*!
|
||||
Setter for HIRES ($C056/$C057; triggers on read or write).
|
||||
|
||||
* Off: if TEXT off, display low-resolution graphics.
|
||||
* On: if TEXT off, display high-resolution or, if DHIRES on, double high-resolution graphics.
|
||||
|
||||
DHIRES doesn't exist on a II/II+; therefore this always selects
|
||||
either high- or low-resolution graphics on those machines.
|
||||
|
||||
Despite Apple's documentation, the IIe also supports double low-resolution
|
||||
graphics, which are the 80-column analogue to ordinary low-resolution 40-column
|
||||
low-resolution graphics.
|
||||
*/
|
||||
void set_high_resolution(bool high_resolution) {
|
||||
external_.high_resolution = high_resolution;
|
||||
deferrer_.defer(delay_, [this, high_resolution] {
|
||||
internal_.high_resolution = high_resolution;
|
||||
});
|
||||
}
|
||||
bool get_high_resolution() const {
|
||||
return external_.high_resolution;
|
||||
}
|
||||
|
||||
/*!
|
||||
Setter for annunciator 3.
|
||||
|
||||
* On: turn on annunciator 3.
|
||||
* Off: turn off annunciator 3.
|
||||
|
||||
This exists on both the II/II+ and the IIe, but has no effect on
|
||||
video on the older machines. It's intended to be used on the IIe
|
||||
to confirm double-high resolution mode but has side effects in
|
||||
selecting mixed mode output and discarding high-resolution
|
||||
delay bits.
|
||||
*/
|
||||
void set_annunciator_3(bool annunciator_3) {
|
||||
external_.annunciator_3 = annunciator_3;
|
||||
deferrer_.defer(delay_, [this, annunciator_3] {
|
||||
internal_.annunciator_3 = annunciator_3;
|
||||
high_resolution_mask_ = annunciator_3 ? 0x7f : 0xff;
|
||||
});
|
||||
}
|
||||
bool get_annunciator_3() const {
|
||||
return external_.annunciator_3;
|
||||
}
|
||||
|
||||
enum class CharacterROM {
|
||||
/// The ROM that shipped with both the Apple II and the II+.
|
||||
II,
|
||||
/// The ROM that shipped with the original IIe.
|
||||
IIe,
|
||||
/// The ROM that shipped with the Enhanced IIe.
|
||||
EnhancedIIe,
|
||||
/// The ROM that shipped with the IIgs.
|
||||
IIgs
|
||||
};
|
||||
|
||||
/// @returns A file-level description of @c rom.
|
||||
static ROMMachine::ROM rom_description(CharacterROM rom) {
|
||||
const std::string machine_name = "AppleII";
|
||||
switch(rom) {
|
||||
case CharacterROM::II:
|
||||
return ROMMachine::ROM(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
|
||||
case CharacterROM::IIe:
|
||||
return ROMMachine::ROM(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1);
|
||||
|
||||
default: // To appease GCC.
|
||||
case CharacterROM::EnhancedIIe:
|
||||
return ROMMachine::ROM(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d);
|
||||
|
||||
case CharacterROM::IIgs:
|
||||
return ROMMachine::ROM(machine_name, "the Apple IIgs character ROM", "apple2gs.chr", 4*1024, 0x91e53cd8);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the character ROM for this video output.
|
||||
void set_character_rom(const std::vector<uint8_t> &rom) {
|
||||
character_rom_ = rom;
|
||||
|
||||
// There's some inconsistency in bit ordering amongst the common ROM dumps;
|
||||
// detect that based arbitrarily on the second line of the $ graphic and
|
||||
// ensure consistency.
|
||||
if(character_rom_[0x121] == 0x3c || character_rom_[0x122] == 0x3c) {
|
||||
for(auto &graphic : character_rom_) {
|
||||
graphic =
|
||||
((graphic & 0x01) ? 0x40 : 0x00) |
|
||||
((graphic & 0x02) ? 0x20 : 0x00) |
|
||||
((graphic & 0x04) ? 0x10 : 0x00) |
|
||||
((graphic & 0x08) ? 0x08 : 0x00) |
|
||||
((graphic & 0x10) ? 0x04 : 0x00) |
|
||||
((graphic & 0x20) ? 0x02 : 0x00) |
|
||||
((graphic & 0x40) ? 0x01 : 0x00);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
GraphicsMode graphics_mode(int row) const {
|
||||
if(
|
||||
internal_.text ||
|
||||
(internal_.mixed && row >= 160 && row < 192)
|
||||
) return internal_.columns_80 ? GraphicsMode::DoubleText : GraphicsMode::Text;
|
||||
if(internal_.high_resolution) {
|
||||
return (internal_.annunciator_3 && internal_.columns_80) ? GraphicsMode::DoubleHighRes : GraphicsMode::HighRes;
|
||||
} else {
|
||||
if(internal_.columns_80) return GraphicsMode::DoubleLowRes;
|
||||
if(internal_.annunciator_3) return GraphicsMode::FatLowRes;
|
||||
return GraphicsMode::LowRes;
|
||||
}
|
||||
}
|
||||
|
||||
int video_page() const {
|
||||
return (internal_.store_80 || !internal_.page2) ? 0 : 1;
|
||||
}
|
||||
|
||||
uint16_t get_row_address(int row) const {
|
||||
const int character_row = row >> 3;
|
||||
const int pixel_row = row & 7;
|
||||
const uint16_t row_address = uint16_t((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
|
||||
const GraphicsMode pixel_mode = graphics_mode(row);
|
||||
return ((pixel_mode == GraphicsMode::HighRes) || (pixel_mode == GraphicsMode::DoubleHighRes)) ?
|
||||
uint16_t(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
|
||||
uint16_t(((video_page()+1) * 0x400) + row_address);
|
||||
}
|
||||
|
||||
/*!
|
||||
Should be called by subclasses at the end of each line of the display;
|
||||
this gives the base class a peg on which to hang flashing-character updates.
|
||||
*/
|
||||
void did_end_line() {
|
||||
// Update character set flashing; flashing is applied only when the alternative
|
||||
// character set is not selected.
|
||||
flash_ = (flash_ + 1) % (2 * flash_length);
|
||||
character_zones_[1].xor_mask = flash_mask() * !internal_.alternative_character_set;
|
||||
}
|
||||
|
||||
private:
|
||||
// Maintain a DeferredQueue for delayed mode switches.
|
||||
const TimeUnit delay_;
|
||||
DeferredQueuePerformer<TimeUnit> deferrer_;
|
||||
|
||||
struct Switches {
|
||||
bool alternative_character_set = false;
|
||||
bool columns_80 = false;
|
||||
bool store_80 = false;
|
||||
bool page2 = false;
|
||||
bool text = true;
|
||||
bool mixed = false;
|
||||
bool high_resolution = false;
|
||||
bool annunciator_3 = false;
|
||||
} external_, internal_;
|
||||
|
||||
int flash_length = 8406;
|
||||
int flash_ = 0;
|
||||
uint8_t flash_mask() const {
|
||||
return uint8_t((flash_ / flash_length) * 0xff);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
// Describes the current text mode mapping from in-memory character index
|
||||
// to output character; subclasses should:
|
||||
//
|
||||
// (i) use the top two-bits of the character code to index character_zones_;
|
||||
// (ii) apply the address_mask to the character code in order to get a character
|
||||
// offset into the character ROM; and
|
||||
// (iii) apply the XOR mask to the output of the character ROM.
|
||||
//
|
||||
// By this means they will properly handle the limited character sets of Apple IIs
|
||||
// prior to the IIe as well as the IIe and onward's alternative character set toggle.
|
||||
struct CharacterMapping {
|
||||
uint8_t address_mask;
|
||||
uint8_t xor_mask;
|
||||
};
|
||||
CharacterMapping character_zones_[4];
|
||||
|
||||
// A mask that should be applied to high-resolution graphics bytes before output;
|
||||
// it acts to retain or remove the top bit, affecting whether the half-pixel delay
|
||||
// bit is effective. On a IIe it's toggleable, on early Apple IIs it doesn't exist.
|
||||
uint8_t high_resolution_mask_ = 0xff;
|
||||
|
||||
// This holds a copy of the character ROM. The regular character
|
||||
// set is assumed to be in the first 64*8 bytes; the alternative
|
||||
// is in the 128*8 bytes after that.
|
||||
std::vector<uint8_t> character_rom_;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* VideoSwitches_h */
|
||||
290
Machines/Apple/AppleIIgs/ADB.cpp
Normal file
290
Machines/Apple/AppleIIgs/ADB.cpp
Normal file
@@ -0,0 +1,290 @@
|
||||
//
|
||||
// ADB.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 31/10/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "ADB.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
|
||||
// TEST.
|
||||
#include "../../../InstructionSets/M50740/Parser.hpp"
|
||||
#include "../../../InstructionSets/Disassembler.hpp"
|
||||
|
||||
#define LOG_PREFIX "[ADB GLU] "
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
using namespace Apple::IIgs::ADB;
|
||||
|
||||
namespace {
|
||||
|
||||
// Flags affecting the CPU-visible status register.
|
||||
enum class CPUFlags: uint8_t {
|
||||
MouseDataFull = 0x80,
|
||||
MouseInterruptEnabled = 0x40,
|
||||
CommandDataIsValid = 0x20,
|
||||
CommandDataInterruptEnabled = 0x10,
|
||||
KeyboardDataFull = 0x08,
|
||||
KeyboardDataInterruptEnabled = 0x04,
|
||||
MouseXIsAvailable = 0x02,
|
||||
CommandRegisterFull = 0x01,
|
||||
};
|
||||
|
||||
// Flags affecting the microcontroller-visible register.
|
||||
enum class MicrocontrollerFlags: uint8_t {
|
||||
CommandRegisterFull = 0x40,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
GLU::GLU() :
|
||||
executor_(*this),
|
||||
bus_(HalfCycles(1'789'772)),
|
||||
controller_id_(bus_.add_device()),
|
||||
mouse_(bus_),
|
||||
keyboard_(bus_) {}
|
||||
|
||||
// MARK: - External interface.
|
||||
|
||||
uint8_t GLU::get_keyboard_data() {
|
||||
// The classic Apple II serial keyboard register:
|
||||
// b7: key strobe.
|
||||
// b6–b0: ASCII code.
|
||||
return (registers_[0] & 0x7f) | ((status_ & uint8_t(CPUFlags::KeyboardDataFull)) ? 0x80 : 0x00);
|
||||
}
|
||||
|
||||
void GLU::clear_key_strobe() {
|
||||
// Clears the key strobe of the classic Apple II serial keyboard register.
|
||||
status_ &= ~uint8_t(CPUFlags::KeyboardDataFull);
|
||||
}
|
||||
|
||||
uint8_t GLU::get_any_key_down() {
|
||||
// The Apple IIe check-for-any-key-down bit.
|
||||
return registers_[5];
|
||||
}
|
||||
|
||||
uint8_t GLU::get_mouse_data() {
|
||||
// Alternates between returning x and y values.
|
||||
//
|
||||
// b7: 1 = button is up; 0 = button is down.
|
||||
// b6: delta sign bit; 1 = negative.
|
||||
// b5–b0: mouse delta.
|
||||
|
||||
const uint8_t result = registers_[visible_mouse_register_];
|
||||
if(visible_mouse_register_ == 2) {
|
||||
++visible_mouse_register_;
|
||||
} else {
|
||||
status_ &= ~uint8_t(CPUFlags::MouseDataFull);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t GLU::get_modifier_status() {
|
||||
// b7: 1 = command key pressed; 0 = not.
|
||||
// b6: option key.
|
||||
// b5: 1 = modifier key latch has been updated, no key has been pressed; 0 = not.
|
||||
// b4: any numeric keypad key.
|
||||
// b3: a key is down.
|
||||
// b2: caps lock is pressed.
|
||||
// b1: control key.
|
||||
// b0: shift key.
|
||||
return registers_[6];
|
||||
}
|
||||
|
||||
uint8_t GLU::get_data() {
|
||||
// b0–2: number of data bytes to be returned.
|
||||
// b3: 1 = a valid service request is pending; 0 = no request pending.
|
||||
// b4: 1 = control, command and delete keys have been pressed simultaneously; 0 = they haven't.
|
||||
// b5: 1 = control, command and reset have all been pressed together; 0 = they haven't.
|
||||
// b6: 1 = ADB controller encountered an error and reset itself; 0 = no error.
|
||||
// b7: 1 = ADB has received a response from the addressed ADB device; 0 = no respone.
|
||||
status_ &= ~uint8_t(CPUFlags::CommandDataIsValid);
|
||||
return registers_[7];
|
||||
}
|
||||
|
||||
uint8_t GLU::get_status() {
|
||||
// b7: 1 = mouse data register is full; 0 = empty.
|
||||
// b6: 1 = mouse interrupt is enabled.
|
||||
// b5: 1 = command/data has valid data.
|
||||
// b4: 1 = command/data interrupt is enabled.
|
||||
// b3: 1 = keyboard data is full.
|
||||
// b2: 1 = keyboard data interrupt is enabled.
|
||||
// b1: 1 = mouse x-data is available; 0 = y.
|
||||
// b0: 1 = command register is full (set when command is written); 0 = empty (cleared when data is read).
|
||||
return status_ | ((visible_mouse_register_ == 2) ? uint8_t(CPUFlags::MouseXIsAvailable) : 0);
|
||||
}
|
||||
|
||||
void GLU::set_status(uint8_t status) {
|
||||
// This permits only the interrupt flags to be set.
|
||||
constexpr uint8_t interrupt_flags =
|
||||
uint8_t(CPUFlags::MouseInterruptEnabled) |
|
||||
uint8_t(CPUFlags::CommandDataInterruptEnabled) |
|
||||
uint8_t(CPUFlags::KeyboardDataInterruptEnabled);
|
||||
status_ = (status_ & ~interrupt_flags) | (status & interrupt_flags);
|
||||
}
|
||||
|
||||
void GLU::set_command(uint8_t command) {
|
||||
registers_[1] = command;
|
||||
registers_[4] |= uint8_t(MicrocontrollerFlags::CommandRegisterFull);
|
||||
status_ |= uint8_t(CPUFlags::CommandRegisterFull);
|
||||
}
|
||||
|
||||
// MARK: - Setup and run.
|
||||
|
||||
void GLU::set_microcontroller_rom(const std::vector<uint8_t> &rom) {
|
||||
executor_.set_rom(rom);
|
||||
|
||||
// TEST invocation.
|
||||
/* InstructionSet::Disassembler<InstructionSet::M50740::Parser, 0x1fff, InstructionSet::M50740::Instruction, uint8_t, uint16_t> disassembler;
|
||||
disassembler.disassemble(rom.data(), 0x1000, uint16_t(rom.size()), 0x1000);
|
||||
|
||||
const auto instructions = disassembler.instructions();
|
||||
const auto entry_points = disassembler.entry_points();
|
||||
for(const auto &pair : instructions) {
|
||||
std::cout << std::hex << pair.first << "\t\t";
|
||||
if(entry_points.find(pair.first) != entry_points.end()) {
|
||||
std::cout << "L" << pair.first << "\t";
|
||||
} else {
|
||||
std::cout << "\t\t";
|
||||
}
|
||||
std::cout << operation_name(pair.second.operation) << " ";
|
||||
std::cout << address(pair.second.addressing_mode, &rom[pair.first - 0x1000], pair.first);
|
||||
|
||||
std::cout << std::endl;
|
||||
}*/
|
||||
}
|
||||
|
||||
void GLU::run_for(Cycles cycles) {
|
||||
executor_.run_for(cycles);
|
||||
}
|
||||
|
||||
// MARK: - M50470 port handler
|
||||
|
||||
void GLU::set_port_output(int port, uint8_t value) {
|
||||
switch(port) {
|
||||
case 0:
|
||||
register_latch_ = value;
|
||||
break;
|
||||
case 1:
|
||||
// printf("Keyboard write: %02x???\n", value);
|
||||
break;
|
||||
case 2: {
|
||||
// printf("ADB data line input: %d???\n", value >> 7);
|
||||
// printf("IIe keyboard reset line: %d\n", (value >> 6)&1);
|
||||
// printf("IIgs reset line: %d\n", (value >> 5)&1);
|
||||
// printf("GLU strobe: %d\n", (value >> 4)&1);
|
||||
// printf("Select GLU register: %d [%02x]\n", value & 0xf, value);
|
||||
|
||||
register_address_ = value & 0xf;
|
||||
|
||||
// This is an ugly hack, I think. Per Neil Parker's Inside the Apple IIGS ADB Controller
|
||||
// http://nparker.llx.com/a2/adb.html#external:
|
||||
//
|
||||
// The protocol for reading an ADB GLU register is as follows:
|
||||
//
|
||||
// 1. Put the register number of the ADB GLU register in port P2 bits 0-3.
|
||||
// 2. Clear bit 4 of port P2, read the data from P0, and set bit 4 of P0.
|
||||
//
|
||||
// The protocol for writing a GLU register is similar:
|
||||
//
|
||||
// 1. Write the register number to port P2 bits 0-3.
|
||||
// 2. Write the data to port P0.
|
||||
// 3. Configure port P0 for output by writing $FF to $E1.
|
||||
// 4. Clear bit 4 of P2, and immediately set it again.
|
||||
// 5. Configure port P0 for input by writing 0 to $E1.
|
||||
//
|
||||
// ---
|
||||
//
|
||||
// I tried: linking a read or write to rising or falling edges of the strobe.
|
||||
// Including with hysteresis as per the "immediately" (which, in practice, seems
|
||||
// to mean "in the very next instruction", i.e. 5 cycles later). That didn't seem
|
||||
// properly to differentiate.
|
||||
//
|
||||
// So I'm focussing on the "configure port P0 for output" bit. Which I don't see
|
||||
// would be visible here unless it is actually an exposed signal, which is unlikely.
|
||||
//
|
||||
// Ergo: ugly. HACK.
|
||||
const bool strobe = value & 0x10;
|
||||
if(strobe != register_strobe_) {
|
||||
register_strobe_ = strobe;
|
||||
|
||||
if(!register_strobe_) {
|
||||
if(executor_.get_output_mask(0)) {
|
||||
registers_[register_address_] = register_latch_;
|
||||
switch(register_address_) {
|
||||
default: break;
|
||||
case 0: status_ |= uint8_t(CPUFlags::KeyboardDataFull); break;
|
||||
case 2:
|
||||
case 3:
|
||||
status_ |= uint8_t(CPUFlags::MouseDataFull);
|
||||
visible_mouse_register_ = 2;
|
||||
printf("Mouse: %d <- %02x\n", register_address_, register_latch_);
|
||||
break;
|
||||
case 7: status_ |= uint8_t(CPUFlags::CommandDataIsValid); break;
|
||||
}
|
||||
} else {
|
||||
register_latch_ = registers_[register_address_];
|
||||
switch(register_address_) {
|
||||
default: break;
|
||||
case 1:
|
||||
registers_[4] &= ~uint8_t(MicrocontrollerFlags::CommandRegisterFull);
|
||||
status_ &= ~uint8_t(CPUFlags::CommandRegisterFull);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
case 3:
|
||||
if(modifier_state_ != (value & 0x30)) {
|
||||
modifier_state_ = value & 0x30;
|
||||
LOG("Modifier state: " << int(value & 0x30));
|
||||
}
|
||||
|
||||
// Output is inverted respective to input; the microcontroller
|
||||
// sets a value of '1' in order to pull the ADB bus low.
|
||||
bus_.set_device_output(controller_id_, !(value & 0x08));
|
||||
break;
|
||||
|
||||
default: assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool GLU::get_command_button() const {
|
||||
return modifier_state_ & 0x20;
|
||||
}
|
||||
|
||||
bool GLU::get_option_button() const {
|
||||
return modifier_state_ & 0x10;
|
||||
}
|
||||
|
||||
uint8_t GLU::get_port_input(int port) {
|
||||
switch(port) {
|
||||
case 0: return register_latch_;
|
||||
case 1:
|
||||
// printf("IIe keyboard read\n");
|
||||
return 0x06;
|
||||
case 2:
|
||||
// printf("ADB data line input, etc\n");
|
||||
return bus_.get_state() ? 0x80 : 0x00;
|
||||
case 3:
|
||||
// printf("ADB data line output, etc\n");
|
||||
return 0x00;
|
||||
|
||||
default: assert(false);
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
void GLU::run_ports_for(Cycles cycles) {
|
||||
bus_.run_for(cycles);
|
||||
}
|
||||
|
||||
void GLU::set_vertical_blank(bool is_blank) {
|
||||
executor_.set_interrupt_line(is_blank);
|
||||
}
|
||||
92
Machines/Apple/AppleIIgs/ADB.hpp
Normal file
92
Machines/Apple/AppleIIgs/ADB.hpp
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// ADB.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 31/10/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Apple_IIgs_ADB_hpp
|
||||
#define Apple_IIgs_ADB_hpp
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "../../../InstructionSets/M50740/Executor.hpp"
|
||||
|
||||
#include "../ADB/Bus.hpp"
|
||||
#include "../ADB/Mouse.hpp"
|
||||
#include "../ADB/Keyboard.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace IIgs {
|
||||
namespace ADB {
|
||||
|
||||
class GLU: public InstructionSet::M50740::PortHandler {
|
||||
public:
|
||||
GLU();
|
||||
|
||||
uint8_t get_keyboard_data();
|
||||
uint8_t get_mouse_data();
|
||||
uint8_t get_modifier_status();
|
||||
uint8_t get_any_key_down();
|
||||
|
||||
uint8_t get_data();
|
||||
uint8_t get_status();
|
||||
|
||||
void set_command(uint8_t);
|
||||
void set_status(uint8_t);
|
||||
void clear_key_strobe();
|
||||
|
||||
void set_microcontroller_rom(const std::vector<uint8_t> &rom);
|
||||
|
||||
void run_for(Cycles cycles);
|
||||
|
||||
bool get_command_button() const;
|
||||
bool get_option_button() const;
|
||||
|
||||
void set_vertical_blank(bool);
|
||||
bool get_vertical_blank() {
|
||||
return vertical_blank_;
|
||||
}
|
||||
|
||||
Apple::ADB::Keyboard &keyboard() {
|
||||
return keyboard_;
|
||||
}
|
||||
Inputs::Mouse &get_mouse() {
|
||||
return mouse_;
|
||||
}
|
||||
|
||||
private:
|
||||
InstructionSet::M50740::Executor executor_;
|
||||
|
||||
void run_ports_for(Cycles) override;
|
||||
void set_port_output(int port, uint8_t value) override;
|
||||
uint8_t get_port_input(int port) override;
|
||||
|
||||
uint8_t registers_[16]{};
|
||||
|
||||
uint8_t register_address_;
|
||||
uint8_t register_latch_ = 0xff;
|
||||
bool register_strobe_ = false;
|
||||
|
||||
uint8_t status_ = 0x00;
|
||||
|
||||
Apple::ADB::Bus bus_;
|
||||
size_t controller_id_;
|
||||
|
||||
uint8_t modifier_state_ = 0;
|
||||
|
||||
bool vertical_blank_ = false;
|
||||
|
||||
int visible_mouse_register_ = 2;
|
||||
|
||||
// For now, attach only a keyboard and mouse.
|
||||
Apple::ADB::Mouse mouse_;
|
||||
Apple::ADB::Keyboard keyboard_;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Apple_IIgs_ADB_hpp */
|
||||
1157
Machines/Apple/AppleIIgs/AppleIIgs.cpp
Normal file
1157
Machines/Apple/AppleIIgs/AppleIIgs.cpp
Normal file
File diff suppressed because it is too large
Load Diff
33
Machines/Apple/AppleIIgs/AppleIIgs.hpp
Normal file
33
Machines/Apple/AppleIIgs/AppleIIgs.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// AppleIIgs.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/10/2020.
|
||||
// Copyright 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Machines_Apple_AppleIIgs_hpp
|
||||
#define Machines_Apple_AppleIIgs_hpp
|
||||
|
||||
#include "../../../Configurable/Configurable.hpp"
|
||||
#include "../../../Configurable/StandardOptions.hpp"
|
||||
#include "../../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace Apple {
|
||||
namespace IIgs {
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
|
||||
/// Creates and returns an AppleIIgs.
|
||||
static Machine *AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Machines_Apple_AppleIIgs_hpp */
|
||||
597
Machines/Apple/AppleIIgs/MemoryMap.hpp
Normal file
597
Machines/Apple/AppleIIgs/MemoryMap.hpp
Normal file
@@ -0,0 +1,597 @@
|
||||
//
|
||||
// MemoryMap.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 25/10/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Machines_Apple_AppleIIgs_MemoryMap_hpp
|
||||
#define Machines_Apple_AppleIIgs_MemoryMap_hpp
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
#include <vector>
|
||||
|
||||
#include "../AppleII/LanguageCardSwitches.hpp"
|
||||
#include "../AppleII/AuxiliaryMemorySwitches.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace IIgs {
|
||||
|
||||
class MemoryMap {
|
||||
public:
|
||||
// MARK: - Initial construction and configuration.
|
||||
|
||||
MemoryMap() : auxiliary_switches_(*this), language_card_(*this) {}
|
||||
|
||||
void set_storage(std::vector<uint8_t> &ram, std::vector<uint8_t> &rom) {
|
||||
// Keep a pointer for later; also note the proper RAM offset.
|
||||
ram_base = ram.data();
|
||||
shadow_base[0] = ram_base; // i.e. all unshadowed writes go to where they've already gone (to make a no-op).
|
||||
shadow_base[1] = &ram[ram.size() - 0x02'0000]; // i.e. all shadowed writes go somewhere in the last
|
||||
// 128bk of RAM.
|
||||
|
||||
// Establish bank mapping.
|
||||
uint8_t next_region = 0;
|
||||
auto region = [&next_region, this]() -> uint8_t {
|
||||
assert(next_region != this->regions.size());
|
||||
return next_region++;
|
||||
};
|
||||
auto set_region = [this](uint8_t bank, uint16_t start, uint16_t end, uint8_t region) {
|
||||
assert((end == 0xffff) || !(end&0xff));
|
||||
assert(!(start&0xff));
|
||||
|
||||
// Fill in memory map.
|
||||
size_t target = size_t((bank << 8) | (start >> 8));
|
||||
for(int c = start; c < end; c += 0x100) {
|
||||
region_map[target] = region;
|
||||
++target;
|
||||
}
|
||||
};
|
||||
auto set_regions = [set_region, region](uint8_t bank, std::initializer_list<uint16_t> addresses, std::vector<uint8_t> allocated_regions = {}) {
|
||||
uint16_t previous = 0x0000;
|
||||
auto next_region = allocated_regions.begin();
|
||||
for(uint16_t address: addresses) {
|
||||
set_region(bank, previous, address, next_region != allocated_regions.end() ? *next_region : region());
|
||||
previous = address;
|
||||
assert(next_region != allocated_regions.end() || allocated_regions.empty());
|
||||
if(next_region != allocated_regions.end()) ++next_region;
|
||||
}
|
||||
assert(next_region == allocated_regions.end());
|
||||
};
|
||||
|
||||
// Current beliefs about the IIgs memory map:
|
||||
//
|
||||
// * language card banking applies to banks $00, $01, $e0 and $e1;
|
||||
// * auxiliary memory switches apply to bank $00 only;
|
||||
// * shadowing may be enabled only on banks $00 and $01, or on all RAM pages; and
|
||||
// * whether bit 16 of the address is passed to the Mega II is selectable — this affects both the destination
|
||||
// of odd-bank shadows, and whether bank $e1 is actually distinct from $e0.
|
||||
//
|
||||
// So:
|
||||
//
|
||||
// * bank $00 needs to be divided by auxiliary and language card zones;
|
||||
// * banks $01, $e0 and $e1 need to be divided by language card zones only; and
|
||||
// * ROM banks and all other fast RAM banks don't need subdivision.
|
||||
|
||||
// Language card zones:
|
||||
//
|
||||
// $D000–$E000 4kb window, into either bank 1 or bank 2
|
||||
// $E000–end 12kb window, always the same RAM.
|
||||
|
||||
// Auxiliary zones:
|
||||
//
|
||||
// $0000–$0200 Zero page (and stack)
|
||||
// $0200–$0400 [space in between]
|
||||
// $0400–$0800 Text Page 1
|
||||
// $0800–$2000 [space in between]
|
||||
// $2000–$4000 High-res Page 1
|
||||
// $4000–$C000 [space in between]
|
||||
|
||||
// Card zones:
|
||||
//
|
||||
// $C100–$C2FF either cards or IIe-style ROM
|
||||
// $C300–$C3FF IIe-supplied 80-column card replacement ROM
|
||||
// $C400–$C7FF either cards or IIe-style ROM
|
||||
// $C800–$CFFF Standard extended card area
|
||||
|
||||
// Reserve region 0 as that for unmapped memory.
|
||||
region();
|
||||
|
||||
// Bank $00: all locations potentially affected by the auxiliary switches or the
|
||||
// language switches.
|
||||
set_regions(0x00, {
|
||||
0x0200, 0x0400, 0x0800,
|
||||
0x2000, 0x4000,
|
||||
0xc000, 0xc100, 0xc300, 0xc400, 0xc800,
|
||||
0xd000, 0xe000,
|
||||
0xffff
|
||||
});
|
||||
|
||||
// Bank $01: all locations potentially affected by the language switches and card switches.
|
||||
set_regions(0x01, {
|
||||
0xc000, 0xc100, 0xc300, 0xc400, 0xc800,
|
||||
0xd000, 0xe000,
|
||||
0xffff
|
||||
});
|
||||
|
||||
// Banks $02–[end of RAM]: a single region.
|
||||
const auto fast_region = region();
|
||||
const uint8_t fast_ram_bank_limit = uint8_t(ram.size() / 0x01'0000);
|
||||
for(uint8_t bank = 0x02; bank < fast_ram_bank_limit; bank++) {
|
||||
set_region(bank, 0x0000, 0xffff, fast_region);
|
||||
}
|
||||
|
||||
// [Banks $80–$e0: empty].
|
||||
|
||||
// Banks $e0, $e1: all locations potentially affected by the language switches or marked for IO.
|
||||
// Alas, separate regions are needed due to the same ROM appearing on both pages.
|
||||
for(uint8_t c = 0; c < 2; c++) {
|
||||
set_regions(0xe0 + c, {0xc000, 0xc100, 0xc300, 0xc400, 0xc800, 0xd000, 0xe000, 0xffff});
|
||||
}
|
||||
|
||||
// [Banks $e2–[ROM start]: empty].
|
||||
|
||||
// ROM banks: directly mapped to ROM.
|
||||
const uint8_t rom_bank_count = uint8_t(rom.size() >> 16);
|
||||
const uint8_t first_rom_bank = uint8_t(0x100 - rom_bank_count);
|
||||
const uint8_t rom_region = region();
|
||||
for(uint8_t c = 0; c < rom_bank_count; ++c) {
|
||||
set_region(first_rom_bank + c, 0x0000, 0xffff, rom_region);
|
||||
}
|
||||
|
||||
// Apply proper storage to those banks.
|
||||
auto set_storage = [this](uint32_t address, const uint8_t *read, uint8_t *write) {
|
||||
// Don't allow the reserved null region to be modified.
|
||||
assert(region_map[address >> 8]);
|
||||
|
||||
// Either set or apply a quick bit of testing as to the logic at play.
|
||||
auto ®ion = regions[region_map[address >> 8]];
|
||||
if(read) read -= address;
|
||||
if(write) write -= address;
|
||||
if(!region.read) {
|
||||
region.read = read;
|
||||
region.write = write;
|
||||
} else {
|
||||
assert(region.read == read);
|
||||
assert(region.write == write);
|
||||
}
|
||||
};
|
||||
|
||||
// This is highly redundant, but decouples this step from the above.
|
||||
for(size_t c = 0; c < 0x80'0000; c += 0x100) {
|
||||
if(c < ram.size() - 0x02'0000) {
|
||||
set_storage(uint32_t(c), &ram[c], &ram[c]);
|
||||
}
|
||||
}
|
||||
uint8_t *const slow_ram = &ram[ram.size() - 0x02'0000] - 0xe0'0000;
|
||||
for(size_t c = 0xe0'0000; c < 0xe2'0000; c += 0x100) {
|
||||
set_storage(uint32_t(c), &slow_ram[c], &slow_ram[c]);
|
||||
}
|
||||
for(uint32_t c = 0; c < uint32_t(rom_bank_count); c++) {
|
||||
set_storage((first_rom_bank + c) << 16, &rom[c << 16], nullptr);
|
||||
}
|
||||
|
||||
// Set shadowing as working from banks 0 and 1 (forever).
|
||||
shadow_banks[0] = true;
|
||||
|
||||
// TODO: set 1Mhz flags.
|
||||
|
||||
// Apply initial language/auxiliary state.
|
||||
set_all_paging();
|
||||
}
|
||||
|
||||
// MARK: - Live bus access notifications and register access.
|
||||
|
||||
void set_shadow_register(uint8_t value) {
|
||||
const uint8_t diff = value ^ shadow_register_;
|
||||
shadow_register_ = value;
|
||||
|
||||
if(diff & 0x40) { // IO/language-card inhibit.
|
||||
set_language_card_paging();
|
||||
set_card_paging();
|
||||
}
|
||||
|
||||
if(diff & 0x3f) {
|
||||
set_shadowing();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t get_shadow_register() const {
|
||||
return shadow_register_;
|
||||
}
|
||||
|
||||
void set_speed_register(uint8_t value) {
|
||||
speed_register_ = value;
|
||||
|
||||
// Enable or disable shadowing from banks 0x02–0x80.
|
||||
for(size_t c = 0x01; c < 0x40; c++) {
|
||||
shadow_banks[c] = speed_register_ & 0x10;
|
||||
}
|
||||
}
|
||||
|
||||
void set_state_register(uint8_t value) {
|
||||
auxiliary_switches_.set_state(value);
|
||||
language_card_.set_state(value);
|
||||
}
|
||||
|
||||
uint8_t get_state_register() const {
|
||||
return language_card_.get_state() | auxiliary_switches_.get_state();
|
||||
}
|
||||
|
||||
void access(uint16_t address, bool is_read) {
|
||||
auxiliary_switches_.access(address, is_read);
|
||||
if((address & 0xfff0) == 0xc080) language_card_.access(address, is_read);
|
||||
}
|
||||
|
||||
using AuxiliaryMemorySwitches = Apple::II::AuxiliaryMemorySwitches<MemoryMap>;
|
||||
const AuxiliaryMemorySwitches &auxiliary_switches() const {
|
||||
return auxiliary_switches_;
|
||||
}
|
||||
|
||||
using LanguageCardSwitches = Apple::II::LanguageCardSwitches<MemoryMap>;
|
||||
const LanguageCardSwitches &language_card_switches() const {
|
||||
return language_card_;
|
||||
}
|
||||
|
||||
private:
|
||||
AuxiliaryMemorySwitches auxiliary_switches_;
|
||||
LanguageCardSwitches language_card_;
|
||||
friend AuxiliaryMemorySwitches;
|
||||
friend LanguageCardSwitches;
|
||||
|
||||
uint8_t shadow_register_ = 0x08;
|
||||
uint8_t speed_register_ = 0x00;
|
||||
|
||||
// MARK: - Memory banking.
|
||||
|
||||
#define assert_is_region(start, 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);
|
||||
|
||||
// Cf. LanguageCardSwitches; this function should update the region from
|
||||
// $D000 onwards as per the state of the language card flags — there may
|
||||
// end up being ROM or RAM (or auxiliary RAM), and the first 4kb of it
|
||||
// may be drawn from either of two pools.
|
||||
void set_language_card_paging() {
|
||||
const auto language_state = language_card_.state();
|
||||
const auto zero_state = auxiliary_switches_.zero_state();
|
||||
const bool inhibit_banks0001 = shadow_register_ & 0x40;
|
||||
|
||||
auto apply = [&language_state, this](uint32_t bank_base, uint8_t *ram) {
|
||||
// This assumes bank 1 is the one before bank 2 when RAM is linear.
|
||||
uint8_t *const d0_ram_bank = ram - (language_state.bank2 ? 0x0000 : 0x1000);
|
||||
|
||||
// Crib the ROM pointer from a page it's always visible on.
|
||||
const uint8_t *const rom = ®ions[region_map[0xffd0]].read[0xff'd000] - ((bank_base << 8) + 0xd000);
|
||||
|
||||
auto &d0_region = regions[region_map[bank_base | 0xd0]];
|
||||
d0_region.read = language_state.read ? d0_ram_bank : rom;
|
||||
d0_region.write = language_state.write ? nullptr : d0_ram_bank;
|
||||
|
||||
auto &e0_region = regions[region_map[bank_base | 0xe0]];
|
||||
e0_region.read = language_state.read ? ram : rom;
|
||||
e0_region.write = language_state.write ? nullptr : ram;
|
||||
|
||||
// Assert assumptions made above re: memory layout.
|
||||
assert(region_map[bank_base | 0xd0] + 1 == region_map[bank_base | 0xe0]);
|
||||
assert(region_map[bank_base | 0xe0] == region_map[bank_base | 0xff]);
|
||||
};
|
||||
auto set_no_card = [this](uint32_t bank_base) {
|
||||
auto &d0_region = regions[region_map[bank_base | 0xd0]];
|
||||
d0_region.read = ram_base;
|
||||
d0_region.write = ram_base;
|
||||
|
||||
auto &e0_region = regions[region_map[bank_base | 0xe0]];
|
||||
e0_region.read = ram_base;
|
||||
e0_region.write = ram_base;
|
||||
|
||||
// Assert assumptions made above re: memory layout.
|
||||
assert(region_map[bank_base | 0xd0] + 1 == region_map[bank_base | 0xe0]);
|
||||
assert(region_map[bank_base | 0xe0] == region_map[bank_base | 0xff]);
|
||||
};
|
||||
|
||||
if(inhibit_banks0001) {
|
||||
set_no_card(0x0000);
|
||||
set_no_card(0x0100);
|
||||
} else {
|
||||
apply(0x0000, zero_state ? &ram_base[0x01'0000] : ram_base);
|
||||
apply(0x0100, ram_base);
|
||||
}
|
||||
|
||||
// The pointer stored in region_map[0xe000] has already been adjusted for
|
||||
// the 0xe0'0000 addressing offset.
|
||||
uint8_t *const e0_ram = regions[region_map[0xe000]].write;
|
||||
apply(0xe000, e0_ram);
|
||||
apply(0xe100, e0_ram);
|
||||
}
|
||||
|
||||
// Cf. AuxiliarySwitches; this should establish whether ROM or card switches
|
||||
// are exposed in the distinct regions C100–C2FF, C300–C3FF, C400–C7FF and
|
||||
// C800–CFFF.
|
||||
//
|
||||
// On the IIgs it intersects with the current shadow register.
|
||||
//
|
||||
// TODO: so... shouldn't the card mask be incorporated here? I've got it implemented
|
||||
// distinctly at present, but does that create any invalid state interactions?
|
||||
void set_card_paging() {
|
||||
const bool inhibit_banks0001 = shadow_register_ & 0x40;
|
||||
const auto state = auxiliary_switches_.card_state();
|
||||
|
||||
auto apply = [&state, this](uint32_t bank_base) {
|
||||
auto &c0_region = regions[region_map[bank_base | 0xc0]];
|
||||
auto &c1_region = regions[region_map[bank_base | 0xc1]];
|
||||
auto &c3_region = regions[region_map[bank_base | 0xc3]];
|
||||
auto &c4_region = regions[region_map[bank_base | 0xc4]];
|
||||
auto &c8_region = regions[region_map[bank_base | 0xc8]];
|
||||
|
||||
const uint8_t *const rom = ®ions[region_map[0xffd0]].read[0xffc100] - ((bank_base << 8) + 0xc100);
|
||||
|
||||
// This is applied dynamically as it may be added or lost in banks $00 and $01.
|
||||
c0_region.flags |= Region::IsIO;
|
||||
|
||||
#define apply_region(flag, region) \
|
||||
if(flag) { \
|
||||
region.read = rom; \
|
||||
region.flags &= ~Region::IsIO; \
|
||||
} else { \
|
||||
region.flags |= Region::IsIO; \
|
||||
}
|
||||
|
||||
apply_region(state.region_C1_C3, c1_region);
|
||||
apply_region(state.region_C3, c3_region);
|
||||
apply_region(state.region_C4_C8, c4_region);
|
||||
apply_region(state.region_C8_D0, c8_region);
|
||||
|
||||
#undef apply_region
|
||||
|
||||
// Sanity checks.
|
||||
assert(region_map[bank_base | 0xc1] == region_map[bank_base | 0xc0]+1);
|
||||
assert(region_map[bank_base | 0xc2] == region_map[bank_base | 0xc1]);
|
||||
assert(region_map[bank_base | 0xc3] == region_map[bank_base | 0xc2]+1);
|
||||
assert(region_map[bank_base | 0xc4] == region_map[bank_base | 0xc3]+1);
|
||||
assert(region_map[bank_base | 0xc7] == region_map[bank_base | 0xc4]);
|
||||
assert(region_map[bank_base | 0xc8] == region_map[bank_base | 0xc7]+1);
|
||||
assert(region_map[bank_base | 0xcf] == region_map[bank_base | 0xc8]);
|
||||
assert(region_map[bank_base | 0xd0] == region_map[bank_base | 0xcf]+1);
|
||||
};
|
||||
|
||||
if(inhibit_banks0001) {
|
||||
// Set no IO in the Cx00 range for banks $00 and $01, just
|
||||
// regular RAM (or possibly auxiliary).
|
||||
const auto auxiliary_state = auxiliary_switches_.main_state();
|
||||
for(uint8_t region = region_map[0x00c0]; region < region_map[0x00d0]; region++) {
|
||||
regions[region].read = auxiliary_state.base.read ? &ram_base[0x01'0000] : ram_base;
|
||||
regions[region].write = auxiliary_state.base.write ? &ram_base[0x01'0000] : ram_base;
|
||||
regions[region].flags &= ~Region::IsIO;
|
||||
}
|
||||
for(uint8_t region = region_map[0x01c0]; region < region_map[0x01d0]; region++) {
|
||||
regions[region].read = regions[region].write = ram_base;
|
||||
regions[region].flags &= ~Region::IsIO;
|
||||
}
|
||||
} else {
|
||||
// Obey the card state for banks $00 and $01.
|
||||
apply(0x0000);
|
||||
apply(0x0100);
|
||||
}
|
||||
|
||||
// Obey the card state for banks $e0 and $e1.
|
||||
apply(0xe000);
|
||||
apply(0xe100);
|
||||
}
|
||||
|
||||
// Cf. LanguageCardSwitches; this should update whether base or auxiliary RAM is
|
||||
// visible in: (i) the zero and stack pages; and (ii) anywhere that the language
|
||||
// card is exposing RAM instead of ROM.
|
||||
void set_zero_page_paging() {
|
||||
// Affects bank $00 only, and should be a single region.
|
||||
auto ®ion = regions[region_map[0]];
|
||||
region.read = region.write = auxiliary_switches_.zero_state() ? &ram_base[0x01'0000] : ram_base;
|
||||
assert(region_map[0x0000] == region_map[0x0001]);
|
||||
assert(region_map[0x0001]+1 == region_map[0x0002]);
|
||||
|
||||
// Switching to or from auxiliary RAM potentially affects the
|
||||
// language card area.
|
||||
set_language_card_paging();
|
||||
}
|
||||
|
||||
// IIgs specific: sets or resets the ::IsShadowed flag across affected banks as
|
||||
// per the current state of the shadow register.
|
||||
//
|
||||
// Completely distinct from the auxiliary and language card switches.
|
||||
void set_shadowing() {
|
||||
// Relevant bits:
|
||||
//
|
||||
// b5: inhibit shadowing, text page 2 [if ROM 03; as if always set otherwise]
|
||||
// b4: inhibit shadowing, auxiliary high-res graphics
|
||||
// b3: inhibit shadowing, super high-res graphics
|
||||
// b2: inhibit shadowing, high-res graphics page 2
|
||||
// b1: inhibit shadowing, high-res graphics page 1
|
||||
// b0: inhibit shadowing, text page 1
|
||||
//
|
||||
// The interpretations of how the overlapping high-res and super high-res inhibit
|
||||
// bits apply used below is taken from The Apple IIgs Technical Reference, P. 178.
|
||||
|
||||
// Of course, zones are:
|
||||
//
|
||||
// $0400–$0800 Text Page 1
|
||||
// $0800–$0C00 Text Page 2 [ROM 03 machines]
|
||||
// $2000–$4000 High-res Page 1, and Super High-res in odd banks
|
||||
// $4000–$6000 High-res Page 2, and Huper High-res in odd banks
|
||||
// $6000–$a000 Odd banks only, rest of Super High-res
|
||||
// [plus IO and language card space, subject to your definition of shadowing]
|
||||
|
||||
constexpr int shadow_shift = 10;
|
||||
constexpr int auxiliary_offset = 0x10000 >> shadow_shift;
|
||||
|
||||
// Text Page 1, main and auxiliary — $0400–$0800.
|
||||
for(size_t c = 0x0400 >> shadow_shift; c < 0x0800 >> shadow_shift; c++) {
|
||||
shadow_pages[c] = shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x01);
|
||||
}
|
||||
|
||||
// Text Page 2, main and auxiliary — 0x0800–0x0c00.
|
||||
// TODO: on a ROM03 machine only.
|
||||
for(size_t c = 0x0800 >> shadow_shift; c < 0x0c00 >> shadow_shift; c++) {
|
||||
shadow_pages[c] = shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x20);
|
||||
}
|
||||
|
||||
// Hi-res graphics Page 1, main and auxiliary — $2000–$4000;
|
||||
// also part of the super high-res graphics page on odd pages.
|
||||
//
|
||||
// Even test applied:
|
||||
// high-res graphics page 1 inhibit bit alone is definitive.
|
||||
//
|
||||
// Odd test:
|
||||
// (high-res graphics inhibit or auxiliary high res graphics inhibit) _and_ (super high-res inhibit).
|
||||
//
|
||||
for(size_t c = 0x2000 >> shadow_shift; c < 0x4000 >> shadow_shift; c++) {
|
||||
shadow_pages[c] = !(shadow_register_ & 0x02);
|
||||
shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x12);
|
||||
}
|
||||
|
||||
// Hi-res graphics Page 2, main and auxiliary — $4000–$6000;
|
||||
// also part of the super high-res graphics page.
|
||||
//
|
||||
// Test applied: much like that for page 1.
|
||||
for(size_t c = 0x4000 >> shadow_shift; c < 0x6000 >> shadow_shift; c++) {
|
||||
shadow_pages[c] = !(shadow_register_ & 0x04);
|
||||
shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x14);
|
||||
}
|
||||
|
||||
// Residue of Super Hi-Res — $6000–$a000 (odd pages only).
|
||||
for(size_t c = 0x6000 >> shadow_shift; c < 0xa000 >> shadow_shift; c++) {
|
||||
shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x08);
|
||||
}
|
||||
}
|
||||
|
||||
// Cf. the AuxiliarySwitches; establishes whether main or auxiliary RAM
|
||||
// is exposed in bank $00 for a bunch of regions.
|
||||
void set_main_paging() {
|
||||
const auto state = auxiliary_switches_.main_state();
|
||||
|
||||
#define set(page, flags) {\
|
||||
auto ®ion = regions[region_map[page]]; \
|
||||
region.read = flags.read ? &ram_base[0x01'0000] : ram_base; \
|
||||
region.write = flags.write ? &ram_base[0x01'0000] : ram_base; \
|
||||
}
|
||||
|
||||
// Base: $0200–$03FF.
|
||||
set(0x02, state.base);
|
||||
assert_is_region(0x02, 0x04);
|
||||
|
||||
// Region $0400–$07ff.
|
||||
set(0x04, state.region_04_08);
|
||||
assert_is_region(0x04, 0x08);
|
||||
|
||||
// Base: $0800–$1FFF.
|
||||
set(0x08, state.base);
|
||||
assert_is_region(0x08, 0x20);
|
||||
|
||||
// Region $2000–$3FFF.
|
||||
set(0x20, state.region_20_40);
|
||||
assert_is_region(0x20, 0x40);
|
||||
|
||||
// Base: $4000–$BFFF.
|
||||
set(0x40, state.base);
|
||||
assert_is_region(0x40, 0xc0);
|
||||
|
||||
#undef set
|
||||
|
||||
// This also affects shadowing flags, if shadowing is enabled at all,
|
||||
// and might affect RAM in the IO area of bank $00 because the language
|
||||
// card can be inhibited on a IIgs.
|
||||
set_card_paging();
|
||||
}
|
||||
|
||||
void set_all_paging() {
|
||||
set_card_paging();
|
||||
set_zero_page_paging(); // ... which calls set_language_card_paging().
|
||||
set_main_paging();
|
||||
set_shadowing();
|
||||
}
|
||||
|
||||
void print_state() {
|
||||
uint8_t region = region_map[0];
|
||||
uint32_t start = 0;
|
||||
for(uint32_t top = 0; top < 65536; top++) {
|
||||
if(region_map[top] == region) continue;
|
||||
|
||||
printf("%06x -> %06x\t", start, top << 8);
|
||||
printf("%c%c\n",
|
||||
(regions[region_map[top] - 1].flags & Region::Is1Mhz) ? '1' : '-',
|
||||
(regions[region_map[top] - 1].flags & Region::IsIO) ? 'x' : '-'
|
||||
);
|
||||
|
||||
start = top << 8;
|
||||
region = region_map[top];
|
||||
}
|
||||
}
|
||||
|
||||
#undef assert_is_region
|
||||
|
||||
public:
|
||||
// Memory layout here is done via double indirection; the main loop should:
|
||||
// (i) use the top two bytes of the address to get an index from memory_map_; and
|
||||
// (ii) use that to index the memory_regions table.
|
||||
//
|
||||
// Pointers are eight bytes at the time of writing, so the extra level of indirection
|
||||
// reduces what would otherwise be a 1.25mb table down to not a great deal more than 64kb.
|
||||
std::array<uint8_t, 65536> region_map{};
|
||||
uint8_t *ram_base = nullptr;
|
||||
uint8_t *shadow_base[2] = {nullptr, nullptr};
|
||||
static constexpr int shadow_mask[2] = {0xff'ffff, 0x01'ffff};
|
||||
|
||||
struct Region {
|
||||
uint8_t *write = nullptr;
|
||||
const uint8_t *read = nullptr;
|
||||
uint8_t flags = 0;
|
||||
|
||||
enum Flag: uint8_t {
|
||||
Is1Mhz = 1 << 0, // Both reads and writes should be synchronised with the 1Mhz clock.
|
||||
IsIO = 1 << 1, // Indicates that this region should be checked for soft switches, registers, etc.
|
||||
};
|
||||
};
|
||||
|
||||
// Shadow_pages: divides the final 128kb of memory into 1kb chunks and includes a flag to indicate whether
|
||||
// each is a potential destination for shadowing.
|
||||
//
|
||||
// Shadow_banks: divides the whole 16mb of memory into 128kb chunks and includes a flag to indicate whether
|
||||
// each is a potential source of shadowing.
|
||||
std::bitset<128> shadow_pages, shadow_banks;
|
||||
|
||||
std::array<Region, 40> regions; // An assert above ensures that this is large enough; there's no
|
||||
// doctrinal reason for it to be whatever size it is now, just
|
||||
// adjust as required.
|
||||
};
|
||||
|
||||
// TODO: branching below on region.read/write is predicated on the idea that extra scratch space
|
||||
// would be less efficient. Verify that?
|
||||
|
||||
#define MemoryMapRegion(map, address) map.regions[map.region_map[address >> 8]]
|
||||
#define IsShadowed(map, region, address) (map.shadow_pages[((®ion.write[address] - map.ram_base) >> 10) & 127] & map.shadow_banks[address >> 17])
|
||||
#define MemoryMapRead(region, address, value) *value = region.read ? region.read[address] : 0xff
|
||||
#define MemoryMapWrite(map, region, address, value) \
|
||||
if(region.write) { \
|
||||
region.write[address] = *value; \
|
||||
const bool _mm_is_shadowed = IsShadowed(map, region, address); \
|
||||
map.shadow_base[_mm_is_shadowed][(®ion.write[address] - map.ram_base) & map.shadow_mask[_mm_is_shadowed]] = *value; \
|
||||
}
|
||||
|
||||
// Quick notes on ::IsShadowed contortions:
|
||||
//
|
||||
// The objective is to support shadowing:
|
||||
// 1. without storing a whole extra pointer, and such that the shadowing flags are orthogonal to the current auxiliary memory settings;
|
||||
// 2. in such a way as to support shadowing both in banks $00/$01 and elsewhere; and
|
||||
// 3. to do so without introducing too much in the way of branching.
|
||||
//
|
||||
// Hence the implemented solution: if shadowing is enabled then use the distance from the start of physical RAM
|
||||
// modulo 128k indexed into the bank $e0/$e1 RAM.
|
||||
//
|
||||
// With a further twist: the modulo and pointer are indexed on ::IsShadowed to eliminate a branch even on that.
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* MemoryMap_h */
|
||||
375
Machines/Apple/AppleIIgs/Sound.cpp
Normal file
375
Machines/Apple/AppleIIgs/Sound.cpp
Normal file
@@ -0,0 +1,375 @@
|
||||
//
|
||||
// Sound.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/11/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Sound.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <numeric>
|
||||
|
||||
// TODO: is it safe not to check for back-pressure in pending_stores_?
|
||||
|
||||
using namespace Apple::IIgs::Sound;
|
||||
|
||||
GLU::GLU(Concurrency::DeferringAsyncTaskQueue &audio_queue) : audio_queue_(audio_queue) {
|
||||
// Reset all pending stores.
|
||||
MemoryWrite disabled_write;
|
||||
disabled_write.enabled = false;
|
||||
for(int c = 0; c < StoreBufferSize; c++) {
|
||||
pending_stores_[c].store(disabled_write);
|
||||
}
|
||||
}
|
||||
|
||||
void GLU::set_data(uint8_t data) {
|
||||
if(local_.control & 0x40) {
|
||||
// RAM access.
|
||||
local_.ram_[address_] = data;
|
||||
|
||||
MemoryWrite write;
|
||||
write.enabled = true;
|
||||
write.address = address_;
|
||||
write.value = data;
|
||||
write.time = pending_store_write_time_;
|
||||
pending_stores_[pending_store_write_].store(write, std::memory_order::memory_order_release);
|
||||
|
||||
pending_store_write_ = (pending_store_write_ + 1) % (StoreBufferSize - 1);
|
||||
} else {
|
||||
// Register access.
|
||||
const auto address = address_; // To make sure I don't inadvertently 'capture' address_.
|
||||
local_.set_register(address, data);
|
||||
audio_queue_.defer([this, address, data] () {
|
||||
remote_.set_register(address, data);
|
||||
});
|
||||
}
|
||||
|
||||
if(local_.control & 0x20) {
|
||||
++address_;
|
||||
}
|
||||
}
|
||||
|
||||
void GLU::EnsoniqState::set_register(uint16_t address, uint8_t value) {
|
||||
switch(address & 0xe0) {
|
||||
case 0x00:
|
||||
oscillators[address & 0x1f].velocity = uint16_t((oscillators[address & 0x1f].velocity & 0xff00) | (value << 0));
|
||||
break;
|
||||
case 0x20:
|
||||
oscillators[address & 0x1f].velocity = uint16_t((oscillators[address & 0x1f].velocity & 0x00ff) | (value << 8));
|
||||
break;
|
||||
case 0x40:
|
||||
oscillators[address & 0x1f].volume = value;
|
||||
break;
|
||||
case 0x60:
|
||||
/* Does setting the last sample make any sense? */
|
||||
break;
|
||||
case 0x80:
|
||||
oscillators[address & 0x1f].address = value;
|
||||
break;
|
||||
case 0xa0: {
|
||||
oscillators[address & 0x1f].control = value;
|
||||
|
||||
// Halt + M0 => reset position.
|
||||
if((oscillators[address & 0x1f].control & 0x3) == 3) {
|
||||
oscillators[address & 0x1f].control |= 1;
|
||||
}
|
||||
} break;
|
||||
case 0xc0:
|
||||
oscillators[address & 0x1f].table_size = value;
|
||||
|
||||
// The most-significant bit that should be used is 16 + (value & 7).
|
||||
oscillators[address & 0x1f].overflow_mask = ~(0xffffff >> (7 - (value & 7)));
|
||||
break;
|
||||
|
||||
default:
|
||||
switch(address & 0xff) {
|
||||
case 0xe0:
|
||||
/* Does setting the interrupt register really make any sense? */
|
||||
break;
|
||||
case 0xe1:
|
||||
oscillator_count = 1 + ((value >> 1) & 31);
|
||||
break;
|
||||
case 0xe2:
|
||||
/* Writing to the analogue to digital input definitely makes no sense. */
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t GLU::get_data() {
|
||||
const auto address = address_;
|
||||
if(local_.control & 0x20) {
|
||||
++address_;
|
||||
}
|
||||
|
||||
switch(address & 0xe0) {
|
||||
case 0x00: return local_.oscillators[address & 0x1f].velocity & 0xff;
|
||||
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;
|
||||
case 0xa0: return local_.oscillators[address & 0x1f].control;
|
||||
case 0xc0: return local_.oscillators[address & 0x1f].table_size;
|
||||
|
||||
default:
|
||||
switch(address & 0xff) {
|
||||
case 0xe0: {
|
||||
// Find the first enabled oscillator that is signalling an interrupt and has interrupts enabled.
|
||||
for(int c = 0; c < local_.oscillator_count; c++) {
|
||||
if(local_.oscillators[c].interrupt_request && (local_.oscillators[c].control & 0x08)) {
|
||||
local_.oscillators[c].interrupt_request = false;
|
||||
return uint8_t(0x41 | (c << 1));
|
||||
}
|
||||
}
|
||||
|
||||
// No interrupt found.
|
||||
return 0xc1;
|
||||
} break;
|
||||
case 0xe1: return uint8_t((local_.oscillator_count - 1) << 1); // TODO: should other bits be 0 or 1?
|
||||
case 0xe2: return 128; // Input audio. Unimplemented!
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool GLU::get_interrupt_line() {
|
||||
// Return @c true if any oscillator currently has its interrupt request
|
||||
// set, and has interrupts enabled.
|
||||
for(int c = 0; c < local_.oscillator_count; c++) {
|
||||
if(local_.oscillators[c].interrupt_request && (local_.oscillators[c].control & 0x08)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// MARK: - Time entry points.
|
||||
|
||||
void GLU::run_for(Cycles cycles) {
|
||||
// Update local state, without generating audio.
|
||||
skip_audio(local_, cycles.as<size_t>());
|
||||
|
||||
// Update the timestamp for memory writes;
|
||||
pending_store_write_time_ += cycles.as<uint32_t>();
|
||||
}
|
||||
|
||||
void GLU::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
// Update remote state, generating audio.
|
||||
generate_audio(number_of_samples, target);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// MARK: - Interface boilerplate.
|
||||
|
||||
void GLU::set_control(uint8_t control) {
|
||||
local_.control = control;
|
||||
audio_queue_.defer([this, control] () {
|
||||
remote_.control = control;
|
||||
});
|
||||
}
|
||||
|
||||
uint8_t GLU::get_control() {
|
||||
return local_.control;
|
||||
}
|
||||
|
||||
void GLU::set_address_low(uint8_t low) {
|
||||
address_ = uint16_t((address_ & 0xff00) | low);
|
||||
}
|
||||
|
||||
uint8_t GLU::get_address_low() {
|
||||
return address_ & 0xff;
|
||||
}
|
||||
|
||||
void GLU::set_address_high(uint8_t high) {
|
||||
address_ = uint16_t((high << 8) | (address_ & 0x00ff));
|
||||
}
|
||||
|
||||
uint8_t GLU::get_address_high() {
|
||||
return address_ >> 8;
|
||||
}
|
||||
|
||||
// MARK: - Update logic.
|
||||
|
||||
Cycles GLU::get_next_sequence_point() const {
|
||||
uint32_t result = std::numeric_limits<decltype(result)>::max();
|
||||
|
||||
for(int c = 0; c < local_.oscillator_count; c++) {
|
||||
// Don't do anything for halted oscillators, or for oscillators that can't hit stops.
|
||||
if((local_.oscillators[c].control&3) != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine how many cycles until a stop is hit and update the pending result
|
||||
// if this is the new soonest-to-expire oscillator.
|
||||
const auto first_overflow_value = (local_.oscillators[c].overflow_mask - 1) << 1;
|
||||
const auto time_until_stop = (first_overflow_value - local_.oscillators[c].position + local_.oscillators[c].velocity - 1) / local_.oscillators[c].velocity;
|
||||
result = std::min(result, time_until_stop);
|
||||
}
|
||||
return Cycles(result);
|
||||
}
|
||||
|
||||
void GLU::skip_audio(EnsoniqState &state, size_t number_of_samples) {
|
||||
// Just advance all oscillator pointers and check for interrupts.
|
||||
// If a read occurs to the current-output level, generate it then.
|
||||
for(int c = 0; c < state.oscillator_count; c++) {
|
||||
// Don't do anything for halted oscillators.
|
||||
if(state.oscillators[c].control&1) continue;
|
||||
|
||||
// Update phase.
|
||||
state.oscillators[c].position += state.oscillators[c].velocity * number_of_samples;
|
||||
|
||||
// Check for stops, and any interrupts that therefore flow.
|
||||
if((state.oscillators[c].control & 2) && (state.oscillators[c].position & state.oscillators[c].overflow_mask)) {
|
||||
// Apply halt, set interrupt request flag.
|
||||
state.oscillators[c].position = 0;
|
||||
state.oscillators[c].control |= 1;
|
||||
state.oscillators[c].interrupt_request = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GLU::generate_audio(size_t number_of_samples, std::int16_t *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++) {
|
||||
|
||||
// TODO: there's a bit of a hack here where it is assumed that the input clock has been
|
||||
// divided in advance. Real hardware divides by 8, I think?
|
||||
|
||||
// Seed output as 0.
|
||||
int output = 0;
|
||||
|
||||
// Apply phase updates to all enabled oscillators.
|
||||
for(int c = 0; c < remote_.oscillator_count; c++) {
|
||||
// Don't do anything for halted oscillators.
|
||||
if(remote_.oscillators[c].control&1) continue;
|
||||
|
||||
remote_.oscillators[c].position += remote_.oscillators[c].velocity;
|
||||
|
||||
// Test for a new halting event.
|
||||
switch(remote_.oscillators[c].control & 6) {
|
||||
case 0: // Free-run mode; don't truncate the position at all, in case the
|
||||
// accumulator bits in use changes.
|
||||
output += remote_.oscillators[c].output(remote_.ram_);
|
||||
break;
|
||||
|
||||
case 2: // One-shot mode; check for end of run. Otherwise update sample.
|
||||
if(remote_.oscillators[c].position & remote_.oscillators[c].overflow_mask) {
|
||||
remote_.oscillators[c].position = 0;
|
||||
remote_.oscillators[c].control |= 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 4: // Sync/AM mode.
|
||||
if(c&1) {
|
||||
// Oscillator is odd-numbered; it will amplitude-modulate the next voice.
|
||||
next_amplitude = remote_.oscillators[c].sample(remote_.ram_);
|
||||
continue;
|
||||
} else {
|
||||
// Oscillator is even-numbered; it will 'sync' to the even voice, i.e. any
|
||||
// time it wraps around, it will reset the next oscillator.
|
||||
if(remote_.oscillators[c].position & remote_.oscillators[c].overflow_mask) {
|
||||
remote_.oscillators[c].position &= remote_.oscillators[c].overflow_mask;
|
||||
remote_.oscillators[c+1].position = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 6: // Swap mode; possibly trigger partner, and update sample.
|
||||
// Per tech note #11: "Whenever a swap occurs from a higher-numbered
|
||||
// oscillator to a lower-numbered one, the output signal from the corresponding
|
||||
// generator temporarily falls to the zero-crossing level (silence)"
|
||||
if(remote_.oscillators[c].position & remote_.oscillators[c].overflow_mask) {
|
||||
remote_.oscillators[c].control |= 1;
|
||||
remote_.oscillators[c].position = 0;
|
||||
remote_.oscillators[c^1].control &= ~1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Don't add output for newly-halted oscillators.
|
||||
if(remote_.oscillators[c].control&1) continue;
|
||||
|
||||
// Append new output.
|
||||
output += (remote_.oscillators[c].output(remote_.ram_) * next_amplitude) / 255;
|
||||
next_amplitude = 255;
|
||||
}
|
||||
|
||||
// Maximum total output was 32 channels times a 16-bit range. Map that down.
|
||||
// TODO: dynamic total volume?
|
||||
target[sample] = (output * output_range_) >> 20;
|
||||
|
||||
// Apply any RAM writes that interleave here.
|
||||
++pending_store_read_time_;
|
||||
if(!next_store.enabled) continue;
|
||||
if(next_store.time != pending_store_read_time_) continue;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t GLU::EnsoniqState::Oscillator::sample(uint8_t *ram) {
|
||||
// Determines how many you'd have to shift a 16-bit pointer to the right for,
|
||||
// in order to hit only the position-supplied bits.
|
||||
const int pointer_shift = 8 - ((table_size >> 3) & 7);
|
||||
|
||||
// Table size mask should be 0x8000 for the largest table size, and 0xff00 for
|
||||
// the smallest.
|
||||
const uint16_t table_size_mask = 0xffff >> pointer_shift;
|
||||
|
||||
// The pointer should use (at most) 15 bits; starting with bit 1 for resolution 0
|
||||
// and starting at bit 8 for resolution 7.
|
||||
const uint16_t table_pointer = uint16_t(position >> ((table_size&7) + pointer_shift));
|
||||
|
||||
// The full pointer is composed of the bits of the programmed address not touched by
|
||||
// the table pointer, plus the table pointer.
|
||||
const uint16_t sample_address = ((address << 8) & ~table_size_mask) | (table_pointer & table_size_mask);
|
||||
|
||||
// Ignored here: bit 6 should select between RAM banks. But for now this is IIgs-centric,
|
||||
// and that has only one bank of RAM.
|
||||
return ram[sample_address];
|
||||
}
|
||||
|
||||
int16_t GLU::EnsoniqState::Oscillator::output(uint8_t *ram) {
|
||||
const auto level = sample(ram);
|
||||
|
||||
// "An oscillator will halt when a zero is encountered in its waveform table."
|
||||
// TODO: only if in free-run mode, I think? Or?
|
||||
if(!level) {
|
||||
control |= 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Samples are unsigned 8-bit; do the proper work to make volume work correctly.
|
||||
return int8_t(level ^ 128) * volume;
|
||||
}
|
||||
111
Machines/Apple/AppleIIgs/Sound.hpp
Normal file
111
Machines/Apple/AppleIIgs/Sound.hpp
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// Sound.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/11/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Apple_IIgs_Sound_hpp
|
||||
#define Apple_IIgs_Sound_hpp
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace IIgs {
|
||||
namespace Sound {
|
||||
|
||||
class GLU: public Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
GLU(Concurrency::DeferringAsyncTaskQueue &audio_queue);
|
||||
|
||||
void set_control(uint8_t);
|
||||
uint8_t get_control();
|
||||
void set_data(uint8_t);
|
||||
uint8_t get_data();
|
||||
void set_address_low(uint8_t);
|
||||
uint8_t get_address_low();
|
||||
void set_address_high(uint8_t);
|
||||
uint8_t get_address_high();
|
||||
|
||||
void run_for(Cycles);
|
||||
Cycles get_next_sequence_point() const;
|
||||
bool get_interrupt_line();
|
||||
|
||||
// SampleSource.
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
void skip_samples(const std::size_t number_of_samples);
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
uint16_t address_ = 0;
|
||||
|
||||
// Use a circular buffer for piping memory alterations onto the audio
|
||||
// thread; it would be prohibitive to defer every write individually.
|
||||
//
|
||||
// Assumed: on most modern architectures, an atomic 64-bit read or
|
||||
// write can be achieved locklessly.
|
||||
struct MemoryWrite {
|
||||
uint32_t time;
|
||||
uint16_t address;
|
||||
uint8_t value;
|
||||
bool enabled;
|
||||
};
|
||||
static_assert(sizeof(MemoryWrite) == 8);
|
||||
constexpr static int StoreBufferSize = 16384;
|
||||
|
||||
std::atomic<MemoryWrite> pending_stores_[StoreBufferSize];
|
||||
uint32_t pending_store_read_ = 0, pending_store_read_time_ = 0;
|
||||
uint32_t pending_store_write_ = 0, pending_store_write_time_ = 0;
|
||||
|
||||
// Maintain state both 'locally' (i.e. on the emulation thread) and
|
||||
// 'remotely' (i.e. on the audio thread).
|
||||
struct EnsoniqState {
|
||||
uint8_t ram_[65536];
|
||||
struct Oscillator {
|
||||
uint32_t position;
|
||||
|
||||
// Programmer-set values.
|
||||
uint16_t velocity;
|
||||
uint8_t volume;
|
||||
uint8_t address;
|
||||
uint8_t control;
|
||||
uint8_t table_size;
|
||||
|
||||
// Derived state.
|
||||
uint32_t overflow_mask; // If a non-zero bit gets anywhere into the overflow mask, this channel
|
||||
// has wrapped around. It's a function of table_size.
|
||||
bool interrupt_request = false; // Will be non-zero if this channel would request an interrupt, were
|
||||
// it currently enabled to do so.
|
||||
|
||||
uint8_t sample(uint8_t *ram);
|
||||
int16_t output(uint8_t *ram);
|
||||
} oscillators[32];
|
||||
|
||||
// Some of these aren't actually needed on both threads.
|
||||
uint8_t control = 0;
|
||||
int oscillator_count = 1;
|
||||
|
||||
void set_register(uint16_t address, uint8_t value);
|
||||
} local_, remote_;
|
||||
|
||||
// Functions to update an EnsoniqState; these don't belong to the state itself
|
||||
// because they also access the pending stores (inter alia).
|
||||
void generate_audio(size_t number_of_samples, std::int16_t *target);
|
||||
void skip_audio(EnsoniqState &state, size_t number_of_samples);
|
||||
|
||||
// Audio-thread state.
|
||||
int16_t output_range_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* SoundGLU_hpp */
|
||||
814
Machines/Apple/AppleIIgs/Video.cpp
Normal file
814
Machines/Apple/AppleIIgs/Video.cpp
Normal file
@@ -0,0 +1,814 @@
|
||||
//
|
||||
// Video.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 31/10/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Video.hpp"
|
||||
|
||||
using namespace Apple::IIgs::Video;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int CyclesPerTick = 7; // One 'tick' being the non-stretched length of a cycle on the old Apple II 1Mhz clock.
|
||||
constexpr int CyclesPerLine = 456; // Each of the Mega II's cycles lasts 7 cycles, making 455/line except for the
|
||||
// final on on a line which lasts an additional 1 (i.e. is 1/7th longer).
|
||||
constexpr int Lines = 262;
|
||||
constexpr int FinalPixelLine = 192;
|
||||
|
||||
constexpr auto FinalColumn = CyclesPerLine / CyclesPerTick;
|
||||
|
||||
// Converts from Apple's RGB ordering to this emulator's.
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
#define PaletteConvulve(x) uint16_t(x)
|
||||
#else
|
||||
#define PaletteConvulve(x) uint16_t(((x&0xf00) >> 8) | ((x&0x0ff) << 8))
|
||||
#endif
|
||||
|
||||
// The 12-bit values used by the Apple IIgs to approximate Apple II colours,
|
||||
// as implied by tech note #63's use of them as border colours.
|
||||
// http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.063.html
|
||||
constexpr uint16_t appleii_palette[16] = {
|
||||
PaletteConvulve(0x0000), // Black.
|
||||
PaletteConvulve(0x0d03), // Deep Red.
|
||||
PaletteConvulve(0x0009), // Dark Blue.
|
||||
PaletteConvulve(0x0d2d), // Purple.
|
||||
PaletteConvulve(0x0072), // Dark Green.
|
||||
PaletteConvulve(0x0555), // Dark Gray.
|
||||
PaletteConvulve(0x022f), // Medium Blue.
|
||||
PaletteConvulve(0x06af), // Light Blue.
|
||||
PaletteConvulve(0x0850), // Brown.
|
||||
PaletteConvulve(0x0f60), // Orange.
|
||||
PaletteConvulve(0x0aaa), // Light Grey.
|
||||
PaletteConvulve(0x0f98), // Pink.
|
||||
PaletteConvulve(0x01d0), // Light Green.
|
||||
PaletteConvulve(0x0ff0), // Yellow.
|
||||
PaletteConvulve(0x04f9), // Aquamarine.
|
||||
PaletteConvulve(0x0fff), // White.
|
||||
};
|
||||
|
||||
// Reasoned guesswork ahoy!
|
||||
//
|
||||
// The IIgs VGC can fetch four bytes per column — I'm unclear physically how, but that's definitely true
|
||||
// since the IIgs modes packs 160 bytes work of graphics into the Apple II's usual 40-cycle fetch area;
|
||||
// it's possible that if I understood the meaning of the linear video bit in the new video flag I'd know more.
|
||||
//
|
||||
// Super Hi-Res also fetches 16*2 = 32 bytes of palette and a control byte sometime before each row.
|
||||
// So it needs five windows for that.
|
||||
//
|
||||
// Guessing four cycles of sync, I've chosen to arrange one output row for this emulator as:
|
||||
//
|
||||
// 5 cycles of back porch; [TODO: include a colour burst]
|
||||
// 8 windows left border, the final five of which fetch palette and control if in IIgs mode;
|
||||
// 40 windows of pixel output;
|
||||
// 8 cycles of right border;
|
||||
// 4 cycles of sync (including the extra 1/7th window, as it has to go _somewhere_).
|
||||
//
|
||||
// Otherwise, the first 200 rows may be pixels and the 192 in the middle of those are the II set.
|
||||
constexpr int first_sync_line = 220; // A complete guess. Information needed.
|
||||
|
||||
constexpr int blank_ticks = 5;
|
||||
constexpr int left_border_ticks = 8;
|
||||
constexpr int pixel_ticks = 40;
|
||||
constexpr int right_border_ticks = 8;
|
||||
|
||||
constexpr int start_of_left_border = blank_ticks;
|
||||
constexpr int start_of_pixels = start_of_left_border + left_border_ticks;
|
||||
constexpr int start_of_right_border = start_of_pixels + pixel_ticks;
|
||||
constexpr int start_of_sync = start_of_right_border + right_border_ticks;
|
||||
constexpr int sync_period = CyclesPerLine - start_of_sync*CyclesPerTick;
|
||||
|
||||
// I have made the guess that this occurs when the Mega II horizontal counter rolls over.
|
||||
// This is just a guess.
|
||||
constexpr int megaii_interrupt_point = 192*CyclesPerLine + (start_of_pixels - 28)*CyclesPerTick - 2;
|
||||
|
||||
|
||||
// A table to map from 7-bit integers to 14-bit versions with all bits doubled.
|
||||
constexpr uint16_t double_bytes[128] = {
|
||||
0x0000, 0x0003, 0x000c, 0x000f, 0x0030, 0x0033, 0x003c, 0x003f,
|
||||
0x00c0, 0x00c3, 0x00cc, 0x00cf, 0x00f0, 0x00f3, 0x00fc, 0x00ff,
|
||||
0x0300, 0x0303, 0x030c, 0x030f, 0x0330, 0x0333, 0x033c, 0x033f,
|
||||
0x03c0, 0x03c3, 0x03cc, 0x03cf, 0x03f0, 0x03f3, 0x03fc, 0x03ff,
|
||||
0x0c00, 0x0c03, 0x0c0c, 0x0c0f, 0x0c30, 0x0c33, 0x0c3c, 0x0c3f,
|
||||
0x0cc0, 0x0cc3, 0x0ccc, 0x0ccf, 0x0cf0, 0x0cf3, 0x0cfc, 0x0cff,
|
||||
0x0f00, 0x0f03, 0x0f0c, 0x0f0f, 0x0f30, 0x0f33, 0x0f3c, 0x0f3f,
|
||||
0x0fc0, 0x0fc3, 0x0fcc, 0x0fcf, 0x0ff0, 0x0ff3, 0x0ffc, 0x0fff,
|
||||
0x3000, 0x3003, 0x300c, 0x300f, 0x3030, 0x3033, 0x303c, 0x303f,
|
||||
0x30c0, 0x30c3, 0x30cc, 0x30cf, 0x30f0, 0x30f3, 0x30fc, 0x30ff,
|
||||
0x3300, 0x3303, 0x330c, 0x330f, 0x3330, 0x3333, 0x333c, 0x333f,
|
||||
0x33c0, 0x33c3, 0x33cc, 0x33cf, 0x33f0, 0x33f3, 0x33fc, 0x33ff,
|
||||
0x3c00, 0x3c03, 0x3c0c, 0x3c0f, 0x3c30, 0x3c33, 0x3c3c, 0x3c3f,
|
||||
0x3cc0, 0x3cc3, 0x3ccc, 0x3ccf, 0x3cf0, 0x3cf3, 0x3cfc, 0x3cff,
|
||||
0x3f00, 0x3f03, 0x3f0c, 0x3f0f, 0x3f30, 0x3f33, 0x3f3c, 0x3f3f,
|
||||
0x3fc0, 0x3fc3, 0x3fcc, 0x3fcf, 0x3ff0, 0x3ff3, 0x3ffc, 0x3fff,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Video::Video() :
|
||||
VideoSwitches<Cycles>(true, Cycles(2), [this] (Cycles cycles) { advance(cycles); }),
|
||||
crt_(CyclesPerLine - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red4Green4Blue4) {
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.097f, 0.1f, 0.85f, 0.85f));
|
||||
|
||||
// Reduce the initial bounce by cueing up the part of the frame that initial drawing actually
|
||||
// starts with. More or less.
|
||||
crt_.output_blank(228*63*2);
|
||||
|
||||
// Establish the shift lookup table for NTSC -> RGB output.
|
||||
for(size_t c = 0; c < sizeof(ntsc_delay_lookup_) / sizeof(*ntsc_delay_lookup_); c++) {
|
||||
const auto old_delay = c >> 2;
|
||||
|
||||
// If delay is 3, 2, 1 or 0 the output is just that minus 1.
|
||||
// Otherwise the output is either still 4, or 3 if the two lowest bits don't match.
|
||||
if(old_delay < 4) {
|
||||
ntsc_delay_lookup_[c] = (old_delay > 0) ? uint8_t(old_delay - 1) : 4;
|
||||
} else {
|
||||
ntsc_delay_lookup_[c] = (c&1) == ((c >> 1)&1) ? 4 : 3;
|
||||
}
|
||||
|
||||
ntsc_delay_lookup_[c] = 4;
|
||||
}
|
||||
}
|
||||
|
||||
void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void Video::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType Video::get_display_type() const {
|
||||
return crt_.get_display_type();
|
||||
}
|
||||
|
||||
void Video::set_internal_ram(const uint8_t *ram) {
|
||||
ram_ = ram;
|
||||
}
|
||||
|
||||
void Video::advance(Cycles cycles) {
|
||||
const int next_cycles_into_frame = cycles_into_frame_ + cycles.as<int>();
|
||||
|
||||
// Check for Mega II-style interrupt sources, prior to updating cycles_into_frame_.
|
||||
if(cycles_into_frame_ < megaii_interrupt_point && next_cycles_into_frame >= megaii_interrupt_point) {
|
||||
++megaii_frame_counter_;
|
||||
megaii_interrupt_state_ |= 0x08 | (megaii_frame_counter_ & 0x10);
|
||||
megaii_frame_counter_ &= 15;
|
||||
// The "quarter second interrupt" is also called the "3.75Hz interrupt" elsewhere.
|
||||
// So trigger it every 16 frames.
|
||||
}
|
||||
|
||||
// Update video output.
|
||||
const int column_start = (cycles_into_frame_ % CyclesPerLine) / CyclesPerTick;
|
||||
const int row_start = cycles_into_frame_ / CyclesPerLine;
|
||||
|
||||
cycles_into_frame_ = next_cycles_into_frame % (CyclesPerLine * Lines);
|
||||
|
||||
const int column_end = (cycles_into_frame_ % CyclesPerLine) / CyclesPerTick;
|
||||
const int row_end = cycles_into_frame_ / CyclesPerLine;
|
||||
|
||||
if(row_end == row_start) {
|
||||
if(column_end != column_start) {
|
||||
output_row(row_start, column_start, column_end);
|
||||
}
|
||||
} else {
|
||||
if(column_start != FinalColumn) {
|
||||
output_row(row_start, column_start, FinalColumn);
|
||||
}
|
||||
for(int row = row_start+1; row < row_end; row++) {
|
||||
output_row(row, 0, FinalColumn);
|
||||
}
|
||||
if(column_end) {
|
||||
output_row(row_end, 0, column_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cycles Video::get_next_sequence_point() const {
|
||||
const int cycles_into_row = cycles_into_frame_ % CyclesPerLine;
|
||||
const int row = cycles_into_frame_ / CyclesPerLine;
|
||||
|
||||
constexpr int sequence_point_offset = (blank_ticks + left_border_ticks) * CyclesPerTick;
|
||||
|
||||
// Seed as the distance to the next row 0.
|
||||
int result = CyclesPerLine + sequence_point_offset - cycles_into_row + (Lines - row - 1)*CyclesPerLine;
|
||||
|
||||
// Replace with the start of the next line, if closer.
|
||||
if(row <= 200) {
|
||||
if(cycles_into_row < sequence_point_offset) return Cycles(sequence_point_offset - cycles_into_row);
|
||||
if(row < 200) result = CyclesPerLine + sequence_point_offset - cycles_into_row;
|
||||
}
|
||||
|
||||
// Replace with the next Mega II interrupt point if those are enabled and it is sooner.
|
||||
if(megaii_interrupt_mask_) {
|
||||
const int time_until_megaii = megaii_interrupt_point - cycles_into_frame_;
|
||||
if(time_until_megaii > 0 && time_until_megaii < result) {
|
||||
result = time_until_megaii;
|
||||
}
|
||||
}
|
||||
|
||||
return Cycles(result);
|
||||
}
|
||||
|
||||
void Video::output_row(int row, int start, int end) {
|
||||
|
||||
// Deal with vertical sync.
|
||||
if(row >= first_sync_line && row < first_sync_line + 3) {
|
||||
// Simplification: just output the whole line at line's end.
|
||||
if(end == FinalColumn) {
|
||||
crt_.output_sync(CyclesPerLine - sync_period);
|
||||
crt_.output_blank(sync_period);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Pixel or pure border => blank as usual.
|
||||
|
||||
// Output blank only at the end of its window.
|
||||
if(start < blank_ticks && end >= blank_ticks) {
|
||||
crt_.output_blank(blank_ticks * CyclesPerTick);
|
||||
start = blank_ticks;
|
||||
if(start == end) return;
|
||||
}
|
||||
|
||||
// The pixel buffer will actually be allocated a column early, to allow double high/low res to start
|
||||
// half a column before everything else.
|
||||
constexpr int pixel_buffer_allocation = start_of_pixels - 1;
|
||||
|
||||
// Possibly output border, pixels, border, if this is a pixel line.
|
||||
if(row < 192 + ((new_video_&0x80) >> 4)) { // i.e. 192 lines for classic Apple II video, 200 for IIgs video.
|
||||
|
||||
// Output left border as far as currently known.
|
||||
if(start >= start_of_left_border && start < pixel_buffer_allocation) {
|
||||
const int end_of_period = std::min(pixel_buffer_allocation, end);
|
||||
|
||||
if(border_colour_) {
|
||||
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
|
||||
if(pixel) *pixel = border_colour_;
|
||||
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
|
||||
} else {
|
||||
crt_.output_blank((end_of_period - start) * CyclesPerTick);
|
||||
}
|
||||
|
||||
start = end_of_period;
|
||||
if(start == end) return;
|
||||
}
|
||||
|
||||
assert(end > start);
|
||||
|
||||
// Fetch and output such pixels as it is time for.
|
||||
if(start >= pixel_buffer_allocation && start < start_of_right_border) {
|
||||
const int end_of_period = std::min(start_of_right_border, end);
|
||||
const auto mode = graphics_mode(row);
|
||||
|
||||
if(start == pixel_buffer_allocation) {
|
||||
// YUCKY HACK. I do not know when the IIgs fetches its super high-res palette
|
||||
// and control byte. Since I do not know, any guess is equally likely negatively
|
||||
// to affect software. Therefore this hack is as good as any other guess:
|
||||
// assume RAM has magical burst bandwidth, and fetch the whole set instantly.
|
||||
// I could spread this stuff out to allow for real bandwidth, but it'd likely be
|
||||
// no more accurate, while having less of an obvious I-HACKED-THIS red flag attached.
|
||||
line_control_ = ram_[0x19d00 + row];
|
||||
const int palette_base = (line_control_ & 15) * 32 + 0x19e00;
|
||||
for(int c = 0; c < 16; c++) {
|
||||
const int entry = ram_[palette_base + (c << 1)] | (ram_[palette_base + (c << 1) + 1] << 8);
|
||||
palette_[c] = PaletteConvulve(entry);
|
||||
}
|
||||
|
||||
// Post an interrupt if requested.
|
||||
if(line_control_ & 0x40) {
|
||||
set_interrupts(0x20);
|
||||
}
|
||||
|
||||
// Set up appropriately for fill mode (or not).
|
||||
for(int c = 0; c < 4; c++) {
|
||||
palette_zero_[c] = (line_control_ & 0x20) ? &palette_[c * 4] : &palette_throwaway_;
|
||||
}
|
||||
|
||||
// Reset NTSC decoding and total line buffering.
|
||||
ntsc_delay_ = 4;
|
||||
pixels_start_column_ = start;
|
||||
}
|
||||
|
||||
if(!next_pixel_ || pixels_format_ != format_for_mode(mode)) {
|
||||
// Flush anything already in a buffer.
|
||||
if(pixels_start_column_ < start) {
|
||||
crt_.output_data((start - pixels_start_column_) * CyclesPerTick, next_pixel_ ? size_t(next_pixel_ - pixels_) : 1);
|
||||
next_pixel_ = pixels_ = nullptr;
|
||||
}
|
||||
|
||||
// Allocate a new buffer; 640 plus one column is as bad as it gets.
|
||||
// TODO: make proper size estimate?
|
||||
next_pixel_ = pixels_ = reinterpret_cast<uint16_t *>(crt_.begin_data(656, 2));
|
||||
pixels_start_column_ = start;
|
||||
pixels_format_ = format_for_mode(mode);
|
||||
}
|
||||
|
||||
if(next_pixel_) {
|
||||
int window_start = start - start_of_pixels;
|
||||
const int window_end = end_of_period - start_of_pixels;
|
||||
|
||||
// Fill in border colour if this is the first column.
|
||||
if(window_start == -1) {
|
||||
if(next_pixel_) {
|
||||
int extra_border_length;
|
||||
switch(mode) {
|
||||
case GraphicsMode::DoubleText:
|
||||
case GraphicsMode::Text:
|
||||
case GraphicsMode::DoubleHighRes:
|
||||
case GraphicsMode::DoubleLowRes:
|
||||
case GraphicsMode::DoubleHighResMono:
|
||||
extra_border_length = 7;
|
||||
break;
|
||||
case GraphicsMode::HighRes:
|
||||
case GraphicsMode::LowRes:
|
||||
case GraphicsMode::FatLowRes:
|
||||
extra_border_length = 14;
|
||||
break;
|
||||
case GraphicsMode::SuperHighRes:
|
||||
extra_border_length = (line_control_ & 0x80) ? 16 : 8;
|
||||
break;
|
||||
default: // Unreachable.
|
||||
extra_border_length = 0;
|
||||
break;
|
||||
}
|
||||
for(int c = 0; c < extra_border_length; c++) {
|
||||
next_pixel_[c] = border_colour_;
|
||||
}
|
||||
next_pixel_ += extra_border_length;
|
||||
}
|
||||
++window_start;
|
||||
if(window_start == window_end) return;
|
||||
}
|
||||
|
||||
switch(mode) {
|
||||
case GraphicsMode::SuperHighRes:
|
||||
next_pixel_ = output_super_high_res(next_pixel_, window_start, window_end, row);
|
||||
break;
|
||||
|
||||
case GraphicsMode::Text:
|
||||
next_pixel_ = output_text(next_pixel_, window_start, window_end, row);
|
||||
break;
|
||||
case GraphicsMode::DoubleText:
|
||||
next_pixel_ = output_double_text(next_pixel_, window_start, window_end, row);
|
||||
break;
|
||||
|
||||
case GraphicsMode::FatLowRes:
|
||||
next_pixel_ = output_fat_low_resolution(next_pixel_, window_start, window_end, row);
|
||||
break;
|
||||
case GraphicsMode::LowRes:
|
||||
next_pixel_ = output_low_resolution(next_pixel_, window_start, window_end, row);
|
||||
break;
|
||||
case GraphicsMode::DoubleLowRes:
|
||||
next_pixel_ = output_double_low_resolution(next_pixel_, window_start, window_end, row);
|
||||
break;
|
||||
|
||||
case GraphicsMode::HighRes:
|
||||
next_pixel_ = output_high_resolution(next_pixel_, window_start, window_end, row);
|
||||
break;
|
||||
case GraphicsMode::DoubleHighRes:
|
||||
next_pixel_ = output_double_high_resolution(next_pixel_, window_start, window_end, row);
|
||||
break;
|
||||
case GraphicsMode::DoubleHighResMono:
|
||||
next_pixel_ = output_double_high_resolution_mono(next_pixel_, window_start, window_end, row);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false); // i.e. other modes yet to do.
|
||||
}
|
||||
}
|
||||
|
||||
if(end_of_period == start_of_right_border) {
|
||||
// Flush what remains in the NTSC queue, if applicable.
|
||||
// TODO: with real NTSC test, why not?
|
||||
if(next_pixel_ && is_colour_ntsc(mode)) {
|
||||
ntsc_shift_ >>= 14;
|
||||
next_pixel_ = output_shift(next_pixel_, 81);
|
||||
}
|
||||
|
||||
crt_.output_data((start_of_right_border - pixels_start_column_) * CyclesPerTick, next_pixel_ ? size_t(next_pixel_ - pixels_) : 1);
|
||||
next_pixel_ = pixels_ = nullptr;
|
||||
}
|
||||
|
||||
start = end_of_period;
|
||||
if(start == end) return;
|
||||
}
|
||||
|
||||
assert(end > start);
|
||||
|
||||
// Output right border as far as currently known.
|
||||
if(start >= start_of_right_border && start < start_of_sync) {
|
||||
const int end_of_period = std::min(start_of_sync, end);
|
||||
|
||||
if(border_colour_) {
|
||||
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
|
||||
if(pixel) *pixel = border_colour_;
|
||||
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
|
||||
} else {
|
||||
crt_.output_blank((end_of_period - start) * CyclesPerTick);
|
||||
}
|
||||
|
||||
// There's no point updating start here; just fall
|
||||
// through to the end == FinalColumn test.
|
||||
}
|
||||
} else {
|
||||
// This line is all border, all the time.
|
||||
if(start >= start_of_left_border && start < start_of_sync) {
|
||||
const int end_of_period = std::min(start_of_sync, end);
|
||||
|
||||
if(border_colour_) {
|
||||
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
|
||||
if(pixel) *pixel = border_colour_;
|
||||
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
|
||||
} else {
|
||||
crt_.output_blank((end_of_period - start) * CyclesPerTick);
|
||||
}
|
||||
|
||||
start = end_of_period;
|
||||
if(start == end) return;
|
||||
}
|
||||
}
|
||||
|
||||
// Output sync if the moment has arrived.
|
||||
if(end == FinalColumn) {
|
||||
crt_.output_sync(sync_period);
|
||||
}
|
||||
}
|
||||
|
||||
bool Video::get_is_vertical_blank(Cycles offset) {
|
||||
// Cf. http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.040.html ;
|
||||
// this bit covers the entire vertical border area, not just the NTSC-sense vertical blank,
|
||||
// and considers the border to begin at 192 even though Super High-res mode is 200 lines.
|
||||
return (cycles_into_frame_ + offset.as<int>())%(Lines * CyclesPerLine) >= FinalPixelLine * CyclesPerLine;
|
||||
}
|
||||
|
||||
Video::Counters Video::get_counters(Cycles offset) {
|
||||
// Tech note #39:
|
||||
//
|
||||
// "The seven-bit horizontal counter starts at $00 and counts from $40 to $7F (the sequence
|
||||
// is $00, $40, $41,...,$7E, $7F, $00, $40,...). The active video time consists of 40 one
|
||||
// microsecond clock cycles starting with $58 and ending with $7F."
|
||||
//
|
||||
// "The nine-bit vertical counter ranges from $FA through $1FF (250 through 511) in NTSC mode
|
||||
// (vertical line count of 262) and from $C8 through $1FF (200 through 511) in PAL video timing
|
||||
// mode (vertical line count of 312). Vertical counter value $100 corresponds to scan line
|
||||
// zero in NTSC mode."
|
||||
|
||||
// Work out the internal offset into frame; a modulo willoccur momentarily...
|
||||
auto cycles_into_frame = cycles_into_frame_ + offset.as<int>();
|
||||
|
||||
// Nudge slightly so that the regular start of line matches mine.
|
||||
// TODO: reorient my drawing around the native offsets?
|
||||
cycles_into_frame = (cycles_into_frame + 25 - start_of_pixels)%(Lines * CyclesPerLine);
|
||||
|
||||
// Break it down.
|
||||
const auto cycles_into_line = (cycles_into_frame % CyclesPerLine) / CyclesPerTick;
|
||||
auto lines_into_frame = cycles_into_frame / CyclesPerLine;
|
||||
|
||||
lines_into_frame += 0x100;
|
||||
return Counters(
|
||||
lines_into_frame - ((lines_into_frame / 0x200) * 0x106), // TODO: this assumes NTSC.
|
||||
cycles_into_line + bool(cycles_into_line) * 0x40);
|
||||
}
|
||||
|
||||
uint8_t Video::get_horizontal_counter(Cycles offset) {
|
||||
const auto counters = get_counters(offset);
|
||||
return uint8_t(counters.horizontal | (counters.vertical << 7));
|
||||
}
|
||||
|
||||
uint8_t Video::get_vertical_counter(Cycles offset) {
|
||||
const auto counters = get_counters(offset);
|
||||
return uint8_t(counters.vertical >> 1);
|
||||
}
|
||||
|
||||
void Video::set_new_video(uint8_t new_video) {
|
||||
new_video_ = new_video;
|
||||
}
|
||||
|
||||
uint8_t Video::get_new_video() {
|
||||
return new_video_;
|
||||
}
|
||||
|
||||
void Video::clear_interrupts(uint8_t mask) {
|
||||
set_interrupts(interrupts_ & ~(mask & 0x60));
|
||||
}
|
||||
|
||||
void Video::set_interrupt_register(uint8_t mask) {
|
||||
set_interrupts(interrupts_ | (mask & 0x6));
|
||||
}
|
||||
|
||||
uint8_t Video::get_interrupt_register() {
|
||||
return interrupts_;
|
||||
}
|
||||
|
||||
bool Video::get_interrupt_line() {
|
||||
return (interrupts_&0x80) || (megaii_interrupt_mask_&megaii_interrupt_state_);
|
||||
}
|
||||
|
||||
void Video::set_megaii_interrupts_enabled(uint8_t mask) {
|
||||
megaii_interrupt_mask_ = mask;
|
||||
}
|
||||
|
||||
uint8_t Video::get_megaii_interrupt_status() {
|
||||
return megaii_interrupt_state_ | (get_annunciator_3() ? 0x20 : 0x00);
|
||||
}
|
||||
|
||||
void Video::clear_megaii_interrupts() {
|
||||
megaii_interrupt_state_ = 0;
|
||||
}
|
||||
|
||||
void Video::notify_clock_tick() {
|
||||
set_interrupts(interrupts_ | 0x40);
|
||||
}
|
||||
|
||||
void Video::set_interrupts(uint8_t new_value) {
|
||||
interrupts_ = new_value & 0x7f;
|
||||
if((interrupts_ >> 4) & interrupts_ & 0x6)
|
||||
interrupts_ |= 0x80;
|
||||
}
|
||||
|
||||
void Video::set_border_colour(uint8_t colour) {
|
||||
border_colour_entry_ = colour & 0x0f;
|
||||
border_colour_ = appleii_palette[border_colour_entry_];
|
||||
}
|
||||
|
||||
uint8_t Video::get_border_colour() {
|
||||
return border_colour_entry_;
|
||||
}
|
||||
|
||||
void Video::set_text_colour(uint8_t colour) {
|
||||
text_colour_entry_ = colour;
|
||||
text_colour_ = appleii_palette[colour >> 4];
|
||||
background_colour_ = appleii_palette[colour & 0xf];
|
||||
}
|
||||
|
||||
uint8_t Video::get_text_colour() {
|
||||
return text_colour_entry_;
|
||||
}
|
||||
|
||||
void Video::set_composite_is_colour(bool) {
|
||||
// TODO.
|
||||
}
|
||||
|
||||
bool Video::get_composite_is_colour() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: - Outputters.
|
||||
|
||||
uint16_t *Video::output_char(uint16_t *target, uint8_t source, int row) const {
|
||||
const int character = source & character_zones_[source >> 6].address_mask;
|
||||
const uint8_t xor_mask = character_zones_[source >> 6].xor_mask;
|
||||
const std::size_t character_address = size_t(character << 3) + (row & 7);
|
||||
const uint8_t character_pattern = character_rom_[character_address] ^ xor_mask;
|
||||
const uint16_t colours[2] = {background_colour_, text_colour_};
|
||||
|
||||
target[0] = colours[(character_pattern & 0x40) >> 6];
|
||||
target[1] = colours[(character_pattern & 0x20) >> 5];
|
||||
target[2] = colours[(character_pattern & 0x10) >> 4];
|
||||
target[3] = colours[(character_pattern & 0x08) >> 3];
|
||||
target[4] = colours[(character_pattern & 0x04) >> 2];
|
||||
target[5] = colours[(character_pattern & 0x02) >> 1];
|
||||
target[6] = colours[(character_pattern & 0x01) >> 0];
|
||||
return target + 7;
|
||||
}
|
||||
|
||||
uint16_t *Video::output_text(uint16_t *target, int start, int end, int row) const {
|
||||
const uint16_t row_address = get_row_address(row);
|
||||
for(int c = start; c < end; c++) {
|
||||
target = output_char(target, ram_[row_address + c], row);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
uint16_t *Video::output_double_text(uint16_t *target, int start, int end, int row) const {
|
||||
const uint16_t row_address = get_row_address(row);
|
||||
for(int c = start; c < end; c++) {
|
||||
target = output_char(target, ram_[0x10000 + row_address + c], row);
|
||||
target = output_char(target, ram_[row_address + c], row);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
uint16_t *Video::output_super_high_res(uint16_t *target, int start, int end, int row) const {
|
||||
const int row_address = row * 160 + 0x12000;
|
||||
|
||||
// The palette_zero_ writes ensure that palette colour 0 is replaced by whatever was last output,
|
||||
// if fill mode is enabled. Otherwise they go to throwaway storage.
|
||||
if(line_control_ & 0x80) {
|
||||
for(int c = start * 4; c < end * 4; c++) {
|
||||
const uint8_t source = ram_[row_address + c];
|
||||
*palette_zero_[3] = target[0] = palette_[0x8 + ((source >> 6) & 0x3)];
|
||||
*palette_zero_[0] = target[1] = palette_[0xc + ((source >> 4) & 0x3)];
|
||||
*palette_zero_[1] = target[2] = palette_[0x0 + ((source >> 2) & 0x3)];
|
||||
*palette_zero_[2] = target[3] = palette_[0x4 + ((source >> 0) & 0x3)];
|
||||
target += 4;
|
||||
}
|
||||
} else {
|
||||
for(int c = start * 4; c < end * 4; c++) {
|
||||
const uint8_t source = ram_[row_address + c];
|
||||
*palette_zero_[0] = target[0] = palette_[(source >> 4) & 0xf];
|
||||
*palette_zero_[0] = target[1] = palette_[source & 0xf];
|
||||
target += 2;
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
uint16_t *Video::output_double_high_resolution_mono(uint16_t *target, int start, int end, int row) {
|
||||
const uint16_t row_address = get_row_address(row);
|
||||
constexpr uint16_t colours[] = {0, 0xffff};
|
||||
for(int c = start; c < end; c++) {
|
||||
const uint8_t source[2] = {
|
||||
ram_[0x10000 + row_address + c],
|
||||
ram_[row_address + c],
|
||||
};
|
||||
|
||||
target[0] = colours[(source[1] >> 0) & 0x1];
|
||||
target[1] = colours[(source[1] >> 1) & 0x1];
|
||||
target[2] = colours[(source[1] >> 2) & 0x1];
|
||||
target[3] = colours[(source[1] >> 3) & 0x1];
|
||||
target[4] = colours[(source[1] >> 4) & 0x1];
|
||||
target[5] = colours[(source[1] >> 5) & 0x1];
|
||||
target[6] = colours[(source[1] >> 6) & 0x1];
|
||||
|
||||
target[7] = colours[(source[0] >> 0) & 0x1];
|
||||
target[8] = colours[(source[0] >> 1) & 0x1];
|
||||
target[9] = colours[(source[0] >> 2) & 0x1];
|
||||
target[10] = colours[(source[0] >> 3) & 0x1];
|
||||
target[11] = colours[(source[0] >> 4) & 0x1];
|
||||
target[12] = colours[(source[0] >> 5) & 0x1];
|
||||
target[13] = colours[(source[0] >> 6) & 0x1];
|
||||
|
||||
target += 14;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
uint16_t *Video::output_low_resolution(uint16_t *target, int start, int end, int row) {
|
||||
const int row_shift = row&4;
|
||||
const uint16_t row_address = get_row_address(row);
|
||||
for(int c = start; c < end; c++) {
|
||||
const uint8_t source = (ram_[row_address + c] >> row_shift) & 0xf;
|
||||
|
||||
// Convulve input as a function of odd/even row.
|
||||
uint32_t long_source;
|
||||
if(c&1) {
|
||||
long_source = uint32_t((source >> 2) | (source << 2) | (source << 6) | (source << 10));
|
||||
} else {
|
||||
long_source = uint32_t((source | (source << 4) | (source << 8) | (source << 12)) & 0x3fff);
|
||||
}
|
||||
|
||||
ntsc_shift_ = (long_source << 18) | (ntsc_shift_ >> 14);
|
||||
target = output_shift(target, 1 + c*2);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
uint16_t *Video::output_fat_low_resolution(uint16_t *target, int start, int end, int row) {
|
||||
const int row_shift = row&4;
|
||||
const uint16_t row_address = get_row_address(row);
|
||||
for(int c = start; c < end; c++) {
|
||||
const uint32_t doubled_source = uint32_t(double_bytes[(ram_[row_address + c] >> row_shift) & 0xf]);
|
||||
const uint32_t long_source = doubled_source | (doubled_source << 8);
|
||||
// TODO: verify the above.
|
||||
|
||||
ntsc_shift_ = (long_source << 18) | (ntsc_shift_ >> 14);
|
||||
target = output_shift(target, 1 + c*2);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
uint16_t *Video::output_double_low_resolution(uint16_t *target, int start, int end, int row) {
|
||||
const int row_shift = row&4;
|
||||
const uint16_t row_address = get_row_address(row);
|
||||
for(int c = start; c < end; c++) {
|
||||
const uint8_t source[2] = {
|
||||
uint8_t((ram_[row_address + c] >> row_shift) & 0xf),
|
||||
uint8_t((ram_[0x10000 + row_address + c] >> row_shift) & 0xf)
|
||||
};
|
||||
|
||||
// Convulve input as a function of odd/even row; this is very much like low
|
||||
// resolution mode except that the first 7 bits to be output will come from
|
||||
// source[1] and the next 7 from source[0]. Also shifting is offset by
|
||||
// half a window compared to regular low resolution, so the conditional
|
||||
// works the other way around.
|
||||
uint32_t long_source;
|
||||
if(c&1) {
|
||||
long_source = uint32_t((source[1] | ((source[1] << 4) & 0x70) | ((source[0] << 4) & 0x80) | (source[0] << 8) | (source[0] << 12)) & 0x3fff);
|
||||
} else {
|
||||
long_source = uint32_t((source[1] >> 2) | (source[1] << 2) | ((source[1] << 6) & 0x40) | ((source[0] << 6) & 0x380) | (source[0] << 10));
|
||||
}
|
||||
|
||||
ntsc_shift_ = (long_source << 18) | (ntsc_shift_ >> 14);
|
||||
target = output_shift(target, c*2);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
uint16_t *Video::output_high_resolution(uint16_t *target, int start, int end, int row) {
|
||||
const uint16_t row_address = get_row_address(row);
|
||||
for(int c = start; c < end; c++) {
|
||||
uint8_t source = ram_[row_address + c];
|
||||
const uint32_t doubled_source = uint32_t(double_bytes[source & 0x7f]);
|
||||
|
||||
// Just append new bits, doubled up (and possibly delayed).
|
||||
// TODO: I can kill the conditional here. Probably?
|
||||
if(source & high_resolution_mask_ & 0x80) {
|
||||
ntsc_shift_ = ((doubled_source & 0x1fff) << 19) | ((ntsc_shift_ >> 13) & 0x40000) | (ntsc_shift_ >> 14);
|
||||
} else {
|
||||
ntsc_shift_ = (doubled_source << 18) | (ntsc_shift_ >> 14);
|
||||
}
|
||||
|
||||
target = output_shift(target, 1 + c*2);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
uint16_t *Video::output_double_high_resolution(uint16_t *target, int start, int end, int row) {
|
||||
const uint16_t row_address = get_row_address(row);
|
||||
for(int c = start; c < end; c++) {
|
||||
const uint8_t source[2] = {
|
||||
ram_[0x10000 + row_address + c],
|
||||
ram_[row_address + c],
|
||||
};
|
||||
|
||||
ntsc_shift_ = unsigned(source[1] << 25) | unsigned(source[0] << 18) | (ntsc_shift_ >> 14);
|
||||
target = output_shift(target, c*2);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
uint16_t *Video::output_shift(uint16_t *target, int column) {
|
||||
// Make sure that at least two columns are enqueued before output begins;
|
||||
// the top bits can't be understood without reference to bits that come afterwards.
|
||||
if(!column) {
|
||||
ntsc_shift_ |= ntsc_shift_ >> 14;
|
||||
return target;
|
||||
}
|
||||
|
||||
// Phase here is kind of arbitrary; it pairs off with the order
|
||||
// I've picked for my rolls table and with my decision to count
|
||||
// columns as aligned with double-mode.
|
||||
const int phase = column * 7 + 3;
|
||||
constexpr uint8_t rolls[4][16] = {
|
||||
{
|
||||
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf
|
||||
},
|
||||
{
|
||||
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
|
||||
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
|
||||
},
|
||||
{
|
||||
0x0, 0x4, 0x8, 0xc, 0x1, 0x5, 0x9, 0xd,
|
||||
0x2, 0x6, 0xa, 0xe, 0x3, 0x7, 0xb, 0xf
|
||||
},
|
||||
{
|
||||
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
|
||||
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
|
||||
},
|
||||
};
|
||||
|
||||
#define OutputPixel(offset) {\
|
||||
ntsc_delay_ = ntsc_delay_lookup_[unsigned(ntsc_delay_ << 2) | ((ntsc_shift_ >> offset)&1) | ((ntsc_shift_ >> (offset + 3))&2)]; \
|
||||
const auto raw_bits = (ntsc_shift_ >> (offset + ntsc_delay_)) & 0x0f; \
|
||||
target[offset] = appleii_palette[rolls[(phase + offset + ntsc_delay_)&3][raw_bits]]; \
|
||||
}
|
||||
|
||||
OutputPixel(0);
|
||||
OutputPixel(1);
|
||||
OutputPixel(2);
|
||||
OutputPixel(3);
|
||||
OutputPixel(4);
|
||||
OutputPixel(5);
|
||||
OutputPixel(6);
|
||||
OutputPixel(7);
|
||||
OutputPixel(8);
|
||||
OutputPixel(9);
|
||||
OutputPixel(10);
|
||||
OutputPixel(11);
|
||||
OutputPixel(12);
|
||||
OutputPixel(13);
|
||||
|
||||
#undef OutputPixel
|
||||
|
||||
return target + 14;
|
||||
}
|
||||
213
Machines/Apple/AppleIIgs/Video.hpp
Normal file
213
Machines/Apple/AppleIIgs/Video.hpp
Normal file
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// Video.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 31/10/2020.
|
||||
// Copyright © 2020 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Apple_IIgs_Video_hpp
|
||||
#define Apple_IIgs_Video_hpp
|
||||
|
||||
#include "../AppleII/VideoSwitches.hpp"
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace IIgs {
|
||||
namespace Video {
|
||||
|
||||
/*!
|
||||
Provides IIgs video output; assumed clocking here is seven times the usual Apple II clock.
|
||||
So it'll produce a single line of video every 456 cycles — 65*7 + 1, allowing for the
|
||||
stretched cycle.
|
||||
*/
|
||||
class Video: public Apple::II::VideoSwitches<Cycles> {
|
||||
public:
|
||||
Video();
|
||||
void set_internal_ram(const uint8_t *);
|
||||
|
||||
bool get_is_vertical_blank(Cycles offset);
|
||||
uint8_t get_horizontal_counter(Cycles offset);
|
||||
uint8_t get_vertical_counter(Cycles offset);
|
||||
|
||||
void set_new_video(uint8_t);
|
||||
uint8_t get_new_video();
|
||||
|
||||
void clear_interrupts(uint8_t);
|
||||
uint8_t get_interrupt_register();
|
||||
void set_interrupt_register(uint8_t);
|
||||
bool get_interrupt_line();
|
||||
|
||||
void notify_clock_tick();
|
||||
|
||||
void set_border_colour(uint8_t);
|
||||
void set_text_colour(uint8_t);
|
||||
uint8_t get_text_colour();
|
||||
uint8_t get_border_colour();
|
||||
|
||||
void set_composite_is_colour(bool);
|
||||
bool get_composite_is_colour();
|
||||
|
||||
/// Sets the scan target.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
/// Gets the type of output.
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
|
||||
/// Determines the period until video might autonomously update its interrupt lines.
|
||||
Cycles get_next_sequence_point() const;
|
||||
|
||||
/// Sets the Mega II interrupt enable state — 1/4-second and VBL interrupts are
|
||||
/// generated here.
|
||||
void set_megaii_interrupts_enabled(uint8_t);
|
||||
|
||||
uint8_t get_megaii_interrupt_status();
|
||||
|
||||
void clear_megaii_interrupts();
|
||||
|
||||
private:
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
// This is coupled to Apple::II::GraphicsMode, but adds detail for the IIgs.
|
||||
enum class GraphicsMode {
|
||||
Text = 0,
|
||||
DoubleText,
|
||||
HighRes,
|
||||
DoubleHighRes,
|
||||
LowRes,
|
||||
DoubleLowRes,
|
||||
FatLowRes,
|
||||
|
||||
// Additions:
|
||||
DoubleHighResMono,
|
||||
SuperHighRes
|
||||
};
|
||||
constexpr bool is_colour_ntsc(GraphicsMode m) { return m >= GraphicsMode::HighRes && m <= GraphicsMode::FatLowRes; }
|
||||
|
||||
GraphicsMode graphics_mode(int row) const {
|
||||
if(new_video_ & 0x80) {
|
||||
return GraphicsMode::SuperHighRes;
|
||||
}
|
||||
|
||||
const auto ii_mode = Apple::II::VideoSwitches<Cycles>::graphics_mode(row);
|
||||
switch(ii_mode) {
|
||||
// Coupling very much assumed here.
|
||||
case Apple::II::GraphicsMode::DoubleHighRes:
|
||||
if(new_video_ & 0x20) {
|
||||
return GraphicsMode::DoubleHighResMono;
|
||||
}
|
||||
[[fallthrough]];
|
||||
|
||||
default: return GraphicsMode(int(ii_mode)); break;
|
||||
}
|
||||
}
|
||||
|
||||
enum class PixelBufferFormat {
|
||||
Text, DoubleText, NTSC, NTSCMono, SuperHighRes
|
||||
};
|
||||
constexpr PixelBufferFormat format_for_mode(GraphicsMode m) {
|
||||
switch(m) {
|
||||
case GraphicsMode::Text: return PixelBufferFormat::Text;
|
||||
case GraphicsMode::DoubleText: return PixelBufferFormat::DoubleText;
|
||||
default: return PixelBufferFormat::NTSC;
|
||||
case GraphicsMode::DoubleHighResMono: return PixelBufferFormat::NTSCMono;
|
||||
case GraphicsMode::SuperHighRes: return PixelBufferFormat::SuperHighRes;
|
||||
}
|
||||
}
|
||||
|
||||
void advance(Cycles);
|
||||
|
||||
uint8_t new_video_ = 0x01;
|
||||
uint8_t interrupts_ = 0x00;
|
||||
void set_interrupts(uint8_t);
|
||||
|
||||
int cycles_into_frame_ = 0;
|
||||
const uint8_t *ram_ = nullptr;
|
||||
|
||||
// The modal colours.
|
||||
uint16_t border_colour_ = 0;
|
||||
uint8_t border_colour_entry_ = 0;
|
||||
uint8_t text_colour_entry_ = 0xf0;
|
||||
uint16_t text_colour_ = 0xffff;
|
||||
uint16_t background_colour_ = 0;
|
||||
|
||||
// Current pixel output buffer and conceptual format.
|
||||
PixelBufferFormat pixels_format_;
|
||||
uint16_t *pixels_ = nullptr, *next_pixel_ = nullptr;
|
||||
int pixels_start_column_;
|
||||
|
||||
void output_row(int row, int start, int end);
|
||||
|
||||
uint16_t *output_super_high_res(uint16_t *target, int start, int end, int row) const;
|
||||
|
||||
uint16_t *output_text(uint16_t *target, int start, int end, int row) const;
|
||||
uint16_t *output_double_text(uint16_t *target, int start, int end, int row) const;
|
||||
uint16_t *output_char(uint16_t *target, uint8_t source, int row) const;
|
||||
|
||||
uint16_t *output_low_resolution(uint16_t *target, int start, int end, int row);
|
||||
uint16_t *output_fat_low_resolution(uint16_t *target, int start, int end, int row);
|
||||
uint16_t *output_double_low_resolution(uint16_t *target, int start, int end, int row);
|
||||
|
||||
uint16_t *output_high_resolution(uint16_t *target, int start, int end, int row);
|
||||
uint16_t *output_double_high_resolution(uint16_t *target, int start, int end, int row);
|
||||
uint16_t *output_double_high_resolution_mono(uint16_t *target, int start, int end, int row);
|
||||
|
||||
// Super high-res per-line state.
|
||||
uint8_t line_control_;
|
||||
uint16_t palette_[16];
|
||||
|
||||
// Storage used for fill mode.
|
||||
uint16_t *palette_zero_[4] = {nullptr, nullptr, nullptr, nullptr}, palette_throwaway_;
|
||||
|
||||
// Lookup tables and state to assist in the IIgs' mapping from NTSC to RGB.
|
||||
//
|
||||
// My understanding of the real-life algorithm is: maintain a four-bit buffer.
|
||||
// Fill it in a circular fashion. Ordinarily, output the result of looking
|
||||
// up the RGB mapping of those four bits of Apple II output (which outputs four
|
||||
// bits per NTSC colour cycle), commuted as per current phase. But if the bit
|
||||
// being inserted differs from that currently in its position in the shift
|
||||
// register, hold the existing output for three shifts.
|
||||
//
|
||||
// From there I am using the following:
|
||||
|
||||
// Maps from:
|
||||
//
|
||||
// b0 = b0 of the shift register
|
||||
// b1 = b4 of the shift register
|
||||
// b2– = current delay count
|
||||
//
|
||||
// to a new delay count.
|
||||
uint8_t ntsc_delay_lookup_[20];
|
||||
uint32_t ntsc_shift_ = 0; // Assumption here: logical shifts will ensue, rather than arithmetic.
|
||||
int ntsc_delay_ = 0;
|
||||
|
||||
/// Outputs the lowest 14 bits from @c ntsc_shift_, mapping to RGB.
|
||||
/// Phase is derived from @c column.
|
||||
uint16_t *output_shift(uint16_t *target, int column);
|
||||
|
||||
// Common getter for the two counters.
|
||||
struct Counters {
|
||||
Counters(int v, int h) : vertical(v), horizontal(h) {}
|
||||
const int vertical, horizontal;
|
||||
};
|
||||
Counters get_counters(Cycles offset);
|
||||
|
||||
// Marshalls the Mega II-style interrupt state.
|
||||
uint8_t megaii_interrupt_mask_ = 0;
|
||||
uint8_t megaii_interrupt_state_ = 0;
|
||||
int megaii_frame_counter_ = 0; // To count up to quarter-second interrupts.
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Video_hpp */
|
||||
|
||||
@@ -8,6 +8,36 @@
|
||||
|
||||
#include "DriveSpeedAccumulator.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
/*
|
||||
For knowledge encapsulate below, all credit goes to the MAME team. No original research here.
|
||||
|
||||
Per their investigation, the bytes collected for PWM output feed a 6-bit LFSR, which then keeps
|
||||
output high until it eventually reaches a state of 0x20. The LFSR shifts rightward and taps bits
|
||||
0 and 1 as the new input into bit 5.
|
||||
|
||||
I've therefore implemented the LFSR as below, feeding into a lookup table to calculate actual
|
||||
pulse widths from the values stored into the PWM buffer.
|
||||
*/
|
||||
template<uint8_t value> constexpr uint8_t lfsr() {
|
||||
if constexpr (value == 0x20 || !value) return 0;
|
||||
return 1+lfsr<(((value ^ (value >> 1))&1) << 5) | (value >> 1)>();
|
||||
}
|
||||
|
||||
constexpr uint8_t pwm_lookup[] = {
|
||||
lfsr<0>(), lfsr<1>(), lfsr<2>(), lfsr<3>(), lfsr<4>(), lfsr<5>(), lfsr<6>(), lfsr<7>(),
|
||||
lfsr<8>(), lfsr<9>(), lfsr<10>(), lfsr<11>(), lfsr<12>(), lfsr<13>(), lfsr<14>(), lfsr<15>(),
|
||||
lfsr<16>(), lfsr<17>(), lfsr<18>(), lfsr<19>(), lfsr<20>(), lfsr<21>(), lfsr<22>(), lfsr<23>(),
|
||||
lfsr<24>(), lfsr<25>(), lfsr<26>(), lfsr<27>(), lfsr<28>(), lfsr<29>(), lfsr<30>(), lfsr<31>(),
|
||||
lfsr<32>(), lfsr<33>(), lfsr<34>(), lfsr<35>(), lfsr<36>(), lfsr<37>(), lfsr<38>(), lfsr<39>(),
|
||||
lfsr<40>(), lfsr<41>(), lfsr<42>(), lfsr<43>(), lfsr<44>(), lfsr<45>(), lfsr<46>(), lfsr<47>(),
|
||||
lfsr<48>(), lfsr<49>(), lfsr<50>(), lfsr<51>(), lfsr<52>(), lfsr<53>(), lfsr<54>(), lfsr<55>(),
|
||||
lfsr<56>(), lfsr<57>(), lfsr<58>(), lfsr<59>(), lfsr<60>(), lfsr<61>(), lfsr<62>(), lfsr<63>(),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
void DriveSpeedAccumulator::post_sample(uint8_t sample) {
|
||||
@@ -17,40 +47,33 @@ void DriveSpeedAccumulator::post_sample(uint8_t sample) {
|
||||
// the samples until there is a certain small quantity of them,
|
||||
// then produce a new estimate of rotation speed and start the
|
||||
// buffer afresh.
|
||||
samples_[sample_pointer_] = sample;
|
||||
++sample_pointer_;
|
||||
|
||||
if(sample_pointer_ == samples_.size()) {
|
||||
sample_pointer_ = 0;
|
||||
//
|
||||
// Note the table lookup here; see text above.
|
||||
sample_total_ += pwm_lookup[sample & 0x3f];
|
||||
++sample_count_;
|
||||
|
||||
if(sample_count_ == samples_per_bucket) {
|
||||
// The below fits for a function like `a + bc`; it encapsultes the following
|
||||
// beliefs:
|
||||
//
|
||||
// (i) motor speed is proportional to voltage supplied;
|
||||
// (ii) with pulse-width modulation it's therefore proportional to the duty cycle;
|
||||
// (iii) the Mac pulse-width modulates whatever it reads from the disk speed buffer;
|
||||
// (iii) the Mac pulse-width modulates whatever it reads from the disk speed buffer, as per the LFSR rules above;
|
||||
// (iv) ... subject to software pulse-width modulation of that pulse-width modulation.
|
||||
//
|
||||
// So, I believe current motor speed is proportional to a low-pass filtering of
|
||||
// the speed buffer. Which I've implemented very coarsely via 'large' bucketed-averages,
|
||||
// noting also that exact disk motor speed is always a little approximate.
|
||||
|
||||
// Sum all samples.
|
||||
// TODO: if the above is the correct test, do it on sample receipt rather than
|
||||
// bothering with an intermediate buffer.
|
||||
int sum = 0;
|
||||
for(auto s: samples_) {
|
||||
sum += s;
|
||||
}
|
||||
|
||||
// The formula below was derived from observing values the Mac wrote into its
|
||||
// disk-speed buffer. Given that it runs a calibration loop before doing so,
|
||||
// I cannot guarantee the accuracy of these numbers beyond being within the
|
||||
// range that the computer would accept.
|
||||
const float normalised_sum = float(sum) / float(samples_.size());
|
||||
const float rotation_speed = (normalised_sum * 27.08f) - 259.0f;
|
||||
const float normalised_sum = float(sample_total_) / float(samples_per_bucket);
|
||||
const float rotation_speed = (normalised_sum - 3.7f) * 17.6f;
|
||||
|
||||
delegate_->drive_speed_accumulator_set_drive_speed(this, rotation_speed);
|
||||
sample_count_ = 0;
|
||||
sample_total_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,9 @@ class DriveSpeedAccumulator {
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<uint8_t, 20> samples_;
|
||||
std::size_t sample_pointer_ = 0;
|
||||
static constexpr int samples_per_bucket = 20;
|
||||
int sample_count_ = 0;
|
||||
int sample_total_ = 0;
|
||||
Delegate *delegate_ = nullptr;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "DeferredAudio.hpp"
|
||||
#include "DriveSpeedAccumulator.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
#include "RealTimeClock.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#include "../../MachineTypes.hpp"
|
||||
@@ -32,6 +31,7 @@
|
||||
#include "../../../Components/5380/ncr5380.hpp"
|
||||
#include "../../../Components/6522/6522.hpp"
|
||||
#include "../../../Components/8530/z8530.hpp"
|
||||
#include "../../../Components/AppleClock/AppleClock.hpp"
|
||||
#include "../../../Components/DiskII/IWM.hpp"
|
||||
#include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp"
|
||||
#include "../../../Processors/68000/68000.hpp"
|
||||
@@ -528,7 +528,10 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
quickboot_ = options->quickboot;
|
||||
if(quickboot_) {
|
||||
|
||||
using Model = Analyser::Static::Macintosh::Target::Model;
|
||||
const bool is_plus_rom = model == Model::Mac512ke || model == Model::MacPlus;
|
||||
if(quickboot_ && is_plus_rom) {
|
||||
// Cf. Big Mess o' Wires' disassembly of the Mac Plus ROM, and the
|
||||
// test at $E00. TODO: adapt as(/if?) necessary for other Macs.
|
||||
ram_[0x02ae] = 0x40;
|
||||
@@ -644,11 +647,11 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
return mouse_;
|
||||
}
|
||||
|
||||
using IWMActor = JustInTimeActor<IWM, 1, 1, HalfCycles, Cycles>;
|
||||
using IWMActor = JustInTimeActor<IWM>;
|
||||
|
||||
class VIAPortHandler: public MOS::MOS6522::PortHandler {
|
||||
public:
|
||||
VIAPortHandler(ConcreteMachine &machine, RealTimeClock &clock, Keyboard &keyboard, DeferredAudio &audio, IWMActor &iwm, Inputs::QuadratureMouse &mouse) :
|
||||
VIAPortHandler(ConcreteMachine &machine, Apple::Clock::SerialClock &clock, Keyboard &keyboard, DeferredAudio &audio, IWMActor &iwm, Inputs::QuadratureMouse &mouse) :
|
||||
machine_(machine), clock_(clock), keyboard_(keyboard), audio_(audio), iwm_(iwm), mouse_(mouse) {}
|
||||
|
||||
using Port = MOS::MOS6522::Port;
|
||||
@@ -751,7 +754,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
|
||||
private:
|
||||
ConcreteMachine &machine_;
|
||||
RealTimeClock &clock_;
|
||||
Apple::Clock::SerialClock &clock_;
|
||||
Keyboard &keyboard_;
|
||||
DeferredAudio &audio_;
|
||||
IWMActor &iwm_;
|
||||
@@ -766,7 +769,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
DeferredAudio audio_;
|
||||
Video video_;
|
||||
|
||||
RealTimeClock clock_;
|
||||
Apple::Clock::SerialClock clock_;
|
||||
Keyboard keyboard_;
|
||||
|
||||
MOS::MOS6522::MOS6522<VIAPortHandler> via_;
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
//
|
||||
// RealTimeClock.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef RealTimeClock_hpp
|
||||
#define RealTimeClock_hpp
|
||||
|
||||
#include "../../Utility/MemoryFuzzer.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
/*!
|
||||
Models the storage component of Apple's real-time clock.
|
||||
|
||||
Since tracking of time is pushed to this class, it is assumed
|
||||
that whomever is translating real time into emulated time
|
||||
will notify the VIA of a potential interrupt.
|
||||
*/
|
||||
class RealTimeClock {
|
||||
public:
|
||||
RealTimeClock() {
|
||||
// TODO: this should persist, if possible, rather than
|
||||
// being default initialised.
|
||||
const uint8_t default_data[] = {
|
||||
0xa8, 0x00, 0x00, 0x00,
|
||||
0xcc, 0x0a, 0xcc, 0x0a,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x02, 0x63, 0x00,
|
||||
0x03, 0x88, 0x00, 0x4c
|
||||
};
|
||||
memcpy(data_, default_data, sizeof(data_));
|
||||
}
|
||||
|
||||
/*!
|
||||
Advances the clock by 1 second.
|
||||
|
||||
The caller should also notify the VIA.
|
||||
*/
|
||||
void update() {
|
||||
for(int c = 0; c < 4; ++c) {
|
||||
++seconds_[c];
|
||||
if(seconds_[c]) break;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current clock and data inputs to the clock.
|
||||
*/
|
||||
void set_input(bool clock, bool data) {
|
||||
/*
|
||||
Documented commands:
|
||||
|
||||
z0000001 Seconds register 0 (lowest order byte)
|
||||
z0000101 Seconds register 1
|
||||
z0001001 Seconds register 2
|
||||
z0001101 Seconds register 3
|
||||
00110001 Test register (write only)
|
||||
00110101 Write-protect register (write only)
|
||||
z010aa01 RAM addresses 0x10 - 0x13
|
||||
z1aaaa01 RAM addresses 0x00 – 0x0f
|
||||
|
||||
z = 1 => a read; z = 0 => a write.
|
||||
|
||||
The top bit of the write-protect register enables (0) or disables (1)
|
||||
writes to other locations.
|
||||
|
||||
All the documentation says about the test register is to set the top
|
||||
two bits to 0 for normal operation. Abnormal operation is undefined.
|
||||
|
||||
The data line is valid when the clock transitions to level 0.
|
||||
*/
|
||||
|
||||
if(clock && !previous_clock_) {
|
||||
// Shift into the command_ register, no matter what.
|
||||
command_ = uint16_t((command_ << 1) | (data ? 1 : 0));
|
||||
result_ <<= 1;
|
||||
|
||||
// Increment phase.
|
||||
++phase_;
|
||||
|
||||
// When phase hits 8, inspect the command.
|
||||
// If it's a read, prepare a result.
|
||||
if(phase_ == 8) {
|
||||
if(command_ & 0x80) {
|
||||
// A read.
|
||||
const auto address = (command_ >> 2) & 0x1f;
|
||||
|
||||
// Begin pessimistically.
|
||||
result_ = 0xff;
|
||||
|
||||
if(address < 4) {
|
||||
result_ = seconds_[address];
|
||||
} else if(address >= 0x10) {
|
||||
result_ = data_[address & 0xf];
|
||||
} else if(address >= 0x8 && address <= 0xb) {
|
||||
result_ = data_[0x10 + (address & 0x3)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If phase hits 16 and this was a read command,
|
||||
// just stop. If it was a write command, do the
|
||||
// actual write.
|
||||
if(phase_ == 16) {
|
||||
if(!(command_ & 0x8000)) {
|
||||
// A write.
|
||||
|
||||
const auto address = (command_ >> 10) & 0x1f;
|
||||
const uint8_t value = uint8_t(command_ & 0xff);
|
||||
|
||||
// First test: is this to the write-protect register?
|
||||
if(address == 0xd) {
|
||||
write_protect_ = value;
|
||||
}
|
||||
|
||||
// No other writing is permitted if the write protect
|
||||
// register won't allow it.
|
||||
if(!(write_protect_ & 0x80)) {
|
||||
if(address < 4) {
|
||||
seconds_[address] = value;
|
||||
} else if(address >= 0x10) {
|
||||
data_[address & 0xf] = value;
|
||||
} else if(address >= 0x8 && address <= 0xb) {
|
||||
data_[0x10 + (address & 0x3)] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A phase of 16 always ends the command, so reset here.
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
previous_clock_ = clock;
|
||||
}
|
||||
|
||||
/*!
|
||||
Reads the current data output level from the clock.
|
||||
*/
|
||||
bool get_data() {
|
||||
return !!(result_ & 0x80);
|
||||
}
|
||||
|
||||
/*!
|
||||
Announces that a serial command has been aborted.
|
||||
*/
|
||||
void abort() {
|
||||
result_ = 0;
|
||||
phase_ = 0;
|
||||
command_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t data_[0x14];
|
||||
uint8_t seconds_[4];
|
||||
uint8_t write_protect_;
|
||||
|
||||
int phase_ = 0;
|
||||
uint16_t command_;
|
||||
uint8_t result_ = 0;
|
||||
|
||||
bool previous_clock_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* RealTimeClock_hpp */
|
||||
@@ -29,7 +29,17 @@ Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulato
|
||||
crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) {
|
||||
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
|
||||
// UGLY HACK. UGLY, UGLY HACK. UGLY!
|
||||
// The OpenGL scan target fails properly to place visible areas which are not 4:3.
|
||||
// The [newer] Metal scan target has no such issue. So assume that Apple => Metal,
|
||||
// and set a visible area to work around the OpenGL issue if required.
|
||||
// TODO: eliminate UGLY HACK.
|
||||
#ifdef __APPLE__
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.08f, 10.0f / 368.0f, 0.82f, 344.0f / 368.0f));
|
||||
#else
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
|
||||
#endif
|
||||
crt_.set_aspect_ratio(1.73f); // The Mac uses a non-standard scanning area.
|
||||
}
|
||||
|
||||
@@ -105,10 +115,13 @@ void Video::run_for(HalfCycles duration) {
|
||||
|
||||
pixel_buffer_ += 16;
|
||||
}
|
||||
} else {
|
||||
video_address_ += size_t(final_pixel_word - first_word);
|
||||
}
|
||||
|
||||
if(final_pixel_word == 32) {
|
||||
crt_.output_data(512);
|
||||
pixel_buffer_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ template<class T> class Cartridge:
|
||||
cycles_since_6532_update_ += Cycles(cycles_run_for / 3);
|
||||
bus_extender_.advance_cycles(cycles_run_for / 3);
|
||||
|
||||
if(operation != CPU::MOS6502::BusOperation::Ready) {
|
||||
if(isAccessOperation(operation)) {
|
||||
// give the cartridge a chance to respond to the bus access
|
||||
bus_extender_.perform_bus_operation(operation, address, value);
|
||||
|
||||
|
||||
@@ -347,18 +347,11 @@ class ConcreteMachine:
|
||||
update_audio();
|
||||
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
cycle.set_value8_high(ay_.get_data_output());
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
cycle.set_value8_high(GI::AY38910::Utility::read(ay_));
|
||||
} else {
|
||||
// Net effect here: addresses with bit 1 set write to a register,
|
||||
// addresses with bit 1 clear select a register.
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(
|
||||
GI::AY38910::BC2 | GI::AY38910::BDIR
|
||||
| ((address&2) ? 0 : GI::AY38910::BC1)
|
||||
));
|
||||
ay_.set_data_input(cycle.value8_high());
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::write(ay_, address&2, cycle.value8_high());
|
||||
}
|
||||
return delay + HalfCycles(2);
|
||||
|
||||
@@ -465,16 +458,14 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Update the video output, checking whether a sequence point has been hit.
|
||||
while(length >= cycles_until_video_event_) {
|
||||
length -= cycles_until_video_event_;
|
||||
video_ += cycles_until_video_event_;
|
||||
cycles_until_video_event_ = video_->get_next_sequence_point();
|
||||
assert(cycles_until_video_event_ > HalfCycles(0));
|
||||
if(video_.will_flush(length)) {
|
||||
length -= video_.cycles_until_implicit_flush();
|
||||
video_ += video_.cycles_until_implicit_flush();
|
||||
|
||||
mfp_->set_timer_event_input(1, video_->display_enabled());
|
||||
update_interrupt_input();
|
||||
}
|
||||
cycles_until_video_event_ -= length;
|
||||
|
||||
video_ += length;
|
||||
}
|
||||
|
||||
@@ -486,12 +477,11 @@ class ConcreteMachine:
|
||||
HalfCycles bus_phase_;
|
||||
|
||||
JustInTimeActor<Video> video_;
|
||||
HalfCycles cycles_until_video_event_;
|
||||
|
||||
// The MFP runs at 819200/2673749ths of the CPU clock rate.
|
||||
JustInTimeActor<Motorola::MFP68901::MFP68901, 819200, 2673749> mfp_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, 16> keyboard_acia_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, 16> midi_acia_;
|
||||
JustInTimeActor<Motorola::MFP68901::MFP68901, HalfCycles, 819200, 2673749> mfp_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> keyboard_acia_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> midi_acia_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
|
||||
@@ -215,7 +215,9 @@ class ConcreteMachine:
|
||||
}
|
||||
const HalfCycles length = cycle.length + penalty;
|
||||
|
||||
vdp_ += length;
|
||||
if(vdp_ += length) {
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
}
|
||||
time_since_sn76489_update_ += length;
|
||||
|
||||
// Act only if necessary.
|
||||
@@ -263,7 +265,6 @@ class ConcreteMachine:
|
||||
case 5:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 7: {
|
||||
@@ -287,9 +288,7 @@ class ConcreteMachine:
|
||||
case 0x52:
|
||||
// Read AY data.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
*cycle.value = ay_.get_data_output();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
*cycle.value = GI::AY38910::Utility::read(ay_);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@@ -306,7 +305,6 @@ class ConcreteMachine:
|
||||
case 5:
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 7:
|
||||
@@ -324,16 +322,12 @@ class ConcreteMachine:
|
||||
case 0x50:
|
||||
// Set AY address.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::BC1);
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::select_register(ay_, *cycle.value);
|
||||
break;
|
||||
case 0x51:
|
||||
// Set AY data.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR));
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::write_data(ay_, *cycle.value);
|
||||
break;
|
||||
case 0x53:
|
||||
super_game_module_.replace_ram = !!((*cycle.value)&0x1);
|
||||
@@ -347,13 +341,6 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
time_until_interrupt_ -= length;
|
||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||
z80_.set_non_maskable_interrupt_line(true, time_until_interrupt_);
|
||||
}
|
||||
}
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
@@ -390,7 +377,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
JustInTimeActor<TI::TMS::TMS9918, 1, 1, HalfCycles> vdp_;
|
||||
JustInTimeActor<TI::TMS::TMS9918> vdp_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
@@ -414,7 +401,6 @@ class ConcreteMachine:
|
||||
bool joysticks_in_keypad_mode_ = false;
|
||||
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
int pc_zero_accesses_ = 0;
|
||||
|
||||
@@ -17,11 +17,16 @@
|
||||
#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 "../Utility/Typer.hpp"
|
||||
#include "../../Analyser/Static/Acorn/Target.hpp"
|
||||
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "Interrupts.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
#include "Plus3.hpp"
|
||||
@@ -31,7 +36,7 @@
|
||||
|
||||
namespace Electron {
|
||||
|
||||
class ConcreteMachine:
|
||||
template <bool has_scsi_bus> class ConcreteMachine:
|
||||
public Machine,
|
||||
public MachineTypes::TimedMachine,
|
||||
public MachineTypes::ScanProducer,
|
||||
@@ -42,11 +47,16 @@ class ConcreteMachine:
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Tape::Delegate,
|
||||
public Utility::TypeRecipient<CharacterMapper>,
|
||||
public Activity::Source {
|
||||
public Activity::Source,
|
||||
public SCSI::Bus::Observer,
|
||||
public ClockingHint::Observer {
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Acorn::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
m6502_(*this),
|
||||
video_output_(ram_),
|
||||
scsi_bus_(4'000'000),
|
||||
hard_drive_(scsi_bus_, 0),
|
||||
scsi_device_(scsi_bus_.add_device()),
|
||||
video_(ram_),
|
||||
sound_generator_(audio_queue_),
|
||||
speaker_(sound_generator_) {
|
||||
memset(key_states_, 0, sizeof(key_states_));
|
||||
@@ -64,14 +74,23 @@ class ConcreteMachine:
|
||||
{machine_name, "the Acorn BASIC II ROM", "basic.rom", 16*1024, 0x79434781},
|
||||
{machine_name, "the Electron MOS ROM", "os.rom", 16*1024, 0xbf63fb1f}
|
||||
};
|
||||
if(target.has_adfs) {
|
||||
const size_t pres_adfs_rom_position = required_roms.size();
|
||||
if(target.has_pres_adfs) {
|
||||
required_roms.emplace_back(machine_name, "the E00 ADFS ROM, first slot", "ADFS-E00_1.rom", 16*1024, 0x51523993);
|
||||
required_roms.emplace_back(machine_name, "the E00 ADFS ROM, second slot", "ADFS-E00_2.rom", 16*1024, 0x8d17de0e);
|
||||
}
|
||||
const size_t acorn_adfs_rom_position = required_roms.size();
|
||||
if(target.has_acorn_adfs) {
|
||||
required_roms.emplace_back(machine_name, "the Acorn ADFS ROM", "adfs.rom", 16*1024, 0x3289bdc6);
|
||||
}
|
||||
const size_t dfs_rom_position = required_roms.size();
|
||||
if(target.has_dfs) {
|
||||
required_roms.emplace_back(machine_name, "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5);
|
||||
}
|
||||
const size_t ap6_rom_position = required_roms.size();
|
||||
if(target.has_ap6_rom) {
|
||||
required_roms.emplace_back(machine_name, "the 8kb Advanced Plus 6 ROM", "AP6v133.rom", 8*1024, 0xe0013cfc);
|
||||
}
|
||||
const auto roms = rom_fetcher(required_roms);
|
||||
|
||||
for(const auto &rom: roms) {
|
||||
@@ -82,15 +101,40 @@ class ConcreteMachine:
|
||||
set_rom(ROM::BASIC, *roms[0], false);
|
||||
set_rom(ROM::OS, *roms[1], false);
|
||||
|
||||
if(target.has_dfs || target.has_adfs) {
|
||||
/*
|
||||
ROM slot mapping applied:
|
||||
|
||||
* the keyboard and BASIC ROMs occupy slots 8, 9, 10 and 11;
|
||||
* the DFS, if in use, occupies slot 1;
|
||||
* the Pres ADFS, if in use, occupies slots 4 and 5;
|
||||
* the Acorn ADFS, if in use, occupies slot 6;
|
||||
* the AP6, if in use, occupies slot 15; and
|
||||
* if sideways RAM was asked for, all otherwise unused slots are populated with sideways RAM.
|
||||
*/
|
||||
if(target.has_dfs || target.has_acorn_adfs || target.has_pres_adfs) {
|
||||
plus3_ = std::make_unique<Plus3>();
|
||||
|
||||
if(target.has_dfs) {
|
||||
set_rom(ROM::Slot0, *roms[dfs_rom_position], true);
|
||||
}
|
||||
if(target.has_adfs) {
|
||||
set_rom(ROM::Slot4, *roms[2], true);
|
||||
set_rom(ROM::Slot5, *roms[3], true);
|
||||
if(target.has_pres_adfs) {
|
||||
set_rom(ROM::Slot4, *roms[pres_adfs_rom_position], true);
|
||||
set_rom(ROM::Slot5, *roms[pres_adfs_rom_position+1], true);
|
||||
}
|
||||
if(target.has_acorn_adfs) {
|
||||
set_rom(ROM::Slot6, *roms[acorn_adfs_rom_position], true);
|
||||
}
|
||||
}
|
||||
|
||||
if(target.has_ap6_rom) {
|
||||
set_rom(ROM::Slot15, *roms[ap6_rom_position], true);
|
||||
}
|
||||
|
||||
if(target.has_sideways_ram) {
|
||||
for(int c = 0; c < 16; c++) {
|
||||
if(rom_inserted_[c]) continue;
|
||||
if(c >= int(ROM::Keyboard) && c < int(ROM::BASIC)+1) continue;
|
||||
set_sideways_ram(ROM(c));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +147,11 @@ class ConcreteMachine:
|
||||
if(target.should_shift_restart) {
|
||||
shift_restart_counter_ = 1000000;
|
||||
}
|
||||
|
||||
if(has_scsi_bus) {
|
||||
scsi_bus_.add_observer(this);
|
||||
scsi_bus_.set_clocking_hint_observer(this);
|
||||
}
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
@@ -168,7 +217,12 @@ class ConcreteMachine:
|
||||
set_rom(slot, cartridge->get_segments().front().data, false);
|
||||
}
|
||||
|
||||
return !media.tapes.empty() || !media.disks.empty() || !media.cartridges.empty();
|
||||
// TODO: allow this only at machine startup?
|
||||
if(!media.mass_storage_devices.empty()) {
|
||||
hard_drive_->set_storage(media.mass_storage_devices.front());
|
||||
}
|
||||
|
||||
return !media.empty();
|
||||
}
|
||||
|
||||
forceinline Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
|
||||
@@ -178,13 +232,15 @@ class ConcreteMachine:
|
||||
if(isReadOperation(operation)) {
|
||||
*value = ram_[address];
|
||||
} else {
|
||||
if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) update_display();
|
||||
if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) {
|
||||
video_.flush();
|
||||
}
|
||||
ram_[address] = *value;
|
||||
}
|
||||
|
||||
// for the entire frame, RAM is accessible only on odd cycles; in modes below 4
|
||||
// it's also accessible only outside of the pixel regions
|
||||
cycles += video_output_.get_cycles_until_next_ram_availability(int(cycles_since_display_update_.as_integral()) + 1);
|
||||
// For the entire frame, RAM is accessible only on odd cycles; in modes below 4
|
||||
// it's also accessible only outside of the pixel regions.
|
||||
cycles += video_.last_valid()->get_cycles_until_next_ram_availability(video_.time_since_flush().template as<int>() + 1);
|
||||
} else {
|
||||
switch(address & 0xff0f) {
|
||||
case 0xfe00:
|
||||
@@ -222,10 +278,8 @@ class ConcreteMachine:
|
||||
case 0xfe08: case 0xfe09: case 0xfe0a: case 0xfe0b:
|
||||
case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f:
|
||||
if(!isReadOperation(operation)) {
|
||||
update_display();
|
||||
video_output_.write(address, *value);
|
||||
video_access_range_ = video_output_.get_memory_access_range();
|
||||
queue_next_display_interrupt();
|
||||
video_->write(address, *value);
|
||||
video_access_range_ = video_.last_valid()->get_memory_access_range();
|
||||
}
|
||||
break;
|
||||
case 0xfe04:
|
||||
@@ -290,7 +344,73 @@ class ConcreteMachine:
|
||||
plus3_->set_control_register(*value);
|
||||
} else *value = 1;
|
||||
}
|
||||
|
||||
if(has_scsi_bus && (address&0x00f0) == 0x0040) {
|
||||
scsi_acknowledge_ = true;
|
||||
if(!isReadOperation(operation)) {
|
||||
scsi_data_ = *value;
|
||||
push_scsi_output();
|
||||
} else {
|
||||
*value = SCSI::data_lines(scsi_bus_.get_state());
|
||||
push_scsi_output();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0xfc03:
|
||||
if(has_scsi_bus && (address&0x00f0) == 0x0040) {
|
||||
scsi_interrupt_state_ = false;
|
||||
scsi_interrupt_mask_ = *value & 1;
|
||||
evaluate_interrupts();
|
||||
}
|
||||
break;
|
||||
case 0xfc01:
|
||||
if(has_scsi_bus && (address&0x00f0) == 0x0040 && isReadOperation(operation)) {
|
||||
// Status byte is:
|
||||
//
|
||||
// b7: SCSI C/D
|
||||
// b6: SCSI I/O
|
||||
// b5: SCSI REQ
|
||||
// b4: interrupt flag
|
||||
// b3: 0
|
||||
// b2: 0
|
||||
// b1: SCSI BSY
|
||||
// b0: SCSI MSG
|
||||
const auto state = scsi_bus_.get_state();
|
||||
*value =
|
||||
(state & SCSI::Line::Control ? 0x80 : 0x00) |
|
||||
(state & SCSI::Line::Input ? 0x40 : 0x00) |
|
||||
(state & SCSI::Line::Request ? 0x20 : 0x00) |
|
||||
((scsi_interrupt_state_ && scsi_interrupt_mask_) ? 0x10 : 0x00) |
|
||||
(state & SCSI::Line::Busy ? 0x02 : 0x00) |
|
||||
(state & SCSI::Line::Message ? 0x01 : 0x00);
|
||||
|
||||
// Empirical guess: this is also the trigger to affect busy/request/acknowledge
|
||||
// signalling. Maybe?
|
||||
if(scsi_select_ && scsi_bus_.get_state() & SCSI::Line::Busy) {
|
||||
scsi_select_ = false;
|
||||
push_scsi_output();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0xfc02:
|
||||
if(has_scsi_bus && (address&0x00f0) == 0x0040) {
|
||||
scsi_select_ = true;
|
||||
push_scsi_output();
|
||||
}
|
||||
break;
|
||||
|
||||
// SCSI locations:
|
||||
//
|
||||
// fc40: data, read and write
|
||||
// fc41: status read
|
||||
// fc42: select write
|
||||
// fc43: interrupt latch
|
||||
//
|
||||
//
|
||||
// Interrupt latch is:
|
||||
//
|
||||
// b0: enable or disable IRQ on REQ
|
||||
// (and, possibly, writing to the latch acknowledges?)
|
||||
|
||||
default:
|
||||
if(address >= 0xc000) {
|
||||
@@ -307,14 +427,14 @@ class ConcreteMachine:
|
||||
(address == 0xf0a8) // 0xf0a8 is from where a service call would normally be
|
||||
// dispatched; we can check whether it would be call 14
|
||||
// (i.e. read byte) and, if so, whether the OS was about to
|
||||
// issue a read byte call to a ROM despite being the tape
|
||||
// issue a read byte call to a ROM despite the tape
|
||||
// FS being selected. If so then this is a get byte that
|
||||
// we should service synthetically. Put the byte into Y
|
||||
// and set A to zero to report that action was taken, then
|
||||
// allow the PC read to return an RTS.
|
||||
)
|
||||
) {
|
||||
uint8_t service_call = uint8_t(m6502_.get_value_of_register(CPU::MOS6502::Register::X));
|
||||
const auto service_call = uint8_t(m6502_.get_value_of_register(CPU::MOS6502::Register::X));
|
||||
if(address == 0xf0a8) {
|
||||
if(!ram_[0x247] && service_call == 14) {
|
||||
tape_.set_delegate(nullptr);
|
||||
@@ -323,7 +443,7 @@ class ConcreteMachine:
|
||||
tape_.clear_interrupts(Interrupt::ReceiveDataFull);
|
||||
while(!tape_.get_tape()->is_at_end()) {
|
||||
tape_.run_for_input_pulse();
|
||||
cycles_left_while_plausibly_in_data--;
|
||||
--cycles_left_while_plausibly_in_data;
|
||||
if(!cycles_left_while_plausibly_in_data) fast_load_is_in_data_ = false;
|
||||
if( (tape_.get_interrupt_status() & Interrupt::ReceiveDataFull) &&
|
||||
(fast_load_is_in_data_ || tape_.get_data_register() == 0x2a)
|
||||
@@ -365,18 +485,14 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
cycles_since_display_update_ += Cycles(int(cycles));
|
||||
if(video_ += Cycles(int(cycles))) {
|
||||
signal_interrupt(video_.last_valid()->get_interrupts());
|
||||
}
|
||||
|
||||
cycles_since_audio_update_ += Cycles(int(cycles));
|
||||
if(cycles_since_audio_update_ > Cycles(16384)) update_audio();
|
||||
tape_.run_for(Cycles(int(cycles)));
|
||||
|
||||
cycles_until_display_interrupt_ -= cycles;
|
||||
if(cycles_until_display_interrupt_ < 0) {
|
||||
signal_interrupt(next_display_interrupt_);
|
||||
update_display();
|
||||
queue_next_display_interrupt();
|
||||
}
|
||||
|
||||
if(typer_) typer_->run_for(Cycles(int(cycles)));
|
||||
if(plus3_) plus3_->run_for(Cycles(4*int(cycles)));
|
||||
if(shift_restart_counter_) {
|
||||
@@ -389,29 +505,35 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (has_scsi_bus) {
|
||||
if(scsi_is_clocked_) {
|
||||
scsi_bus_.run_for(Cycles(int(cycles)));
|
||||
}
|
||||
}
|
||||
|
||||
return Cycles(int(cycles));
|
||||
}
|
||||
|
||||
forceinline void flush() {
|
||||
update_display();
|
||||
video_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_output_.set_scan_target(scan_target);
|
||||
video_.last_valid()->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_output_.get_scaled_scan_status();
|
||||
return video_.last_valid()->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_output_.set_display_type(display_type);
|
||||
video_.last_valid()->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const final {
|
||||
return video_output_.get_display_type();
|
||||
return video_.last_valid()->get_display_type();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
@@ -422,13 +544,44 @@ class ConcreteMachine:
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void scsi_bus_did_change(SCSI::Bus *, SCSI::BusState new_state, double) final {
|
||||
// Release acknowledge when request is released.
|
||||
if(scsi_acknowledge_ && !(new_state & SCSI::Line::Request)) {
|
||||
scsi_acknowledge_ = false;
|
||||
push_scsi_output();
|
||||
}
|
||||
|
||||
// Output occurs only while SCSI::Line::Input is inactive; therefore a change
|
||||
// in that line affects what's on the bus.
|
||||
if(((new_state^previous_bus_state_)&SCSI::Line::Input)) {
|
||||
push_scsi_output();
|
||||
}
|
||||
|
||||
scsi_interrupt_state_ |= (new_state^previous_bus_state_)&new_state & SCSI::Line::Request;
|
||||
previous_bus_state_ = new_state;
|
||||
evaluate_interrupts();
|
||||
}
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference preference) final {
|
||||
scsi_is_clocked_ = preference != ClockingHint::Preference::None;
|
||||
}
|
||||
|
||||
void tape_did_change_interrupt_status(Tape *) final {
|
||||
interrupt_status_ = (interrupt_status_ & ~(Interrupt::TransmitDataEmpty | Interrupt::ReceiveDataFull | Interrupt::HighToneDetect)) | tape_.get_interrupt_status();
|
||||
evaluate_interrupts();
|
||||
}
|
||||
|
||||
HalfCycles get_typer_delay() const final {
|
||||
return m6502_.get_is_resetting() ? Cycles(750'000) : Cycles(0);
|
||||
HalfCycles get_typer_delay(const std::string &text) const final {
|
||||
if(!m6502_.get_is_resetting()) {
|
||||
return Cycles(0);
|
||||
}
|
||||
|
||||
// Add a longer delay for a command at reset that involves pressing a modifier;
|
||||
// empirically this seems to be a requirement, in order to avoid a collision with
|
||||
// the system's built-in modifier-at-startup test (e.g. to perform shift+break).
|
||||
CharacterMapper test_mapper;
|
||||
const uint16_t *const sequence = test_mapper.sequence_for_character(text[0]);
|
||||
return is_modifier(Key(sequence[0])) ? Cycles(1'000'000) : Cycles(750'000);
|
||||
}
|
||||
|
||||
HalfCycles get_typer_frequency() const final {
|
||||
@@ -469,10 +622,14 @@ class ConcreteMachine:
|
||||
if(activity_observer_) {
|
||||
activity_observer_->register_led(caps_led);
|
||||
activity_observer_->set_led_status(caps_led, caps_led_state_);
|
||||
}
|
||||
|
||||
if(plus3_) {
|
||||
plus3_->set_activity_observer(observer);
|
||||
}
|
||||
if(plus3_) {
|
||||
plus3_->set_activity_observer(observer);
|
||||
}
|
||||
|
||||
if(has_scsi_bus) {
|
||||
scsi_bus_.set_activity_observer(observer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,23 +674,23 @@ class ConcreteMachine:
|
||||
rom_ptr += size_to_copy;
|
||||
}
|
||||
|
||||
if(int(slot) < 16)
|
||||
if(int(slot) < 16) {
|
||||
rom_inserted_[int(slot)] = true;
|
||||
}
|
||||
|
||||
// MARK: - Work deferral updates.
|
||||
inline void update_display() {
|
||||
if(cycles_since_display_update_ > 0) {
|
||||
video_output_.run_for(cycles_since_display_update_.flush<Cycles>());
|
||||
}
|
||||
}
|
||||
|
||||
inline void queue_next_display_interrupt() {
|
||||
VideoOutput::Interrupt next_interrupt = video_output_.get_next_interrupt();
|
||||
cycles_until_display_interrupt_ = next_interrupt.cycles;
|
||||
next_display_interrupt_ = next_interrupt.interrupt;
|
||||
/*!
|
||||
Enables @c slot as sideways RAM; ensures that it does not currently contain a valid ROM signature.
|
||||
*/
|
||||
void set_sideways_ram(ROM slot) {
|
||||
std::memset(roms_[int(slot)], 0xff, 16*1024);
|
||||
if(int(slot) < 16) {
|
||||
rom_inserted_[int(slot)] = true;
|
||||
rom_write_masks_[int(slot)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Work deferral updates.
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(SoundGenerator::clock_rate_divider)));
|
||||
}
|
||||
@@ -554,7 +711,12 @@ class ConcreteMachine:
|
||||
} else {
|
||||
interrupt_status_ &= ~1;
|
||||
}
|
||||
m6502_.set_irq_line(interrupt_status_ & 1);
|
||||
|
||||
if constexpr (has_scsi_bus) {
|
||||
m6502_.set_irq_line((scsi_interrupt_state_ && scsi_interrupt_mask_) | (interrupt_status_ & 1));
|
||||
} else {
|
||||
m6502_.set_irq_line(interrupt_status_ & 1);
|
||||
}
|
||||
}
|
||||
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
@@ -578,10 +740,7 @@ class ConcreteMachine:
|
||||
Electron::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
// Counters related to simultaneous subsystems
|
||||
Cycles cycles_since_display_update_ = 0;
|
||||
Cycles cycles_since_audio_update_ = 0;
|
||||
int cycles_until_display_interrupt_ = 0;
|
||||
Interrupt next_display_interrupt_ = Interrupt::RealTimeClock;
|
||||
VideoOutput::Range video_access_range_ = {0, 0xffff};
|
||||
|
||||
// Tape
|
||||
@@ -598,8 +757,27 @@ class ConcreteMachine:
|
||||
bool is_holding_shift_ = false;
|
||||
int shift_restart_counter_ = 0;
|
||||
|
||||
// Hard drive.
|
||||
SCSI::Bus scsi_bus_;
|
||||
SCSI::Target::Target<SCSI::DirectAccessDevice> hard_drive_;
|
||||
SCSI::BusState previous_bus_state_ = SCSI::DefaultBusState;
|
||||
const size_t scsi_device_ = 0;
|
||||
uint8_t scsi_data_ = 0;
|
||||
bool scsi_select_ = false;
|
||||
bool scsi_acknowledge_ = false;
|
||||
bool scsi_is_clocked_ = false;
|
||||
bool scsi_interrupt_state_ = false;
|
||||
bool scsi_interrupt_mask_ = false;
|
||||
void push_scsi_output() {
|
||||
scsi_bus_.set_device_output(scsi_device_,
|
||||
(scsi_bus_.get_state()&SCSI::Line::Input ? 0 : scsi_data_) |
|
||||
(scsi_select_ ? SCSI::Line::SelectTarget : 0) |
|
||||
(scsi_acknowledge_ ? SCSI::Line::Acknowledge : 0)
|
||||
);
|
||||
}
|
||||
|
||||
// Outputs
|
||||
VideoOutput video_output_;
|
||||
JustInTimeActor<VideoOutput, Cycles> video_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
SoundGenerator sound_generator_;
|
||||
@@ -620,7 +798,12 @@ using namespace Electron;
|
||||
Machine *Machine::Electron(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
using Target = Analyser::Static::Acorn::Target;
|
||||
const Target *const acorn_target = dynamic_cast<const Target *>(target);
|
||||
return new Electron::ConcreteMachine(*acorn_target, rom_fetcher);
|
||||
|
||||
if(acorn_target->media.mass_storage_devices.empty()) {
|
||||
return new Electron::ConcreteMachine<false>(*acorn_target, rom_fetcher);
|
||||
} else {
|
||||
return new Electron::ConcreteMachine<true>(*acorn_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
|
||||
@@ -16,6 +16,7 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const {
|
||||
default: break;
|
||||
|
||||
BIND(BackTick, KeyCopy);
|
||||
BIND(Backslash, KeyCopy);
|
||||
|
||||
BIND(k0, Key0); BIND(k1, Key1); BIND(k2, Key2); BIND(k3, Key3); BIND(k4, Key4);
|
||||
BIND(k5, Key5); BIND(k6, Key6); BIND(k7, Key7); BIND(k8, Key8); BIND(k9, Key9);
|
||||
|
||||
@@ -31,11 +31,15 @@ enum Key: uint16_t {
|
||||
KeyShift = 0x00d0 | 0x08, KeyControl = 0x00d0 | 0x04, KeyFunc = 0x00d0 | 0x02, KeyEscape = 0x00d0 | 0x01,
|
||||
|
||||
// Virtual keys.
|
||||
KeyF1 = 0xfff0, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, KeyF7, KeyF8, KeyF9, KeyF0,
|
||||
KeyF1 = 0xfff0, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, KeyF7, KeyF8, KeyF9, KeyF0,
|
||||
|
||||
KeyBreak = 0xfffd,
|
||||
};
|
||||
|
||||
constexpr bool is_modifier(Key key) {
|
||||
return (key == KeyShift) || (key == KeyControl) || (key == KeyFunc);
|
||||
}
|
||||
|
||||
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const final;
|
||||
};
|
||||
|
||||
@@ -234,7 +234,23 @@ void VideoOutput::output_pixels(int number_of_cycles) {
|
||||
|
||||
void VideoOutput::run_for(const Cycles cycles) {
|
||||
int number_of_cycles = int(cycles.as_integral());
|
||||
const auto start_position = output_position_;
|
||||
output_position_ = (output_position_ + number_of_cycles) % cycles_per_frame;
|
||||
|
||||
if(
|
||||
(start_position < real_time_clock_interrupt_1 && output_position_ >= real_time_clock_interrupt_1) ||
|
||||
(start_position < real_time_clock_interrupt_2 && output_position_ >= real_time_clock_interrupt_2)
|
||||
) {
|
||||
interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::RealTimeClock);
|
||||
}
|
||||
|
||||
if(
|
||||
(start_position < display_end_interrupt_1 && output_position_ >= display_end_interrupt_1) ||
|
||||
(start_position < display_end_interrupt_2 && output_position_ >= display_end_interrupt_2)
|
||||
) {
|
||||
interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::DisplayEnd);
|
||||
}
|
||||
|
||||
while(number_of_cycles) {
|
||||
int draw_action_length = screen_map_[screen_map_pointer_].length;
|
||||
int time_left_in_action = std::min(number_of_cycles, draw_action_length - cycles_into_draw_action_);
|
||||
@@ -354,36 +370,30 @@ void VideoOutput::setup_base_address() {
|
||||
|
||||
// MARK: - Interrupts
|
||||
|
||||
VideoOutput::Interrupt VideoOutput::get_next_interrupt() {
|
||||
VideoOutput::Interrupt interrupt;
|
||||
|
||||
Cycles VideoOutput::get_next_sequence_point() {
|
||||
if(output_position_ < real_time_clock_interrupt_1) {
|
||||
interrupt.cycles = real_time_clock_interrupt_1 - output_position_;
|
||||
interrupt.interrupt = RealTimeClock;
|
||||
return interrupt;
|
||||
return real_time_clock_interrupt_1 - output_position_;
|
||||
}
|
||||
|
||||
if(output_position_ < display_end_interrupt_1) {
|
||||
interrupt.cycles = display_end_interrupt_1 - output_position_;
|
||||
interrupt.interrupt = DisplayEnd;
|
||||
return interrupt;
|
||||
return display_end_interrupt_1 - output_position_;
|
||||
}
|
||||
|
||||
if(output_position_ < real_time_clock_interrupt_2) {
|
||||
interrupt.cycles = real_time_clock_interrupt_2 - output_position_;
|
||||
interrupt.interrupt = RealTimeClock;
|
||||
return interrupt;
|
||||
return real_time_clock_interrupt_2 - output_position_;
|
||||
}
|
||||
|
||||
if(output_position_ < display_end_interrupt_2) {
|
||||
interrupt.cycles = display_end_interrupt_2 - output_position_;
|
||||
interrupt.interrupt = DisplayEnd;
|
||||
return interrupt;
|
||||
return display_end_interrupt_2 - output_position_;
|
||||
}
|
||||
|
||||
interrupt.cycles = real_time_clock_interrupt_1 + cycles_per_frame - output_position_;
|
||||
interrupt.interrupt = RealTimeClock;
|
||||
return interrupt;
|
||||
return real_time_clock_interrupt_1 + cycles_per_frame - output_position_;
|
||||
}
|
||||
|
||||
Electron::Interrupt VideoOutput::get_interrupts() {
|
||||
const auto interrupts = interrupts_;
|
||||
interrupts_ = Electron::Interrupt(0);
|
||||
return interrupts;
|
||||
}
|
||||
|
||||
// MARK: - RAM timing and access information
|
||||
|
||||
@@ -54,15 +54,6 @@ class VideoOutput {
|
||||
*/
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*!
|
||||
Describes an interrupt the video hardware will generate by its identity and scheduling time.
|
||||
*/
|
||||
struct Interrupt {
|
||||
/// The interrupt that will be signalled.
|
||||
Electron::Interrupt interrupt;
|
||||
/// The number of cycles until it is signalled.
|
||||
int cycles;
|
||||
};
|
||||
/*!
|
||||
@returns the next interrupt that should be generated as a result of the video hardware.
|
||||
The time until signalling returned is the number of cycles after the final one triggered
|
||||
@@ -70,7 +61,12 @@ class VideoOutput {
|
||||
|
||||
This result may be mutated by calls to @c write.
|
||||
*/
|
||||
Interrupt get_next_interrupt();
|
||||
Cycles get_next_sequence_point();
|
||||
|
||||
/*!
|
||||
@returns a bit mask of all interrupts that have been triggered since the last call to get_interrupt().
|
||||
*/
|
||||
Electron::Interrupt get_interrupts();
|
||||
|
||||
/*!
|
||||
@returns the number of cycles after (final cycle of last run_for batch + @c from_time)
|
||||
@@ -136,6 +132,8 @@ class VideoOutput {
|
||||
void emplace_pixel_line();
|
||||
std::size_t screen_map_pointer_ = 0;
|
||||
int cycles_into_draw_action_ = 0;
|
||||
|
||||
Electron::Interrupt interrupts_ = Electron::Interrupt(0);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ struct KeyActions {
|
||||
Instructs that all keys should now be treated as released.
|
||||
*/
|
||||
virtual void clear_all_keys() {}
|
||||
|
||||
/*!
|
||||
Indicates whether a machine most naturally accepts logical rather than physical input.
|
||||
*/
|
||||
virtual bool prefers_logical_input() { return false; }
|
||||
};
|
||||
|
||||
/*!
|
||||
|
||||
@@ -419,7 +419,9 @@ class ConcreteMachine:
|
||||
// but otherwise runs without pause.
|
||||
const HalfCycles addition((cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode) ? 2 : 0);
|
||||
const HalfCycles total_length = addition + cycle.length;
|
||||
vdp_ += total_length;
|
||||
if(vdp_ += total_length) {
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
|
||||
}
|
||||
time_since_ay_update_ += total_length;
|
||||
memory_slots_[0].cycles_since_update += total_length;
|
||||
memory_slots_[1].cycles_since_update += total_length;
|
||||
@@ -520,14 +522,11 @@ class ConcreteMachine:
|
||||
case 0x98: case 0x99:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 0xa2:
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
*cycle.value = ay_.get_data_output();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
*cycle.value = GI::AY38910::Utility::read(ay_);
|
||||
break;
|
||||
|
||||
case 0xa8: case 0xa9:
|
||||
@@ -547,14 +546,11 @@ class ConcreteMachine:
|
||||
case 0x98: case 0x99:
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 0xa0: case 0xa1:
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | ((port == 0xa0) ? GI::AY38910::BC1 : 0)));
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::write(ay_, port == 0xa1, *cycle.value);
|
||||
break;
|
||||
|
||||
case 0xa8: case 0xa9:
|
||||
@@ -610,12 +606,6 @@ class ConcreteMachine:
|
||||
if(!tape_player_is_sleeping_)
|
||||
tape_player_.run_for(int(cycle.length.as_integral()));
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
time_until_interrupt_ -= total_length;
|
||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||
z80_.set_interrupt_line(true, time_until_interrupt_);
|
||||
}
|
||||
}
|
||||
return addition;
|
||||
}
|
||||
|
||||
@@ -657,7 +647,6 @@ class ConcreteMachine:
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
|
||||
set_video_signal_configurable(options->output);
|
||||
allow_fast_tape_ = options->quickload;
|
||||
set_use_fast_tape();
|
||||
@@ -790,7 +779,6 @@ class ConcreteMachine:
|
||||
uint8_t unpopulated_[8192];
|
||||
|
||||
HalfCycles time_since_ay_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
|
||||
uint8_t key_states_[16];
|
||||
int selected_key_line_ = 0;
|
||||
|
||||
@@ -214,7 +214,9 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
vdp_ += cycle.length;
|
||||
if(vdp_ += cycle.length) {
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
|
||||
}
|
||||
time_since_sn76489_update_ += cycle.length;
|
||||
|
||||
if(cycle.is_terminal()) {
|
||||
@@ -266,7 +268,6 @@ class ConcreteMachine:
|
||||
case 0x80: case 0x81:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
case 0xc0: {
|
||||
if(memory_control_ & 0x4) {
|
||||
@@ -330,7 +331,6 @@ class ConcreteMachine:
|
||||
case 0x80: case 0x81: // i.e. ports 0x80–0xbf.
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
case 0xc1: case 0xc0: // i.e. ports 0xc0–0xff.
|
||||
if(has_fm_audio_) {
|
||||
@@ -374,13 +374,6 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
time_until_interrupt_ -= cycle.length;
|
||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||
z80_.set_interrupt_line(true, time_until_interrupt_);
|
||||
}
|
||||
}
|
||||
|
||||
// The pause button is debounced and takes effect only one line before pixels
|
||||
// begin; time_until_debounce_ keeps track of the time until then.
|
||||
time_until_debounce_ -= cycle.length;
|
||||
@@ -505,7 +498,6 @@ class ConcreteMachine:
|
||||
bool reset_is_pressed_ = false, pause_is_pressed_ = false;
|
||||
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
HalfCycles time_until_debounce_;
|
||||
|
||||
uint8_t ram_[8*1024];
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "../Utility/MemoryFuzzer.hpp"
|
||||
#include "../Utility/StringSerialiser.hpp"
|
||||
|
||||
#include "../../Processors/6502/6502.hpp"
|
||||
#include "../../Processors/6502Esque/6502Selector.hpp"
|
||||
#include "../../Components/6522/6522.hpp"
|
||||
#include "../../Components/AY38910/AY38910.hpp"
|
||||
#include "../../Components/DiskII/DiskII.hpp"
|
||||
@@ -34,6 +34,8 @@
|
||||
|
||||
#include "../../Analyser/Static/Oric/Target.hpp"
|
||||
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
@@ -41,6 +43,7 @@
|
||||
namespace Oric {
|
||||
|
||||
using DiskInterface = Analyser::Static::Oric::Target::DiskInterface;
|
||||
using Processor = Analyser::Static::Oric::Target::Processor;
|
||||
using AY = GI::AY38910::AY38910<false>;
|
||||
using Speaker = Outputs::Speaker::LowpassSpeaker<AY>;
|
||||
|
||||
@@ -205,7 +208,7 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
Keyboard &keyboard_;
|
||||
};
|
||||
|
||||
template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class ConcreteMachine:
|
||||
template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS6502Esque::Type processor_type> class ConcreteMachine:
|
||||
public MachineTypes::TimedMachine,
|
||||
public MachineTypes::ScanProducer,
|
||||
public MachineTypes::AudioProducer,
|
||||
@@ -216,7 +219,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
|
||||
public Storage::Tape::BinaryTapePlayer::Delegate,
|
||||
public DiskController::Delegate,
|
||||
public ClockingHint::Observer,
|
||||
public Activity::Source,
|
||||
public Machine,
|
||||
public Keyboard::SpecialKeyHandler {
|
||||
@@ -224,7 +226,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Oric::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
m6502_(*this),
|
||||
video_output_(ram_),
|
||||
video_(ram_),
|
||||
ay8910_(GI::AY38910::Personality::AY38910, audio_queue_),
|
||||
speaker_(ay8910_),
|
||||
via_port_handler_(audio_queue_, ay8910_, speaker_, tape_player_, keyboard_),
|
||||
@@ -246,10 +248,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
ram_[c] |= 0x40;
|
||||
}
|
||||
|
||||
if constexpr (disk_interface == DiskInterface::Pravetz) {
|
||||
diskii_.set_clocking_hint_observer(this);
|
||||
}
|
||||
|
||||
const std::string machine_name = "Oric";
|
||||
std::vector<ROMMachine::ROM> rom_names = { {machine_name, "the Oric colour ROM", "colour.rom", 128, 0xd50fca65} };
|
||||
switch(target.rom) {
|
||||
@@ -291,7 +289,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
}
|
||||
}
|
||||
|
||||
video_output_.set_colour_rom(*roms[0]);
|
||||
video_->set_colour_rom(*roms[0]);
|
||||
rom_ = std::move(*roms[1]);
|
||||
|
||||
switch(disk_interface) {
|
||||
@@ -312,7 +310,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
pravetz_rom_ = std::move(*roms[2]);
|
||||
pravetz_rom_.resize(512);
|
||||
|
||||
diskii_.set_state_machine(*roms[diskii_state_machine_index]);
|
||||
diskii_->set_state_machine(*roms[diskii_state_machine_index]);
|
||||
} break;
|
||||
}
|
||||
|
||||
@@ -401,7 +399,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
case DiskInterface::BD500: inserted |= insert_disks(media, bd500_, 4); break;
|
||||
case DiskInterface::Jasmin: inserted |= insert_disks(media, jasmin_, 4); break;
|
||||
case DiskInterface::Microdisc: inserted |= insert_disks(media, microdisc_, 4); break;
|
||||
case DiskInterface::Pravetz: inserted |= insert_disks(media, diskii_, 2); break;
|
||||
case DiskInterface::Pravetz: inserted |= insert_disks(media, *diskii_.last_valid(), 2); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -412,7 +410,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
// to satisfy CPU::MOS6502::BusHandler
|
||||
forceinline Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
|
||||
if(address > ram_top_) {
|
||||
if(isReadOperation(operation)) *value = paged_rom_[address - ram_top_ - 1];
|
||||
if(!isWriteOperation(operation)) *value = paged_rom_[address - ram_top_ - 1];
|
||||
|
||||
// 024D = 0 => fast; otherwise slow
|
||||
// E6C9 = read byte: return byte in A
|
||||
@@ -424,46 +422,46 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
!tape_player_.get_tape()->is_at_end()) {
|
||||
|
||||
uint8_t next_byte = tape_player_.get_next_byte(!ram_[tape_speed_address_]);
|
||||
m6502_.set_value_of_register(CPU::MOS6502::A, next_byte);
|
||||
m6502_.set_value_of_register(CPU::MOS6502::Flags, next_byte ? 0 : CPU::MOS6502::Flag::Zero);
|
||||
m6502_.set_value_of_register(CPU::MOS6502Esque::A, next_byte);
|
||||
m6502_.set_value_of_register(CPU::MOS6502Esque::Flags, next_byte ? 0 : CPU::MOS6502::Flag::Zero);
|
||||
*value = 0x60; // i.e. RTS
|
||||
}
|
||||
} else {
|
||||
if((address & 0xff00) == 0x0300) {
|
||||
if(address < 0x0310 || (disk_interface == DiskInterface::None)) {
|
||||
if(isReadOperation(operation)) *value = via_.read(address);
|
||||
if(!isWriteOperation(operation)) *value = via_.read(address);
|
||||
else via_.write(address, *value);
|
||||
} else {
|
||||
switch(disk_interface) {
|
||||
default: break;
|
||||
case DiskInterface::BD500:
|
||||
if(isReadOperation(operation)) *value = bd500_.read(address);
|
||||
if(!isWriteOperation(operation)) *value = bd500_.read(address);
|
||||
else bd500_.write(address, *value);
|
||||
break;
|
||||
case DiskInterface::Jasmin:
|
||||
if(address >= 0x3f4) {
|
||||
if(isReadOperation(operation)) *value = jasmin_.read(address);
|
||||
if(!isWriteOperation(operation)) *value = jasmin_.read(address);
|
||||
else jasmin_.write(address, *value);
|
||||
}
|
||||
break;
|
||||
case DiskInterface::Microdisc:
|
||||
switch(address) {
|
||||
case 0x0310: case 0x0311: case 0x0312: case 0x0313:
|
||||
if(isReadOperation(operation)) *value = microdisc_.read(address);
|
||||
if(!isWriteOperation(operation)) *value = microdisc_.read(address);
|
||||
else microdisc_.write(address, *value);
|
||||
break;
|
||||
case 0x314: case 0x315: case 0x316: case 0x317:
|
||||
if(isReadOperation(operation)) *value = microdisc_.get_interrupt_request_register();
|
||||
if(!isWriteOperation(operation)) *value = microdisc_.get_interrupt_request_register();
|
||||
else microdisc_.set_control_register(*value);
|
||||
break;
|
||||
case 0x318: case 0x319: case 0x31a: case 0x31b:
|
||||
if(isReadOperation(operation)) *value = microdisc_.get_data_request_register();
|
||||
if(!isWriteOperation(operation)) *value = microdisc_.get_data_request_register();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case DiskInterface::Pravetz:
|
||||
if(address >= 0x0320) {
|
||||
if(isReadOperation(operation)) *value = pravetz_rom_[pravetz_rom_base_pointer_ + (address & 0xff)];
|
||||
if(!isWriteOperation(operation)) *value = pravetz_rom_[pravetz_rom_base_pointer_ + (address & 0xff)];
|
||||
else {
|
||||
switch(address) {
|
||||
case 0x380: case 0x381: case 0x382: case 0x383:
|
||||
@@ -473,18 +471,17 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flush_diskii();
|
||||
const int disk_value = diskii_.read_address(address);
|
||||
if(isReadOperation(operation) && disk_value != diskii_.DidNotLoad) *value = uint8_t(disk_value);
|
||||
const int disk_value = diskii_->read_address(address);
|
||||
if(!isWriteOperation(operation) && disk_value != Apple::DiskII::DidNotLoad) *value = uint8_t(disk_value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(isReadOperation(operation))
|
||||
if(!isWriteOperation(operation))
|
||||
*value = ram_[address];
|
||||
else {
|
||||
if(address >= 0x9800 && address <= 0xc000) update_video();
|
||||
if(address >= 0x9800 && address <= 0xc000) video_.flush();
|
||||
ram_[address] = *value;
|
||||
}
|
||||
}
|
||||
@@ -507,7 +504,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
bd500_.run_for(Cycles(9)); // i.e. effective clock rate of 9Mhz.
|
||||
break;
|
||||
case DiskInterface::Jasmin:
|
||||
jasmin_.run_for(Cycles(8));; // i.e. effective clock rate of 8Mhz.
|
||||
jasmin_.run_for(Cycles(8)); // i.e. effective clock rate of 8Mhz.
|
||||
|
||||
// Jasmin autostart hack: wait for a period, then trigger a reset, having forced
|
||||
// the Jasmin to page its ROM in first. I assume the latter being what the Jasmin's
|
||||
@@ -520,43 +517,41 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
}
|
||||
break;
|
||||
case DiskInterface::Microdisc:
|
||||
microdisc_.run_for(Cycles(8));; // i.e. effective clock rate of 8Mhz.
|
||||
microdisc_.run_for(Cycles(8)); // i.e. effective clock rate of 8Mhz.
|
||||
break;
|
||||
case DiskInterface::Pravetz:
|
||||
if(diskii_clocking_preference_ == ClockingHint::Preference::RealTime) {
|
||||
diskii_.set_data_input(*value);
|
||||
diskii_.run_for(Cycles(2));; // i.e. effective clock rate of 2Mhz.
|
||||
} else {
|
||||
cycles_since_diskii_update_ += Cycles(2);
|
||||
if(diskii_.clocking_preference() == ClockingHint::Preference::RealTime) {
|
||||
diskii_->set_data_input(*value);
|
||||
}
|
||||
diskii_ += Cycles(2); // i.e. effective clock rate of 2Mhz.
|
||||
break;
|
||||
}
|
||||
cycles_since_video_update_++;
|
||||
|
||||
video_ += Cycles(1);
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
forceinline void flush() {
|
||||
update_video();
|
||||
video_.flush();
|
||||
via_.flush();
|
||||
flush_diskii();
|
||||
diskii_.flush();
|
||||
}
|
||||
|
||||
// to satisfy CRTMachine::Machine
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_output_.set_scan_target(scan_target);
|
||||
video_.last_valid()->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_output_.get_scaled_scan_status();
|
||||
return video_.last_valid()->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_output_.set_display_type(display_type);
|
||||
video_.last_valid()->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const final {
|
||||
return video_output_.get_display_type();
|
||||
return video_.last_valid()->get_display_type();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
@@ -643,35 +638,27 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
microdisc_.set_activity_observer(observer);
|
||||
break;
|
||||
case DiskInterface::Pravetz:
|
||||
diskii_.set_activity_observer(observer);
|
||||
diskii_->set_activity_observer(observer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final {
|
||||
diskii_clocking_preference_ = diskii_.preferred_clocking();
|
||||
}
|
||||
|
||||
private:
|
||||
const uint16_t basic_invisible_ram_top_ = 0xffff;
|
||||
const uint16_t basic_visible_ram_top_ = 0xbfff;
|
||||
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
CPU::MOS6502Esque::Processor<processor_type, ConcreteMachine, false> m6502_;
|
||||
|
||||
// RAM and ROM
|
||||
std::vector<uint8_t> rom_, disk_rom_;
|
||||
uint8_t ram_[65536];
|
||||
Cycles cycles_since_video_update_;
|
||||
inline void update_video() {
|
||||
video_output_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
|
||||
// ROM bookkeeping
|
||||
uint16_t tape_get_byte_address_ = 0, tape_speed_address_ = 0;
|
||||
int keyboard_read_count_ = 0;
|
||||
|
||||
// Outputs
|
||||
VideoOutput video_output_;
|
||||
JustInTimeActor<VideoOutput, Cycles> video_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay8910_;
|
||||
@@ -699,14 +686,9 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
BD500 bd500_;
|
||||
|
||||
// the Pravetz/Disk II, if in use.
|
||||
Apple::DiskII diskii_;
|
||||
Cycles cycles_since_diskii_update_;
|
||||
void flush_diskii() {
|
||||
diskii_.run_for(cycles_since_diskii_update_.flush<Cycles>());
|
||||
}
|
||||
JustInTimeActor<Apple::DiskII, Cycles> diskii_;
|
||||
std::vector<uint8_t> pravetz_rom_;
|
||||
std::size_t pravetz_rom_base_pointer_ = 0;
|
||||
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
|
||||
// Overlay RAM
|
||||
uint16_t ram_top_ = basic_visible_ram_top_;
|
||||
@@ -759,13 +741,24 @@ using namespace Oric;
|
||||
|
||||
Machine *Machine::Oric(const Analyser::Static::Target *target_hint, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
auto *const oric_target = dynamic_cast<const Analyser::Static::Oric::Target *>(target_hint);
|
||||
switch(oric_target->disk_interface) {
|
||||
default: return new ConcreteMachine<DiskInterface::None>(*oric_target, rom_fetcher);
|
||||
case DiskInterface::Microdisc: return new ConcreteMachine<DiskInterface::Microdisc>(*oric_target, rom_fetcher);
|
||||
case DiskInterface::Pravetz: return new ConcreteMachine<DiskInterface::Pravetz>(*oric_target, rom_fetcher);
|
||||
case DiskInterface::Jasmin: return new ConcreteMachine<DiskInterface::Jasmin>(*oric_target, rom_fetcher);
|
||||
case DiskInterface::BD500: return new ConcreteMachine<DiskInterface::BD500>(*oric_target, rom_fetcher);
|
||||
|
||||
#define DiskInterfaceSwitch(processor) \
|
||||
switch(oric_target->disk_interface) { \
|
||||
default: return new ConcreteMachine<DiskInterface::None, processor>(*oric_target, rom_fetcher); \
|
||||
case DiskInterface::Microdisc: return new ConcreteMachine<DiskInterface::Microdisc, processor>(*oric_target, rom_fetcher); \
|
||||
case DiskInterface::Pravetz: return new ConcreteMachine<DiskInterface::Pravetz, processor>(*oric_target, rom_fetcher); \
|
||||
case DiskInterface::Jasmin: return new ConcreteMachine<DiskInterface::Jasmin, processor>(*oric_target, rom_fetcher); \
|
||||
case DiskInterface::BD500: return new ConcreteMachine<DiskInterface::BD500, processor>(*oric_target, rom_fetcher); \
|
||||
}
|
||||
|
||||
switch(oric_target->processor) {
|
||||
case Processor::WDC65816: DiskInterfaceSwitch(CPU::MOS6502Esque::Type::TWDC65816);
|
||||
case Processor::MOS6502: DiskInterfaceSwitch(CPU::MOS6502Esque::Type::T6502);
|
||||
}
|
||||
|
||||
#undef DiskInterfaceSwitch
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user